exprforge 0.2.1 → 0.3.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/README.md +161 -6
- package/emitters/cobol.js +71 -3
- package/emitters/exprsyntax.js +81 -0
- package/emitters/registry.js +1 -0
- package/evaluate.js +109 -0
- package/expr.js +313 -0
- package/fn.js +117 -0
- package/index.js +14 -2
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
Author a math expression once, as a small AST, and emit verified,
|
|
22
22
|
identical-behavior implementations in JavaScript, TypeScript, Python, C#,
|
|
23
23
|
Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme
|
|
24
|
-
(Guile), and COBOL (GnuCOBOL)
|
|
24
|
+
(Guile), and COBOL (GnuCOBOL) — plus a native in-process evaluator and a
|
|
25
|
+
printer for exprforge's own readable syntax (see `fn`/`expr` below).
|
|
25
26
|
|
|
26
|
-
No
|
|
27
|
-
functions
|
|
27
|
+
No required dependencies. You can build the AST directly with plain JS
|
|
28
|
+
functions, or author it as readable infix text via `expr`/`fn` (see
|
|
29
|
+
below) — either way, the same tree is walked once per target.
|
|
28
30
|
|
|
29
31
|
## Why
|
|
30
32
|
|
|
@@ -52,6 +54,46 @@ Neither is "translate my code for me" — it's "prove two independent
|
|
|
52
54
|
implementations of one formula actually match," which is a narrower,
|
|
53
55
|
checkable claim.
|
|
54
56
|
|
|
57
|
+
## Layered by design
|
|
58
|
+
|
|
59
|
+
Everything here is layered, and the layers don't reach back into each
|
|
60
|
+
other — worth being explicit about, especially if you're evaluating this
|
|
61
|
+
for something like a migration and want to know exactly what you're
|
|
62
|
+
trusting:
|
|
63
|
+
|
|
64
|
+
- **The AST and its emitters — the whole value proposition.** `ast.js`'s
|
|
65
|
+
builders (`num`, `v`, `add`, `mul`, `letIn`, `select`, `outputs`, ...)
|
|
66
|
+
build a plain tree of plain objects; every `emitters/<lang>.js` file
|
|
67
|
+
turns that tree into target-language source text. This is also the
|
|
68
|
+
*entire* dependency graph for it: every emitter requires only
|
|
69
|
+
`emitters/base.js` and `ast.js` — nothing else in this repo. No parser,
|
|
70
|
+
no custom syntax, no interpreter sits between your AST and the code it
|
|
71
|
+
emits. Every example in `samples/` is built this way, and the library
|
|
72
|
+
worked exactly this way for its first two published releases, before
|
|
73
|
+
anything below existed.
|
|
74
|
+
- **`expr`/`fn` — optional authoring sugar.** Nested builder calls
|
|
75
|
+
(`add(mul(v("a"), v("b")), num(1))`) get hard to read past a few terms.
|
|
76
|
+
`expr`/`fn` are a small hand-rolled tokenizer and recursive-descent
|
|
77
|
+
parser that turn ordinary infix text (`` expr`a * b + 1` ``) into
|
|
78
|
+
*exactly* the same tree the builders would — checked by structural unit
|
|
79
|
+
tests and a full print-reparse-evaluate round trip across every sample
|
|
80
|
+
this project has (see "Testing" below), not just asserted. It's
|
|
81
|
+
genuinely optional: nothing in the AST/emitter layer above calls into
|
|
82
|
+
it or imports it. Don't want a parser in your dependency graph, for a
|
|
83
|
+
security review or otherwise? Don't call `expr`/`fn` — build the tree
|
|
84
|
+
with the plain functions instead, and every emitter behaves identically
|
|
85
|
+
either way.
|
|
86
|
+
- **The native evaluator and the `expr`-syntax printer — additive
|
|
87
|
+
conveniences.** `evaluate()` (compute a result in-process, no
|
|
88
|
+
target-language toolchain needed) and the `expr` emitter target (print
|
|
89
|
+
any AST back out as readable text) sit off to the side the same way
|
|
90
|
+
`expr`/`fn` do — nothing else depends on them either.
|
|
91
|
+
|
|
92
|
+
If you only care about "does this correctly turn my AST into
|
|
93
|
+
COBOL/Java/whatever" — `ast.js` and the one `emitters/<lang>.js` file you
|
|
94
|
+
care about are the entire surface that matters. Everything else is there
|
|
95
|
+
if you want it, invisible if you don't.
|
|
96
|
+
|
|
55
97
|
## Install
|
|
56
98
|
|
|
57
99
|
```
|
|
@@ -61,12 +103,12 @@ npm install exprforge
|
|
|
61
103
|
## Usage
|
|
62
104
|
|
|
63
105
|
```js
|
|
64
|
-
const {
|
|
106
|
+
const { expr, emitAll } = require("exprforge");
|
|
65
107
|
|
|
66
108
|
const fn = {
|
|
67
109
|
name: "lerp",
|
|
68
110
|
params: ["a", "b", "t"],
|
|
69
|
-
body:
|
|
111
|
+
body: expr`(b - a) * t + a`,
|
|
70
112
|
};
|
|
71
113
|
|
|
72
114
|
const outputs = emitAll(fn);
|
|
@@ -74,6 +116,13 @@ console.log(outputs.rust.source);
|
|
|
74
116
|
console.log(outputs.c.source);
|
|
75
117
|
```
|
|
76
118
|
|
|
119
|
+
`` expr`(b - a) * t + a` `` and `add(v("a"), mul(sub(v("b"), v("a")), v("t")))`
|
|
120
|
+
build the *exact same tree* — `expr` (see below) is optional infix syntax
|
|
121
|
+
sugar over the same builders, not a different API. Every example below
|
|
122
|
+
still uses the builders directly, since that's what `expr` compiles down
|
|
123
|
+
to and what you'll reach for once a formula needs `${...}`-spliced
|
|
124
|
+
sub-expressions.
|
|
125
|
+
|
|
77
126
|
## Samples
|
|
78
127
|
|
|
79
128
|
`samples/` has worked, non-trivial examples (also exported from the
|
|
@@ -236,6 +285,98 @@ See [`docs/planned-additions.md`](./docs/planned-additions.md) for the
|
|
|
236
285
|
full design rationale, including why the naive "guard division with
|
|
237
286
|
select" pattern is wrong.
|
|
238
287
|
|
|
288
|
+
## Infix expression syntax (`` expr` ` ``)
|
|
289
|
+
|
|
290
|
+
`add(mul(v("a"), v("b")), num(1))` is exactly what gets built, but it's
|
|
291
|
+
not what a human reads at a glance. `expr` is a tagged template literal
|
|
292
|
+
that parses ordinary infix math syntax into that same tree — same nodes,
|
|
293
|
+
different spelling, no new capability:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
const { v, expr } = require("exprforge");
|
|
297
|
+
|
|
298
|
+
expr`a * b + 1`
|
|
299
|
+
// identical tree to add(mul(v("a"), v("b")), num(1))
|
|
300
|
+
|
|
301
|
+
expr`(-b + sqrt(b^2 - 4*a*c)) / (2*a)`
|
|
302
|
+
// the quadratic formula, readable as the quadratic formula
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Syntax | Lowers to |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `+ - * /` | `add`/`sub`/`mul`/`div` — standard precedence, left-associative |
|
|
308
|
+
| `^` | `call("pow", base, exponent)` — **not** a `bin` node (there is no `"^"` operator in the AST; every emitter's `calls` table keys `pow` by name, even targets whose own syntax has a native `^`/`**`). Right-associative and binds *tighter* than unary minus, standard math convention: `-2^2` is `-4`, `2^3^2` is `512`. |
|
|
309
|
+
| `-x` | `neg(x)` |
|
|
310
|
+
| `name(args...)` | `call("name", ...args)` — not checked against the 22 known functions at parse time, same deferred-to-emission-time error every hand-built `call()` already gets |
|
|
311
|
+
| bare `name` | `v("name")` |
|
|
312
|
+
| `cond ? then : else` | `select(cmp(left, op, right), then, else)` — the **only** place a comparison (`> < >= <= == !=`) is valid, matching `cmp()`'s own documented constraint that it's never a general boolean expression. A bare `a > b` with no `?` is a parse-time error, not a deferred one. Chains naturally: `a>0 ? 1 : b>0 ? 2 : 3`. |
|
|
313
|
+
| `${...}` | Splices in an existing AST node as-is, or a plain JS number (auto-wrapped via `num()`). Anything else throws immediately. Plain strings aren't interpolatable — a bare identifier in the template text already means "variable", with no `${}` needed. |
|
|
314
|
+
|
|
315
|
+
Deliberately **not** in the grammar: `let`/`outputs` blocks (it's a pure
|
|
316
|
+
expression grammar, same "expression AST, not a program AST" boundary as
|
|
317
|
+
the rest of exprforge — wrap the result in `letIn`/`letChain`/`outputs`,
|
|
318
|
+
or reach for `fn` below, which adds exactly that) and `&&`/`||` (the AST
|
|
319
|
+
has no boolean-combinator node to lower them to).
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// Named subexpressions still go around expr(), not inside it:
|
|
323
|
+
letIn("mag", expr`sqrt(x^2 + y^2)`, expr`x / mag`)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Full-program syntax (`` fn`...` ``)
|
|
327
|
+
|
|
328
|
+
`expr` covers one expression; `fn` covers a whole function body —
|
|
329
|
+
`let` bindings plus a `return`, on top of the exact same expression
|
|
330
|
+
grammar (every expression inside a `fn` template is parsed by the same
|
|
331
|
+
engine `expr` uses). Lowers to real `letChain`/`outputs` calls, same
|
|
332
|
+
"same nodes, different spelling" guarantee as `expr` itself:
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
const { fn } = require("exprforge");
|
|
336
|
+
|
|
337
|
+
const body = fn`
|
|
338
|
+
let mag = sqrt(x^2 + y^2);
|
|
339
|
+
return { nx: x / mag, ny: y / mag };
|
|
340
|
+
`;
|
|
341
|
+
// identical tree to:
|
|
342
|
+
// letIn("mag", call("sqrt", ...), outputs({ nx: div(v("x"), v("mag")), ny: ... }))
|
|
343
|
+
|
|
344
|
+
const normalize2 = { name: "normalize2", params: ["x", "y"], body };
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
| Syntax | Lowers to |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `let name = expr;` | one `[name, valueNode]` pair, in order — a later `let` can reference an earlier one's name |
|
|
350
|
+
| `return expr;` | the chain's final expression |
|
|
351
|
+
| `return { name: expr, ... };` | `outputs({ name: node, ... })` as the chain's final expression |
|
|
352
|
+
|
|
353
|
+
Duplicate `let` names aren't rejected by the parser itself — same
|
|
354
|
+
deferred-to-`collectLets` behavior every hand-built `letIn`/`letChain`
|
|
355
|
+
already has. A `fn` body with no `let` statements at all is just
|
|
356
|
+
`return expr;`, equivalent to a bare `expr` call.
|
|
357
|
+
|
|
358
|
+
## Printing an AST back out, and a native evaluator
|
|
359
|
+
|
|
360
|
+
Two things that fall out of `fn` existing: `emitters.expr` is a real,
|
|
361
|
+
registered 18th target that prints any AST *back out* as `fn`/`expr`
|
|
362
|
+
source text (the reverse of parsing it) — useful for debugging a
|
|
363
|
+
formula built from several composed helpers, or just getting a readable
|
|
364
|
+
string to log or paste into a future `fn`/`expr` call. And `evaluate(fn,
|
|
365
|
+
args)` (also exported from the main package) is a native tree-walking
|
|
366
|
+
interpreter over the same AST, computing a result directly in JS with no
|
|
367
|
+
codegen or compile step — the same node types every emitter already
|
|
368
|
+
handles, backed by the real `Math.*` functions.
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
const { emitAll, evaluate } = require("exprforge");
|
|
372
|
+
|
|
373
|
+
emitAll(normalize2).expr.source;
|
|
374
|
+
// "let mag = sqrt(((x^2) + (y^2)));\nreturn { nx: (x / mag), ny: (y / mag) };\n"
|
|
375
|
+
|
|
376
|
+
evaluate(normalize2, [3, 4]);
|
|
377
|
+
// { nx: 0.6, ny: 0.8 }
|
|
378
|
+
```
|
|
379
|
+
|
|
239
380
|
## Multiple named outputs
|
|
240
381
|
|
|
241
382
|
`outputs({ name: Node, ... })` computes several named values from ONE
|
|
@@ -295,13 +436,27 @@ pre-declared locals the way Go's named returns are.
|
|
|
295
436
|
npm test
|
|
296
437
|
```
|
|
297
438
|
|
|
298
|
-
Runs `node --test`. For each sample, that's
|
|
439
|
+
Runs `node --test`. For each sample, that's three kinds of check:
|
|
299
440
|
|
|
300
441
|
- Emitted JS vs. an independently hand-written reference implementation
|
|
301
442
|
(catches a wrong formula in the AST itself).
|
|
302
443
|
- Every other emitted target vs. that same JS, compiled (and, for
|
|
303
444
|
TypeScript, also type-checked under `--strict`) and run, with the sample
|
|
304
445
|
inputs as arguments (catches an emitter bug).
|
|
446
|
+
- The `expr`-syntax printer (`emitters/exprsyntax.js`) vs. `fn`'s own
|
|
447
|
+
parser: every sample AST in this suite is printed back out as `fn`/
|
|
448
|
+
`expr` source text, reparsed, and evaluated (via `evaluate()`) to
|
|
449
|
+
confirm the round trip behaves identically to the original. This is a
|
|
450
|
+
stronger claim than either piece being separately unit-tested — the
|
|
451
|
+
printer and the parser are two independent pieces of code that have to
|
|
452
|
+
agree with each other across every real formula this project has, not
|
|
453
|
+
just cases either one's own author thought to hand-write a test for.
|
|
454
|
+
It's also not hypothetical: this exact check caught a real bug during
|
|
455
|
+
development (a ternary printed without enough parens, so
|
|
456
|
+
`crossX / (rLen > eps ? rLen : 1)` reparsed with the wrong grouping)
|
|
457
|
+
that every other check here — including full cross-language conformance
|
|
458
|
+
— had no way to catch, since it's specific to the printer/parser pair
|
|
459
|
+
and nothing else in the pipeline touches that code path.
|
|
305
460
|
|
|
306
461
|
The compiled/interpreted-language checks need their toolchain on `PATH`
|
|
307
462
|
and skip (not fail) when it's missing, so `npm test` degrades gracefully
|
package/emitters/cobol.js
CHANGED
|
@@ -62,6 +62,35 @@ function checkReservedNames(names) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Separate from COBOL_RESERVED above (general keywords/intrinsics, safe
|
|
66
|
+
// to forbid everywhere including internal let-bindings) and checked only
|
|
67
|
+
// against names that end up in a CALL/PROCEDURE DIVISION USING clause
|
|
68
|
+
// (fn.name, params, output fields) -- NOT let-bindings. A bare "c"
|
|
69
|
+
// (case-insensitive) breaks specifically in a USING identifier list --
|
|
70
|
+
// confirmed against a real compiler ("syntax error, unexpected C"),
|
|
71
|
+
// reproducible in isolation, and not shared by neighboring single letters
|
|
72
|
+
// (b/d/e/... all compile fine in the identical position; likely GnuCOBOL
|
|
73
|
+
// misparsing it as an attempted abbreviation of BY CONTENT). Deliberately
|
|
74
|
+
// NOT added to COBOL_RESERVED: a plain WORKING-STORAGE item named "c"
|
|
75
|
+
// referenced only in COMPUTE statements compiles fine (confirmed too),
|
|
76
|
+
// and samples/spline-frame.js already has a working "c" let-binding (for
|
|
77
|
+
// cos(rad)) that would break for no real reason if this were checked
|
|
78
|
+
// there as well.
|
|
79
|
+
const COBOL_USING_RESERVED = new Set(["c"]);
|
|
80
|
+
|
|
81
|
+
function checkUsingClauseNames(names) {
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
if (COBOL_USING_RESERVED.has(name.toLowerCase())) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`emitter for .cob: "${name}" can't be a function/parameter/output name -- it breaks ` +
|
|
86
|
+
`GnuCOBOL's CALL ... USING clause specifically (confirmed against a real compiler), ` +
|
|
87
|
+
`even though it's fine as an internal let-binding name (see COBOL_USING_RESERVED in ` +
|
|
88
|
+
`emitters/cobol.js) -- rename it`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
function fn1(name) {
|
|
66
95
|
return ([x]) => `FUNCTION ${name}(${x})`;
|
|
67
96
|
}
|
|
@@ -241,6 +270,35 @@ function expandExponential(s) {
|
|
|
241
270
|
return sign + result;
|
|
242
271
|
}
|
|
243
272
|
|
|
273
|
+
// "**" must never end up nested inside ANOTHER call's argument -- that's
|
|
274
|
+
// the actual, general form of the confirmed bug (see the file header):
|
|
275
|
+
// some GnuCOBOL 4.x builds route a "**" expression through a broken
|
|
276
|
+
// internal decimal codegen path (missing header for cob_decimal) when
|
|
277
|
+
// it's nested inside a FUNCTION(...) argument, e.g. sqrt(a^2 + b^2) --
|
|
278
|
+
// confirmed against real CI, not reproducible against this project's own
|
|
279
|
+
// (older) local GnuCOBOL. A plain top-level "**" (kitchen-sink's
|
|
280
|
+
// pow(x, y), summed directly, never nested as another call's argument)
|
|
281
|
+
// is fine.
|
|
282
|
+
//
|
|
283
|
+
// Rather than rely on every caller remembering to keep a pow() result out
|
|
284
|
+
// of anywhere risky, pow ALWAYS spills its own result here: call("pow", a,
|
|
285
|
+
// b) unconditionally evaluates to a bare temp name, so "**" can only ever
|
|
286
|
+
// appear in its own dedicated COMPUTE statement, never nested inside
|
|
287
|
+
// anything else regardless of what the surrounding expression does with
|
|
288
|
+
// the result. hypot's internal sum-of-squares reuses this directly, since
|
|
289
|
+
// it builds "**" itself rather than going through call("pow", ...).
|
|
290
|
+
//
|
|
291
|
+
// Arrow function callers below, not plain ones: base.js's emitExpr calls
|
|
292
|
+
// `this.calls[node.name](args)` unbound -- this closes over the `emitter`
|
|
293
|
+
// const the same way atan2 already does (see its own comment further
|
|
294
|
+
// down), fully assigned by the time this ever actually runs.
|
|
295
|
+
function spillPow(baseRaw, exponentRaw) {
|
|
296
|
+
const pool = emitter._pool;
|
|
297
|
+
const base = pool.spill(baseRaw);
|
|
298
|
+
const exponent = pool.spill(exponentRaw);
|
|
299
|
+
return pool.spill(`(${base} ** ${exponent})`);
|
|
300
|
+
}
|
|
301
|
+
|
|
244
302
|
const emitter = new CobolEmitter({
|
|
245
303
|
ext: "cob",
|
|
246
304
|
formatNumber: (v) => {
|
|
@@ -252,13 +310,21 @@ const emitter = new CobolEmitter({
|
|
|
252
310
|
asin: fn1("ASIN"), acos: fn1("ACOS"), atan: fn1("ATAN"),
|
|
253
311
|
exp: fn1("EXP"), log: fn1("LOG"), log10: fn1("LOG10"),
|
|
254
312
|
min: fn2("MIN"), max: fn2("MAX"),
|
|
255
|
-
pow: ([x, y]) =>
|
|
313
|
+
pow: ([x, y]) => spillPow(x, y),
|
|
256
314
|
// No LOG2 intrinsic -- derive it (nesting two intrinsics is fine;
|
|
257
315
|
// only nesting a call inside a USER-DEFINED function's argument
|
|
258
316
|
// was the confirmed problem -- see the file header).
|
|
259
317
|
log2: ([x]) => `(FUNCTION LOG(${x}) / FUNCTION LOG(2.0))`,
|
|
260
|
-
// No HYPOT intrinsic -- derive it
|
|
261
|
-
|
|
318
|
+
// No HYPOT intrinsic -- derive it via spillPow (see its own
|
|
319
|
+
// comment): the sum-of-squares itself also gets spilled to its
|
|
320
|
+
// own temp before FUNCTION SQRT ever sees it, so SQRT's argument
|
|
321
|
+
// is always a bare name, never a "**"-containing expression.
|
|
322
|
+
hypot: ([aRaw, bRaw]) => {
|
|
323
|
+
const aSq = spillPow(aRaw, "2.0");
|
|
324
|
+
const bSq = spillPow(bRaw, "2.0");
|
|
325
|
+
const sumSq = emitter._pool.spill(`${aSq} + ${bSq}`);
|
|
326
|
+
return `FUNCTION SQRT(${sumSq})`;
|
|
327
|
+
},
|
|
262
328
|
// FUNCTION INTEGER is floor (greatest integer <= x, confirmed
|
|
263
329
|
// against a real compiler, including for negatives). COMPUTE's
|
|
264
330
|
// automatic numeric conversion hands it back as COMP-2 with no
|
|
@@ -358,6 +424,7 @@ const emitter = new CobolEmitter({
|
|
|
358
424
|
// the program name as a plain string literal, immune to that.
|
|
359
425
|
formatFunction: (fn, body, letLines, letDecls, bodyLines) => {
|
|
360
426
|
checkReservedNames([fn.name, ...fn.params]);
|
|
427
|
+
checkUsingClauseNames([fn.name, ...fn.params]);
|
|
361
428
|
const linkageParams = [...fn.params, "ef-result"];
|
|
362
429
|
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
363
430
|
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
@@ -382,6 +449,7 @@ const emitter = new CobolEmitter({
|
|
|
382
449
|
formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
|
|
383
450
|
const outputNames = Object.keys(outputStrs);
|
|
384
451
|
checkReservedNames([fn.name, ...fn.params, ...outputNames]);
|
|
452
|
+
checkUsingClauseNames([fn.name, ...fn.params, ...outputNames]);
|
|
385
453
|
const linkageParams = [...fn.params, ...outputNames];
|
|
386
454
|
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
387
455
|
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// exprforge/emitters/exprsyntax.js
|
|
2
|
+
//
|
|
3
|
+
// Prints an AST back out as fn`...`/expr`...`-compatible source text --
|
|
4
|
+
// the reverse direction of expr.js/fn.js. Not a third-party language:
|
|
5
|
+
// there's no external compiler/interpreter to run this output through,
|
|
6
|
+
// so it's verified by round-trip instead (emit -> reparse via fn() ->
|
|
7
|
+
// deepStrictEqual the original, see test/exprsyntax-emitter.test.js).
|
|
8
|
+
// Registered as a real target anyway (emitters.expr) since fn/expr
|
|
9
|
+
// syntax is a real, standalone, human-pasteable output format in its
|
|
10
|
+
// own right, not just a test fixture.
|
|
11
|
+
//
|
|
12
|
+
// "call" is overridden wholesale rather than populated via a `calls`
|
|
13
|
+
// table: every intrinsic passes through uniformly as `name(args...)`
|
|
14
|
+
// except pow, which recovers fn/expr's own `^` sugar -- both forms
|
|
15
|
+
// lower back to the identical call("pow", ...) node (see expr.js's
|
|
16
|
+
// grammar comment on rule 2), so this is a readability choice, not a
|
|
17
|
+
// correctness one. A side effect: this is the one emitter that never
|
|
18
|
+
// needs a table update when a new intrinsic is added anywhere else in
|
|
19
|
+
// the project.
|
|
20
|
+
const Emitter = require("./base.js");
|
|
21
|
+
|
|
22
|
+
class ExprSyntaxEmitter extends Emitter {
|
|
23
|
+
emitExpr(node) {
|
|
24
|
+
if (node.type === "call" && node.name === "pow" && node.args.length === 2) {
|
|
25
|
+
const [base, exponent] = node.args.map((a) => this.emitExpr(a));
|
|
26
|
+
return `(${base}^${exponent})`;
|
|
27
|
+
}
|
|
28
|
+
if (node.type === "call") {
|
|
29
|
+
const args = node.args.map((a) => this.emitExpr(a));
|
|
30
|
+
return `${node.name}(${args.join(", ")})`;
|
|
31
|
+
}
|
|
32
|
+
return super.emitExpr(node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function letLines(letBindings) {
|
|
37
|
+
return letBindings.map(({ name, valueStr }) => `let ${name} = ${valueStr};`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const emitter = new ExprSyntaxEmitter({
|
|
41
|
+
ext: "fn",
|
|
42
|
+
// fn/expr accepts JS-style numeric literal syntax directly -- it's
|
|
43
|
+
// literally the same NUMBER lexing rule (see expr.js's tokenizer).
|
|
44
|
+
formatNumber: (v) => String(v),
|
|
45
|
+
// base.js's default _defaultSelect wraps just the comparison in its
|
|
46
|
+
// own parens ("((L op R) ? then : else)") -- fn/expr's ternary
|
|
47
|
+
// grammar has no parenthesized-COMPARISON form (see expr.js's
|
|
48
|
+
// grammar comment: `ternary := additive compOp additive "?" ...`),
|
|
49
|
+
// so "(L op R)" alone would try to parse as a complete, paren-
|
|
50
|
+
// wrapped ternary condition with no "?" in sight and hit the parser's
|
|
51
|
+
// own "comparison must be used as a ternary condition" error. This
|
|
52
|
+
// wraps the WHOLE ternary in one outer paren pair instead -- still a
|
|
53
|
+
// valid `primary := ... | "(" expression ")" | ...`, and unlike the
|
|
54
|
+
// unwrapped form, safe to embed as a sub-expression anywhere (e.g.
|
|
55
|
+
// `crossX / (rLen > eps ? rLen : 1)`) without the surrounding
|
|
56
|
+
// operator's precedence reaching into the ternary's own condition.
|
|
57
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
58
|
+
const L = this.emitExpr(condNode.left);
|
|
59
|
+
const R = this.emitExpr(condNode.right);
|
|
60
|
+
return `(${L} ${condNode.op} ${R} ? ${thenStr} : ${elseStr})`;
|
|
61
|
+
},
|
|
62
|
+
// Output is bare fn/expr source text -- no JS wrapper/boilerplate.
|
|
63
|
+
// That's both the most direct thing to paste into a real
|
|
64
|
+
// `` fn`...` `` call, and exactly what the round-trip test reparses
|
|
65
|
+
// with zero unwrapping first.
|
|
66
|
+
formatFunction: (fn, bodyStr, letBindings = []) => {
|
|
67
|
+
const lines = letLines(letBindings);
|
|
68
|
+
lines.push(`return ${bodyStr};`);
|
|
69
|
+
return lines.join("\n") + "\n";
|
|
70
|
+
},
|
|
71
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
72
|
+
const lines = letLines(letBindings);
|
|
73
|
+
const fields = Object.entries(outputStrs)
|
|
74
|
+
.map(([name, valueStr]) => `${name}: ${valueStr}`)
|
|
75
|
+
.join(", ");
|
|
76
|
+
lines.push(`return { ${fields} };`);
|
|
77
|
+
return lines.join("\n") + "\n";
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = emitter;
|
package/emitters/registry.js
CHANGED
package/evaluate.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// exprforge/evaluate.js
|
|
2
|
+
//
|
|
3
|
+
// A native tree-walking interpreter over the exact same AST every
|
|
4
|
+
// emitter compiles from -- evaluate(fn, args) computes a result (or a
|
|
5
|
+
// {name: value} object for a multi-output suite) directly in JS, no
|
|
6
|
+
// codegen/compile/subprocess step involved. Reuses collectLets (ast.js)
|
|
7
|
+
// for the same let-lifting every emitter already goes through, so this
|
|
8
|
+
// walks nodes in the identical dependency order every target does, and
|
|
9
|
+
// there's exactly one node-shape contract (ast.js's own header comment)
|
|
10
|
+
// for both this file and every emitters/<lang>.js to agree with.
|
|
11
|
+
//
|
|
12
|
+
// Every intrinsic name maps 1:1 onto emitters/js.js's own `calls` table
|
|
13
|
+
// keys (the simplest existing source of truth for "what the ~22
|
|
14
|
+
// intrinsics are called") straight to the real Math.* function -- this
|
|
15
|
+
// target has no codegen step to route an intermediate string through.
|
|
16
|
+
const { collectLets } = require("./ast.js");
|
|
17
|
+
|
|
18
|
+
const CMP_OPS = {
|
|
19
|
+
">": (a, b) => a > b,
|
|
20
|
+
"<": (a, b) => a < b,
|
|
21
|
+
">=": (a, b) => a >= b,
|
|
22
|
+
"<=": (a, b) => a <= b,
|
|
23
|
+
"==": (a, b) => a === b,
|
|
24
|
+
"!=": (a, b) => a !== b,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const BIN_OPS = {
|
|
28
|
+
"+": (a, b) => a + b,
|
|
29
|
+
"-": (a, b) => a - b,
|
|
30
|
+
"*": (a, b) => a * b,
|
|
31
|
+
"/": (a, b) => a / b,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CALLS = {
|
|
35
|
+
sqrt: Math.sqrt, abs: Math.abs, sin: Math.sin, cos: Math.cos, tan: Math.tan,
|
|
36
|
+
asin: Math.asin, acos: Math.acos, atan: Math.atan, log: Math.log,
|
|
37
|
+
log2: Math.log2, log10: Math.log10, exp: Math.exp, floor: Math.floor,
|
|
38
|
+
ceil: Math.ceil, round: Math.round, trunc: Math.trunc, sign: Math.sign,
|
|
39
|
+
pow: Math.pow, atan2: Math.atan2, min: Math.min, max: Math.max, hypot: Math.hypot,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Handles every node type EXCEPT "let"/"outputs" -- those are only ever
|
|
43
|
+
// valid pre-collectLets (a function's top-level let-chain/body shape),
|
|
44
|
+
// never nested inside a bin/call/select, same constraint every emitter
|
|
45
|
+
// already relies on (see ast.js's own comments on letIn/outputs).
|
|
46
|
+
function evalNode(node, env) {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "num":
|
|
49
|
+
return node.value;
|
|
50
|
+
case "var":
|
|
51
|
+
if (!(node.name in env)) {
|
|
52
|
+
throw new Error(`evaluate(): unbound variable "${node.name}"`);
|
|
53
|
+
}
|
|
54
|
+
return env[node.name];
|
|
55
|
+
case "bin": {
|
|
56
|
+
const op = BIN_OPS[node.op];
|
|
57
|
+
if (!op) throw new Error(`evaluate(): unknown bin op "${node.op}"`);
|
|
58
|
+
return op(evalNode(node.left, env), evalNode(node.right, env));
|
|
59
|
+
}
|
|
60
|
+
case "call": {
|
|
61
|
+
const impl = CALLS[node.name];
|
|
62
|
+
if (!impl) throw new Error(`evaluate(): no mapping for Math function "${node.name}"`);
|
|
63
|
+
return impl(...node.args.map((a) => evalNode(a, env)));
|
|
64
|
+
}
|
|
65
|
+
case "select": {
|
|
66
|
+
const cmpFn = CMP_OPS[node.cond.op];
|
|
67
|
+
if (!cmpFn) throw new Error(`evaluate(): unknown cmp op "${node.cond.op}"`);
|
|
68
|
+
const cond = cmpFn(evalNode(node.cond.left, env), evalNode(node.cond.right, env));
|
|
69
|
+
return evalNode(cond ? node.then : node.else, env);
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(
|
|
73
|
+
`evaluate(): unexpected node type "${node.type}" -- "let"/"outputs" must already ` +
|
|
74
|
+
`be lifted out by collectLets before evalNode runs`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// evaluate(fn, args) -- fn is a {name, params, body} definition (the
|
|
80
|
+
// same shape emitAll() consumes), args is a plain array positional to
|
|
81
|
+
// fn.params. Returns a number for a plain body, or a {name: value}
|
|
82
|
+
// object for a multi-output (outputs()) body -- matching the shape
|
|
83
|
+
// test/conformance.test.js's own parseSuiteOutput() already expects
|
|
84
|
+
// back from every other target.
|
|
85
|
+
function evaluate(fn, args) {
|
|
86
|
+
if (args.length !== fn.params.length) {
|
|
87
|
+
throw new Error(`evaluate(): ${fn.name} expects ${fn.params.length} argument(s), got ${args.length}`);
|
|
88
|
+
}
|
|
89
|
+
const env = {};
|
|
90
|
+
fn.params.forEach((name, i) => {
|
|
91
|
+
env[name] = args[i];
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const { bindings, body } = collectLets(fn.body);
|
|
95
|
+
for (const { name, node } of bindings) {
|
|
96
|
+
env[name] = evalNode(node, env);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (body.type === "outputs") {
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const [name, node] of Object.entries(body.fields)) {
|
|
102
|
+
result[name] = evalNode(node, env);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
return evalNode(body, env);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { evaluate };
|
package/expr.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// exprforge/expr.js
|
|
2
|
+
//
|
|
3
|
+
// Infix syntax sugar over ast.js's own builders -- NOT a new node type or
|
|
4
|
+
// a new capability. `` expr`a * b + 1` `` builds exactly the same tree
|
|
5
|
+
// `add(mul(v("a"), v("b")), num(1))` would, by calling num/v/add/sub/mul/
|
|
6
|
+
// div/neg/call/cmp/select directly (never constructing a raw {type: ...}
|
|
7
|
+
// object by hand), so every guarantee those builders already have
|
|
8
|
+
// (collectLets round-tripping, emitter compatibility) carries over for
|
|
9
|
+
// free. Unlike util.js's forComponents, this DOES return a Node -- it's
|
|
10
|
+
// closer in spirit to letChain (also in ast.js): a different way to
|
|
11
|
+
// spell the same tree, not a different tree.
|
|
12
|
+
//
|
|
13
|
+
// A tagged template literal, not a plain string function: `expr` is
|
|
14
|
+
// called by JS itself as expr(strings, ...values) -- see the grammar
|
|
15
|
+
// comment below for why that's the whole interpolation mechanism, with
|
|
16
|
+
// no `${` text syntax of its own to parse.
|
|
17
|
+
//
|
|
18
|
+
// Grammar:
|
|
19
|
+
//
|
|
20
|
+
// expression := ternary
|
|
21
|
+
// ternary := additive ( compOp additive "?" expression ":" expression )?
|
|
22
|
+
// compOp := ">" | "<" | ">=" | "<=" | "==" | "!="
|
|
23
|
+
// additive := multiplicative ( ("+"|"-") multiplicative )*
|
|
24
|
+
// multiplicative := unary ( ("*"|"/") unary )*
|
|
25
|
+
// unary := "-" unary | power
|
|
26
|
+
// power := primary ( "^" unary )?
|
|
27
|
+
// primary := NUMBER | IDENT ("(" args ")")? | "(" expression ")" | HOLE
|
|
28
|
+
// args := expression ("," expression)*
|
|
29
|
+
//
|
|
30
|
+
// Deliberately NOT supported (see the plan doc's "Scope decisions"):
|
|
31
|
+
// - No let/outputs blocks -- this is a pure expression grammar, same
|
|
32
|
+
// "expression AST, not a program AST" boundary as ast.js itself.
|
|
33
|
+
// Wrap the result in letIn/letChain/outputs instead.
|
|
34
|
+
// - No &&/|| -- the AST has no boolean-combinator node to lower them
|
|
35
|
+
// to. A comparison is ONLY ever valid as a ternary's condition
|
|
36
|
+
// (matching cmp()'s own documented constraint in ast.js), enforced
|
|
37
|
+
// here at PARSE time with a clear error, not deferred to the
|
|
38
|
+
// "cmp used outside select()" throw every emitter already has.
|
|
39
|
+
// - "^" lowers to call("pow", left, right), never a bin node -- bin.op
|
|
40
|
+
// is only ever "+"|"-"|"*"|"/" (see ast.js), and every emitter's
|
|
41
|
+
// `calls` table keys "pow" by name, even for targets whose own
|
|
42
|
+
// syntax has a native ^/** operator.
|
|
43
|
+
const { num, v, add, sub, mul, div, neg, call, cmp, select } = require("./ast.js");
|
|
44
|
+
|
|
45
|
+
const COMPARE_OPS = [">", "<", ">=", "<=", "==", "!="];
|
|
46
|
+
|
|
47
|
+
// Tokenizes one template-literal string segment, appending {type, value,
|
|
48
|
+
// pos} tokens to `tokens` (pos is an offset into the reconstructed full
|
|
49
|
+
// source string built in expr() below, used only for error messages).
|
|
50
|
+
// `label` is just which tag function's name shows up in error messages
|
|
51
|
+
// -- fn.js passes "fn()" here so a lex error inside `` fn`...` `` isn't
|
|
52
|
+
// misattributed to expr().
|
|
53
|
+
function tokenizeSegment(str, offset, tokens, label = "expr()") {
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < str.length) {
|
|
56
|
+
const ch = str[i];
|
|
57
|
+
const start = i;
|
|
58
|
+
if (/\s/.test(ch)) {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// NUMBER: 123, 123.45, .5, 1e-9, 1.5E+10
|
|
63
|
+
if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(str[i + 1] || ""))) {
|
|
64
|
+
i++;
|
|
65
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
66
|
+
if (str[i] === ".") {
|
|
67
|
+
i++;
|
|
68
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
69
|
+
}
|
|
70
|
+
if (str[i] === "e" || str[i] === "E") {
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
if (str[j] === "+" || str[j] === "-") j++;
|
|
73
|
+
if (/[0-9]/.test(str[j] || "")) {
|
|
74
|
+
i = j;
|
|
75
|
+
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
tokens.push({ type: "NUMBER", value: Number(str.slice(start, i)), pos: offset + start });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// IDENT: variable names and function names, e.g. wy_wire, sqrt.
|
|
82
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
83
|
+
i++;
|
|
84
|
+
while (i < str.length && /[A-Za-z0-9_]/.test(str[i])) i++;
|
|
85
|
+
tokens.push({ type: "IDENT", value: str.slice(start, i), pos: offset + start });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Two-character comparison operators before their one-character
|
|
89
|
+
// prefixes, so ">=" doesn't get lexed as ">" followed by "=".
|
|
90
|
+
if ((ch === ">" || ch === "<" || ch === "=" || ch === "!") && str[i + 1] === "=") {
|
|
91
|
+
tokens.push({ type: "OP", value: str.slice(i, i + 2), pos: offset + start });
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// ";", "{", "}", "=" aren't used by expr()'s own grammar -- they're
|
|
96
|
+
// here for fn.js's statement syntax (let name = ...; / return
|
|
97
|
+
// {...};) to reuse this same tokenizer instead of forking it.
|
|
98
|
+
// Inert for expr(): nothing that parses successfully today could
|
|
99
|
+
// contain them anyway ("=" alone was always a lex error before,
|
|
100
|
+
// since only "==" was recognized).
|
|
101
|
+
if ("+-*/^(),?:><;{}=".includes(ch)) {
|
|
102
|
+
tokens.push({ type: "OP", value: ch, pos: offset + start });
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`${label}: unexpected character "${ch}" at position ${offset + start}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A HOLE's value is resolved to a Node right where it's produced (not
|
|
111
|
+
// deferred into the parser), so a bad interpolation fails immediately
|
|
112
|
+
// with a clear error rather than surfacing as a confusing parse error
|
|
113
|
+
// somewhere else in the tree. `label` -- see tokenizeSegment above.
|
|
114
|
+
function holeToNode(value, label = "expr()") {
|
|
115
|
+
if (typeof value === "number") return num(value);
|
|
116
|
+
if (value && typeof value === "object" && typeof value.type === "string") return value;
|
|
117
|
+
const shown = typeof value === "string" ? `"${value}"` : JSON.stringify(value);
|
|
118
|
+
throw new Error(
|
|
119
|
+
`${label}: interpolated value must be an AST node or a plain number, got ${shown} -- ` +
|
|
120
|
+
`a bare variable name doesn't need interpolation, just write it directly in the template text`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class Parser {
|
|
125
|
+
// `label` -- see tokenizeSegment above; also threaded through to
|
|
126
|
+
// holeToNode so a bad interpolation inside `` fn`...` `` reports
|
|
127
|
+
// "fn():" too, not just lex/parse errors.
|
|
128
|
+
constructor(tokens, source, label = "expr()") {
|
|
129
|
+
this.tokens = tokens;
|
|
130
|
+
this.source = source;
|
|
131
|
+
this.i = 0;
|
|
132
|
+
this.label = label;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
peek() {
|
|
136
|
+
return this.tokens[this.i];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
next() {
|
|
140
|
+
return this.tokens[this.i++];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isOp(value) {
|
|
144
|
+
const t = this.peek();
|
|
145
|
+
return t.type === "OP" && t.value === value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expectOp(value) {
|
|
149
|
+
if (!this.isOp(value)) this.error(`expected "${value}"`);
|
|
150
|
+
return this.next();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
error(message) {
|
|
154
|
+
const t = this.peek();
|
|
155
|
+
const tokDesc = t.type === "EOF" ? "end of input" : `"${t.value}"`;
|
|
156
|
+
throw new Error(`${this.label}: ${message} -- found ${tokDesc} at position ${t.pos} in \`${this.source}\``);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parseExpression() {
|
|
160
|
+
return this.parseTernary();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Only place a comparison is ever accepted -- matches cmp()'s own
|
|
164
|
+
// documented constraint in ast.js exactly (only valid as select()'s
|
|
165
|
+
// cond). A bare comparison with no trailing "?" is a parse error
|
|
166
|
+
// here, not deferred to the "cmp used outside select()" throw every
|
|
167
|
+
// emitter already has.
|
|
168
|
+
parseTernary() {
|
|
169
|
+
const left = this.parseAdditive();
|
|
170
|
+
const t = this.peek();
|
|
171
|
+
if (t.type === "OP" && COMPARE_OPS.includes(t.value)) {
|
|
172
|
+
const op = this.next().value;
|
|
173
|
+
const right = this.parseAdditive();
|
|
174
|
+
if (!this.isOp("?")) {
|
|
175
|
+
this.error(`comparison ("${op}") must be used as a ternary condition ("cond ${op} ... ? then : else")`);
|
|
176
|
+
}
|
|
177
|
+
this.next(); // consume "?"
|
|
178
|
+
const thenNode = this.parseExpression();
|
|
179
|
+
this.expectOp(":");
|
|
180
|
+
const elseNode = this.parseExpression();
|
|
181
|
+
return select(cmp(left, op, right), thenNode, elseNode);
|
|
182
|
+
}
|
|
183
|
+
if (this.isOp("?")) {
|
|
184
|
+
this.error(`"?" needs an explicit comparison as its condition (e.g. "a > 0 ? x : y") -- a bare value can't be a select() condition`);
|
|
185
|
+
}
|
|
186
|
+
return left;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
parseAdditive() {
|
|
190
|
+
let node = this.parseMultiplicative();
|
|
191
|
+
while (this.isOp("+") || this.isOp("-")) {
|
|
192
|
+
const op = this.next().value;
|
|
193
|
+
const right = this.parseMultiplicative();
|
|
194
|
+
node = op === "+" ? add(node, right) : sub(node, right);
|
|
195
|
+
}
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
parseMultiplicative() {
|
|
200
|
+
let node = this.parseUnary();
|
|
201
|
+
while (this.isOp("*") || this.isOp("/")) {
|
|
202
|
+
const op = this.next().value;
|
|
203
|
+
const right = this.parseUnary();
|
|
204
|
+
node = op === "*" ? mul(node, right) : div(node, right);
|
|
205
|
+
}
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Unary minus binds LOOSER than "^" (-2^2 = -4, not 4) -- standard
|
|
210
|
+
// math convention, and the reason `power` sits below `unary` here
|
|
211
|
+
// rather than the other way around.
|
|
212
|
+
parseUnary() {
|
|
213
|
+
if (this.isOp("-")) {
|
|
214
|
+
this.next();
|
|
215
|
+
return neg(this.parseUnary());
|
|
216
|
+
}
|
|
217
|
+
if (this.isOp("+")) {
|
|
218
|
+
this.next(); // unary plus: no-op
|
|
219
|
+
return this.parseUnary();
|
|
220
|
+
}
|
|
221
|
+
return this.parsePower();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Right-associative (2^3^2 = 2^(3^2) = 512): the exponent recurses
|
|
225
|
+
// into `unary`, not `power`, which is also what lets the exponent
|
|
226
|
+
// itself carry a leading unary minus (2^-1 = 0.5).
|
|
227
|
+
parsePower() {
|
|
228
|
+
const base = this.parsePrimary();
|
|
229
|
+
if (this.isOp("^")) {
|
|
230
|
+
this.next();
|
|
231
|
+
const exponent = this.parseUnary();
|
|
232
|
+
return call("pow", base, exponent);
|
|
233
|
+
}
|
|
234
|
+
return base;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parsePrimary() {
|
|
238
|
+
const t = this.peek();
|
|
239
|
+
if (t.type === "NUMBER") {
|
|
240
|
+
this.next();
|
|
241
|
+
return num(t.value);
|
|
242
|
+
}
|
|
243
|
+
if (t.type === "HOLE") {
|
|
244
|
+
this.next();
|
|
245
|
+
return holeToNode(t.value, this.label);
|
|
246
|
+
}
|
|
247
|
+
if (t.type === "IDENT") {
|
|
248
|
+
this.next();
|
|
249
|
+
if (this.isOp("(")) {
|
|
250
|
+
this.next();
|
|
251
|
+
const args = [];
|
|
252
|
+
if (!this.isOp(")")) {
|
|
253
|
+
args.push(this.parseExpression());
|
|
254
|
+
while (this.isOp(",")) {
|
|
255
|
+
this.next();
|
|
256
|
+
args.push(this.parseExpression());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.expectOp(")");
|
|
260
|
+
// Not validated against the 22 known Math functions here
|
|
261
|
+
// -- deferred to the same "no mapping for Math function"
|
|
262
|
+
// check every hand-built call() node already goes
|
|
263
|
+
// through in emitters/base.js, so there's only one list
|
|
264
|
+
// of known function names to keep in sync, not two.
|
|
265
|
+
return call(t.value, ...args);
|
|
266
|
+
}
|
|
267
|
+
return v(t.value);
|
|
268
|
+
}
|
|
269
|
+
if (this.isOp("(")) {
|
|
270
|
+
this.next();
|
|
271
|
+
const node = this.parseExpression();
|
|
272
|
+
this.expectOp(")");
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
this.error("expected a number, identifier, function call, or parenthesized expression");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The tagged-template tag function itself: `` expr`a + b` `` is called by
|
|
280
|
+
// JS as expr(["a + b"], ) -- with interpolations, `` expr`${x} + b` ``
|
|
281
|
+
// is called as expr(["", " + b"], x). strings.length is always
|
|
282
|
+
// values.length + 1. There is no "${" text syntax to lex: JS has already
|
|
283
|
+
// done that splitting before this function ever runs, so a HOLE token is
|
|
284
|
+
// just spliced into the token stream at each boundary, carrying the
|
|
285
|
+
// already-evaluated JS value through untouched.
|
|
286
|
+
function expr(strings, ...values) {
|
|
287
|
+
const tokens = [];
|
|
288
|
+
let source = "";
|
|
289
|
+
for (let i = 0; i < strings.length; i++) {
|
|
290
|
+
tokenizeSegment(strings[i], source.length, tokens);
|
|
291
|
+
source += strings[i];
|
|
292
|
+
if (i < values.length) {
|
|
293
|
+
tokens.push({ type: "HOLE", value: values[i], pos: source.length });
|
|
294
|
+
source += "${...}";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
tokens.push({ type: "EOF", value: null, pos: source.length });
|
|
298
|
+
|
|
299
|
+
const parser = new Parser(tokens, source);
|
|
300
|
+
const node = parser.parseExpression();
|
|
301
|
+
if (parser.peek().type !== "EOF") {
|
|
302
|
+
parser.error("unexpected trailing input");
|
|
303
|
+
}
|
|
304
|
+
return node;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Parser/tokenizeSegment/holeToNode are exported alongside expr itself so
|
|
308
|
+
// fn.js (full function-body syntax: let/return on top of this same
|
|
309
|
+
// expression grammar) can reuse this tokenizer and parsing engine
|
|
310
|
+
// directly instead of forking it -- "fn's contain expr's" literally, not
|
|
311
|
+
// just as a description. Nothing here is part of expr()'s own public
|
|
312
|
+
// contract; treat these as internal to the expr/fn syntax family.
|
|
313
|
+
module.exports = { expr, Parser, tokenizeSegment, holeToNode };
|
package/fn.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// exprforge/fn.js
|
|
2
|
+
//
|
|
3
|
+
// Full function-body syntax on top of expr.js's expression grammar: adds
|
|
4
|
+
// `let` bindings and a `return` statement, so a whole function body
|
|
5
|
+
// (let-chain + a single or multi-output result) can be authored as text
|
|
6
|
+
// instead of nested letChain()/outputs() calls. Every individual
|
|
7
|
+
// expression inside a `fn` template -- each let's value, the returned
|
|
8
|
+
// expression(s) -- is parsed by the *same* Parser class expr.js uses,
|
|
9
|
+
// via its parseExpression() entry point. fn's own grammar is a thin
|
|
10
|
+
// statement-sequence wrapper around that, lowering to the real ast.js
|
|
11
|
+
// builders (letChain, outputs), never a new node shape:
|
|
12
|
+
//
|
|
13
|
+
// program := stmt* returnStmt
|
|
14
|
+
// stmt := "let" IDENT "=" expression ";"
|
|
15
|
+
// returnStmt := "return" expression ";"
|
|
16
|
+
// | "return" "{" IDENT ":" expression ("," IDENT ":" expression)* "}" ";"
|
|
17
|
+
//
|
|
18
|
+
// "let"/"return" are recognized contextually -- an IDENT token whose
|
|
19
|
+
// value happens to be "let"/"return" at statement-start position. They
|
|
20
|
+
// are NOT reserved words in expr.js's own grammar, so nothing about
|
|
21
|
+
// expr()'s behavior changes: `` expr`let * 2` `` still means
|
|
22
|
+
// v("let") * 2 today, same as before this file existed.
|
|
23
|
+
//
|
|
24
|
+
// Duplicate let-names are deliberately NOT checked here -- letChain()
|
|
25
|
+
// doesn't check either (ast.js); collectLets() already does, at
|
|
26
|
+
// emission time. Same "defer semantic validation to emission" precedent
|
|
27
|
+
// expr.js itself follows for function/call names.
|
|
28
|
+
const { letChain, outputs } = require("./ast.js");
|
|
29
|
+
const { Parser, tokenizeSegment } = require("./expr.js");
|
|
30
|
+
|
|
31
|
+
function isKeyword(parser, word) {
|
|
32
|
+
const t = parser.peek();
|
|
33
|
+
return t.type === "IDENT" && t.value === word;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expectIdent(parser, context) {
|
|
37
|
+
const t = parser.peek();
|
|
38
|
+
if (t.type !== "IDENT") {
|
|
39
|
+
parser.error(`expected an identifier ${context}`);
|
|
40
|
+
}
|
|
41
|
+
parser.next();
|
|
42
|
+
return t.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseLetStatement(parser) {
|
|
46
|
+
parser.next(); // consume "let", already confirmed present by the caller
|
|
47
|
+
const name = expectIdent(parser, 'after "let"');
|
|
48
|
+
parser.expectOp("=");
|
|
49
|
+
const value = parser.parseExpression();
|
|
50
|
+
parser.expectOp(";");
|
|
51
|
+
return [name, value];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseReturnStatement(parser) {
|
|
55
|
+
parser.next(); // consume "return", already confirmed present by the caller
|
|
56
|
+
if (parser.isOp("{")) {
|
|
57
|
+
parser.next();
|
|
58
|
+
const fields = {};
|
|
59
|
+
const readField = () => {
|
|
60
|
+
const name = expectIdent(parser, 'as an output name inside "return { ... }"');
|
|
61
|
+
parser.expectOp(":");
|
|
62
|
+
fields[name] = parser.parseExpression();
|
|
63
|
+
};
|
|
64
|
+
if (!parser.isOp("}")) {
|
|
65
|
+
readField();
|
|
66
|
+
while (parser.isOp(",")) {
|
|
67
|
+
parser.next();
|
|
68
|
+
readField();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
parser.expectOp("}");
|
|
72
|
+
parser.expectOp(";");
|
|
73
|
+
return outputs(fields);
|
|
74
|
+
}
|
|
75
|
+
const node = parser.parseExpression();
|
|
76
|
+
parser.expectOp(";");
|
|
77
|
+
return node;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseProgram(parser) {
|
|
81
|
+
const bindings = [];
|
|
82
|
+
while (isKeyword(parser, "let")) {
|
|
83
|
+
bindings.push(parseLetStatement(parser));
|
|
84
|
+
}
|
|
85
|
+
if (!isKeyword(parser, "return")) {
|
|
86
|
+
parser.error('expected "return" (a fn`...` body is zero or more "let" statements followed by a "return")');
|
|
87
|
+
}
|
|
88
|
+
const body = parseReturnStatement(parser);
|
|
89
|
+
return bindings.length > 0 ? letChain(bindings, body) : body;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Same token-splicing loop expr() uses in expr.js -- see that file's
|
|
93
|
+
// header comment for why there's no "${" text syntax to lex separately;
|
|
94
|
+
// the only difference here is the entry point (parseProgram instead of
|
|
95
|
+
// parser.parseExpression()).
|
|
96
|
+
function fn(strings, ...values) {
|
|
97
|
+
const tokens = [];
|
|
98
|
+
let source = "";
|
|
99
|
+
for (let i = 0; i < strings.length; i++) {
|
|
100
|
+
tokenizeSegment(strings[i], source.length, tokens, "fn()");
|
|
101
|
+
source += strings[i];
|
|
102
|
+
if (i < values.length) {
|
|
103
|
+
tokens.push({ type: "HOLE", value: values[i], pos: source.length });
|
|
104
|
+
source += "${...}";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
tokens.push({ type: "EOF", value: null, pos: source.length });
|
|
108
|
+
|
|
109
|
+
const parser = new Parser(tokens, source, "fn()");
|
|
110
|
+
const node = parseProgram(parser);
|
|
111
|
+
if (parser.peek().type !== "EOF") {
|
|
112
|
+
parser.error("unexpected trailing input");
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { fn };
|
package/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// exprforge/index.js
|
|
2
2
|
const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets } = require("./ast.js");
|
|
3
3
|
const { forComponents } = require("./util.js");
|
|
4
|
+
const { expr } = require("./expr.js");
|
|
5
|
+
const { fn } = require("./fn.js");
|
|
6
|
+
const { evaluate } = require("./evaluate.js");
|
|
4
7
|
const emitters = require("./emitters/registry.js");
|
|
5
8
|
const { catmullRomAst } = require("./samples/catmull-rom.js");
|
|
6
9
|
const { fibonacciAst } = require("./samples/fibonacci.js");
|
|
@@ -12,10 +15,10 @@ const { mathDemoAst } = require("./samples/math-demo.js");
|
|
|
12
15
|
* Run every registered emitter against one AST function definition.
|
|
13
16
|
* Returns { [lang]: { ext, source } }.
|
|
14
17
|
*/
|
|
15
|
-
function emitAll(
|
|
18
|
+
function emitAll(fnDef) {
|
|
16
19
|
const result = {};
|
|
17
20
|
for (const [lang, emitter] of Object.entries(emitters)) {
|
|
18
|
-
result[lang] = { ext: emitter.ext, source: emitter.emitFunction(
|
|
21
|
+
result[lang] = { ext: emitter.ext, source: emitter.emitFunction(fnDef) };
|
|
19
22
|
}
|
|
20
23
|
return result;
|
|
21
24
|
}
|
|
@@ -25,6 +28,15 @@ module.exports = {
|
|
|
25
28
|
num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets,
|
|
26
29
|
// Authoring convenience — not an AST primitive, see util.js.
|
|
27
30
|
forComponents,
|
|
31
|
+
// Infix syntax sugar over the builders above — same Nodes, see expr.js.
|
|
32
|
+
expr,
|
|
33
|
+
// Full function-body syntax (let/return) on top of expr's grammar —
|
|
34
|
+
// see fn.js. "fn's contain expr's": every expression inside a fn`...`
|
|
35
|
+
// template is parsed by the exact same engine expr() uses.
|
|
36
|
+
fn,
|
|
37
|
+
// A native interpreter over the AST -- evaluate(fn, args) computes a
|
|
38
|
+
// result directly in JS, no codegen/compile step. See evaluate.js.
|
|
39
|
+
evaluate,
|
|
28
40
|
// Built-in example formulas — see samples/ for the source.
|
|
29
41
|
catmullRomAst,
|
|
30
42
|
fibonacciAst,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exprforge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Author a math expression once as an AST (or readable infix text via expr/fn), emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL, plus a native evaluator and its own readable syntax printer.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"exports": {
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"index.js",
|
|
14
14
|
"ast.js",
|
|
15
15
|
"util.js",
|
|
16
|
+
"expr.js",
|
|
17
|
+
"fn.js",
|
|
18
|
+
"evaluate.js",
|
|
16
19
|
"build.js",
|
|
17
20
|
"emitters/",
|
|
18
21
|
"samples/",
|