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
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
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml)
|
|
4
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml)
|
|
5
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml)
|
|
6
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-lua.yml)
|
|
7
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-qb64.yml)
|
|
8
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-c.yml)
|
|
9
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml)
|
|
10
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml)
|
|
11
|
+
[](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
|
+
}
|
package/emitters/base.js
ADDED
|
@@ -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;
|