From 1042046adf978bfdcf973911e0cc57a31bb7799b Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 16:22:31 +0200 Subject: [PATCH 1/7] wip --- package-lock.json | 8 ++++---- package.json | 2 +- packages/cli/package-lock.json | 14 +++++++------- packages/cli/package.json | 2 +- packages/extras/package-lock.json | 10 +++++----- packages/extras/package.json | 4 ++-- packages/transpiler/package-lock.json | 14 +++++++------- packages/transpiler/package.json | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1479474a6..f9b450ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@eslint/compat": "^2.1.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^9.39.4", @@ -36,9 +36,9 @@ } }, "node_modules/@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a2d7412ca..faa2890fc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "homepage": "https://github.com/abaplint/transpiler_poc#readme", "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@eslint/compat": "^2.1.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^9.39.4", diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 9695ec65c..29b812592 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -12,7 +12,7 @@ "abap_transpile": "abap_transpile" }, "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46", "@types/glob": "^8.1.0", "@types/node": "^24.12.2", @@ -30,9 +30,9 @@ } }, "node_modules/@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "dev": true, "license": "MIT", "dependencies": { @@ -1945,9 +1945,9 @@ }, "dependencies": { "@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "dev": true, "requires": { "fast-xml-parser": "^5.9.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index c83ba9819..3b139aae0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,7 +27,7 @@ "author": "abaplint", "license": "MIT", "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46", "@types/glob": "^8.1.0", "@types/node": "^24.12.2", diff --git a/packages/extras/package-lock.json b/packages/extras/package-lock.json index 29c39e9b4..2381cf75b 100644 --- a/packages/extras/package-lock.json +++ b/packages/extras/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.2", "license": "PolyForm-Noncommercial-1.0.0", "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", @@ -20,14 +20,14 @@ "typescript": "^6.0.3" }, "peerDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46" } }, "node_modules/@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/packages/extras/package.json b/packages/extras/package.json index 745eaec7c..f4fae0b7e 100644 --- a/packages/extras/package.json +++ b/packages/extras/package.json @@ -28,11 +28,11 @@ "author": "Heliconia Labs ApS", "license": "PolyForm-Noncommercial-1.0.0", "peerDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46" }, "devDependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "@abaplint/transpiler": "^2.13.46", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", diff --git a/packages/transpiler/package-lock.json b/packages/transpiler/package-lock.json index 34b2ec3f2..98e54ced1 100644 --- a/packages/transpiler/package-lock.json +++ b/packages/transpiler/package-lock.json @@ -9,7 +9,7 @@ "version": "2.13.46", "license": "MIT", "dependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "source-map": "^0.7.6" }, "devDependencies": { @@ -25,9 +25,9 @@ } }, "node_modules/@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "license": "MIT", "dependencies": { "fast-xml-parser": "^5.9.3", @@ -1327,9 +1327,9 @@ }, "dependencies": { "@abaplint/core": { - "version": "2.120.5", - "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.5.tgz", - "integrity": "sha512-+vSTtFE8/9p/VmNaP1i5ocHyfLa7NPhVcFdtjfafw3aLUL4jSeiWxSofgI3Jzy4a0Y5hhBbHD86FEj2HP2dCiQ==", + "version": "2.120.6", + "resolved": "https://registry.npmjs.org/@abaplint/core/-/core-2.120.6.tgz", + "integrity": "sha512-2JFbMa9tlfYHdSHufgjhhiCbTDq4wA5K69sFRCHT31+LHaFIs8XkoF7VCR/ckW+nd5U66GWdskVD4OHeJE0fQw==", "requires": { "fast-xml-parser": "^5.9.3", "json5": "^2.2.3", diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 1ac9d05d7..1c59d3282 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -29,7 +29,7 @@ "author": "abaplint", "license": "MIT", "dependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "source-map": "^0.7.6" }, "devDependencies": { From a083ed375805dd940e0edbbd734f71df56229ced Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 16:57:08 +0200 Subject: [PATCH 2/7] bugfix --- packages/transpiler/src/expressions/constant.ts | 13 +++++++++---- packages/transpiler/src/statements/data.ts | 2 +- packages/transpiler/src/traversal.ts | 2 +- test/builtin/strlen.ts | 9 +++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/transpiler/src/expressions/constant.ts b/packages/transpiler/src/expressions/constant.ts index 841ce4f6c..16dcdf944 100644 --- a/packages/transpiler/src/expressions/constant.ts +++ b/packages/transpiler/src/expressions/constant.ts @@ -53,10 +53,7 @@ export class ConstantTranspiler implements IExpressionTranspiler { const code = "new abap.types.String().set(" + ConstantTranspiler.escape(res) + ")"; return new Chunk().append(code, node, traversal); } else { - if (res.startsWith("'")) { - res = "'" + res.substring(1, res.length - 1).trimEnd() + "'"; - } - const code = ConstantTranspiler.escape(res); + const code = ConstantTranspiler.escape(ConstantTranspiler.trimTextFieldLiteral(res)); return new Chunk().append(code, node, traversal); } } @@ -99,6 +96,14 @@ export class ConstantTranspiler implements IExpressionTranspiler { return code; } + // text field literals are of type c, trailing blanks are not part of the value + public static trimTextFieldLiteral(str: string): string { + if (str.startsWith("'") && str.endsWith("'") && str.length >= 2) { + return "'" + str.substring(1, str.length - 1).trimEnd() + "'"; + } + return str; + } + public static escape(str: string): string { str = str.replace(/\\/g, "\\\\"); diff --git a/packages/transpiler/src/statements/data.ts b/packages/transpiler/src/statements/data.ts index af124c856..bffab3dd6 100644 --- a/packages/transpiler/src/statements/data.ts +++ b/packages/transpiler/src/statements/data.ts @@ -79,7 +79,7 @@ export class DataTranspiler implements IStatementTranspiler { int = val.findFirstExpression(abaplint.Expressions.ConstantString); } if (int) { - const escaped = ConstantTranspiler.escape(int.concatTokens()); + const escaped = ConstantTranspiler.escape(ConstantTranspiler.trimTextFieldLiteral(int.concatTokens())); value = "\n" + name + ".set(" + escaped + ");"; } else if (val.getChildren()[1].get() instanceof abaplint.Expressions.SimpleFieldChain) { const s = new FieldChainTranspiler().transpile(val.getChildren()[1] as abaplint.Nodes.ExpressionNode, traversal).getCode(); diff --git a/packages/transpiler/src/traversal.ts b/packages/transpiler/src/traversal.ts index 7ef4b7e01..5c8f86bcd 100644 --- a/packages/transpiler/src/traversal.ts +++ b/packages/transpiler/src/traversal.ts @@ -940,7 +940,7 @@ this.INTERNAL_ID = abap.internalIdCounter++;\n`; const handle = (val: any, name: string) => { if (typeof val === "string") { - const e = ConstantTranspiler.escape(val); + const e = ConstantTranspiler.escape(ConstantTranspiler.trimTextFieldLiteral(val)); ret += name + ".set(" + e + ");\n"; } else if (typeof val === "object") { const a: any = val; diff --git a/test/builtin/strlen.ts b/test/builtin/strlen.ts index 95ce45c02..4d61eb4d2 100644 --- a/test/builtin/strlen.ts +++ b/test/builtin/strlen.ts @@ -62,4 +62,13 @@ describe("Builtin functions - strlen", () => { expect(abap.console.get()).to.equal("5"); }); + it("trailing blanks in text field literal are not significant", async () => { + const code = ` + DATA lv_error_separator TYPE string VALUE ': '. + ASSERT strlen( lv_error_separator ) = 1.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + }); From b864b4e427f801528ec4c476ea722c1f6062e0c6 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 17:00:36 +0200 Subject: [PATCH 3/7] fix --- packages/transpiler/src/expressions/constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transpiler/src/expressions/constant.ts b/packages/transpiler/src/expressions/constant.ts index 16dcdf944..dadbaa26b 100644 --- a/packages/transpiler/src/expressions/constant.ts +++ b/packages/transpiler/src/expressions/constant.ts @@ -45,7 +45,7 @@ export class ConstantTranspiler implements IExpressionTranspiler { str = node.findDirectExpression(Expressions.TextElementString); } if (str) { - let res = str.getFirstToken().getStr(); + const res = str.getFirstToken().getStr(); if (res.startsWith("'") && this.addGet === false) { const code = this.handleCharacter(res); return new Chunk().append(code, node, traversal); From d25010ba625a58f442e749923a7aab1d84a25a21 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 18:05:05 +0200 Subject: [PATCH 4/7] add etstcase --- test/statements/while.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/statements/while.ts b/test/statements/while.ts index 6bead951c..882e88c1c 100644 --- a/test/statements/while.ts +++ b/test/statements/while.ts @@ -61,4 +61,19 @@ ENDWHILE.`; 00500`); }); + it.only("DATA declared inside WHILE body is not re-initialized", async () => { + const code = ` +TYPES ty_ints TYPE STANDARD TABLE OF i WITH DEFAULT KEY. +DATA lv_index TYPE i. +WHILE lv_index < 3. + lv_index = lv_index + 1. + DATA lt_arguments TYPE ty_ints. + APPEND lv_index TO lt_arguments. + ASSERT lines( lt_arguments ) = lv_index. +ENDWHILE.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + }); \ No newline at end of file From b2b83b72f79e0aeaae718ed1c9a32b95018628f4 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 18:20:21 +0200 Subject: [PATCH 5/7] fix --- packages/transpiler/src/statements/data.ts | 39 ++++++++++++++++++++-- packages/transpiler/src/structures/data.ts | 8 ++++- test/statements/while.ts | 5 +-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/transpiler/src/statements/data.ts b/packages/transpiler/src/statements/data.ts index bffab3dd6..e597990bf 100644 --- a/packages/transpiler/src/statements/data.ts +++ b/packages/transpiler/src/statements/data.ts @@ -7,6 +7,34 @@ import {FieldChainTranspiler} from "../expressions"; import {Chunk} from "../chunk"; export class DataTranspiler implements IStatementTranspiler { + private readonly skipLoopScoping: boolean; + private variableName = ""; + private loopScoped = false; + + public constructor(options?: {skipLoopScoping?: boolean}) { + this.skipLoopScoping = options?.skipLoopScoping === true; + } + + /** name of the declared javascript variable, set by transpile() */ + public getVariableName(): string { + return this.variableName; + } + + /** set by transpile(), true if the declaration is inside a loop */ + public isLoopScoped(): boolean { + return this.loopScoped; + } + + /** DATA is scoped to the enclosing method/form/program, but when the statement is inside a + * loop the generated declaration ends up inside the javascript block of the loop. Declaring + * with "var" hoists it out of the block, and the guard makes sure its constructed only on the + * first pass, ie. contents are kept across iterations, matching ABAP */ + public static wrapLoopScoped(name: string, chunk: Chunk): Chunk { + return new Chunk() + .appendString(`if (${name} === undefined) {\n`) + .appendChunk(chunk) + .appendString("\n}"); + } public transpile(node: abaplint.Nodes.StatementNode, traversal: Traversal): Chunk { const token = node.findFirstExpression(abaplint.Expressions.DefinitionName)?.getFirstToken(); @@ -39,13 +67,20 @@ export class DataTranspiler implements IStatementTranspiler { } } + this.variableName = Traversal.prefixVariable(Traversal.escapeNamespace(found.getName().toLowerCase())); + this.loopScoped = traversal.isInsideLoop(node); + const ret = new Chunk() - .appendString("let ") - .appendString(Traversal.prefixVariable(Traversal.escapeNamespace(found.getName().toLowerCase()))) + .appendString(this.loopScoped === true ? "var " : "let ") + .appendString(this.variableName) .appendString(" = " + TranspileTypes.toType(found.getType())) .appendString(";") .appendString(value); + if (this.loopScoped === true && this.skipLoopScoping === false) { + return DataTranspiler.wrapLoopScoped(this.variableName, ret); + } + return ret; } diff --git a/packages/transpiler/src/structures/data.ts b/packages/transpiler/src/structures/data.ts index 17f32a933..5c1f694ea 100644 --- a/packages/transpiler/src/structures/data.ts +++ b/packages/transpiler/src/structures/data.ts @@ -13,7 +13,9 @@ export class DataTranspiler implements IStructureTranspiler { } const topName = begin.findDirectExpression(abaplint.Expressions.DefinitionName)?.concatTokens().toLowerCase(); - const chunk = new DataStatementTranspiler().transpile(begin, traversal).ensureStartMapping(begin, traversal); + // the component values are part of the declaration, so they must stay inside the loop guard + const statement = new DataStatementTranspiler({skipLoopScoping: true}); + let chunk = statement.transpile(begin, traversal).ensureStartMapping(begin, traversal); for (const d of node.findDirectStatements(abaplint.Statements.Data)) { const subName = d.findFirstExpression(abaplint.Expressions.DefinitionName)?.concatTokens().toLowerCase(); @@ -22,6 +24,10 @@ export class DataTranspiler implements IStructureTranspiler { } } + if (statement.isLoopScoped() === true) { + chunk = DataStatementTranspiler.wrapLoopScoped(statement.getVariableName(), chunk); + } + chunk.appendString("\n"); return chunk; } diff --git a/test/statements/while.ts b/test/statements/while.ts index 882e88c1c..dedc19135 100644 --- a/test/statements/while.ts +++ b/test/statements/while.ts @@ -61,7 +61,7 @@ ENDWHILE.`; 00500`); }); - it.only("DATA declared inside WHILE body is not re-initialized", async () => { + it("DATA declared inside WHILE body is not re-initialized", async () => { const code = ` TYPES ty_ints TYPE STANDARD TABLE OF i WITH DEFAULT KEY. DATA lv_index TYPE i. @@ -70,7 +70,8 @@ WHILE lv_index < 3. DATA lt_arguments TYPE ty_ints. APPEND lv_index TO lt_arguments. ASSERT lines( lt_arguments ) = lv_index. -ENDWHILE.`; +ENDWHILE. +ASSERT lines( lt_arguments ) = 3.`; const js = await run(code); const f = new AsyncFunction("abap", js); await f(abap); From 407d695a802eada598a918bcb6113ed7bad9a38c Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 19:06:26 +0200 Subject: [PATCH 6/7] bugfix --- packages/runtime/src/builtin/abs.ts | 2 +- packages/runtime/src/builtin/ceil.ts | 2 +- packages/runtime/src/builtin/floor.ts | 2 +- packages/runtime/src/builtin/frac.ts | 5 +++-- packages/runtime/src/builtin/trunc.ts | 2 +- packages/runtime/src/operators/add.ts | 6 +++--- packages/runtime/src/operators/divide.ts | 15 +++++++++++++-- packages/runtime/src/operators/minus.ts | 4 ++-- packages/runtime/src/operators/multiply.ts | 4 ++-- packages/runtime/src/types/float.ts | 18 ++++++++++++++++++ packages/runtime/src/types/integer.ts | 12 ++++++++++++ test/builtin/trunc.ts | 10 ++++++++++ 12 files changed, 67 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/builtin/abs.ts b/packages/runtime/src/builtin/abs.ts index 7ca8affb1..0041e9f36 100644 --- a/packages/runtime/src/builtin/abs.ts +++ b/packages/runtime/src/builtin/abs.ts @@ -10,7 +10,7 @@ export function abs(input: {val: number | string | ICharacter | INumeric}) { } else if (typeof input.val === "string") { num_in = parseFloat(input.val); } else if (input.val instanceof Float) { - num_in = input.val.getRaw(); + num_in = input.val.getCalculationValue(); } else { num_in = parseFloat(input.val.get().toString()); } diff --git a/packages/runtime/src/builtin/ceil.ts b/packages/runtime/src/builtin/ceil.ts index 2456e2557..e7a9d4a5f 100644 --- a/packages/runtime/src/builtin/ceil.ts +++ b/packages/runtime/src/builtin/ceil.ts @@ -10,7 +10,7 @@ export function ceil(input: {val: number | string | ICharacter | INumeric}) { } else if ( typeof input.val === "string") { num_in = parseFloat(input.val); } else if (input.val instanceof Float) { - num_in = input.val.getRaw(); + num_in = input.val.getCalculationValue(); } else { num_in = parseFloat(input.val.get().toString()); } diff --git a/packages/runtime/src/builtin/floor.ts b/packages/runtime/src/builtin/floor.ts index 51a9f1259..d6026d856 100644 --- a/packages/runtime/src/builtin/floor.ts +++ b/packages/runtime/src/builtin/floor.ts @@ -10,7 +10,7 @@ export function floor(input: {val: number | string | ICharacter | INumeric}) { } else if (typeof input.val === "string") { num_in = parseFloat(input.val); } else if (input.val instanceof Float) { - num_in = input.val.getRaw(); + num_in = input.val.getCalculationValue(); } else { num_in = parseFloat(input.val.get().toString()); } diff --git a/packages/runtime/src/builtin/frac.ts b/packages/runtime/src/builtin/frac.ts index 372fb7f0a..f1b23ce3e 100644 --- a/packages/runtime/src/builtin/frac.ts +++ b/packages/runtime/src/builtin/frac.ts @@ -11,8 +11,9 @@ export function frac(input: {val: number | string | ICharacter | INumeric}) { num_in = input.val; } else if (typeof input.val === "string") { num_in = parseFloat(input.val); - } else if (input.val instanceof DecFloat34 - || input.val instanceof Float) { + } else if (input.val instanceof Float) { + num_in = input.val.getCalculationValue(); + } else if (input.val instanceof DecFloat34) { num_in = input.val.getRaw(); } else { num_in = parseFloat(input.val.get().toString()); diff --git a/packages/runtime/src/builtin/trunc.ts b/packages/runtime/src/builtin/trunc.ts index 69002e4d4..bd0eefd59 100644 --- a/packages/runtime/src/builtin/trunc.ts +++ b/packages/runtime/src/builtin/trunc.ts @@ -9,7 +9,7 @@ export function trunc(input: {val: number | string | ICharacter | INumeric}) { } else if (typeof input.val === "string") { num_in = parseFloat(input.val); } else if (input.val instanceof Float) { - num_in = input.val.getRaw(); + num_in = input.val.getCalculationValue(); } else { num_in = parseFloat(input.val.get().toString()); } diff --git a/packages/runtime/src/operators/add.ts b/packages/runtime/src/operators/add.ts index 2c2cc7f0d..6a8ec2836 100644 --- a/packages/runtime/src/operators/add.ts +++ b/packages/runtime/src/operators/add.ts @@ -23,14 +23,14 @@ export function add(left: INumeric | ICharacter | string | number | Float | Inte return new Integer().set(left.get() + right); } else if ((left instanceof String || left instanceof Character) && isIntegerCharacter(left) && right instanceof Integer) { - return new Integer().set(Number.parseInt(left.get(), 10) + right.get()); + return new Integer().set(Number.parseInt(left.get(), 10) + right.get()).clearIntegerCalculationType(); } else if ((right instanceof String || right instanceof Character) && isIntegerCharacter(right) && left instanceof Integer) { - return new Integer().set(left.get() + Number.parseInt(right.get(), 10)); + return new Integer().set(left.get() + Number.parseInt(right.get(), 10)).clearIntegerCalculationType(); } else if ((left instanceof String || left instanceof Character) && (right instanceof String || right instanceof Character) && isIntegerCharacter(left) && isIntegerCharacter(right)) { - return new Integer().set(Number.parseInt(left.get(), 10) + Number.parseInt(right.get(), 10)); + return new Integer().set(Number.parseInt(left.get(), 10) + Number.parseInt(right.get(), 10)).clearIntegerCalculationType(); } else if (left instanceof Integer8) { if (right instanceof Integer8) { return new Integer8().set(left.get() + right.get()); diff --git a/packages/runtime/src/operators/divide.ts b/packages/runtime/src/operators/divide.ts index 383668582..4011d147b 100644 --- a/packages/runtime/src/operators/divide.ts +++ b/packages/runtime/src/operators/divide.ts @@ -32,5 +32,16 @@ export function divide(left: INumeric | ICharacter | Integer8 | string | number, } const val = l / r; - return new Float().set(val); -} \ No newline at end of file + const ret = new Float().set(val); + if (isIntegerOperand(left) && isIntegerOperand(right)) { + ret.setIntegerCalculationType(); + } + return ret; +} + +function isIntegerOperand(val: INumeric | ICharacter | Integer8 | string | number): boolean { + if (val instanceof Integer) { + return val.isIntegerCalculationType(); + } + return val instanceof Integer8; +} diff --git a/packages/runtime/src/operators/minus.ts b/packages/runtime/src/operators/minus.ts index 1481ca968..8b6080db5 100644 --- a/packages/runtime/src/operators/minus.ts +++ b/packages/runtime/src/operators/minus.ts @@ -38,9 +38,9 @@ export function minus(left: INumeric | ICharacter | string | Integer8 | number | return new Integer().set(left.get() - right); } else if ((left instanceof String || left instanceof Character) && Number.isInteger(Number(left.get())) && right instanceof Integer) { - return new Integer().set(Number.parseInt(left.get(), 10) - right.get()); + return new Integer().set(Number.parseInt(left.get(), 10) - right.get()).clearIntegerCalculationType(); } else if ((right instanceof String || right instanceof Character) && Number.isInteger(Number(right)) && left instanceof Integer) { - return new Integer().set(left.get() - Number.parseInt(right.get(), 10)); + return new Integer().set(left.get() - Number.parseInt(right.get(), 10)).clearIntegerCalculationType(); } return new Float().set(parse(left) - parse(right)); diff --git a/packages/runtime/src/operators/multiply.ts b/packages/runtime/src/operators/multiply.ts index 6e18f0de9..6956109e4 100644 --- a/packages/runtime/src/operators/multiply.ts +++ b/packages/runtime/src/operators/multiply.ts @@ -25,10 +25,10 @@ export function multiply(left: INumeric | ICharacter | string | Integer8 | numbe return new Integer().set(val); } else if ((left instanceof String || left instanceof Character) && Number.isInteger(Number(left.get())) && right instanceof Integer) { const val = Number.parseInt(left.get(), 10) * right.get(); - return new Integer().set(val); + return new Integer().set(val).clearIntegerCalculationType(); } else if ((right instanceof String || right instanceof Character) && Number.isInteger(Number(right)) && left instanceof Integer) { const val = left.get() * Number.parseInt(right.get(), 10); - return new Integer().set(val); + return new Integer().set(val).clearIntegerCalculationType(); } return new Float().set(parse(left) * parse(right)); diff --git a/packages/runtime/src/types/float.ts b/packages/runtime/src/types/float.ts index 3e77857bd..77db912df 100644 --- a/packages/runtime/src/types/float.ts +++ b/packages/runtime/src/types/float.ts @@ -22,6 +22,7 @@ function getNumberParts(x: number) { export class Float { private value: number; + private integerCalculationType = false; private readonly qualifiedName: string | undefined; public constructor(input?: {qualifiedName?: string}) { @@ -35,6 +36,23 @@ export class Float { return n; } + /** ABAP calculates in type i if all operands are integers, ie. "3 / 2" is 2. The exact value is + * kept here, so assigning to a float or packed target, which raises the calculation type, still + * gives the exact result. Consumers without a target type must use getCalculationValue() */ + public setIntegerCalculationType(): Float { + this.integerCalculationType = true; + return this; + } + + /** value as seen by the calculation type, ie. rounded if the calculation type is integer */ + public getCalculationValue(): number { + if (this.integerCalculationType === true) { + // ABAP rounds half away from zero + return this.value < 0 ? -Math.round(-this.value) : Math.round(this.value); + } + return this.value; + } + public getQualifiedName() { return this.qualifiedName; } diff --git a/packages/runtime/src/types/integer.ts b/packages/runtime/src/types/integer.ts index 833a28577..8e2f27183 100644 --- a/packages/runtime/src/types/integer.ts +++ b/packages/runtime/src/types/integer.ts @@ -34,6 +34,7 @@ export function toInteger(value: string, exception = true): number { export class Integer implements INumeric { private value: number; private constant: boolean = false; + private integerCalculationType = true; private readonly qualifiedName: string | undefined; public constructor(input?: {qualifiedName?: string}) { @@ -41,6 +42,17 @@ export class Integer implements INumeric { this.qualifiedName = input?.qualifiedName; } + /** ABAP determines the calculation type from the operand types, character-like operands raise it + * above type i even if the intermediate result is integer, ie. "'1.0' * 18 / 16" is not rounded */ + public clearIntegerCalculationType(): Integer { + this.integerCalculationType = false; + return this; + } + + public isIntegerCalculationType(): boolean { + return this.integerCalculationType; + } + public getQualifiedName() { return this.qualifiedName; } diff --git a/test/builtin/trunc.ts b/test/builtin/trunc.ts index 5f87945ec..52373b879 100644 --- a/test/builtin/trunc.ts +++ b/test/builtin/trunc.ts @@ -80,4 +80,14 @@ START-OF-SELECTION. expect(abap.console.getTrimmed()).to.equal("10"); }); + it("integer division, calculation type is integer", async () => { + const code = ` + DATA lv_eleven TYPE i VALUE 11. + ASSERT trunc( lv_eleven / 16 ) = 1. + ASSERT trunc( 3 / 2 ) = 2.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + }); From 3a1ecb7df445622d2ca88ee9d0415101735332f2 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Mon, 27 Jul 2026 19:19:48 +0200 Subject: [PATCH 7/7] 2.13.47 --- packages/cli/package-lock.json | 22 +++++++++++----------- packages/cli/package.json | 4 ++-- packages/extras/package-lock.json | 12 ++++++------ packages/extras/package.json | 4 ++-- packages/runtime/package-lock.json | 4 ++-- packages/runtime/package.json | 2 +- packages/transpiler/package-lock.json | 4 ++-- packages/transpiler/package.json | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 29b812592..89276197d 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,19 +1,19 @@ { "name": "@abaplint/transpiler-cli", - "version": "2.13.46", + "version": "2.13.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@abaplint/transpiler-cli", - "version": "2.13.46", + "version": "2.13.47", "license": "MIT", "bin": { "abap_transpile": "abap_transpile" }, "devDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46", + "@abaplint/transpiler": "^2.13.47", "@types/glob": "^8.1.0", "@types/node": "^24.12.2", "@types/progress": "^2.0.7", @@ -48,13 +48,13 @@ } }, "node_modules/@abaplint/transpiler": { - "version": "2.13.46", - "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.46.tgz", - "integrity": "sha512-NnZ3CofKne1qiU/kSzUwgYWZqKY7GarUMCToyS+xYlZFDfXVNg1Gpcw0SSBghSEPAERkrnjcLGukpINiekjKqA==", + "version": "2.13.47", + "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.47.tgz", + "integrity": "sha512-JcvXphDqLy1zwPtXeWp4jEqepR7NGymkuKPMKWwzb92xxOTqFMfWuVhRUJSyVdctdVK7V/tmnFck9S3Tqoc7qw==", "dev": true, "license": "MIT", "dependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "source-map": "^0.7.6" }, "funding": { @@ -1956,12 +1956,12 @@ } }, "@abaplint/transpiler": { - "version": "2.13.46", - "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.46.tgz", - "integrity": "sha512-NnZ3CofKne1qiU/kSzUwgYWZqKY7GarUMCToyS+xYlZFDfXVNg1Gpcw0SSBghSEPAERkrnjcLGukpINiekjKqA==", + "version": "2.13.47", + "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.47.tgz", + "integrity": "sha512-JcvXphDqLy1zwPtXeWp4jEqepR7NGymkuKPMKWwzb92xxOTqFMfWuVhRUJSyVdctdVK7V/tmnFck9S3Tqoc7qw==", "dev": true, "requires": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "source-map": "^0.7.6" } }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 3b139aae0..07aea68c2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@abaplint/transpiler-cli", - "version": "2.13.46", + "version": "2.13.47", "description": "Transpiler - Command Line Interface", "funding": "https://github.com/sponsors/larshp", "bin": { @@ -28,7 +28,7 @@ "license": "MIT", "devDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46", + "@abaplint/transpiler": "^2.13.47", "@types/glob": "^8.1.0", "@types/node": "^24.12.2", "@types/progress": "^2.0.7", diff --git a/packages/extras/package-lock.json b/packages/extras/package-lock.json index 2381cf75b..c0be48c77 100644 --- a/packages/extras/package-lock.json +++ b/packages/extras/package-lock.json @@ -10,7 +10,7 @@ "license": "PolyForm-Noncommercial-1.0.0", "devDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46", + "@abaplint/transpiler": "^2.13.47", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "^24.13.2", @@ -21,7 +21,7 @@ }, "peerDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46" + "@abaplint/transpiler": "^2.13.47" } }, "node_modules/@abaplint/core": { @@ -43,13 +43,13 @@ } }, "node_modules/@abaplint/transpiler": { - "version": "2.13.46", - "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.46.tgz", - "integrity": "sha512-NnZ3CofKne1qiU/kSzUwgYWZqKY7GarUMCToyS+xYlZFDfXVNg1Gpcw0SSBghSEPAERkrnjcLGukpINiekjKqA==", + "version": "2.13.47", + "resolved": "https://registry.npmjs.org/@abaplint/transpiler/-/transpiler-2.13.47.tgz", + "integrity": "sha512-JcvXphDqLy1zwPtXeWp4jEqepR7NGymkuKPMKWwzb92xxOTqFMfWuVhRUJSyVdctdVK7V/tmnFck9S3Tqoc7qw==", "dev": true, "license": "MIT", "dependencies": { - "@abaplint/core": "^2.120.5", + "@abaplint/core": "^2.120.6", "source-map": "^0.7.6" }, "funding": { diff --git a/packages/extras/package.json b/packages/extras/package.json index f4fae0b7e..0331fa25e 100644 --- a/packages/extras/package.json +++ b/packages/extras/package.json @@ -29,11 +29,11 @@ "license": "PolyForm-Noncommercial-1.0.0", "peerDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46" + "@abaplint/transpiler": "^2.13.47" }, "devDependencies": { "@abaplint/core": "^2.120.6", - "@abaplint/transpiler": "^2.13.46", + "@abaplint/transpiler": "^2.13.47", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "^24.13.2", diff --git a/packages/runtime/package-lock.json b/packages/runtime/package-lock.json index 54797204d..c92b3e9c9 100644 --- a/packages/runtime/package-lock.json +++ b/packages/runtime/package-lock.json @@ -1,12 +1,12 @@ { "name": "@abaplint/runtime", - "version": "2.13.46", + "version": "2.13.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@abaplint/runtime", - "version": "2.13.46", + "version": "2.13.47", "license": "MIT", "dependencies": { "temporal-polyfill": "^0.3.2" diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d007fdfb3..5a7d381e3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@abaplint/runtime", - "version": "2.13.46", + "version": "2.13.47", "description": "Transpiler - Runtime", "main": "build/src/index.js", "typings": "build/src/index.d.ts", diff --git a/packages/transpiler/package-lock.json b/packages/transpiler/package-lock.json index 98e54ced1..740dc5ea7 100644 --- a/packages/transpiler/package-lock.json +++ b/packages/transpiler/package-lock.json @@ -1,12 +1,12 @@ { "name": "@abaplint/transpiler", - "version": "2.13.46", + "version": "2.13.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@abaplint/transpiler", - "version": "2.13.46", + "version": "2.13.47", "license": "MIT", "dependencies": { "@abaplint/core": "^2.120.6", diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 1c59d3282..2c6faad00 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@abaplint/transpiler", - "version": "2.13.46", + "version": "2.13.47", "description": "Transpiler", "main": "build/src/index.js", "typings": "build/src/index.d.ts",