exprforge 0.1.0 ā 0.2.1
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/README.md +199 -16
- package/ast.js +22 -1
- package/emitters/cobol.js +410 -0
- package/emitters/fortran.js +169 -0
- package/emitters/julia.js +67 -0
- package/emitters/perl.js +95 -0
- package/emitters/php.js +79 -0
- package/emitters/registry.js +7 -0
- package/emitters/scheme.js +154 -0
- package/emitters/zig.js +126 -0
- package/index.js +7 -2
- package/math/index.js +113 -0
- package/package.json +9 -3
- package/samples/math-demo.js +48 -0
- package/samples/spline-frame.js +60 -42
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# ExprForge š¢šØ
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/exprforge)
|
|
3
4
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml)
|
|
4
5
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml)
|
|
5
6
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml)
|
|
@@ -9,10 +10,18 @@
|
|
|
9
10
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml)
|
|
10
11
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml)
|
|
11
12
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-rust.yml)
|
|
13
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-perl.yml)
|
|
14
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-php.yml)
|
|
15
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-julia.yml)
|
|
16
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-fortran.yml)
|
|
17
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-zig.yml)
|
|
18
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-scheme.yml)
|
|
19
|
+
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-cobol.yml)
|
|
12
20
|
|
|
13
21
|
Author a math expression once, as a small AST, and emit verified,
|
|
14
22
|
identical-behavior implementations in JavaScript, TypeScript, Python, C#,
|
|
15
|
-
Lua, QB64, C, Java, Go,
|
|
23
|
+
Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme
|
|
24
|
+
(Guile), and COBOL (GnuCOBOL).
|
|
16
25
|
|
|
17
26
|
No parser, no dependencies. You build the AST directly with plain JS
|
|
18
27
|
functions; the same tree is walked once per target language.
|
|
@@ -26,6 +35,23 @@ mainstream languages. This exists for two things SymPy doesn't do:
|
|
|
26
35
|
- A conformance test harness that actually proves the emitted targets
|
|
27
36
|
agree numerically, not just that they compile.
|
|
28
37
|
|
|
38
|
+
Two shapes of real use this tends to fall into:
|
|
39
|
+
|
|
40
|
+
- **Keeping concurrent codebases in sync.** A client/server split (game
|
|
41
|
+
client prediction + authoritative server, or any two independently
|
|
42
|
+
deployed services) where both sides need to compute the *same* formula
|
|
43
|
+
and disagree ā desync, or a cheat signal ā the moment they drift. One
|
|
44
|
+
AST, not two hand-maintained implementations that quietly diverge.
|
|
45
|
+
- **De-risking a migration.** Replacing an older implementation (a COBOL
|
|
46
|
+
batch job, a Fortran numerical kernel) with a new one doesn't require
|
|
47
|
+
trusting a manual port ā emit the same formula into both the legacy
|
|
48
|
+
target and the new one, and let the conformance suite prove they agree
|
|
49
|
+
before cutover, not after.
|
|
50
|
+
|
|
51
|
+
Neither is "translate my code for me" ā it's "prove two independent
|
|
52
|
+
implementations of one formula actually match," which is a narrower,
|
|
53
|
+
checkable claim.
|
|
54
|
+
|
|
29
55
|
## Install
|
|
30
56
|
|
|
31
57
|
```
|
|
@@ -69,6 +95,9 @@ package individually, or together as `samples`):
|
|
|
69
95
|
purely as a conformance-test fixture. It's what caught Go's and Rust's
|
|
70
96
|
`sign()` disagreeing with everyone else at exactly zero (see below) ā
|
|
71
97
|
the other samples between them only ever exercised 5 of the 22.
|
|
98
|
+
- `samples/math-demo.js` ā also not a worked example: a conformance-test
|
|
99
|
+
fixture exercising every `exprforge/math` helper (see below) in one
|
|
100
|
+
suite.
|
|
72
101
|
|
|
73
102
|
`npm run build` emits all of them, for every target language, into `out/`.
|
|
74
103
|
|
|
@@ -80,12 +109,75 @@ floor ceil round trunc sign min max hypot`
|
|
|
80
109
|
Add more by extending a target's `calls` table in `emitters/<lang>.js`.
|
|
81
110
|
Requesting an unmapped function throws at build time, not silently.
|
|
82
111
|
|
|
112
|
+
## Math utilities (`exprforge/math`)
|
|
113
|
+
|
|
114
|
+
A separate, additive export ā `require("exprforge")` is unchanged ā of
|
|
115
|
+
pre-built compositions of the core AST builders for common 3-D math
|
|
116
|
+
patterns, so consumers stop re-implementing the same safe-math and vector
|
|
117
|
+
code in every project (`samples/spline-frame.js` had local, hand-rolled
|
|
118
|
+
versions of most of these before this module existed).
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
const { num, v } = require("exprforge");
|
|
122
|
+
const { safeDiv, dot3, len3, cross3, normalize3, clamp, EPS } = require("exprforge/math");
|
|
123
|
+
|
|
124
|
+
// Safe-normalize x component, falling back to 0 near zero length.
|
|
125
|
+
safeDiv(v("x"), len3(v("x"), v("y"), v("z")), num(0));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- `safeDiv(numerator, denominatorExpr, fallback)` ā `numerator /
|
|
129
|
+
denominatorExpr` when `|denominatorExpr| > EPS`, else `fallback`. Clamps
|
|
130
|
+
the denominator before dividing rather than guarding the division
|
|
131
|
+
directly, since `select()` always evaluates both branches (see below).
|
|
132
|
+
- `dot3(ax, ay, az, bx, by, bz)` ā `ax*bx + ay*by + az*bz`.
|
|
133
|
+
- `len3(x, y, z)` ā `sqrt(x² + y² + z²)`.
|
|
134
|
+
- `cross3(ax, ay, az, bx, by, bz)` ā 3-D cross product. Returns a plain JS
|
|
135
|
+
object `{ x, y, z }` of AST nodes (not a Node itself), for destructuring
|
|
136
|
+
into your own `letIn` chain.
|
|
137
|
+
- `normalize3(x, y, z, fx?, fy?, fz?)` ā safe-normalize; same `{ x, y, z }`
|
|
138
|
+
shape as `cross3`. Falls back to `(fx, fy, fz)` (default `(0, 1, 0)`)
|
|
139
|
+
below `EPS` length. Computes the length once and shares it across all
|
|
140
|
+
three divisions.
|
|
141
|
+
- `clamp(val, lo, hi)` ā clamps to `[lo, hi]` via nested `select`/`cmp`; no
|
|
142
|
+
runtime intrinsic.
|
|
143
|
+
- `EPS` ā `num(0.000001)`, the epsilon every guard above uses; exported for
|
|
144
|
+
callers who want the same threshold in their own `cmp()` calls.
|
|
145
|
+
|
|
146
|
+
What deliberately stays out (project-specific conventions, not general
|
|
147
|
+
math): a "near-vertical" world-up check, baked-in-PI degree/radian
|
|
148
|
+
conversion, Rodrigues rotation, and a full Gram-Schmidt frame ā see
|
|
149
|
+
`samples/spline-frame.js` for those, and
|
|
150
|
+
`docs/v0.2.0-math-utilities.md` for the full design rationale.
|
|
151
|
+
|
|
83
152
|
## Adding a language
|
|
84
153
|
|
|
85
154
|
Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
|
|
86
155
|
existing file as a template), then add one line to
|
|
87
156
|
`emitters/registry.js`. Nothing else changes ā proven by the TypeScript
|
|
88
157
|
emitter, added with no changes to `base.js`, `build.js`, or `index.js`.
|
|
158
|
+
`Emitter` is a real class (not just a factory function), so a target that
|
|
159
|
+
needs to intercept how expressions themselves get rendered ā not just
|
|
160
|
+
`calls`/`emitSelect`/`formatFunction`, all ordinary config ā can subclass
|
|
161
|
+
it instead: Perl/PHP override `emitExpr`'s `"var"` case to add the `$`
|
|
162
|
+
sigil every reference needs, Scheme overrides the `"bin"` case for prefix
|
|
163
|
+
notation. See `emitters/scheme.js` and `emitters/perl.js`.
|
|
164
|
+
|
|
165
|
+
### Reserved-word collisions
|
|
166
|
+
|
|
167
|
+
Several emitters (QB64, Fortran, Zig, Scheme, COBOL) guard against a
|
|
168
|
+
generated variable/parameter/function name colliding with that language's
|
|
169
|
+
own reserved words or builtins ā a `<LANG>_RESERVED` set checked at
|
|
170
|
+
emission time, throwing a clear error instead of producing code that fails
|
|
171
|
+
to compile somewhere downstream with no context (see e.g. `QB64_RESERVED`
|
|
172
|
+
in `emitters/qb64.js`). **These lists are not, and can't practically be,
|
|
173
|
+
exhaustive** ā each covers the collisions that came up in this project's
|
|
174
|
+
own samples plus the obvious/common ones for that language, not every
|
|
175
|
+
reserved word in every language's full grammar. If you're naming your own
|
|
176
|
+
functions/params/`letIn` bindings, especially ones you know will target a
|
|
177
|
+
specific language, it's still on you to know that language's reserved
|
|
178
|
+
words ā Perl/PHP mostly sidestep this (every variable is `$`-sigiled, so
|
|
179
|
+
it can't collide with a bareword keyword), but the sigil-free languages
|
|
180
|
+
above genuinely can't be fully guarded against in advance.
|
|
89
181
|
|
|
90
182
|
## Named subexpressions and conditional values
|
|
91
183
|
|
|
@@ -97,6 +189,33 @@ expression model without introducing control flow:
|
|
|
97
189
|
components by it). Every `let` in a function gets lifted into an ordered
|
|
98
190
|
list of local declarations ahead of the return statement/expression, in
|
|
99
191
|
every target.
|
|
192
|
+
|
|
193
|
+
Chaining several is normally hand-nested `letIn` calls, one inside the
|
|
194
|
+
next, closing parens piling up at the end with no real hierarchy behind
|
|
195
|
+
them ā just bookkeeping to get everything hoisted before it's used.
|
|
196
|
+
**`letChain(bindings, body)`** is that same nesting, built for you from a
|
|
197
|
+
flat, ordered list instead:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const { v, num, mul, add, letChain, outputs } = require("exprforge");
|
|
201
|
+
|
|
202
|
+
letChain(
|
|
203
|
+
[
|
|
204
|
+
["t2", mul(v("t"), v("t"))],
|
|
205
|
+
["t3", mul(v("t2"), v("t"))],
|
|
206
|
+
],
|
|
207
|
+
outputs({ t2: v("t2"), t3: v("t3") }),
|
|
208
|
+
);
|
|
209
|
+
// same tree as letIn("t2", ..., letIn("t3", ..., outputs({...})))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`bindings` is an ordered array of `[name, valueNode]` pairs, not a
|
|
213
|
+
`{name: valueNode}` object like `outputs()` takes ā order is
|
|
214
|
+
load-bearing here (a later binding's value can reference an earlier
|
|
215
|
+
one's name), and that's clearer as an explicit sequence than resting on
|
|
216
|
+
an object's key order. Pure authoring sugar: builds the identical `let`
|
|
217
|
+
node structure `letIn` would, so it needs no emitter changes and
|
|
218
|
+
round-trips through `collectLets` the same way.
|
|
100
219
|
- **`select(cond, then, else)` + `cmp(left, op, right)`** ā conditional
|
|
101
220
|
*value* selection. Every target has a genuinely different way to spell
|
|
102
221
|
this: a native ternary where one exists (C, Java, C#), `if`-as-expression
|
|
@@ -143,11 +262,14 @@ multi-value idiom it has, since none of them agree:
|
|
|
143
262
|
| Target | Shape |
|
|
144
263
|
|---|---|
|
|
145
264
|
| JS | object literal |
|
|
146
|
-
| Go, Lua | native multiple return values |
|
|
147
|
-
| C
|
|
148
|
-
| C / Rust | a small `...Result` struct, returned by value |
|
|
265
|
+
| Go, Lua, Scheme | native multiple return values (`(values ...)` in Scheme) |
|
|
266
|
+
| C#, Julia | a native named value tuple / named tuple |
|
|
267
|
+
| C / Rust / Zig | a small `...Result` struct, returned by value |
|
|
149
268
|
| Java, Python | a nested/local `Result` class |
|
|
150
|
-
| QB64 | a `SUB` with the outputs as trailing by-reference parameters |
|
|
269
|
+
| QB64, Fortran | a `SUB`/`subroutine` with the outputs as trailing by-reference (`intent(out)`) parameters |
|
|
270
|
+
| Perl | a hash ref (`{ rx => ..., ry => ... }`) |
|
|
271
|
+
| PHP | an associative array (`['rx' => ..., 'ry' => ...]`) |
|
|
272
|
+
| COBOL | a callable `PROGRAM-ID`, every output as a trailing `BY REFERENCE` parameter, invoked via `CALL "name" USING ...` ā COBOL's *scalar* case uses this same shape too, not a `FUNCTION`-style return (see Testing below) |
|
|
151
273
|
|
|
152
274
|
Go specifically does **not** use *named* return values (`(rx, ry float64)`)
|
|
153
275
|
even though Go supports them and it reads nicer: those are sugar for
|
|
@@ -183,20 +305,21 @@ Runs `node --test`. For each sample, that's two kinds of check:
|
|
|
183
305
|
|
|
184
306
|
The compiled/interpreted-language checks need their toolchain on `PATH`
|
|
185
307
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
308
|
+
on any one machine. Every one of `tsc`/`qb64pe`/`dotnet`/`python3`/`lua`/
|
|
309
|
+
`perl`/`php`/`julia`/`gfortran`/`zig`/`guile3.0`/`cobc` is treated exactly
|
|
310
|
+
like gcc/go/rustc/javac: looked up on `PATH`, never a project
|
|
311
|
+
dependency ā exprforge only ever generates source text for these, it
|
|
312
|
+
doesn't execute or type-check any of it itself. `package.json` has zero
|
|
313
|
+
dependencies of any kind, matching this.
|
|
191
314
|
|
|
192
315
|
CI is one workflow file per target language (`.github/workflows/test-*.yml`),
|
|
193
316
|
run in parallel ā they have nothing to do with each other, so there's no
|
|
194
|
-
reason to serialize installing
|
|
195
|
-
built from source and cached by version, takes several minutes)
|
|
196
|
-
job, and splitting by file rather than by job within one file is
|
|
197
|
-
what gets each language its own real status badge above, not just
|
|
198
|
-
combined "did everything pass" badge. Each workflow installs only its
|
|
199
|
-
toolchain and runs `EXPRFORGE_TEST_TARGETS=<Label> npm test`; that
|
|
317
|
+
reason to serialize installing sixteen different toolchains (QB64-PE
|
|
318
|
+
alone, built from source and cached by version, takes several minutes)
|
|
319
|
+
into one job, and splitting by file rather than by job within one file is
|
|
320
|
+
also what gets each language its own real status badge above, not just
|
|
321
|
+
one combined "did everything pass" badge. Each workflow installs only its
|
|
322
|
+
own toolchain and runs `EXPRFORGE_TEST_TARGETS=<Label> npm test`; that
|
|
200
323
|
environment variable (read once in `test/conformance.test.js`) filters
|
|
201
324
|
the target lists down to just that one language, plus the toolchain-
|
|
202
325
|
independent JS/reference checks, which every workflow repeats ā cheap,
|
|
@@ -233,6 +356,66 @@ compiling/running against a real toolchain rather than assumed to work:
|
|
|
233
356
|
`math.atan2` (use two-argument `math.atan(y, x)`); there's no
|
|
234
357
|
`math.round` or `math.trunc` or `math.sign` at any version (manual
|
|
235
358
|
`floor(x+0.5)`, `math.modf(x)`, and an `and`/`or` chain respectively).
|
|
359
|
+
- **Perl / PHP**: every variable reference needs a `$` sigil, which
|
|
360
|
+
`base.js`'s shared `emitExpr` doesn't produce for anything ā both
|
|
361
|
+
subclass `Emitter` to override just the `"var"` case (see "Adding a
|
|
362
|
+
language" above) rather than needing a new hook every other emitter
|
|
363
|
+
would have to ignore. Perl has no `log2()`/`trunc()`/`hypot()` in core
|
|
364
|
+
(POSIX supplies `trunc`/`hypot`, `log2` is derived); PHP has no
|
|
365
|
+
`trunc()` at all (`floor`/`ceil` picked by sign instead, not an `(int)`
|
|
366
|
+
cast, which would misbehave outside PHP's platform integer range).
|
|
367
|
+
- **Julia**: `round()` defaults to ties-to-even (banker's rounding), not
|
|
368
|
+
ties-away-from-zero like every other target here ā
|
|
369
|
+
`round(x, RoundNearestTiesAway)` used explicitly to actually match,
|
|
370
|
+
not just avoid the untested case. `sign(-0.0)` returns `-0.0`, which is
|
|
371
|
+
numerically equal to `0.0` for the tolerance-based comparisons this
|
|
372
|
+
project uses, so it isn't a real divergence.
|
|
373
|
+
- **Fortran**: a literal without the `D0` exponent marker is parsed as
|
|
374
|
+
*single*-precision first, then widened ā silently losing precision
|
|
375
|
+
before it reaches a `real(8)` variable, unlike every other target's
|
|
376
|
+
literals ā so every literal gets it, not just ones already in
|
|
377
|
+
scientific notation. `FLOOR`/`CEILING` return the default `INTEGER`
|
|
378
|
+
kind, not `REAL`, wrapped back with `REAL(..., 8)`. No ternary, but
|
|
379
|
+
`MERGE(then, else, mask)` is a genuine expression-level conditional ā
|
|
380
|
+
confirmed to evaluate both branches regardless of `mask`, matching
|
|
381
|
+
`select()`'s own contract exactly. The native 2-argument `SIGN(A, B)`
|
|
382
|
+
("magnitude of A, sign of B") is *not* this project's `sign(x)` ā
|
|
383
|
+
`SIGN(1.0, 0.0)` returns `1.0`, not `0.0` ā built from `MERGE` instead.
|
|
384
|
+
- **Zig**: `std.debug.print` writes to **stderr** by design, not
|
|
385
|
+
stdout ā the conformance harness has to use
|
|
386
|
+
`std.io.getStdOut().writer()` instead, or every result silently comes
|
|
387
|
+
back empty. A fully-literal expression with no runtime operand (e.g.
|
|
388
|
+
`sqrt(2.0)` alone) gets evaluated at Zig's extended `comptime_float`
|
|
389
|
+
precision instead of truncated to an actual IEEE double, unless
|
|
390
|
+
explicitly `@as(f64, ...)`-cast ā every literal gets that cast, not
|
|
391
|
+
just ones that would otherwise hit this.
|
|
392
|
+
- **Scheme (Guile)**: a bare integer literal like `2` is *exact* in
|
|
393
|
+
Scheme's reader syntax, and exact arithmetic that never touches an
|
|
394
|
+
inexact (float) operand stays exact ā `(/ 1 3)` prints as the fraction
|
|
395
|
+
`1/3`, not `0.333...`. Every literal gets `.0` appended unless it
|
|
396
|
+
already has a decimal point or exponent, forcing inexactness by literal
|
|
397
|
+
syntax alone rather than relying on some other operand in the same
|
|
398
|
+
expression happening to already be a float.
|
|
399
|
+
- **COBOL (GnuCOBOL)**: has no expression-level conditional at all ā no
|
|
400
|
+
ternary, no `MERGE`-equivalent. `select()` is built from six small
|
|
401
|
+
helper `FUNCTION-ID` modules (one per comparator), but confirmed
|
|
402
|
+
against a real compile+run that a user-defined `FUNCTION` call
|
|
403
|
+
*silently miscomputes* ā no error, just a wrong number ā when given a
|
|
404
|
+
complex argument (one containing its own nested call); every argument
|
|
405
|
+
to a helper gets spilled into its own `COMPUTE`d temp first, always,
|
|
406
|
+
not just when an argument "looks complex." `BY VALUE` parameter passing
|
|
407
|
+
is explicitly flagged "unfinished" by the compiler ā every function
|
|
408
|
+
uses `BY REFERENCE` (the default) instead, which is also why COBOL is
|
|
409
|
+
the one target where even a *scalar* function's return value is a
|
|
410
|
+
trailing by-reference parameter (see the outputs table above), not a
|
|
411
|
+
`FUNCTION`-style return: calling a user `FUNCTION` by name breaks if
|
|
412
|
+
that name contains an underscore (confirmed against a real compiler),
|
|
413
|
+
while `CALL "name"` takes it as a plain string literal, immune to that.
|
|
414
|
+
Source lines have a real ~512-byte cap ā long expressions (e.g.
|
|
415
|
+
`samples/kitchen-sink.js`'s summed call to all 22 functions) get
|
|
416
|
+
wrapped at word boundaries. The native `FUNCTION SIGN` is
|
|
417
|
+
1-argument (`SIGN(x)`), unlike Fortran's identically-named
|
|
418
|
+
2-argument intrinsic ā and unlike Fortran's, is genuinely zero-safe.
|
|
236
419
|
|
|
237
420
|
One test (`normalizeX`) is deliberately excluded from the QB64 check
|
|
238
421
|
only: it exists specifically to demonstrate the "don't guard division
|
package/ast.js
CHANGED
|
@@ -62,6 +62,27 @@ function letIn(name, value, body) {
|
|
|
62
62
|
return { type: "let", name, value, body };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Chains N letIn bindings without hand-nesting them (and hand-balancing the
|
|
66
|
+
// resulting N closing parens ā the nesting depth reflects no real
|
|
67
|
+
// hierarchy, only that each binding must be lifted ahead of anything using
|
|
68
|
+
// it). Builds the exact same nested `let` structure letIn() would if
|
|
69
|
+
// written out by hand: pure authoring sugar, not a new node type, so
|
|
70
|
+
// collectLets and every emitter already understand the result unchanged.
|
|
71
|
+
//
|
|
72
|
+
// `bindings` is an ORDERED array of [name, valueNode] pairs, not a
|
|
73
|
+
// {name: valueNode} object like outputs() takes ā order is load-bearing
|
|
74
|
+
// here (a later binding's value can reference an earlier one's name via
|
|
75
|
+
// v(name)), and a plain object's key order isn't reliably that: a binding
|
|
76
|
+
// named e.g. "0" would silently sort ahead of everything else. An array
|
|
77
|
+
// keeps "this is a strict sequence" explicit instead of resting on that.
|
|
78
|
+
//
|
|
79
|
+
// Doesn't check for duplicate names itself ā collectLets already does,
|
|
80
|
+
// with the whole function body in view (see its doc comment); duplicating
|
|
81
|
+
// that check here would only see this one chain, not the whole picture.
|
|
82
|
+
function letChain(bindings, body) {
|
|
83
|
+
return bindings.reduceRight((acc, [name, value]) => letIn(name, value, acc), body);
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
// Comparison predicate ā only valid as the `cond` of a select(); not a
|
|
66
87
|
// general boolean expression, and shouldn't appear anywhere else in a tree.
|
|
67
88
|
function cmp(left, op, right) {
|
|
@@ -139,4 +160,4 @@ function collectLets(node) {
|
|
|
139
160
|
return { bindings, body };
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets };
|
|
163
|
+
module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets };
|