exprforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ // exprforge/emitters/csharp.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `Math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `Math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ function capitalize(s) {
13
+ return s.charAt(0).toUpperCase() + s.slice(1);
14
+ }
15
+
16
+ // NOT just capitalize(fn.name): C# forbids a member from sharing its
17
+ // enclosing type's exact name (CS0542), unlike Java, which permits a
18
+ // same-named non-constructor method. Fine as long as capitalize() changes
19
+ // the name (e.g. "fibonacci" -> "Fibonacci"), but every AST-author name
20
+ // that's already capitalized (e.g. this project's SpEf-prefixed samples)
21
+ // makes capitalize() a no-op -- class and method end up identical, a real
22
+ // compile error confirmed against a real sample, not a hypothetical.
23
+ function wrapperClassName(fnName) {
24
+ return `${capitalize(fnName)}Impl`;
25
+ }
26
+
27
+ const emitter = new Emitter({
28
+ ext: "cs",
29
+ // Always suffixed with d: a bare integer-valued literal like `5` is a
30
+ // C# int by default, and int/int is integer division -- silently
31
+ // wrong if both operands of a div() ever happened to be int-valued
32
+ // literals. The suffix forces double everywhere, no exceptions,
33
+ // rather than only suffixing the cases that would otherwise break.
34
+ formatNumber: (v) => `${v}d`,
35
+ calls: {
36
+ sqrt: fn1("Sqrt"), abs: fn1("Abs"), sin: fn1("Sin"), cos: fn1("Cos"), tan: fn1("Tan"),
37
+ asin: fn1("Asin"), acos: fn1("Acos"), atan: fn1("Atan"), log: fn1("Log"),
38
+ log2: fn1("Log2"), log10: fn1("Log10"), exp: fn1("Exp"), floor: fn1("Floor"),
39
+ ceil: fn1("Ceiling"), round: fn1("Round"), trunc: fn1("Truncate"),
40
+ pow: fn2("Pow"), atan2: fn2("Atan2"), min: fn2("Min"), max: fn2("Max"),
41
+ // Math.Sign returns int, not double -- our AST is float64-only throughout.
42
+ sign: ([x]) => `((double) Math.Sign(${x}))`,
43
+ // No Math.Hypot in .NET -- same manual formula as QB64's/C's.
44
+ hypot: ([a, b]) => `Math.Sqrt((${a}) * (${a}) + (${b}) * (${b}))`,
45
+ },
46
+ // C# has a native ?: ternary, same syntax as JS/C/Java -- no override
47
+ // needed, this is exactly what base.js's default already emits.
48
+ formatFunction: (fn, body, letBindings = []) => {
49
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
50
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
51
+ const letsBlock = lets ? lets + "\n" : "";
52
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
53
+ `public static class ${wrapperClassName(fn.name)}\n` +
54
+ `{\n` +
55
+ ` public static double ${fn.name}(${params})\n` +
56
+ ` {\n` +
57
+ letsBlock +
58
+ ` return ${body};\n` +
59
+ ` }\n` +
60
+ `}\n`;
61
+ },
62
+ // Multiple named outputs from one call: a native C# value tuple with
63
+ // named elements -- real field access (result.rx) with no wrapper
64
+ // type to declare, and (unlike Go's named returns) no risk of
65
+ // colliding with a same-named let binding, since a tuple literal's
66
+ // element names aren't pre-declared locals in the method body.
67
+ formatSuite: (fn, outputStrs, letBindings = []) => {
68
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
69
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
70
+ const letsBlock = lets ? lets + "\n" : "";
71
+ const outputNames = Object.keys(outputStrs);
72
+ const returnType = `(${outputNames.map((n) => `double ${n}`).join(", ")})`;
73
+ const returnExpr = `(${outputNames.map((n) => `${n}: ${outputStrs[n]}`).join(", ")})`;
74
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
75
+ `public static class ${wrapperClassName(fn.name)}\n` +
76
+ `{\n` +
77
+ ` public static ${returnType} ${fn.name}(${params})\n` +
78
+ ` {\n` +
79
+ letsBlock +
80
+ ` return ${returnExpr};\n` +
81
+ ` }\n` +
82
+ `}\n`;
83
+ },
84
+ });
85
+
86
+ module.exports = emitter;
package/emitters/go.js ADDED
@@ -0,0 +1,95 @@
1
+ // exprforge/emitters/go.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ const emitter = new Emitter({
13
+ ext: "go",
14
+ formatNumber: (v) => (Number.isInteger(v) ? `${v}.0` : String(v)),
15
+ calls: {
16
+ sqrt: fn1("Sqrt"), abs: fn1("Abs"), sin: fn1("Sin"), cos: fn1("Cos"), tan: fn1("Tan"),
17
+ asin: fn1("Asin"), acos: fn1("Acos"), atan: fn1("Atan"), log: fn1("Log"),
18
+ log2: fn1("Log2"), log10: fn1("Log10"), exp: fn1("Exp"), floor: fn1("Floor"),
19
+ ceil: fn1("Ceil"), round: fn1("Round"), trunc: fn1("Trunc"),
20
+ pow: fn2("Pow"), atan2: fn2("Atan2"), min: fn2("Min"), max: fn2("Max"), hypot: fn2("Hypot"),
21
+ // NOT math.Copysign(1, x): that only reads the sign bit, so it
22
+ // returns ±1 at x == 0 too, unlike JS's Math.sign(0) === 0 (and
23
+ // C's/Java's sign, which both special-case zero). Found by the
24
+ // kitchen-sink conformance test at exactly x - y == 0.
25
+ sign: ([x]) => `func() float64 { if ${x} > 0.0 { return 1.0 }; if ${x} < 0.0 { return -1.0 }; return 0.0 }()`,
26
+ },
27
+ // Go has no ternary operator at all (a deliberate language design
28
+ // choice) and `if` is a statement, not an expression — so there's no
29
+ // C-style `cond ? a : b` to fall back on. The standard idiom for an
30
+ // inline conditional *expression* is an immediately-invoked anonymous
31
+ // function; it's still one short-circuiting expression, so no change
32
+ // needed elsewhere.
33
+ emitSelect: function (condNode, thenStr, elseStr) {
34
+ const L = this.emitExpr(condNode.left);
35
+ const R = this.emitExpr(condNode.right);
36
+ return `func() float64 { if ${L} ${condNode.op} ${R} { return ${thenStr} }; return ${elseStr} }()`;
37
+ },
38
+ formatFunction: (fn, body, letBindings = []) => {
39
+ const params = fn.params.map((p) => `${p} float64`).join(", ");
40
+ const lets = letBindings.map(({ name, valueStr }) => `\tvar ${name} float64 = ${valueStr}`).join("\n");
41
+ // A let-chain can bind more names than any one function's body
42
+ // reads (e.g. a shared chain computes ux/uy/uz for three sibling
43
+ // single-component functions, each returning only one) — harmless
44
+ // everywhere else, but Go treats an unused local as a hard compile
45
+ // error, not a warning. Blank-discard every binding unconditionally
46
+ // rather than tracking which ones a given body actually reaches.
47
+ const guards = letBindings.map(({ name }) => `\t_ = ${name}`).join("\n");
48
+ const letsBlock = lets ? lets + "\n" + guards + "\n" : "";
49
+ // Only every emitter template here calls math.*, so this substring
50
+ // check is exact: omit the import when neither the body nor any let
51
+ // binding has a math call, or Go's "imported and not used" fails
52
+ // the build.
53
+ const mathImport = (body + lets).includes("math.") ? `import "math"\n\n` : "";
54
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
55
+ `package exprforge\n\n` +
56
+ mathImport +
57
+ `func ${capitalize(fn.name)}(${params}) float64 {\n` +
58
+ letsBlock +
59
+ `\treturn ${body}\n` +
60
+ `}\n`;
61
+ },
62
+ // Multiple named outputs from one call: Go's native multiple return
63
+ // values. NOT named return values (`(rx, ry float64)`) even though
64
+ // that reads nicer in the signature: those are sugar for pre-declared
65
+ // locals in the function's own scope, which collides — a real,
66
+ // hit-on-the-first-real-sample bug — whenever an output name matches
67
+ // a let-binding name (e.g. a "rx" output alongside an internal "rx"
68
+ // intermediate). Plain unnamed return types sidestep that whole class
69
+ // of collision regardless of what any AST author names things; a
70
+ // leading doc comment documents the order instead.
71
+ formatSuite: (fn, outputStrs, letBindings = []) => {
72
+ const params = fn.params.map((p) => `${p} float64`).join(", ");
73
+ const lets = letBindings.map(({ name, valueStr }) => `\tvar ${name} float64 = ${valueStr}`).join("\n");
74
+ const guards = letBindings.map(({ name }) => `\t_ = ${name}`).join("\n");
75
+ const letsBlock = lets ? lets + "\n" + guards + "\n" : "";
76
+ const outputNames = Object.keys(outputStrs);
77
+ const returnTypes = outputNames.map(() => "float64").join(", ");
78
+ const returnStmt = outputNames.map((n) => outputStrs[n]).join(", ");
79
+ const mathImport = (lets + returnStmt).includes("math.") ? `import "math"\n\n` : "";
80
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
81
+ `package exprforge\n\n` +
82
+ mathImport +
83
+ `// Returns (${outputNames.join(", ")}).\n` +
84
+ `func ${capitalize(fn.name)}(${params}) (${returnTypes}) {\n` +
85
+ letsBlock +
86
+ `\treturn ${returnStmt}\n` +
87
+ `}\n`;
88
+ },
89
+ });
90
+
91
+ function capitalize(s) {
92
+ return s.charAt(0).toUpperCase() + s.slice(1);
93
+ }
94
+
95
+ module.exports = emitter;
@@ -0,0 +1,76 @@
1
+ // exprforge/emitters/java.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `Math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `Math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ const emitter = new Emitter({
13
+ ext: "java",
14
+ formatNumber: (v) => (Number.isInteger(v) ? `${v}.0` : String(v)),
15
+ calls: {
16
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
17
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), log: fn1("log"),
18
+ log10: fn1("log10"), exp: fn1("exp"), floor: fn1("floor"), ceil: fn1("ceil"),
19
+ signum: fn1("signum"),
20
+ pow: fn2("pow"), atan2: fn2("atan2"), min: fn2("min"), max: fn2("max"), hypot: fn2("hypot"),
21
+ // Math has no log2; Java also lacks round-to-double and trunc directly.
22
+ log2: ([x]) => `(Math.log(${x}) / Math.log(2.0))`,
23
+ round: ([x]) => `((double) Math.round(${x}))`,
24
+ trunc: ([x]) => `(double) (long) (${x})`,
25
+ sign: fn1("signum"),
26
+ },
27
+ formatFunction: (fn, body, letBindings = []) => {
28
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
29
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
30
+ const letsBlock = lets ? lets + "\n" : "";
31
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
32
+ `public final class ${capitalize(fn.name)} {\n` +
33
+ ` private ${capitalize(fn.name)}() {\n` +
34
+ ` }\n\n` +
35
+ ` public static double ${fn.name}(${params}) {\n` +
36
+ letsBlock +
37
+ ` return ${body};\n` +
38
+ ` }\n` +
39
+ `}\n`;
40
+ },
41
+ // Multiple named outputs from one call: Java has no native multi-return,
42
+ // so this nests a small immutable Result class inside the same
43
+ // top-level class every function here already gets, keeping everything
44
+ // in the one per-function file this project's output layout assumes.
45
+ formatSuite: (fn, outputStrs, letBindings = []) => {
46
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
47
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
48
+ const letsBlock = lets ? lets + "\n" : "";
49
+ const outputNames = Object.keys(outputStrs);
50
+ const fields = outputNames.map((n) => ` public final double ${n};`).join("\n");
51
+ const ctorParams = outputNames.map((n) => `double ${n}`).join(", ");
52
+ const ctorAssigns = outputNames.map((n) => ` this.${n} = ${n};`).join("\n");
53
+ const newArgs = outputNames.map((n) => outputStrs[n]).join(", ");
54
+ return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
55
+ `public final class ${capitalize(fn.name)} {\n` +
56
+ ` private ${capitalize(fn.name)}() {\n` +
57
+ ` }\n\n` +
58
+ ` public static final class Result {\n` +
59
+ `${fields}\n\n` +
60
+ ` Result(${ctorParams}) {\n` +
61
+ `${ctorAssigns}\n` +
62
+ ` }\n` +
63
+ ` }\n\n` +
64
+ ` public static Result ${fn.name}(${params}) {\n` +
65
+ letsBlock +
66
+ ` return new Result(${newArgs});\n` +
67
+ ` }\n` +
68
+ `}\n`;
69
+ },
70
+ });
71
+
72
+ function capitalize(s) {
73
+ return s.charAt(0).toUpperCase() + s.slice(1);
74
+ }
75
+
76
+ module.exports = emitter;
package/emitters/js.js ADDED
@@ -0,0 +1,49 @@
1
+ // exprforge/emitters/js.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `Math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `Math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ const emitter = new Emitter({
13
+ ext: "js",
14
+ formatNumber: (v) => String(v),
15
+ calls: {
16
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
17
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), log: fn1("log"),
18
+ log2: fn1("log2"), log10: fn1("log10"), exp: fn1("exp"), floor: fn1("floor"),
19
+ ceil: fn1("ceil"), round: fn1("round"), trunc: fn1("trunc"), sign: fn1("sign"),
20
+ pow: fn2("pow"), atan2: fn2("atan2"), min: fn2("min"), max: fn2("max"), hypot: fn2("hypot"),
21
+ },
22
+ formatFunction: (fn, body, letBindings = []) => {
23
+ const lets = letBindings.map(({ name, valueStr }) => ` const ${name} = ${valueStr};`).join("\n");
24
+ const letsBlock = lets ? lets + "\n" : "";
25
+ return `// AUTO-GENERATED by ExprForge — do not hand-edit.\n` +
26
+ `function ${fn.name}(${fn.params.join(", ")}) {\n` +
27
+ letsBlock +
28
+ ` return ${body};\n` +
29
+ `}\n\n` +
30
+ `module.exports = { ${fn.name} };\n`;
31
+ },
32
+ // Multiple named outputs from one call: a plain object literal, JS's
33
+ // native idiom for this — no new type needed.
34
+ formatSuite: (fn, outputStrs, letBindings = []) => {
35
+ const lets = letBindings.map(({ name, valueStr }) => ` const ${name} = ${valueStr};`).join("\n");
36
+ const letsBlock = lets ? lets + "\n" : "";
37
+ const fields = Object.entries(outputStrs)
38
+ .map(([name, valueStr]) => ` ${name}: ${valueStr},`)
39
+ .join("\n");
40
+ return `// AUTO-GENERATED by ExprForge — do not hand-edit.\n` +
41
+ `function ${fn.name}(${fn.params.join(", ")}) {\n` +
42
+ letsBlock +
43
+ ` return {\n${fields}\n };\n` +
44
+ `}\n\n` +
45
+ `module.exports = { ${fn.name} };\n`;
46
+ },
47
+ });
48
+
49
+ module.exports = emitter;
@@ -0,0 +1,103 @@
1
+ // exprforge/emitters/lua.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ const emitter = new Emitter({
13
+ ext: "lua",
14
+ // Lua accepts JS-style numeric literal syntax directly, including
15
+ // exponential notation ("1e-9") -- no suffix or conversion needed.
16
+ // Integer-valued literals may be Lua's integer subtype (5.3+), but
17
+ // that's safe here: every bin op comes from ast.js's own +-*/ set,
18
+ // and Lua's `/` is always true (float) division regardless of
19
+ // operand types, unlike C#'s int/int trap.
20
+ formatNumber: (v) => String(v),
21
+ calls: {
22
+ sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
23
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), exp: fn1("exp"),
24
+ min: fn2("min"), max: fn2("max"),
25
+ // Lua 5.3+ removed math.atan2 -- math.atan(y, x) with a second
26
+ // argument is the replacement.
27
+ atan2: fn2("atan"),
28
+ // Lua 5.3+ removed math.pow -- ^ is the exponentiation operator
29
+ // (always float-producing, like / ).
30
+ pow: ([x, y]) => `(${x} ^ ${y})`,
31
+ // math.log takes an optional base -- covers log/log2/log10 in one
32
+ // function rather than three.
33
+ log: fn1("log"),
34
+ log2: ([x]) => `math.log(${x}, 2)`,
35
+ log10: ([x]) => `math.log(${x}, 10)`,
36
+ // math.floor/math.ceil return Lua's integer subtype (5.3+), not
37
+ // float -- +0.0 coerces back, matching this project's float64-only
38
+ // model everywhere else.
39
+ floor: ([x]) => `(math.floor(${x}) + 0.0)`,
40
+ ceil: ([x]) => `(math.ceil(${x}) + 0.0)`,
41
+ // No math.round in Lua at any version -- the standard manual idiom
42
+ // (floor(x+0.5)) matches JS's/Java's round-half-up convention, not
43
+ // C/Go/Rust's round-half-away-from-zero -- same already-documented,
44
+ // already-avoided-in-tests divergence as elsewhere in this project,
45
+ // not a new one.
46
+ round: ([x]) => `(math.floor(${x} + 0.5) + 0.0)`,
47
+ // No math.trunc either -- math.modf(x) returns (integral,
48
+ // fractional) parts as its two results; parenthesizing the call
49
+ // selects just the first (Lua's standard idiom for narrowing a
50
+ // multi-return to one value), which is exactly trunc(x), and
51
+ // already float-typed.
52
+ trunc: ([x]) => `(math.modf(${x}))`,
53
+ // No math.sign either. Lua's and/or conditional idiom is safe here
54
+ // specifically because only nil/false are falsy in Lua -- 0.0 (or
55
+ // any number) is always truthy, unlike JS/Python/C. Zero-aware by
56
+ // construction (see Go's/Rust's sign() history in this project for
57
+ // what happens when it isn't).
58
+ sign: ([x]) => `((${x} > 0) and 1.0 or ((${x} < 0) and -1.0 or 0.0))`,
59
+ // No math.hypot in Lua's standard library at any version.
60
+ hypot: ([a, b]) => `math.sqrt((${a}) * (${a}) + (${b}) * (${b}))`,
61
+ },
62
+ // Lua has no ?: ternary. `cond and a or b` is the standard idiom --
63
+ // safe here for the same reason sign() above is: every value in this
64
+ // AST is a number, and only nil/false are ever falsy in Lua, so `a`
65
+ // (the then-branch) is never mistaken for "falsy" regardless of its
66
+ // numeric value (unlike this same idiom in some other languages).
67
+ emitSelect: function (condNode, thenStr, elseStr) {
68
+ const L = this.emitExpr(condNode.left);
69
+ const R = this.emitExpr(condNode.right);
70
+ return `((${L} ${condNode.op} ${R}) and (${thenStr}) or (${elseStr}))`;
71
+ },
72
+ formatFunction: (fn, body, letBindings = []) => {
73
+ const params = fn.params.join(", ");
74
+ const lets = letBindings.map(({ name, valueStr }) => ` local ${name} = ${valueStr}`).join("\n");
75
+ const letsBlock = lets ? lets + "\n" : "";
76
+ return `-- AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
77
+ `function ${fn.name}(${params})\n` +
78
+ letsBlock +
79
+ ` return ${body}\n` +
80
+ `end\n`;
81
+ },
82
+ // Multiple named outputs from one call: Lua's native multiple return
83
+ // values -- the cleanest of any target here, no wrapper type and no
84
+ // collision risk (unlike Go's named returns, `local` declarations in
85
+ // the body and the trailing `return` list are entirely separate
86
+ // namespaces). A leading comment documents the order, since Lua's
87
+ // returns are positional, not named, at the call site.
88
+ formatSuite: (fn, outputStrs, letBindings = []) => {
89
+ const params = fn.params.join(", ");
90
+ const lets = letBindings.map(({ name, valueStr }) => ` local ${name} = ${valueStr}`).join("\n");
91
+ const letsBlock = lets ? lets + "\n" : "";
92
+ const outputNames = Object.keys(outputStrs);
93
+ const returnStmt = outputNames.map((n) => outputStrs[n]).join(", ");
94
+ return `-- AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
95
+ `-- Returns (${outputNames.join(", ")}).\n` +
96
+ `function ${fn.name}(${params})\n` +
97
+ letsBlock +
98
+ ` return ${returnStmt}\n` +
99
+ `end\n`;
100
+ },
101
+ });
102
+
103
+ module.exports = emitter;
@@ -0,0 +1,85 @@
1
+ // exprforge/emitters/python.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `math.${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `math.${name}(${a}, ${b})`;
10
+ }
11
+
12
+ function capitalize(s) {
13
+ return s.charAt(0).toUpperCase() + s.slice(1);
14
+ }
15
+
16
+ const emitter = new Emitter({
17
+ ext: "py",
18
+ // Python accepts JS-style numeric literal syntax directly, including
19
+ // exponential notation ("1e-9"), unlike QB64/C# -- no conversion or
20
+ // suffix needed. Integer-valued literals stay Python ints, but that's
21
+ // safe here: every bin op comes from ast.js's own +-*/ set, and
22
+ // Python 3's `/` is always true (float) division regardless of
23
+ // operand types, unlike C#'s int/int trap.
24
+ formatNumber: (v) => String(v),
25
+ calls: {
26
+ sqrt: fn1("sqrt"), abs: ([x]) => `abs(${x})`, sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
27
+ asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), atan2: fn2("atan2"),
28
+ log: fn1("log"), log2: fn1("log2"), log10: fn1("log10"), exp: fn1("exp"),
29
+ pow: fn2("pow"), min: ([a, b]) => `min(${a}, ${b})`, max: ([a, b]) => `max(${a}, ${b})`,
30
+ hypot: fn2("hypot"),
31
+ // math.floor/ceil/trunc and builtin round() all return int in
32
+ // Python 3, not float -- wrap to stay float64 throughout, matching
33
+ // every other target here, rather than silently switching types.
34
+ floor: ([x]) => `float(math.floor(${x}))`,
35
+ ceil: ([x]) => `float(math.ceil(${x}))`,
36
+ trunc: ([x]) => `float(math.trunc(${x}))`,
37
+ round: ([x]) => `float(round(${x}))`,
38
+ // No math.sign in Python's stdlib -- build it directly. Zero-aware
39
+ // by construction (see Go's/Rust's sign() history in this project
40
+ // for what happens when it isn't).
41
+ sign: ([x]) => `(1.0 if ${x} > 0 else (-1.0 if ${x} < 0 else 0.0))`,
42
+ },
43
+ // Python has no ?: ternary; `a if cond else b` is its conditional
44
+ // expression instead, and it's just as short-circuiting.
45
+ emitSelect: function (condNode, thenStr, elseStr) {
46
+ const L = this.emitExpr(condNode.left);
47
+ const R = this.emitExpr(condNode.right);
48
+ return `(${thenStr} if (${L} ${condNode.op} ${R}) else ${elseStr})`;
49
+ },
50
+ formatFunction: (fn, body, letBindings = []) => {
51
+ const params = fn.params.join(", ");
52
+ const lets = letBindings.map(({ name, valueStr }) => ` ${name} = ${valueStr}`).join("\n");
53
+ const letsBlock = lets ? lets + "\n" : "";
54
+ return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
55
+ `import math\n\n\n` +
56
+ `def ${fn.name}(${params}):\n` +
57
+ letsBlock +
58
+ ` return ${body}\n`;
59
+ },
60
+ // Multiple named outputs from one call: a small local class (dot
61
+ // access, result.rx) rather than a plain dict -- consistent with
62
+ // every other target here (C/Rust's struct, C#/Go's tuple, Java's
63
+ // nested Result class), and a fixed, self-documenting field set
64
+ // instead of an untyped mapping.
65
+ formatSuite: (fn, outputStrs, letBindings = []) => {
66
+ const params = fn.params.join(", ");
67
+ const lets = letBindings.map(({ name, valueStr }) => ` ${name} = ${valueStr}`).join("\n");
68
+ const letsBlock = lets ? lets + "\n" : "";
69
+ const outputNames = Object.keys(outputStrs);
70
+ const className = `${capitalize(fn.name)}Result`;
71
+ const ctorParams = outputNames.join(", ");
72
+ const ctorAssigns = outputNames.map((n) => ` self.${n} = ${n}`).join("\n");
73
+ const ctorArgs = outputNames.map((n) => outputStrs[n]).join(", ");
74
+ return `# AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
75
+ `import math\n\n\n` +
76
+ `class ${className}:\n` +
77
+ ` def __init__(self, ${ctorParams}):\n` +
78
+ `${ctorAssigns}\n\n\n` +
79
+ `def ${fn.name}(${params}):\n` +
80
+ letsBlock +
81
+ ` return ${className}(${ctorArgs})\n`;
82
+ },
83
+ });
84
+
85
+ module.exports = emitter;
@@ -0,0 +1,127 @@
1
+ // exprforge/emitters/qb64.js
2
+ const Emitter = require("./base.js");
3
+
4
+ const HALF_PI = "1.5707963267948966#";
5
+
6
+ // QB64/BASIC builtins that silently conflict with a variable or parameter
7
+ // of the same name (case-insensitive) -- confirmed painful in practice
8
+ // (see this project's QB64 gotchas memory, from a sibling game project).
9
+ // Checked below so a collision fails loudly at emission time with a
10
+ // specific name to fix, instead of as a cryptic QB64 compiler error later.
11
+ const QB64_RESERVED = new Set([
12
+ "len", "val", "str", "int", "abs", "sqr", "sgn", "fix", "rnd", "log", "exp",
13
+ "sin", "cos", "tan", "atn",
14
+ "left", "right", "mid", "asc", "chr", "instr", "ltrim", "rtrim", "ucase", "lcase",
15
+ "space", "string", "hex", "oct",
16
+ "peek", "inp", "out", "timer", "date", "time", "tab", "spc", "pos",
17
+ ]);
18
+
19
+ function checkReservedNames(names) {
20
+ for (const name of names) {
21
+ if (QB64_RESERVED.has(name.toLowerCase())) {
22
+ throw new Error(
23
+ `emitter for .bas: "${name}" is a reserved QB64 builtin and can't be used as a ` +
24
+ `variable/parameter name -- rename it (see QB64_RESERVED in emitters/qb64.js)`,
25
+ );
26
+ }
27
+ }
28
+ }
29
+
30
+ const emitter = new Emitter({
31
+ ext: "bas",
32
+ // JS renders very small/large magnitudes in exponential notation
33
+ // (String(1e-9) === "1e-9"), and QB64 doesn't accept that combined
34
+ // with a # suffix ("Invalid expression", confirmed against a real
35
+ // compiler). Classic BASIC's own exponential form uses D (not E) as
36
+ // the marker for a double-precision literal -- and needs no separate
37
+ // # suffix, since D already says "double".
38
+ formatNumber: (v) => {
39
+ const s = String(v);
40
+ return /e/i.test(s) ? s.replace(/e/i, "D") : `${s}#`;
41
+ },
42
+ calls: {
43
+ sqrt: ([x]) => `SQR(${x})`,
44
+ abs: ([x]) => `ABS(${x})`,
45
+ sin: ([x]) => `SIN(${x})`,
46
+ cos: ([x]) => `COS(${x})`,
47
+ tan: ([x]) => `TAN(${x})`,
48
+ atan: ([x]) => `ATN(${x})`,
49
+ exp: ([x]) => `EXP(${x})`,
50
+ log: ([x]) => `LOG(${x})`,
51
+ sign: ([x]) => `SGN(${x})`,
52
+ min: ([a, b]) => `_MIN(${a}, ${b})`,
53
+ max: ([a, b]) => `_MAX(${a}, ${b})`,
54
+ round: ([x]) => `_ROUND(${x})`,
55
+ pow: ([base, exp]) => `(${base} ^ ${exp})`,
56
+ asin: ([x]) => `ATN(${x} / SQR(-(${x}) * (${x}) + 1#))`,
57
+ acos: ([x]) => `(${HALF_PI} - ATN(${x} / SQR(-(${x}) * (${x}) + 1#)))`,
58
+ atan2: ([y, x]) => `_ATAN2(${y}, ${x})`,
59
+ log2: ([x]) => `(LOG(${x}) / LOG(2#))`,
60
+ log10: ([x]) => `(LOG(${x}) / LOG(10#))`,
61
+ floor: ([x]) => `INT(${x})`,
62
+ ceil: ([x]) => `(-INT(-(${x})))`,
63
+ trunc: ([x]) => `(SGN(${x}) * INT(ABS(${x})))`,
64
+ hypot: ([a, b]) => `SQR((${a}) * (${a}) + (${b}) * (${b}))`,
65
+ },
66
+ // QB64 has no ternary operator. Comparison operators return -1 (true)
67
+ // or 0 (false), so the algebraically equivalent expression is:
68
+ // (-1 * then) * cond + else * (1 + cond)
69
+ // cond=-1 (true): (-1*then)*-1 + else*0 = then
70
+ // cond= 0 (false): (-1*then)* 0 + else*1 = else
71
+ // Both `then` and `else` are always evaluated here (see select()'s
72
+ // doc comment in ast.js) — same as every other target.
73
+ emitSelect: function (condNode, thenStr, elseStr) {
74
+ const L = this.emitExpr(condNode.left);
75
+ const R = this.emitExpr(condNode.right);
76
+ const cond = `(${L} ${condNode.op} ${R})`;
77
+ return `((-1# * ${thenStr}) * ${cond} + ${elseStr} * (1# + ${cond}))`;
78
+ },
79
+ formatFunction: (fn, body, letBindings = []) => {
80
+ checkReservedNames([fn.name, ...fn.params, ...letBindings.map((b) => b.name)]);
81
+ const params = fn.params.map((p) => `${p} AS DOUBLE`).join(", ");
82
+ // NOT `Dim name# AS DOUBLE`: combining the # sigil with an AS
83
+ // DOUBLE clause on the same DIM is a syntax error in QB64
84
+ // ("DIM: Expected ,") -- confirmed against a real compiler. Every
85
+ // reference elsewhere is already the bare (unsuffixed) name (see
86
+ // emitExpr's "var" case in base.js), so the fix is just dropping
87
+ // the sigil here too, not adding it anywhere else.
88
+ const lets = letBindings
89
+ .map(({ name, valueStr }) => ` Dim ${name} AS DOUBLE : ${name} = ${valueStr}`)
90
+ .join("\n");
91
+ const letsBlock = lets ? lets + "\n" : "";
92
+ return `' AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
93
+ `FUNCTION ${fn.name}# (${params})\n` +
94
+ letsBlock +
95
+ ` ${fn.name}# = ${body}\n` +
96
+ `END FUNCTION\n`;
97
+ },
98
+ // Multiple named outputs from one call: QB64 has no struct/tuple return,
99
+ // so this emits a SUB instead of a FUNCTION, with the outputs as
100
+ // trailing parameters — SUB params are by reference by default in
101
+ // QB64/BASIC, so assigning to them writes back to the caller's
102
+ // variables. This is the classic BASIC multi-output idiom.
103
+ formatSuite: (fn, outputStrs, letBindings = []) => {
104
+ const outputNames = Object.keys(outputStrs);
105
+ checkReservedNames([fn.name, ...fn.params, ...outputNames, ...letBindings.map((b) => b.name)]);
106
+ const inParams = fn.params.map((p) => `${p} AS DOUBLE`);
107
+ const outParams = outputNames.map((n) => `${n} AS DOUBLE`);
108
+ // NOT `Dim name# AS DOUBLE`: combining the # sigil with an AS
109
+ // DOUBLE clause on the same DIM is a syntax error in QB64
110
+ // ("DIM: Expected ,") -- confirmed against a real compiler. Every
111
+ // reference elsewhere is already the bare (unsuffixed) name (see
112
+ // emitExpr's "var" case in base.js), so the fix is just dropping
113
+ // the sigil here too, not adding it anywhere else.
114
+ const lets = letBindings
115
+ .map(({ name, valueStr }) => ` Dim ${name} AS DOUBLE : ${name} = ${valueStr}`)
116
+ .join("\n");
117
+ const letsBlock = lets ? lets + "\n" : "";
118
+ const assigns = outputNames.map((n) => ` ${n} = ${outputStrs[n]}`).join("\n");
119
+ return `' AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
120
+ `SUB ${fn.name} (${[...inParams, ...outParams].join(", ")})\n` +
121
+ letsBlock +
122
+ `${assigns}\n` +
123
+ `END SUB\n`;
124
+ },
125
+ });
126
+
127
+ module.exports = emitter;
@@ -0,0 +1,15 @@
1
+ // exprforge/emitters/registry.js
2
+ // Add a new language: write emitters/<lang>.js exporting an Emitter instance,
3
+ // then add one line here. Nothing else in the project needs to change.
4
+ module.exports = {
5
+ js: require("./js.js"),
6
+ ts: require("./typescript.js"),
7
+ qb64: require("./qb64.js"),
8
+ c: require("./c.js"),
9
+ java: require("./java.js"),
10
+ go: require("./go.js"),
11
+ rust: require("./rust.js"),
12
+ csharp: require("./csharp.js"),
13
+ python: require("./python.js"),
14
+ lua: require("./lua.js"),
15
+ };