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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Don Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # ExprForge 🔢🔨
2
+
3
+ [![TypeScript](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml)
4
+ [![Python](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml)
5
+ [![C#](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml)
6
+ [![Lua](https://github.com/theraccoonbear/exprforge/actions/workflows/test-lua.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-lua.yml)
7
+ [![QB64](https://github.com/theraccoonbear/exprforge/actions/workflows/test-qb64.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-qb64.yml)
8
+ [![C](https://github.com/theraccoonbear/exprforge/actions/workflows/test-c.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-c.yml)
9
+ [![Java](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml)
10
+ [![Go](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml)
11
+ [![Rust](https://github.com/theraccoonbear/exprforge/actions/workflows/test-rust.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-rust.yml)
12
+
13
+ Author a math expression once, as a small AST, and emit verified,
14
+ identical-behavior implementations in JavaScript, TypeScript, Python, C#,
15
+ Lua, QB64, C, Java, Go, and Rust.
16
+
17
+ No parser, no dependencies. You build the AST directly with plain JS
18
+ functions; the same tree is walked once per target language.
19
+
20
+ ## Why
21
+
22
+ Codegen tools like SymPy already turn math expressions into code for
23
+ mainstream languages. This exists for two things SymPy doesn't do:
24
+
25
+ - Targets like QB64/BASIC that no general codegen project supports.
26
+ - A conformance test harness that actually proves the emitted targets
27
+ agree numerically, not just that they compile.
28
+
29
+ ## Install
30
+
31
+ ```
32
+ npm install exprforge
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```js
38
+ const { num, v, bin, mul, add, sub, emitAll } = require("exprforge");
39
+
40
+ const fn = {
41
+ name: "lerp",
42
+ params: ["a", "b", "t"],
43
+ body: add(v("a"), mul(sub(v("b"), v("a")), v("t"))),
44
+ };
45
+
46
+ const outputs = emitAll(fn);
47
+ console.log(outputs.rust.source);
48
+ console.log(outputs.c.source);
49
+ ```
50
+
51
+ ## Samples
52
+
53
+ `samples/` has worked, non-trivial examples (also exported from the
54
+ package individually, or together as `samples`):
55
+
56
+ - `samples/catmull-rom.js` — uniform Catmull-Rom spline interpolation.
57
+ - `samples/fibonacci.js` — nth Fibonacci number via Binet's closed form.
58
+ There's no loop/recursion version because exprforge has no control flow
59
+ (see below) — this is what "fibonacci" looks like as a pure expression.
60
+ - `samples/spline-frame.js` — Gram-Schmidt frame construction for spline
61
+ paths (worldUp selection, tangent normalization with a safe-division
62
+ fallback, roll). 4 suites exercising `letIn`/`cmp`/`select`/`outputs` on
63
+ a real-world case — this is the actual motivating use case for those
64
+ node types, not a toy. Used to be 19 separate functions, each
65
+ independently re-deriving the same let-chain (including a `sqrt`) for
66
+ one output; now each related group shares that work once per call.
67
+ - `samples/kitchen-sink.js` — not a worked example: a synthetic function
68
+ that calls all 22 supported Math functions in one expression, existing
69
+ purely as a conformance-test fixture. It's what caught Go's and Rust's
70
+ `sign()` disagreeing with everyone else at exactly zero (see below) —
71
+ the other samples between them only ever exercised 5 of the 22.
72
+
73
+ `npm run build` emits all of them, for every target language, into `out/`.
74
+
75
+ ## Supported Math functions
76
+
77
+ `sqrt abs pow sin cos tan asin acos atan atan2 log log2 log10 exp
78
+ floor ceil round trunc sign min max hypot`
79
+
80
+ Add more by extending a target's `calls` table in `emitters/<lang>.js`.
81
+ Requesting an unmapped function throws at build time, not silently.
82
+
83
+ ## Adding a language
84
+
85
+ Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
86
+ existing file as a template), then add one line to
87
+ `emitters/registry.js`. Nothing else changes — proven by the TypeScript
88
+ emitter, added with no changes to `base.js`, `build.js`, or `index.js`.
89
+
90
+ ## Named subexpressions and conditional values
91
+
92
+ Beyond `num`/`v`/`bin`/`call`, two more node types stay inside the
93
+ expression model without introducing control flow:
94
+
95
+ - **`letIn(name, value, body)`** — name a subexpression to avoid
96
+ recomputing it (e.g. `sqrt(x²+y²+z²)` once, then divide three
97
+ components by it). Every `let` in a function gets lifted into an ordered
98
+ list of local declarations ahead of the return statement/expression, in
99
+ every target.
100
+ - **`select(cond, then, else)` + `cmp(left, op, right)`** — conditional
101
+ *value* selection. Every target has a genuinely different way to spell
102
+ this: a native ternary where one exists (C, Java, C#), `if`-as-expression
103
+ in Rust, `a if cond else b` in Python, `cond and a or b` in Lua (safe
104
+ there specifically because only `nil`/`false` are falsy in Lua — a
105
+ number is always truthy, so this never mis-selects at zero), an
106
+ immediately-invoked function in Go (which has neither ternary nor an
107
+ `if`-expression), and the equivalent arithmetic expression in QB64
108
+ (which has no conditional expression syntax whatsoever).
109
+
110
+ **`select` is not a branch** — every target evaluates both `then` and
111
+ `else`. Don't use it to guard division by zero or anything else
112
+ undefined; clamp the operand itself with its own `select` first (see
113
+ `safeDiv` in `samples/spline-frame.js`), or keep a real guard as
114
+ hand-written code around the generated function.
115
+
116
+ See [`docs/planned-additions.md`](./docs/planned-additions.md) for the
117
+ full design rationale, including why the naive "guard division with
118
+ select" pattern is wrong.
119
+
120
+ ## Multiple named outputs
121
+
122
+ `outputs({ name: Node, ... })` computes several named values from ONE
123
+ shared `letIn` chain, instead of one function per value each re-deriving
124
+ the whole chain from scratch:
125
+
126
+ ```js
127
+ const { num, v, add, sub, letIn, outputs } = require("exprforge");
128
+
129
+ const sumAndDiff = {
130
+ name: "sumAndDiff",
131
+ params: ["a", "b"],
132
+ body: letIn("total", add(v("a"), v("b")),
133
+ letIn("delta", sub(v("a"), v("b")),
134
+ outputs({ sum: v("total"), diff: v("delta") })
135
+ )),
136
+ };
137
+ ```
138
+
139
+ Only valid as a function's top-level body (wrap it in `letIn`s, don't nest
140
+ it inside `bin`/`call`/`select`). Each target renders it as whatever
141
+ multi-value idiom it has, since none of them agree:
142
+
143
+ | Target | Shape |
144
+ |---|---|
145
+ | JS | object literal |
146
+ | Go, Lua | native multiple return values |
147
+ | C# | a native named value tuple (`(double rx, double ry)`) |
148
+ | C / Rust | a small `...Result` struct, returned by value |
149
+ | Java, Python | a nested/local `Result` class |
150
+ | QB64 | a `SUB` with the outputs as trailing by-reference parameters |
151
+
152
+ Go specifically does **not** use *named* return values (`(rx, ry float64)`)
153
+ even though Go supports them and it reads nicer: those are sugar for
154
+ pre-declared locals in the function's own scope, and that collides — for
155
+ real, on the first suite this feature was built for — whenever an output
156
+ name matches an internal `letIn` name. Plain unnamed return types side-step
157
+ the whole collision class regardless of naming; a leading comment documents
158
+ the order instead (same reason Lua's return, also positional, gets one).
159
+ C#'s tuple has no such risk — a tuple literal's element names aren't
160
+ pre-declared locals the way Go's named returns are.
161
+
162
+ ## What this deliberately doesn't do
163
+
164
+ - No control flow (loops, branches, calling other generated functions) —
165
+ this is an expression AST, not a program AST.
166
+ - No RNG — can't be made to produce identical output across languages,
167
+ so it isn't offered as if it could.
168
+ - No arbitrary precision / complex numbers — float64 only, for now.
169
+
170
+ ## Testing
171
+
172
+ ```
173
+ npm test
174
+ ```
175
+
176
+ Runs `node --test`. For each sample, that's two kinds of check:
177
+
178
+ - Emitted JS vs. an independently hand-written reference implementation
179
+ (catches a wrong formula in the AST itself).
180
+ - Every other emitted target vs. that same JS, compiled (and, for
181
+ TypeScript, also type-checked under `--strict`) and run, with the sample
182
+ inputs as arguments (catches an emitter bug).
183
+
184
+ The compiled/interpreted-language checks need their toolchain on `PATH`
185
+ and skip (not fail) when it's missing, so `npm test` degrades gracefully
186
+ on any one machine. Every one of `tsc`/`qb64pe`/`dotnet`/`python3`/`lua`
187
+ is treated exactly like gcc/go/rustc/javac: looked up on `PATH`, never a
188
+ project dependency — exprforge only ever generates source text for these,
189
+ it doesn't execute or type-check any of it itself. `package.json` has
190
+ zero dependencies of any kind, matching this.
191
+
192
+ CI is one workflow file per target language (`.github/workflows/test-*.yml`),
193
+ run in parallel — they have nothing to do with each other, so there's no
194
+ reason to serialize installing nine different toolchains (QB64-PE alone,
195
+ built from source and cached by version, takes several minutes) into one
196
+ job, and splitting by file rather than by job within one file is also
197
+ what gets each language its own real status badge above, not just one
198
+ combined "did everything pass" badge. Each workflow installs only its own
199
+ toolchain and runs `EXPRFORGE_TEST_TARGETS=<Label> npm test`; that
200
+ environment variable (read once in `test/conformance.test.js`) filters
201
+ the target lists down to just that one language, plus the toolchain-
202
+ independent JS/reference checks, which every workflow repeats — cheap,
203
+ and a redundant sanity check each time. Unset locally, so a plain
204
+ `npm test` still runs everything your own machine's installed toolchains
205
+ allow.
206
+
207
+ A few of these needed real debugging to get right, all found by actually
208
+ compiling/running against a real toolchain rather than assumed to work:
209
+
210
+ - **QB64**: `Dim x# AS DOUBLE` (sigil *and* an `AS` clause together) is a
211
+ syntax error; has to be `Dim x AS DOUBLE`. Its own exponential notation
212
+ uses `D`, not `E` (`1D-9`, not `1e-9#`) — including when reading its
213
+ `PRINT` output back, not just in literals. A chunk of QB64/BASIC
214
+ builtins (`len`, `val`, `pos`, `log`, ... — see `QB64_RESERVED` in
215
+ `emitters/qb64.js`) silently conflict with a same-named variable; the
216
+ emitter throws a clear error at emission time instead of failing to
217
+ compile later with no context. The test harness runs compiled binaries
218
+ headless via the `$CONSOLE:ONLY` metacommand, so no display (real or
219
+ virtual) is needed — no `xvfb-run` required for these console-only test
220
+ programs, unlike a typical QB64 build.
221
+ - **C#**: forbids a member sharing its enclosing type's *exact* name
222
+ (`CS0542`) — every SpEf-prefixed sample name here is already
223
+ capitalized, so the obvious `capitalize(fn.name)` wrapper-class name
224
+ collided with the method name outright; see `wrapperClassName` in
225
+ `emitters/csharp.js`. Bare integer-valued literals are `int` by
226
+ default, and `int / int` is integer division — every literal is
227
+ suffixed `d` unconditionally to rule that out, not just the cases that
228
+ would otherwise break.
229
+ - **Python**: `math.floor`/`math.ceil`/`math.trunc`/`round` all return
230
+ `int`, not `float` — wrapped with `float(...)` to stay float64
231
+ throughout, matching every other target.
232
+ - **Lua**: 5.3+ removed `math.pow` (use the `^` operator) and
233
+ `math.atan2` (use two-argument `math.atan(y, x)`); there's no
234
+ `math.round` or `math.trunc` or `math.sign` at any version (manual
235
+ `floor(x+0.5)`, `math.modf(x)`, and an `and`/`or` chain respectively).
236
+
237
+ One test (`normalizeX`) is deliberately excluded from the QB64 check
238
+ only: it exists specifically to demonstrate the "don't guard division
239
+ with `select`" pitfall from the section above, and QB64 is the one
240
+ target where that pitfall actually produces `NaN` (every other target,
241
+ including Lua's `and`/`or`, genuinely short-circuits around it) — that's
242
+ the AST being correctly unsafe on purpose, not an emitter bug.
243
+
244
+ ## License
245
+
246
+ MIT — see [LICENSE](./LICENSE).
package/ast.js ADDED
@@ -0,0 +1,142 @@
1
+ // exprforge/ast.js
2
+ // Generic AST builder primitives — this is the actual library API. Compose
3
+ // these into your own expression trees; see samples/ for worked examples.
4
+ //
5
+ // Node shapes:
6
+ // { type: "num", value: number }
7
+ // { type: "var", name: string }
8
+ // { type: "bin", op: "+" | "-" | "*" | "/", left: Node, right: Node }
9
+ // { type: "call", name: string, args: Node[] } // any Math.* function
10
+ // { type: "let", name: string, value: Node, body: Node }
11
+ // { type: "cmp", op: ">" | "<" | ">=" | "<=" | "==" | "!=", left: Node, right: Node }
12
+ // { type: "select", cond: CmpNode, then: Node, else: Node }
13
+ // { type: "outputs", fields: { [name: string]: Node } }
14
+ //
15
+ // Every "bin" node is emitted with explicit parens in every target, so
16
+ // operation order (and therefore floating-point rounding behavior) is
17
+ // identical everywhere.
18
+
19
+ function num(value) {
20
+ return { type: "num", value };
21
+ }
22
+
23
+ function v(name) {
24
+ return { type: "var", name };
25
+ }
26
+
27
+ function bin(op, left, right) {
28
+ return { type: "bin", op, left, right };
29
+ }
30
+
31
+ function call(name, ...args) {
32
+ return { type: "call", name, args };
33
+ }
34
+
35
+ function add(...terms) {
36
+ return terms.reduce((acc, t) => (acc === null ? t : bin("+", acc, t)), null);
37
+ }
38
+
39
+ function mul(...terms) {
40
+ return terms.reduce((acc, t) => (acc === null ? t : bin("*", acc, t)), null);
41
+ }
42
+
43
+ function sub(a, b) {
44
+ return bin("-", a, b);
45
+ }
46
+
47
+ function div(a, b) {
48
+ return bin("/", a, b);
49
+ }
50
+
51
+ // Unary negation, since there's no unary operator in the AST — every
52
+ // operator here is binary. `0 - x` rather than `-1 * x`: both are always
53
+ // safe to emit, but subtraction from zero is the more direct reading.
54
+ function neg(x) {
55
+ return sub(num(0), x);
56
+ }
57
+
58
+ // Name a subexpression to avoid recomputing it (e.g. sqrt(x²+y²+z²) once,
59
+ // then divide three components by it). Lifted out by collectLets before
60
+ // emission — see there for how `v(name)` ends up referring to it.
61
+ function letIn(name, value, body) {
62
+ return { type: "let", name, value, body };
63
+ }
64
+
65
+ // Comparison predicate — only valid as the `cond` of a select(); not a
66
+ // general boolean expression, and shouldn't appear anywhere else in a tree.
67
+ function cmp(left, op, right) {
68
+ return { type: "cmp", op, left, right };
69
+ }
70
+
71
+ // Conditional *value* selection, not a branch — both `then` and `else` are
72
+ // always evaluated by every emitter (this is a value expression, not
73
+ // control flow). Do not use this to guard division by zero or any other
74
+ // undefined operation: ensure the operands are already safe (e.g. clamp a
75
+ // denominator with its own select before dividing by it), or keep a real
76
+ // guard as hand-written code in the caller of the generated function.
77
+ function select(cond, thenNode, elseNode) {
78
+ return { type: "select", cond, then: thenNode, else: elseNode };
79
+ }
80
+
81
+ // Multiple named outputs computed from ONE shared let-chain, instead of N
82
+ // separate function definitions each re-deriving the whole chain from
83
+ // scratch. Only valid as a function's (post-let-lifting) top-level body —
84
+ // wrap it, don't nest it inside bin/call/select. Each emitter renders it as
85
+ // whatever multi-value idiom its language has (a struct, a native multiple
86
+ // return, an object literal, output parameters) — see formatSuite in each
87
+ // emitters/<lang>.js.
88
+ function outputs(fields) {
89
+ return { type: "outputs", fields };
90
+ }
91
+
92
+ // Lifts every `let` node out of the tree into a flat, ordered list of
93
+ // { name, node } bindings, replacing each with a plain v(name) reference.
94
+ // The list is in dependency order — safe to declare/assign top-to-bottom.
95
+ // Throws if two bindings share a name: they'd silently shadow or fail to
96
+ // redeclare depending on the target language, and there's no lexical
97
+ // scoping here to make that meaningful — every binding lands in one flat
98
+ // list per function.
99
+ function collectLets(node) {
100
+ const bindings = [];
101
+
102
+ function walk(n) {
103
+ if (n.type === "let") {
104
+ const { bindings: inner, body: val } = collectLets(n.value);
105
+ bindings.push(...inner);
106
+ bindings.push({ name: n.name, node: val });
107
+ return walk(n.body);
108
+ }
109
+ if (n.type === "bin") return { ...n, left: walk(n.left), right: walk(n.right) };
110
+ if (n.type === "call") return { ...n, args: n.args.map(walk) };
111
+ if (n.type === "select") {
112
+ return {
113
+ ...n,
114
+ then: walk(n.then),
115
+ else: walk(n.else),
116
+ cond: { ...n.cond, left: walk(n.cond.left), right: walk(n.cond.right) },
117
+ };
118
+ }
119
+ if (n.type === "outputs") {
120
+ const fields = {};
121
+ for (const [name, fieldNode] of Object.entries(n.fields)) {
122
+ fields[name] = walk(fieldNode);
123
+ }
124
+ return { ...n, fields };
125
+ }
126
+ return n; // num, var
127
+ }
128
+
129
+ const body = walk(node);
130
+
131
+ const seen = new Set();
132
+ for (const { name } of bindings) {
133
+ if (seen.has(name)) {
134
+ throw new Error(`collectLets: duplicate let binding name "${name}" in one function`);
135
+ }
136
+ seen.add(name);
137
+ }
138
+
139
+ return { bindings, body };
140
+ }
141
+
142
+ module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets };
package/build.js ADDED
@@ -0,0 +1,35 @@
1
+ // exprforge/build.js
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const emitters = require("./emitters/registry.js");
5
+ const { catmullRomAst } = require("./samples/catmull-rom.js");
6
+ const { fibonacciAst } = require("./samples/fibonacci.js");
7
+ const { splineFrameAsts } = require("./samples/spline-frame.js");
8
+ const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
9
+
10
+ const samples = { "catmull-rom": catmullRomAst, fibonacci: fibonacciAst, "kitchen-sink": kitchenSinkAst };
11
+
12
+ const outDir = path.join(__dirname, "out");
13
+ if (!fs.existsSync(outDir)) {
14
+ fs.mkdirSync(outDir);
15
+ }
16
+
17
+ for (const [sampleName, ast] of Object.entries(samples)) {
18
+ for (const [lang, emitter] of Object.entries(emitters)) {
19
+ const source = emitter.emitFunction(ast);
20
+ const outPath = path.join(outDir, `${sampleName}.generated.${emitter.ext}`);
21
+ fs.writeFileSync(outPath, source);
22
+ console.log(`[${lang}] wrote ${path.relative(__dirname, outPath)}`);
23
+ }
24
+ }
25
+
26
+ // spline-frame.js exports many functions (one per output component) rather
27
+ // than one AST, so it gets its own loop: one file per (function, language).
28
+ for (const ast of splineFrameAsts) {
29
+ for (const [lang, emitter] of Object.entries(emitters)) {
30
+ const source = emitter.emitFunction(ast);
31
+ const outPath = path.join(outDir, `spline-frame.${ast.name}.generated.${emitter.ext}`);
32
+ fs.writeFileSync(outPath, source);
33
+ console.log(`[${lang}] wrote ${path.relative(__dirname, outPath)}`);
34
+ }
35
+ }
@@ -0,0 +1,113 @@
1
+ // exprforge/emitters/base.js
2
+ //
3
+ // A language plugin is just a config object passed to `new Emitter(config)`.
4
+ // This is the extension point: adding a new language means writing ONE new
5
+ // file that builds an Emitter, not touching this file or any other emitter.
6
+ //
7
+ // config shape:
8
+ // ext — output file extension, e.g. "rs"
9
+ // formatNumber — (value: number) => string (literal syntax)
10
+ // calls — { [mathFnName]: (argStrs: string[]) => string }
11
+ // Covers BOTH "direct" (sqrt -> `sqrt(${x})`) and
12
+ // "expanded" (sign -> ternary expression) cases, and
13
+ // postfix/method-call languages like Rust (`${x}.sqrt()`),
14
+ // uniformly — it's just a string template either way.
15
+ // formatFunction — (fn: {name, params, body}, bodyStr: string, letBindings: {name, valueStr}[]) => string
16
+ // Full source text for one function, including any
17
+ // language-specific signature/type/wrapper syntax.
18
+ // letBindings is [] for let-free expressions.
19
+ // emitSelect — optional override: (condNode, thenStr, elseStr) => string
20
+ // Default is a ternary; QB64 has no ternary and overrides
21
+ // this with the equivalent arithmetic expression instead.
22
+ // formatSuite — (fn: {name, params}, outputStrs: {name: string}, letBindings) => string
23
+ // Only needed for targets that support multi-output
24
+ // suites (see ast.js's outputs()). Renders whatever
25
+ // multi-value idiom the language has. Required if any
26
+ // suite gets emitted through this emitter; omitted
27
+ // otherwise.
28
+
29
+ const { collectLets } = require("../ast.js");
30
+
31
+ class Emitter {
32
+ constructor(config) {
33
+ this.ext = config.ext;
34
+ this.formatNumber = config.formatNumber;
35
+ this.calls = config.calls || {};
36
+ this.formatFunctionImpl = config.formatFunction;
37
+ this.formatSuiteImpl = config.formatSuite || null;
38
+ this.emitSelectImpl = config.emitSelect
39
+ ? config.emitSelect.bind(this)
40
+ : this._defaultSelect.bind(this);
41
+ }
42
+
43
+ // Default ternary (cond ? a : b) — correct for JS, C, and Java, which
44
+ // all share this syntax. Go has no ternary at all (and `if` is a
45
+ // statement, not an expression); Rust uses `if`-as-expression instead
46
+ // of ?:; QB64 has no conditional expression syntax whatsoever. Those
47
+ // three override emitSelect with their own syntax — see their files.
48
+ _defaultSelect(condNode, thenStr, elseStr) {
49
+ const L = this.emitExpr(condNode.left);
50
+ const R = this.emitExpr(condNode.right);
51
+ return `((${L} ${condNode.op} ${R}) ? ${thenStr} : ${elseStr})`;
52
+ }
53
+
54
+ emitExpr(node) {
55
+ switch (node.type) {
56
+ case "num": {
57
+ return this.formatNumber(node.value);
58
+ }
59
+ case "var": {
60
+ return node.name;
61
+ }
62
+ case "bin": {
63
+ return `(${this.emitExpr(node.left)} ${node.op} ${this.emitExpr(node.right)})`;
64
+ }
65
+ case "call": {
66
+ const args = node.args.map((a) => this.emitExpr(a));
67
+ const template = this.calls[node.name];
68
+ if (!template) {
69
+ throw new Error(`emitter for .${this.ext}: no mapping for Math function "${node.name}"`);
70
+ }
71
+ return template(args);
72
+ }
73
+ case "select": {
74
+ const thenStr = this.emitExpr(node.then);
75
+ const elseStr = this.emitExpr(node.else);
76
+ return this.emitSelectImpl(node.cond, thenStr, elseStr);
77
+ }
78
+ case "cmp": {
79
+ // cmp only ever appears as a select's cond, consumed directly
80
+ // by emitSelectImpl above — it never reaches emitExpr in a
81
+ // well-formed tree. Landing here means a cmp node was used
82
+ // somewhere else (e.g. as a plain operand), which isn't
83
+ // supported: cmp isn't a general boolean expression.
84
+ throw new Error(`emitter for .${this.ext}: "cmp" is only valid inside a select() — got it elsewhere`);
85
+ }
86
+ default: {
87
+ throw new Error(`emitter for .${this.ext}: unknown node type "${node.type}"`);
88
+ }
89
+ }
90
+ }
91
+
92
+ emitFunction(fn) {
93
+ const { bindings, body } = collectLets(fn.body);
94
+ const letBindings = bindings.map(({ name, node }) => ({
95
+ name,
96
+ valueStr: this.emitExpr(node),
97
+ }));
98
+ if (body.type === "outputs") {
99
+ if (!this.formatSuiteImpl) {
100
+ throw new Error(`emitter for .${this.ext}: no formatSuite configured — multi-output suites aren't supported for this target yet`);
101
+ }
102
+ const outputStrs = {};
103
+ for (const [name, node] of Object.entries(body.fields)) {
104
+ outputStrs[name] = this.emitExpr(node);
105
+ }
106
+ return this.formatSuiteImpl(fn, outputStrs, letBindings);
107
+ }
108
+ const bodyStr = this.emitExpr(body);
109
+ return this.formatFunctionImpl(fn, bodyStr, letBindings);
110
+ }
111
+ }
112
+
113
+ module.exports = Emitter;
package/emitters/c.js ADDED
@@ -0,0 +1,64 @@
1
+ // exprforge/emitters/c.js
2
+ const Emitter = require("./base.js");
3
+
4
+ function fn1(name) {
5
+ return ([x]) => `${name}(${x})`;
6
+ }
7
+
8
+ function fn2(name) {
9
+ return ([a, b]) => `${name}(${a}, ${b})`;
10
+ }
11
+
12
+ const emitter = new Emitter({
13
+ ext: "c",
14
+ formatNumber: (v) => (Number.isInteger(v) ? `${v}.0` : String(v)),
15
+ calls: {
16
+ sqrt: fn1("sqrt"), abs: fn1("fabs"), 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("fmin"), max: fn2("fmax"), hypot: fn2("hypot"),
21
+ // No standard libm "sign" function — emit the comparison directly.
22
+ sign: ([x]) => `((${x}) > 0.0 ? 1.0 : ((${x}) < 0.0 ? -1.0 : 0.0))`,
23
+ },
24
+ formatFunction: (fn, body, letBindings = []) => {
25
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
26
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
27
+ const letsBlock = lets ? lets + "\n" : "";
28
+ return `/* AUTO-GENERATED by ExprForge -- do not hand-edit. */\n` +
29
+ `#include <math.h>\n\n` +
30
+ `double ${fn.name}(${params})\n` +
31
+ `{\n` +
32
+ letsBlock +
33
+ ` return ${body};\n` +
34
+ `}\n`;
35
+ },
36
+ // Multiple named outputs from one call: C has no native multi-return,
37
+ // so this emits a small out-struct alongside the function and returns
38
+ // it by value (a C99 designated-initializer compound literal) — named
39
+ // field access at the call site, not a positional tuple that's easy to
40
+ // mix up.
41
+ formatSuite: (fn, outputStrs, letBindings = []) => {
42
+ const params = fn.params.map((p) => `double ${p}`).join(", ");
43
+ const lets = letBindings.map(({ name, valueStr }) => ` double ${name} = ${valueStr};`).join("\n");
44
+ const letsBlock = lets ? lets + "\n" : "";
45
+ const outputNames = Object.keys(outputStrs);
46
+ const structName = `${capitalize(fn.name)}Result`;
47
+ const structFields = outputNames.map((n) => ` double ${n};`).join("\n");
48
+ const initFields = outputNames.map((n) => `.${n} = ${outputStrs[n]}`).join(", ");
49
+ return `/* AUTO-GENERATED by ExprForge -- do not hand-edit. */\n` +
50
+ `#include <math.h>\n\n` +
51
+ `typedef struct {\n${structFields}\n} ${structName};\n\n` +
52
+ `${structName} ${fn.name}(${params})\n` +
53
+ `{\n` +
54
+ letsBlock +
55
+ ` return (${structName}){ ${initFields} };\n` +
56
+ `}\n`;
57
+ },
58
+ });
59
+
60
+ function capitalize(s) {
61
+ return s.charAt(0).toUpperCase() + s.slice(1);
62
+ }
63
+
64
+ module.exports = emitter;