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.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/ast.js +142 -0
- package/build.js +35 -0
- package/emitters/base.js +113 -0
- package/emitters/c.js +64 -0
- package/emitters/csharp.js +86 -0
- package/emitters/go.js +95 -0
- package/emitters/java.js +76 -0
- package/emitters/js.js +49 -0
- package/emitters/lua.js +103 -0
- package/emitters/python.js +85 -0
- package/emitters/qb64.js +127 -0
- package/emitters/registry.js +15 -0
- package/emitters/rust.js +90 -0
- package/emitters/typescript.js +63 -0
- package/index.js +44 -0
- package/package.json +44 -0
- package/samples/catmull-rom.js +26 -0
- package/samples/fibonacci.js +24 -0
- package/samples/kitchen-sink.js +56 -0
- package/samples/spline-frame.js +184 -0
- package/util.js +15 -0
|
@@ -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;
|
package/emitters/java.js
ADDED
|
@@ -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;
|
package/emitters/lua.js
ADDED
|
@@ -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;
|
package/emitters/qb64.js
ADDED
|
@@ -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
|
+
};
|