From 8a488165561b5868431ec6e0c018f92fc8844445 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Thu, 30 Jul 2026 08:47:07 +0200 Subject: [PATCH 1/3] bugfix --- packages/runtime/src/compare/eq.ts | 19 +++++++++++++++---- test/types/decfloat34.ts | 12 ++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/compare/eq.ts b/packages/runtime/src/compare/eq.ts index 205099b0b..3cba6468b 100644 --- a/packages/runtime/src/compare/eq.ts +++ b/packages/runtime/src/compare/eq.ts @@ -1,5 +1,5 @@ /* eslint-disable max-len */ -import {ABAPObject, Character, Date, FieldSymbol, Float, HashedTable, String, Hex, Integer, Numc, Structure, Table, DataReference, toInteger, XString, Integer8, Packed, HexUInt8, Time} from "../types"; +import {ABAPObject, Character, Date, DecFloat34, FieldSymbol, Float, HashedTable, String, Hex, Integer, Numc, Structure, Table, DataReference, toInteger, XString, Integer8, Packed, HexUInt8, Time} from "../types"; import {ICharacter} from "../types/_character"; import {INumeric} from "../types/_numeric"; import {parse} from "../operators/_parse"; @@ -50,6 +50,7 @@ const DateC = Date; const TimeC = Time; const PackedC = Packed; const FloatC = Float; +const DecFloat34C = DecFloat34; const TableC = Table; const HashedTableC = HashedTable; const StructureC = Structure; @@ -61,8 +62,8 @@ const ABAPObjectC = ABAPObject; const DataReferenceC = DataReference; export function eq( - left: number | string | ICharacter | Integer8 | INumeric | Float | String | ABAPObject | Structure | Hex | HashedTable | Table | FieldSymbol, - right: number | string | ICharacter | Integer8 | INumeric | Float | String | ABAPObject | Structure | Hex | HashedTable | Table | FieldSymbol): boolean { + left: number | string | ICharacter | Integer8 | INumeric | Float | DecFloat34 | String | ABAPObject | Structure | Hex | HashedTable | Table | FieldSymbol, + right: number | string | ICharacter | Integer8 | INumeric | Float | DecFloat34 | String | ABAPObject | Structure | Hex | HashedTable | Table | FieldSymbol): boolean { /* console.dir(left); console.dir(right); @@ -92,6 +93,8 @@ export function eq( return (left as Packed).get() === (right as Packed).get(); } else if (leftConstructor === FloatC) { return (left as Float).getRaw() === (right as Float).getRaw(); + } else if (leftConstructor === DecFloat34C) { + return (left as DecFloat34).getRaw() === (right as DecFloat34).getRaw(); } else if (leftConstructor === TableC || leftConstructor === HashedTableC) { return compareTables(left as Table, right as Table); } else if (leftConstructor === StructureC) { @@ -225,6 +228,8 @@ export function eq( return right.get() === left.get(); } else if (left instanceof Integer) { return right.get() === left.get(); + } else if (left instanceof DecFloat34) { + return right.get() === left.getRaw(); } } else if (right instanceof Integer8) { if (left instanceof Integer @@ -241,6 +246,12 @@ export function eq( || left instanceof Integer8) { return right.getRaw() === left.get(); } + } else if (right instanceof DecFloat34) { + if (left instanceof Float || left instanceof DecFloat34) { + return right.getRaw() === left.getRaw(); + } else if (left instanceof Integer || left instanceof Packed) { + return right.getRaw() === left.get(); + } } if (left instanceof Structure || right instanceof Structure) { @@ -325,4 +336,4 @@ export function eq( console.dir(r); */ return l === r; -} \ No newline at end of file +} diff --git a/test/types/decfloat34.ts b/test/types/decfloat34.ts index e1d2f0654..1a25fcc34 100644 --- a/test/types/decfloat34.ts +++ b/test/types/decfloat34.ts @@ -80,6 +80,18 @@ WRITE / foo.`; await f(abap); }); + it("compare packed and decfloat34, EQ", async () => { + const code = ` +DATA lv_packed TYPE p LENGTH 8 DECIMALS 3. +DATA lv_decfloat TYPE decfloat34. +lv_packed = '11.500'. +lv_decfloat = '11.5'. +ASSERT lv_packed = lv_decfloat.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + it("thousand", async () => { const code = ` DATA foo TYPE decfloat34. From 3b9b1d3b553950873d3cd2c9e6792182d264d42f Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Fri, 31 Jul 2026 10:04:34 +0200 Subject: [PATCH 2/3] more math --- packages/runtime/src/builtin/acos.ts | 17 ++++++++ packages/runtime/src/builtin/asin.ts | 17 ++++++++ packages/runtime/src/builtin/atan.ts | 17 ++++++++ packages/runtime/src/builtin/cosh.ts | 17 ++++++++ packages/runtime/src/builtin/exp.ts | 17 ++++++++ packages/runtime/src/builtin/index.ts | 11 +++++- packages/runtime/src/builtin/log.ts | 17 ++++++++ packages/runtime/src/builtin/log10.ts | 17 ++++++++ packages/runtime/src/builtin/sinh.ts | 17 ++++++++ packages/runtime/src/builtin/tanh.ts | 17 ++++++++ test/builtin/math.ts | 56 +++++++++++++++++++++++++++ 11 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 packages/runtime/src/builtin/acos.ts create mode 100644 packages/runtime/src/builtin/asin.ts create mode 100644 packages/runtime/src/builtin/atan.ts create mode 100644 packages/runtime/src/builtin/cosh.ts create mode 100644 packages/runtime/src/builtin/exp.ts create mode 100644 packages/runtime/src/builtin/log.ts create mode 100644 packages/runtime/src/builtin/log10.ts create mode 100644 packages/runtime/src/builtin/sinh.ts create mode 100644 packages/runtime/src/builtin/tanh.ts create mode 100644 test/builtin/math.ts diff --git a/packages/runtime/src/builtin/acos.ts b/packages/runtime/src/builtin/acos.ts new file mode 100644 index 000000000..fa5006314 --- /dev/null +++ b/packages/runtime/src/builtin/acos.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function acos(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.acos(num_in); +} diff --git a/packages/runtime/src/builtin/asin.ts b/packages/runtime/src/builtin/asin.ts new file mode 100644 index 000000000..c126eb9d0 --- /dev/null +++ b/packages/runtime/src/builtin/asin.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function asin(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.asin(num_in); +} diff --git a/packages/runtime/src/builtin/atan.ts b/packages/runtime/src/builtin/atan.ts new file mode 100644 index 000000000..24f503847 --- /dev/null +++ b/packages/runtime/src/builtin/atan.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function atan(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.atan(num_in); +} diff --git a/packages/runtime/src/builtin/cosh.ts b/packages/runtime/src/builtin/cosh.ts new file mode 100644 index 000000000..6bb95f141 --- /dev/null +++ b/packages/runtime/src/builtin/cosh.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function cosh(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.cosh(num_in); +} diff --git a/packages/runtime/src/builtin/exp.ts b/packages/runtime/src/builtin/exp.ts new file mode 100644 index 000000000..5284c0cd8 --- /dev/null +++ b/packages/runtime/src/builtin/exp.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function exp(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.exp(num_in); +} diff --git a/packages/runtime/src/builtin/index.ts b/packages/runtime/src/builtin/index.ts index 4c53f1984..d1044c18e 100644 --- a/packages/runtime/src/builtin/index.ts +++ b/packages/runtime/src/builtin/index.ts @@ -1,15 +1,20 @@ import {Character} from "../types"; export * from "./abs"; +export * from "./acos"; +export * from "./asin"; +export * from "./atan"; export * from "./boolc"; export * from "./ceil"; export * from "./concat_lines_of"; export * from "./condense"; export * from "./contains"; export * from "./cos"; +export * from "./cosh"; export * from "./count_any_of"; export * from "./count"; export * from "./escape"; +export * from "./exp"; export * from "./find"; export * from "./floor"; export * from "./frac"; @@ -19,6 +24,8 @@ export * from "./ipow"; export * from "./line_exists"; export * from "./line_index"; export * from "./lines"; +export * from "./log"; +export * from "./log10"; export * from "./match"; export * from "./matches"; export * from "./nmax"; @@ -33,6 +40,7 @@ export * from "./shift_left"; export * from "./shift_right"; export * from "./sign"; export * from "./sin"; +export * from "./sinh"; export * from "./sqrt"; export * from "./strlen"; export * from "./substring_after"; @@ -40,6 +48,7 @@ export * from "./substring_before"; export * from "./substring"; export * from "./sy"; export * from "./tan"; +export * from "./tanh"; export * from "./to_lower"; export * from "./to_mixed"; export * from "./to_upper"; @@ -61,4 +70,4 @@ export const $_vertical_tab = new Character(1).set("\v").setConstant(); /* export const $_maxchar = new Character(1).set(Buffer.from("FDFF", "hex").toString()).setConstant(); export const $_minchar = new Character(1).set(Buffer.from("0000", "hex").toString()).setConstant(); -*/ \ No newline at end of file +*/ diff --git a/packages/runtime/src/builtin/log.ts b/packages/runtime/src/builtin/log.ts new file mode 100644 index 000000000..6a3a89174 --- /dev/null +++ b/packages/runtime/src/builtin/log.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function log(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.log(num_in); +} diff --git a/packages/runtime/src/builtin/log10.ts b/packages/runtime/src/builtin/log10.ts new file mode 100644 index 000000000..2bb8cd8a4 --- /dev/null +++ b/packages/runtime/src/builtin/log10.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function log10(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.log10(num_in); +} diff --git a/packages/runtime/src/builtin/sinh.ts b/packages/runtime/src/builtin/sinh.ts new file mode 100644 index 000000000..66e3c2701 --- /dev/null +++ b/packages/runtime/src/builtin/sinh.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function sinh(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.sinh(num_in); +} diff --git a/packages/runtime/src/builtin/tanh.ts b/packages/runtime/src/builtin/tanh.ts new file mode 100644 index 000000000..99346e152 --- /dev/null +++ b/packages/runtime/src/builtin/tanh.ts @@ -0,0 +1,17 @@ +import {Float} from "../types"; +import {ICharacter} from "../types/_character"; +import {INumeric} from "../types/_numeric"; + +export function tanh(input: {val: number | string | ICharacter | INumeric}) { + let num_in: number | undefined = undefined; + if (typeof input.val === "number") { + num_in = input.val; + } else if (typeof input.val === "string") { + num_in = parseFloat(input.val); + } else if (input.val instanceof Float) { + num_in = input.val.getRaw(); + } else { + num_in = parseFloat(input.val.get().toString()); + } + return Math.tanh(num_in); +} diff --git a/test/builtin/math.ts b/test/builtin/math.ts new file mode 100644 index 000000000..deda75096 --- /dev/null +++ b/test/builtin/math.ts @@ -0,0 +1,56 @@ +import {ABAP, MemoryConsole} from "../../packages/runtime/src"; +import {AsyncFunction, runFiles} from "../_utils"; + +let abap: ABAP; + +async function run(contents: string) { + return runFiles(abap, [{filename: "zfoobar.prog.abap", contents}]); +} + +describe("Builtin math functions", () => { + + beforeEach(async () => { + abap = new ABAP({console: new MemoryConsole()}); + }); + + it("inverse trigonometric functions", async () => { + const code = ` + ASSERT acos( 1 ) = 0. + ASSERT acos( 0 ) > 1. + ASSERT asin( 0 ) = 0. + ASSERT asin( 1 ) > 1. + ASSERT atan( 0 ) = 0. + ASSERT atan( 1 ) > 0.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + + it("hyperbolic functions", async () => { + const code = ` + ASSERT cosh( 0 ) = 1. + ASSERT cosh( 1 ) > 1. + ASSERT sinh( 0 ) = 0. + ASSERT sinh( 1 ) > 1. + ASSERT tanh( 0 ) = 0. + ASSERT tanh( 1 ) > 0. + ASSERT tanh( 1 ) < 1.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + + it("exponential and logarithmic functions", async () => { + const code = ` + ASSERT exp( 0 ) = 1. + ASSERT exp( 1 ) > 2. + ASSERT log( 1 ) = 0. + ASSERT log( 3 ) > 1. + ASSERT log10( 1 ) = 0. + ASSERT log10( 100 ) = 2.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + }); + +}); From 9fea76188339005b6d2a10a08e50b0ce726ce133 Mon Sep 17 00:00:00 2001 From: Lars Hvam Date: Fri, 31 Jul 2026 10:43:19 +0200 Subject: [PATCH 3/3] update --- .../transpiler/src/expressions/filter_body.ts | 147 +++++++- .../transpiler/src/expressions/reduce_body.ts | 337 +++++++++++++----- test/expressions/filter.ts | 70 +++- test/expressions/reduce.ts | 103 +++++- 4 files changed, 535 insertions(+), 122 deletions(-) diff --git a/packages/transpiler/src/expressions/filter_body.ts b/packages/transpiler/src/expressions/filter_body.ts index d1f5d73b9..aa773b005 100644 --- a/packages/transpiler/src/expressions/filter_body.ts +++ b/packages/transpiler/src/expressions/filter_body.ts @@ -4,42 +4,151 @@ import {Chunk} from "../chunk"; import {TypeNameOrInfer} from "./type_name_or_infer"; import {TranspileTypes} from "../transpile_types"; import {UniqueIdentifier} from "../unique_identifier"; +import {ComponentChainSimpleTranspiler} from "./component_chain_simple"; export class FilterBodyTranspiler { public transpile(typ: Nodes.ExpressionNode, body: Nodes.ExpressionNode, traversal: Traversal): Chunk { - if (!(typ.get() instanceof Expressions.TypeNameOrInfer)) { throw new Error("FilterBodyTranspiler, Expected TypeNameOrInfer"); - } else if (body.findDirectTokenByText("EXCEPT")) { - return new Chunk(`(() => { throw new Error("FilterBodyTranspiler EXCEPT in, not supported, transpiler"); })()`); } - const source = traversal.traverse(body.findDirectExpression(Expressions.Source)).getCode(); + const sources = body.findDirectExpressions(Expressions.Source); + if (sources.length === 0) { + throw new Error("FilterBodyTranspiler, source not found"); + } + const source = traversal.traverse(sources[0]).getCode(); const type = new TypeNameOrInfer().findType(typ, traversal); const target = TranspileTypes.toType(type); + const whereNode = body.findDirectExpression(Expressions.ComponentCond); + if (whereNode === undefined) { + throw new Error("FilterBodyTranspiler, WHERE not found"); + } + + if (sources.length > 1) { + return this.transpileIn(target, source, traversal.traverse(sources[1]).getCode(), whereNode, + body.findDirectTokenByText("EXCEPT") !== undefined, body, traversal); + } + return this.transpileSingle(target, source, whereNode, + body.findDirectTokenByText("EXCEPT") !== undefined, body, traversal); + } + + private transpileSingle(target: string, source: string, whereNode: Nodes.ExpressionNode, + except: boolean, body: Nodes.ExpressionNode, traversal: Traversal): Chunk { + const result = UniqueIdentifier.get(); + const row = UniqueIdentifier.get(); + const where = traversal.traverse(whereNode).getCode(); + const options: string[] = []; + options.push(except ? `where: async (I) => !((${where})(I))` : `where: async ${where}`); + const key = body.findDirectExpression(Expressions.SimpleName); + if (key) { + options.push(`usingKey: "${key.concatTokens().toLowerCase()}"`); + } const ret = new Chunk(); + ret.appendString("(await (async () => {\n"); + ret.appendString(`const ${result} = ${target};\n`); + ret.appendString(`for await (const ${row} of abap.statements.loop(${source}, {${options.join(", ")}})) {\n`); + ret.appendString(`abap.statements.insertInternal({"table": ${result}, "data": ${row}});\n`); + ret.appendString("}\n"); + ret.appendString(`return ${result};\n`); + ret.appendString("})())"); + return ret; + } + + private transpileIn(target: string, source: string, filterSource: string, whereNode: Nodes.ExpressionNode, + except: boolean, body: Nodes.ExpressionNode, traversal: Traversal): Chunk { + const result = UniqueIdentifier.get(); + const sourceRow = UniqueIdentifier.get(); + const filterRow = UniqueIdentifier.get(); + const matched = UniqueIdentifier.get(); + const condition = this.transpileInCondition(whereNode, traversal, sourceRow, filterRow); + const key = body.findDirectExpression(Expressions.SimpleName); + const filterOptions = key ? `, {usingKey: "${key.concatTokens().toLowerCase()}"}` : ""; + const selection = except ? `!${matched}` : matched; + const ret = new Chunk(); ret.appendString("(await (async () => {\n"); + ret.appendString(`const ${result} = ${target};\n`); + ret.appendString(`for await (const ${sourceRow} of abap.statements.loop(${source})) {\n`); + ret.appendString(`let ${matched} = false;\n`); + ret.appendString(`for await (const ${filterRow} of abap.statements.loop(${filterSource}${filterOptions})) {\n`); + ret.appendString(`if (${condition}) {\n`); + ret.appendString(`${matched} = true;\n`); + ret.appendString("break;\n}\n}\n"); + ret.appendString(`if (${selection}) {\n`); + ret.appendString(`abap.statements.insertInternal({"table": ${result}, "data": ${sourceRow}});\n`); + ret.appendString("}\n}\n"); + ret.appendString(`return ${result};\n`); + ret.appendString("})())"); + return ret; + } - let loopWhere = ""; - const whereNode = body.findDirectExpression(Expressions.ComponentCond); - if (whereNode) { - const where = traversal.traverse(whereNode).getCode(); - loopWhere = `, {"where": async ` + where + `}`; + private transpileInCondition(node: Nodes.ExpressionNode, traversal: Traversal, + sourceRow: string, filterRow: string): string { + if (node.get() instanceof Expressions.ComponentCompare) { + return this.transpileInCompare(node, traversal, sourceRow, filterRow); } - const id = UniqueIdentifier.get(); - const loop = UniqueIdentifier.get(); - ret.appendString(`const ${id} = ${target};\n`); - ret.appendString(`for await (const ${loop} of abap.statements.loop(${source}${loopWhere})) {\n`); - ret.appendString(`abap.statements.insertInternal({"table": ${id}, "data": ${loop}});\n`); - ret.appendString(`}\n`); - ret.appendString(`return ${id};\n`); - - ret.appendString("})())"); + let ret = ""; + for (const child of node.getChildren()) { + if (child instanceof Nodes.ExpressionNode) { + ret += this.transpileInCondition(child, traversal, sourceRow, filterRow); + } else { + switch (child.concatTokens().toUpperCase()) { + case "AND": ret += " && "; break; + case "OR": ret += " || "; break; + case "NOT": ret += "!"; break; + case "(": ret += "("; break; + case ")": ret += ")"; break; + default: throw new Error("FilterBodyTranspiler, unexpected condition token " + child.concatTokens()); + } + } + } return ret; } -} \ No newline at end of file + private transpileInCompare(node: Nodes.ExpressionNode, traversal: Traversal, + sourceRow: string, filterRow: string): string { + const leftNode = node.findDirectExpression(Expressions.ComponentChainSimple); + if (leftNode === undefined) { + throw new Error("FilterBodyTranspiler, comparison component not found"); + } + const left = new ComponentChainSimpleTranspiler(`${sourceRow}.get().`).transpile(leftNode, traversal).getCode(); + const sources = node.findDirectExpressions(Expressions.Source); + const concat = node.concatTokens().toUpperCase(); + const negate = concat.startsWith("NOT ") ? "!" : ""; + + const operator = node.findDirectExpression(Expressions.CompareOperator); + if (operator && sources[0]) { + const compare = traversal.traverse(operator).getCode(); + return `${negate}abap.compare.${compare}(${left}, ${this.filterOperand(sources[0], traversal, filterRow)})`; + } + if (concat.includes(" BETWEEN ") && sources.length === 2) { + const between = `abap.compare.ge(${left}, ${this.filterOperand(sources[0], traversal, filterRow)}) && ` + + `abap.compare.le(${left}, ${this.filterOperand(sources[1], traversal, filterRow)})`; + return concat.includes(" NOT BETWEEN ") ? `!(${between})` : `(${between})`; + } + if (concat.endsWith("IS INITIAL")) { + return `${negate}abap.compare.initial(${left})`; + } else if (concat.endsWith("IS NOT INITIAL")) { + return `!abap.compare.initial(${left})`; + } + throw new Error("FilterBodyTranspiler, unsupported IN comparison " + node.concatTokens()); + } + + private filterOperand(source: Nodes.ExpressionNode, traversal: Traversal, filterRow: string): string { + const code = traversal.traverse(source).getCode(); + const sourceField = source.findFirstExpression(Expressions.SourceField); + const name = sourceField?.findDirectExpression(Expressions.Field)?.concatTokens(); + if (name === undefined) { + return code; + } + const escaped = Traversal.escapeNamespace(name)?.replace("~", "$").toLowerCase(); + const variable = Traversal.prefixVariable(Traversal.escapeNamespace(name)!); + if (escaped === undefined || code.startsWith(variable) === false) { + return code; + } + return `${filterRow}.get().${escaped}` + code.substring(variable.length); + } +} diff --git a/packages/transpiler/src/expressions/reduce_body.ts b/packages/transpiler/src/expressions/reduce_body.ts index ef62edb80..2578647f6 100644 --- a/packages/transpiler/src/expressions/reduce_body.ts +++ b/packages/transpiler/src/expressions/reduce_body.ts @@ -3,169 +3,318 @@ import {Traversal} from "../traversal"; import {Chunk} from "../chunk"; import {TranspileTypes} from "../transpile_types"; import {TargetTranspiler} from "./target"; +import {LetTranspiler} from "./let"; +import {FieldSymbolTranspiler} from "../statements"; +import {SourceFieldSymbolTranspiler} from "./source_field_symbol"; +import {UniqueIdentifier} from "../unique_identifier"; + +interface LoopDescriptor { + beforeLoop: string[]; + open: string; + preBody: string[]; + postBody: string[]; + close: string; +} export class ReduceBodyTranspiler { public transpile(typ: Nodes.ExpressionNode, body: Nodes.ExpressionNode, traversal: Traversal): Chunk { - if (!(typ.get() instanceof Expressions.TypeNameOrInfer)) { throw new Error("ReduceBodyTranspiler, Expected TypeNameOrInfer"); - } else if (body.findDirectExpression(Expressions.Let) !== undefined) { - return new Chunk(`(() => { throw new Error("ReduceBodyTranspiler LET, not supported, transpiler"); })()`); } const forExpressions = body.findDirectExpressions(Expressions.For); - const forExpression = forExpressions[0]; if (forExpressions.length === 0) { throw new Error("ReduceBodyTranspiler, expected FOR"); - } else if (forExpressions.length > 1) { - throw new Error("ReduceBodyTranspiler, multiple FOR not supported, " + body.concatTokens()); } - const loopExpression = forExpression.findDirectExpression(Expressions.InlineLoopDefinition); + const ret = new Chunk(); + ret.appendString("(await (async () => {\n"); - if (loopExpression === undefined) { - // index based FOR, eg. "FOR i = 1 WHILE i <= 5" - return this.transpileIndex(body, forExpression, traversal); - } else if (["THEN", "UNTIL", "WHILE", "FROM", "TO", "GROUPS"].some(token => forExpression.findDirectTokenByText(token))) { - throw new Error("ValueBody FOR todo, " + body.concatTokens()); + const outerLet = body.findDirectExpression(Expressions.Let); + if (outerLet) { + ret.appendString(new LetTranspiler().transpile(outerLet, traversal).getCode() + "\n"); } - const loopSource = traversal.traverse(loopExpression?.findDirectExpression(Expressions.Source)).getCode(); + const returnField = this.declareInit(body, traversal, ret); + const declarations: string[] = []; + const descriptors = forExpressions.map(forExpression => + this.describeFor(forExpression, body, traversal, declarations)); + for (const declaration of declarations) { + ret.appendString(declaration + "\n"); + } - const loopVariable = traversal.traverse(loopExpression?.findDirectExpression(Expressions.TargetField) - || loopExpression?.findDirectExpression(Expressions.TargetFieldSymbol)).getCode(); + let indent = ""; + const levelIndents: string[] = []; + for (const descriptor of descriptors) { + this.appendBlocks(ret, descriptor.beforeLoop, indent); + ret.appendString(indent + descriptor.open + "\n"); + indent += " "; + levelIndents.push(indent); + this.appendBlocks(ret, descriptor.preBody, indent); + } -// const type = new TypeNameOrInfer().findType(typ, traversal); -// const target = TranspileTypes.toType(type); + this.appendBlock(ret, this.transpileNext(body, traversal), indent); - const ret = new Chunk(); + for (let i = descriptors.length - 1; i >= 0; i--) { + const descriptor = descriptors[i]; + const currentIndent = levelIndents[i]; + this.appendBlocks(ret, descriptor.postBody, currentIndent); + indent = currentIndent.substring(0, Math.max(0, currentIndent.length - 2)); + ret.appendString(indent + descriptor.close + "\n"); + } - ret.appendString("(await (async () => {\n"); + ret.appendString(`return ${returnField};\n`); + ret.appendString("})())"); + return ret; + } - let loopWhere = ""; - const whereNode = forExpression?.findDirectExpression(Expressions.ComponentCond); - if (whereNode) { - const where = traversal.traverse(whereNode).getCode(); - loopWhere = `, {"where": async ` + where + `}`; + private describeFor(forExpression: Nodes.ExpressionNode, body: Nodes.ExpressionNode, + traversal: Traversal, declarations: string[]): LoopDescriptor { + if (forExpression.findDirectTokenByText("GROUPS")) { + return this.describeGroupsFor(forExpression, body, traversal); + } + const loopExpression = forExpression.findDirectExpression(Expressions.InlineLoopDefinition); + if (loopExpression === undefined) { + return this.describeIndexFor(forExpression, body, traversal); } - /* - const returnId = UniqueIdentifier.get(); - ret.appendString(`const ${returnId} = ${target};\n`); - */ - - const returnField = this.declareInit(body, traversal, ret); + const sourceNode = loopExpression.findDirectExpression(Expressions.Source); + if (sourceNode === undefined) { + throw new Error("ReduceBodyTranspiler FOR missing source, " + body.concatTokens()); + } + const loopSource = traversal.traverse(sourceNode).getCode(); + const options: string[] = []; - ret.appendString(`for await (const ${loopVariable} of abap.statements.loop(${loopSource}${loopWhere})) {\n`); + const whereNode = forExpression.findDirectExpression(Expressions.ComponentCond); + if (whereNode) { + options.push("where: async " + traversal.traverse(whereNode).getCode()); + } + const fromNode = forExpression.findExpressionAfterToken("FROM"); + if (fromNode && fromNode instanceof Nodes.ExpressionNode) { + options.push("from: " + traversal.traverse(fromNode).getCode()); + } + const toNode = forExpression.findExpressionAfterToken("TO"); + if (toNode && toNode instanceof Nodes.ExpressionNode) { + options.push("to: " + traversal.traverse(toNode).getCode()); + } + const keyNode = loopExpression.findExpressionAfterToken("KEY"); + if (keyNode) { + options.push(`usingKey: "${keyNode.concatTokens().toLowerCase()}"`); + } - ret.appendString(this.transpileNext(body, traversal)); + const unique = UniqueIdentifier.get(); + const preBody: string[] = []; + const postBody: string[] = []; + const fieldSymbol = loopExpression.findDirectExpression(Expressions.TargetFieldSymbol); + if (fieldSymbol) { + declarations.push(new FieldSymbolTranspiler().transpile(fieldSymbol, traversal).getCode()); + const target = new SourceFieldSymbolTranspiler().transpile(fieldSymbol, traversal).getCode(); + preBody.push(`${target}.assign(${unique});`); + postBody.push(`${target}.unassign();`); + } else { + const field = loopExpression.findDirectExpression(Expressions.TargetField); + if (field === undefined) { + throw new Error("ReduceBodyTranspiler FOR missing target, " + body.concatTokens()); + } + preBody.push(`const ${traversal.traverse(field).getCode()} = ${unique}.clone();`); + } - ret.appendString(`}\n`); + const indexTarget = loopExpression.findExpressionAfterToken("INTO"); + const beforeLoop: string[] = []; + if (indexTarget && indexTarget instanceof Nodes.ExpressionNode) { + const indexName = UniqueIdentifier.get(); + const indexCode = traversal.traverse(indexTarget).getCode(); + beforeLoop.push(`let ${indexName} = 1;`); + preBody.push(`const ${indexCode} = new abap.types.Integer().set(${indexName});`); + postBody.push(`${indexName}++;`); + } - ret.appendString(`return ${returnField};\n`); + const letNode = forExpression.findDirectExpression(Expressions.Let); + if (letNode) { + preBody.push(new LetTranspiler().transpile(letNode, traversal).getCode()); + } - ret.appendString("})())"); - return ret; + const opts = options.length === 0 ? "" : `, {${options.join(", ")}}`; + return { + beforeLoop, + open: `for await (const ${unique} of abap.statements.loop(${loopSource}${opts})) {`, + preBody, + postBody, + close: "}", + }; } - private transpileIndex(body: Nodes.ExpressionNode, forExpression: Nodes.ExpressionNode, traversal: Traversal): Chunk { - if (["FROM", "TO", "GROUPS"].some(token => forExpression.findDirectTokenByText(token))) { - throw new Error("ValueBody FOR todo, " + body.concatTokens()); + private describeGroupsFor(forExpression: Nodes.ExpressionNode, body: Nodes.ExpressionNode, + traversal: Traversal): LoopDescriptor { + const targets = forExpression.findDirectExpressions(Expressions.TargetField); + const source = forExpression.findDirectExpression(Expressions.Source); + const groupBy = forExpression.findDirectExpression(Expressions.FieldChain); + if (targets.length !== 2 || source === undefined || groupBy === undefined) { + throw new Error("ReduceBodyTranspiler invalid GROUPS FOR, " + body.concatTokens()); } - const counter = forExpression.findDirectExpression(Expressions.InlineFieldDefinition); - if (counter === undefined) { - throw new Error("ValueBody FOR todo, " + body.concatTokens()); - } + const groupTarget = traversal.traverse(targets[0]).getCode(); + const memberTarget = traversal.traverse(targets[1]).getCode(); + const sourceCode = traversal.traverse(source).getCode(); + const groupByCode = traversal.traverse(groupBy).getCode(); + const groups = UniqueIdentifier.get(); + const row = UniqueIdentifier.get(); + const key = UniqueIdentifier.get(); + const rawKey = UniqueIdentifier.get(); + const entry = UniqueIdentifier.get(); + + const generator = `(async function*() {\n` + + `const ${groups} = new Map();\n` + + `for await (const ${row} of abap.statements.loop(${sourceCode})) {\n` + + `const ${memberTarget} = ${row}.clone();\n` + + `const ${key} = ${groupByCode};\n` + + `const ${rawKey} = ${key}.get();\n` + + `let ${entry} = ${groups}.get(${rawKey});\n` + + `if (${entry} === undefined) {\n` + + `${entry} = {key: ${key}.clone(), members: []};\n` + + `${groups}.set(${rawKey}, ${entry});\n` + + `}\n` + + `${entry}.members.push(${row});\n` + + `}\n` + + `for (const value of ${groups}.values()) { yield value; }\n` + + `})()`; + + const loopEntry = UniqueIdentifier.get(); + return { + beforeLoop: [], + open: `for await (const ${loopEntry} of ${generator}) {`, + preBody: [ + `const ${groupTarget} = ${loopEntry}.key.clone();`, + `const ${memberTarget} = ${loopEntry}.members[0].clone();`, + ], + postBody: [], + close: "}", + }; + } + private describeIndexFor(forExpression: Nodes.ExpressionNode, body: Nodes.ExpressionNode, + traversal: Traversal): LoopDescriptor { + const counter = forExpression.findDirectExpression(Expressions.InlineFieldDefinition); const cond = forExpression.findDirectExpression(Expressions.Cond); - if (cond === undefined) { - throw new Error("ValueBody FOR missing condition, " + body.concatTokens()); + if (counter === undefined || cond === undefined) { + throw new Error("ReduceBodyTranspiler invalid index FOR, " + body.concatTokens()); } const hasUntil = forExpression.findDirectTokenByText("UNTIL") !== undefined; const hasWhile = forExpression.findDirectTokenByText("WHILE") !== undefined; if ((hasUntil ? 1 : 0) + (hasWhile ? 1 : 0) !== 1) { - throw new Error("ValueBody FOR todo, condition, " + body.concatTokens()); + throw new Error("ReduceBodyTranspiler index FOR requires WHILE or UNTIL, " + body.concatTokens()); } const fieldName = counter.findDirectExpression(Expressions.Field)?.concatTokens().toLowerCase(); - if (fieldName === undefined) { - throw new Error("ValueBody FOR todo, inline field, " + body.concatTokens()); + const source = counter.findDirectExpression(Expressions.Source); + if (fieldName === undefined || source === undefined) { + throw new Error("ReduceBodyTranspiler invalid index definition, " + body.concatTokens()); } - const scope = traversal.findCurrentScopeByToken(counter.getFirstToken()); - const variable = scope?.findVariable(fieldName); + const variable = traversal.findCurrentScopeByToken(counter.getFirstToken())?.findVariable(fieldName); if (variable === undefined) { - throw new Error("ValueBody FOR todo, variable, " + body.concatTokens()); + throw new Error(`ReduceBodyTranspiler: variable ${fieldName} not found`); } - const counterName = Traversal.prefixVariable(fieldName); - - const startSource = counter.findDirectExpression(Expressions.Source); - if (startSource === undefined) { - throw new Error("ValueBody FOR missing initial value, " + body.concatTokens()); - } - const start = traversal.traverse(startSource).getCode(); + const counterName = Traversal.prefixVariable(fieldName); const thenExpr = forExpression.findExpressionAfterToken("THEN"); - let incrementExpression = ""; - if (thenExpr && thenExpr instanceof Nodes.ExpressionNode) { - incrementExpression = traversal.traverse(thenExpr).getCode(); - } else { - incrementExpression = `abap.operators.add(${counterName}, new abap.types.Integer().set(1))`; - } + const increment = thenExpr && thenExpr instanceof Nodes.ExpressionNode + ? traversal.traverse(thenExpr).getCode() + : `abap.operators.add(${counterName}, new abap.types.Integer().set(1))`; const condCode = traversal.traverse(cond).getCode(); - - const ret = new Chunk(); - ret.appendString("(await (async () => {\n"); - - const returnField = this.declareInit(body, traversal, ret); - - ret.appendString(TranspileTypes.declare(variable) + `\n`); - ret.appendString(`${counterName}.set(${start});\n`); - ret.appendString(`while (true) {\n`); + const preBody: string[] = []; + const postBody = [`${counterName}.set(${increment});`]; if (hasWhile) { - ret.appendString(`if (!(${condCode})) {\nbreak;\n}\n`); + preBody.push(`if (!(${condCode})) {\nbreak;\n}`); + } + const letNode = forExpression.findDirectExpression(Expressions.Let); + if (letNode) { + preBody.push(new LetTranspiler().transpile(letNode, traversal).getCode()); } - - ret.appendString(this.transpileNext(body, traversal)); - - ret.appendString(`${counterName}.set(${incrementExpression});\n`); if (hasUntil) { - ret.appendString(`if (${condCode}) {\nbreak;\n}\n`); + postBody.push(`if (${condCode}) {\nbreak;\n}`); } - ret.appendString(`}\n`); - ret.appendString(`return ${returnField};\n`); - ret.appendString("})())"); - return ret; + return { + beforeLoop: [TranspileTypes.declare(variable), `${counterName}.set(${traversal.traverse(source).getCode()});`], + open: "while (true) {", + preBody, + postBody, + close: "}", + }; } private declareInit(body: Nodes.ExpressionNode, traversal: Traversal, ret: Chunk): string { let returnField = ""; for (const init of body.findDirectExpressions(Expressions.InlineFieldDefinition)) { - const fieldName = init.findDirectExpression(Expressions.Field)!.concatTokens().toLowerCase(); - returnField = fieldName; - const scope = traversal.findCurrentScopeByToken(init.getFirstToken()); - const variable = scope?.findVariable(fieldName); + const fieldName = init.findDirectExpression(Expressions.Field)?.concatTokens().toLowerCase(); + if (fieldName === undefined) { + throw new Error("ReduceBodyTranspiler INIT missing field"); + } + if (returnField === "") { + returnField = Traversal.prefixVariable(fieldName); + } + const variable = traversal.findCurrentScopeByToken(init.getFirstToken())?.findVariable(fieldName); if (variable === undefined) { throw new Error(`ReduceBodyTranspiler: variable ${fieldName} not found`); } - ret.appendString(TranspileTypes.declare(variable) + `\n`); + const target = Traversal.prefixVariable(fieldName); + ret.appendString(TranspileTypes.declare(variable) + "\n"); + const source = init.findDirectExpression(Expressions.Source); + if (source) { + ret.appendString(`${target}.set(${traversal.traverse(source).getCode()});\n`); + } + } + if (returnField === "") { + throw new Error("ReduceBodyTranspiler INIT missing"); } return returnField; } private transpileNext(body: Nodes.ExpressionNode, traversal: Traversal): string { let ret = ""; - for (const nextChild of body.findDirectExpression(Expressions.ReduceNext)?.getChildren() || []) { - if (nextChild.get() instanceof Expressions.SimpleTarget && nextChild instanceof Nodes.ExpressionNode) { - ret += new TargetTranspiler().transpile(nextChild, traversal).getCode() + ".set("; - } else if (nextChild.get() instanceof Expressions.Source && nextChild instanceof Nodes.ExpressionNode) { - ret += traversal.traverse(nextChild).getCode() + ");\n"; + const children = body.findDirectExpression(Expressions.ReduceNext)?.getChildren() || []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (!(child instanceof Nodes.ExpressionNode) || !(child.get() instanceof Expressions.SimpleTarget)) { + continue; + } + const source = children.slice(i + 1).find(candidate => + candidate instanceof Nodes.ExpressionNode && candidate.get() instanceof Expressions.Source); + if (!(source instanceof Nodes.ExpressionNode)) { + throw new Error("ReduceBodyTranspiler NEXT missing source"); } + const target = new TargetTranspiler().transpile(child, traversal).getCode(); + const value = traversal.traverse(source).getCode(); + const between = children.slice(i + 1, children.indexOf(source)).map(candidate => candidate.concatTokens()).join(""); + const operators: {[key: string]: string} = { + "+=": "add", + "-=": "minus", + "*=": "multiply", + "/=": "divide", + "&&=": "concat", + }; + const operator = operators[between]; + ret += operator + ? `${target}.set(abap.operators.${operator}(${target}, ${value}));\n` + : `${target}.set(${value});\n`; + i = children.indexOf(source); } return ret; } -} \ No newline at end of file + private appendBlocks(ret: Chunk, blocks: string[], indent: string): void { + for (const block of blocks) { + this.appendBlock(ret, block, indent); + } + } + + private appendBlock(ret: Chunk, block: string, indent: string): void { + for (const line of block.split("\n")) { + if (line.trim() !== "") { + ret.appendString(indent + line.replace(/\r/g, "") + "\n"); + } + } + } +} diff --git a/test/expressions/filter.ts b/test/expressions/filter.ts index de3723ae1..b9be739a2 100644 --- a/test/expressions/filter.ts +++ b/test/expressions/filter.ts @@ -4,8 +4,8 @@ import {AsyncFunction, runFiles} from "../_utils"; let abap: ABAP; -async function run(contents: string, skipVersionCheck = false) { - return runFiles(abap, [{filename: "zfoobar_filter.prog.abap", contents}], {skipVersionCheck}); +async function run(contents: string, skipVersionCheck = false, ignoreSyntaxCheck = false) { + return runFiles(abap, [{filename: "zfoobar_filter.prog.abap", contents}], {skipVersionCheck, ignoreSyntaxCheck}); } describe("Running expressions - FILTER", () => { @@ -38,7 +38,7 @@ START-OF-SELECTION. expect(abap.console.get()).to.equal("1"); }); - it.skip("EXCEPT IN, different names", async () => { + it("EXCEPT IN, different names", async () => { const code = ` TYPES: BEGIN OF ty, id TYPE i, @@ -59,13 +59,12 @@ et_list = FILTER #( et_list EXCEPT IN lt_list WHERE id = id2 ). WRITE / lines( et_list ).`; const js = await run(code); - console.dir(js); const f = new AsyncFunction("abap", js); await f(abap); expect(abap.console.get()).to.equal("1"); }); - it.skip("EXCEPT IN, empty list", async () => { + it("EXCEPT IN, empty list", async () => { const code = ` TYPES: BEGIN OF ty, id TYPE i, @@ -85,7 +84,7 @@ WRITE / lines( et_list ).`; expect(abap.console.get()).to.equal("1"); }); - it.skip("EXCEPT IN, filter hit", async () => { + it("EXCEPT IN, filter hit", async () => { const code = ` TYPES: BEGIN OF ty, id TYPE i, @@ -106,7 +105,7 @@ WRITE / lines( et_list ).`; expect(abap.console.get()).to.equal("0"); }); - it.skip("EXCEPT IN, filter miss", async () => { + it("EXCEPT IN, filter miss", async () => { const code = ` TYPES: BEGIN OF ty, id TYPE i, @@ -127,6 +126,61 @@ WRITE / lines( et_list ).`; expect(abap.console.get()).to.equal("1"); }); + it("IN includes matching rows", async () => { + const code = ` +TYPES: BEGIN OF ty, + id TYPE i, + END OF ty. +DATA input TYPE SORTED TABLE OF ty WITH UNIQUE KEY id. +DATA filter TYPE SORTED TABLE OF ty WITH UNIQUE KEY id. +INSERT VALUE #( id = 1 ) INTO TABLE input. +INSERT VALUE #( id = 2 ) INTO TABLE input. +INSERT VALUE #( id = 2 ) INTO TABLE filter. +DATA result LIKE input. +result = FILTER #( input IN filter WHERE id = id ). +WRITE / lines( result ).`; + const js = await run(code, false, true); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("1"); + }); + + it("EXCEPT without IN negates WHERE", async () => { + const code = ` +TYPES: BEGIN OF ty, + id TYPE i, + END OF ty. +DATA input TYPE SORTED TABLE OF ty WITH UNIQUE KEY id. +INSERT VALUE #( id = 1 ) INTO TABLE input. +INSERT VALUE #( id = 2 ) INTO TABLE input. +DATA result LIKE input. +result = FILTER #( input EXCEPT WHERE id = 2 ). +WRITE / lines( result ).`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("1"); + }); + + it("IN honors USING KEY", async () => { + const code = ` +TYPES: BEGIN OF ty, + id TYPE i, + END OF ty. +DATA input TYPE SORTED TABLE OF ty WITH UNIQUE KEY id. +DATA filter TYPE STANDARD TABLE OF ty + WITH NON-UNIQUE SORTED KEY by_id COMPONENTS id. +INSERT VALUE #( id = 1 ) INTO TABLE input. +INSERT VALUE #( id = 2 ) INTO TABLE input. +INSERT VALUE #( id = 2 ) INTO TABLE filter. +input = FILTER #( input EXCEPT IN filter USING KEY by_id WHERE id = id ). +WRITE / lines( input ).`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("1"); + }); + it("filter with CONV inference", async () => { const code = ` FORM run. @@ -148,4 +202,4 @@ START-OF-SELECTION. expect(abap.console.get()).to.equal("1"); }); -}); \ No newline at end of file +}); diff --git a/test/expressions/reduce.ts b/test/expressions/reduce.ts index e8c21984b..65e954d50 100644 --- a/test/expressions/reduce.ts +++ b/test/expressions/reduce.ts @@ -186,4 +186,105 @@ WRITE / sum.`; expect(abap.console.get()).to.equal("12"); }); -}); \ No newline at end of file + it("assigns INIT and returns the first INIT field", async () => { + const code = ` +TYPES ints TYPE STANDARD TABLE OF i WITH EMPTY KEY. +DATA input TYPE ints. +DATA result TYPE i. +input = VALUE ints( ( 2 ) ( 3 ) ). +result = REDUCE i( INIT sum = 10 count = 40 + FOR value IN input + NEXT sum = sum + value count = count + 1 ). +WRITE / result.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("15"); + }); + + it("outer LET", async () => { + const code = ` +TYPES ints TYPE STANDARD TABLE OF i WITH EMPTY KEY. +DATA input TYPE ints. +DATA result TYPE i. +input = VALUE ints( ( 2 ) ). +result = REDUCE i( LET base = 5 IN + INIT sum = base + FOR value IN input + NEXT sum = sum + value ). +WRITE / result.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("7"); + }); + + it("multiple nested FOR clauses", async () => { + const code = ` +TYPES ints TYPE STANDARD TABLE OF i WITH EMPTY KEY. +DATA input TYPE ints. +DATA result TYPE i. +input = VALUE ints( ( 1 ) ( 2 ) ). +result = REDUCE i( INIT sum = 0 + FOR left IN input + FOR right IN input + NEXT sum += left * right ). +WRITE / result.`; + const js = await run(code, true); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("9"); + }); + + it("table iteration FROM and TO", async () => { + const code = ` +TYPES ints TYPE STANDARD TABLE OF i WITH EMPTY KEY. +DATA input TYPE ints. +DATA result TYPE i. +input = VALUE ints( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ). +result = REDUCE i( INIT sum = 0 + FOR value IN input FROM 2 TO 3 + NEXT sum = sum + value ). +WRITE / result.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("5"); + }); + + it("FOR LET and INDEX INTO", async () => { + const code = ` +TYPES ints TYPE STANDARD TABLE OF i WITH EMPTY KEY. +DATA input TYPE ints. +DATA result TYPE i. +input = VALUE ints( ( 2 ) ( 3 ) ). +result = REDUCE i( INIT sum = 0 + FOR value IN input INDEX INTO index + LET weighted = value * index IN + NEXT sum = sum + weighted ). +WRITE / result.`; + const js = await run(code); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("8"); + }); + + it("GROUPS iteration", async () => { + const code = ` +TYPES: BEGIN OF row_type, + id TYPE i, + END OF row_type. +DATA input TYPE STANDARD TABLE OF row_type WITH EMPTY KEY. +DATA result TYPE i. +input = VALUE #( ( id = 1 ) ( id = 1 ) ( id = 2 ) ). +result = REDUCE i( INIT sum = 0 + FOR GROUPS group OF row IN input GROUP BY row-id + NEXT sum = sum + group ). +WRITE / result.`; + const js = await run(code, true); + const f = new AsyncFunction("abap", js); + await f(abap); + expect(abap.console.get()).to.equal("3"); + }); + +});