exprforge 0.2.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 +128 -16
- 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/math/index.js +9 -1
- package/package.json +2 -2
- package/samples/math-demo.js +8 -4
package/README.md
CHANGED
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml)
|
|
11
11
|
[](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml)
|
|
12
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)
|
|
13
20
|
|
|
14
21
|
Author a math expression once, as a small AST, and emit verified,
|
|
15
22
|
identical-behavior implementations in JavaScript, TypeScript, Python, C#,
|
|
16
|
-
Lua, QB64, C, Java, Go,
|
|
23
|
+
Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme
|
|
24
|
+
(Guile), and COBOL (GnuCOBOL).
|
|
17
25
|
|
|
18
26
|
No parser, no dependencies. You build the AST directly with plain JS
|
|
19
27
|
functions; the same tree is walked once per target language.
|
|
@@ -27,6 +35,23 @@ mainstream languages. This exists for two things SymPy doesn't do:
|
|
|
27
35
|
- A conformance test harness that actually proves the emitted targets
|
|
28
36
|
agree numerically, not just that they compile.
|
|
29
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
|
+
|
|
30
55
|
## Install
|
|
31
56
|
|
|
32
57
|
```
|
|
@@ -130,6 +155,29 @@ Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
|
|
|
130
155
|
existing file as a template), then add one line to
|
|
131
156
|
`emitters/registry.js`. Nothing else changes — proven by the TypeScript
|
|
132
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.
|
|
133
181
|
|
|
134
182
|
## Named subexpressions and conditional values
|
|
135
183
|
|
|
@@ -214,11 +262,14 @@ multi-value idiom it has, since none of them agree:
|
|
|
214
262
|
| Target | Shape |
|
|
215
263
|
|---|---|
|
|
216
264
|
| JS | object literal |
|
|
217
|
-
| Go, Lua | native multiple return values |
|
|
218
|
-
| C
|
|
219
|
-
| 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 |
|
|
220
268
|
| Java, Python | a nested/local `Result` class |
|
|
221
|
-
| 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) |
|
|
222
273
|
|
|
223
274
|
Go specifically does **not** use *named* return values (`(rx, ry float64)`)
|
|
224
275
|
even though Go supports them and it reads nicer: those are sugar for
|
|
@@ -254,20 +305,21 @@ Runs `node --test`. For each sample, that's two kinds of check:
|
|
|
254
305
|
|
|
255
306
|
The compiled/interpreted-language checks need their toolchain on `PATH`
|
|
256
307
|
and skip (not fail) when it's missing, so `npm test` degrades gracefully
|
|
257
|
-
on any one machine. Every one of `tsc`/`qb64pe`/`dotnet`/`python3`/`lua
|
|
258
|
-
is treated exactly
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
262
314
|
|
|
263
315
|
CI is one workflow file per target language (`.github/workflows/test-*.yml`),
|
|
264
316
|
run in parallel — they have nothing to do with each other, so there's no
|
|
265
|
-
reason to serialize installing
|
|
266
|
-
built from source and cached by version, takes several minutes)
|
|
267
|
-
job, and splitting by file rather than by job within one file is
|
|
268
|
-
what gets each language its own real status badge above, not just
|
|
269
|
-
combined "did everything pass" badge. Each workflow installs only its
|
|
270
|
-
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
|
|
271
323
|
environment variable (read once in `test/conformance.test.js`) filters
|
|
272
324
|
the target lists down to just that one language, plus the toolchain-
|
|
273
325
|
independent JS/reference checks, which every workflow repeats — cheap,
|
|
@@ -304,6 +356,66 @@ compiling/running against a real toolchain rather than assumed to work:
|
|
|
304
356
|
`math.atan2` (use two-argument `math.atan(y, x)`); there's no
|
|
305
357
|
`math.round` or `math.trunc` or `math.sign` at any version (manual
|
|
306
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.
|
|
307
419
|
|
|
308
420
|
One test (`normalizeX`) is deliberately excluded from the QB64 check
|
|
309
421
|
only: it exists specifically to demonstrate the "don't guard division
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// exprforge/emitters/cobol.js
|
|
2
|
+
//
|
|
3
|
+
// Targets GnuCOBOL (free-format source, USAGE COMP-2 for double precision)
|
|
4
|
+
// -- see test/conformance.test.js for the exact `cobc` invocation this is
|
|
5
|
+
// verified against.
|
|
6
|
+
//
|
|
7
|
+
// select()/cmp() needed a genuinely different emission strategy than every
|
|
8
|
+
// other target here, because of one real, confirmed-by-compiling
|
|
9
|
+
// structural limit: GnuCOBOL has no expression-level conditional at all --
|
|
10
|
+
// no ternary, no Fortran-style MERGE(). A user-defined `FUNCTION-ID`
|
|
11
|
+
// module CAN be called from inside an expression (`FUNCTION name(...)`),
|
|
12
|
+
// which looks like a way to build one (a tiny "pick a branch" helper
|
|
13
|
+
// function) -- but confirmed against a real compile+run that GnuCOBOL 3.2
|
|
14
|
+
// silently miscomputes (no error, just a wrong/garbage numeric result)
|
|
15
|
+
// when such a call receives a COMPLEX argument, i.e. one containing its
|
|
16
|
+
// own nested FUNCTION call, rather than a bare variable or literal. Every
|
|
17
|
+
// real select() usage has exactly that shape (see
|
|
18
|
+
// samples/spline-frame.js, math/index.js's safeDiv/clamp/normalize3).
|
|
19
|
+
//
|
|
20
|
+
// The fix: six small helper FUNCTION-ID modules (one per comparator, EF_CMP
|
|
21
|
+
// below), always prepended to the emitted source, PLUS forcing every
|
|
22
|
+
// argument passed to them through its own COMPUTE into a fresh temp
|
|
23
|
+
// variable first -- confirmed safe, since a bare-variable-argument call to
|
|
24
|
+
// a user-defined FUNCTION-ID was the one case that worked correctly in
|
|
25
|
+
// testing. That turns emitSelect from a pure "node -> expression string"
|
|
26
|
+
// function (every other emitter's shape, including this file's own calls
|
|
27
|
+
// table) into a STATEFUL one that also spills COMPUTE statements -- see
|
|
28
|
+
// the CobolEmitter class below, which is the one thing in this file that
|
|
29
|
+
// isn't just config passed to `new Emitter(...)` like every other target.
|
|
30
|
+
//
|
|
31
|
+
// No FUNCTION ATAN2 exists in GnuCOBOL either (FUNCTION ATAN is
|
|
32
|
+
// 1-argument only), and the standard quadrant-corrected formula needs
|
|
33
|
+
// exactly this same branching -- built from the same select-hoisting
|
|
34
|
+
// machinery as select() itself, not a separate mechanism.
|
|
35
|
+
const Emitter = require("./base.js");
|
|
36
|
+
|
|
37
|
+
// COBOL reserved words plus every intrinsic-function name this emitter's
|
|
38
|
+
// calls table depends on -- same role as QB64_RESERVED in emitters/qb64.js.
|
|
39
|
+
// COBOL is case-insensitive, so names are checked lowercased. Not
|
|
40
|
+
// exhaustive (COBOL's real reserved-word list runs into the hundreds), but
|
|
41
|
+
// covers the words a generated variable/parameter/function name could
|
|
42
|
+
// plausibly collide with in practice.
|
|
43
|
+
const COBOL_RESERVED = new Set([
|
|
44
|
+
"identification", "division", "program-id", "function-id", "environment",
|
|
45
|
+
"configuration", "repository", "data", "working-storage", "linkage", "section",
|
|
46
|
+
"procedure", "using", "returning", "intent", "usage", "comp-2", "value", "by",
|
|
47
|
+
"reference", "content", "if", "else", "end-if", "compute", "move", "to", "call",
|
|
48
|
+
"goback", "stop", "run", "display", "perform", "until", "end-perform", "and",
|
|
49
|
+
"or", "not", "true", "false", "zero", "zeros", "zeroes", "spaces", "high-value",
|
|
50
|
+
"low-value", "function", "sqrt", "abs", "sin", "cos", "tan", "asin", "acos",
|
|
51
|
+
"atan", "exp", "log", "log10", "integer", "integer-part", "sign", "min", "max", "mod",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
function checkReservedNames(names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
if (COBOL_RESERVED.has(name.toLowerCase())) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`emitter for .cob: "${name}" is a reserved COBOL word/intrinsic and can't be used as a ` +
|
|
59
|
+
`function/variable/parameter name -- rename it (see COBOL_RESERVED in emitters/cobol.js)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fn1(name) {
|
|
66
|
+
return ([x]) => `FUNCTION ${name}(${x})`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fn2(name) {
|
|
70
|
+
return ([a, b]) => `FUNCTION ${name}(${a}, ${b})`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// One helper FUNCTION-ID per comparator, each named ef-cmp-<suffix> --
|
|
74
|
+
// hyphenated, never underscored (confirmed against a real compiler that a
|
|
75
|
+
// user-defined FUNCTION call breaks on an underscored name -- see
|
|
76
|
+
// samples/spline-frame.js's "wy_wire" param for why that's not just a
|
|
77
|
+
// theoretical concern). "ne" uses NOT = rather than the symbolic <>,
|
|
78
|
+
// simply because NOT = is unambiguously standard COBOL and <> wasn't worth
|
|
79
|
+
// separately confirming for a helper this narrow.
|
|
80
|
+
const CMP_HELPERS = {
|
|
81
|
+
">": { suffix: "gt", test: "L > R" },
|
|
82
|
+
"<": { suffix: "lt", test: "L < R" },
|
|
83
|
+
">=": { suffix: "ge", test: "L >= R" },
|
|
84
|
+
"<=": { suffix: "le", test: "L <= R" },
|
|
85
|
+
"==": { suffix: "eq", test: "L = R" },
|
|
86
|
+
"!=": { suffix: "ne", test: "L NOT = R" },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// The REPOSITORY paragraph every caller of an ef-cmp-* helper needs --
|
|
90
|
+
// shared between formatFunction/formatSuite below, and terminated with a
|
|
91
|
+
// period only after the LAST entry (REPOSITORY is one sentence, not one
|
|
92
|
+
// statement per line -- confirmed against a real compiler that a missing
|
|
93
|
+
// trailing period here breaks the DATA DIVISION that follows).
|
|
94
|
+
const CMP_REPOSITORY =
|
|
95
|
+
` REPOSITORY.\n` +
|
|
96
|
+
Object.values(CMP_HELPERS)
|
|
97
|
+
.map(({ suffix }, i, arr) => ` FUNCTION ef-cmp-${suffix}${i === arr.length - 1 ? "." : ""}`)
|
|
98
|
+
.join("\n") +
|
|
99
|
+
"\n";
|
|
100
|
+
|
|
101
|
+
const CMP_HELPER_SOURCE = Object.values(CMP_HELPERS)
|
|
102
|
+
.map(
|
|
103
|
+
({ suffix, test }) => ` IDENTIFICATION DIVISION.
|
|
104
|
+
FUNCTION-ID. ef-cmp-${suffix}.
|
|
105
|
+
DATA DIVISION.
|
|
106
|
+
LINKAGE SECTION.
|
|
107
|
+
01 L USAGE COMP-2.
|
|
108
|
+
01 R USAGE COMP-2.
|
|
109
|
+
01 THEN-VAL USAGE COMP-2.
|
|
110
|
+
01 ELSE-VAL USAGE COMP-2.
|
|
111
|
+
01 RESULT USAGE COMP-2.
|
|
112
|
+
PROCEDURE DIVISION USING L R THEN-VAL ELSE-VAL RETURNING RESULT.
|
|
113
|
+
IF ${test}
|
|
114
|
+
MOVE THEN-VAL TO RESULT
|
|
115
|
+
ELSE
|
|
116
|
+
MOVE ELSE-VAL TO RESULT
|
|
117
|
+
END-IF
|
|
118
|
+
GOBACK.
|
|
119
|
+
END FUNCTION ef-cmp-${suffix}.
|
|
120
|
+
`,
|
|
121
|
+
)
|
|
122
|
+
.join("\n");
|
|
123
|
+
|
|
124
|
+
// GnuCOBOL caps physical source line length (confirmed against a real
|
|
125
|
+
// compiler: "source text exceeds 512 bytes, will be truncated", on
|
|
126
|
+
// samples/kitchen-sink.js's single expression summing all 22 Math
|
|
127
|
+
// functions -- the one AST big enough to ever hit this). Free-format
|
|
128
|
+
// COBOL allows a statement to simply continue on the next line with no
|
|
129
|
+
// continuation marker (confirmed against a real compiler), so a long line
|
|
130
|
+
// just gets broken at word boundaries, well under the real limit.
|
|
131
|
+
function wrapLine(line, maxWidth = 100) {
|
|
132
|
+
if (line.length <= maxWidth) return line;
|
|
133
|
+
const words = line.split(" ");
|
|
134
|
+
const wrapped = [];
|
|
135
|
+
let current = "";
|
|
136
|
+
for (const word of words) {
|
|
137
|
+
if (current && current.length + 1 + word.length > maxWidth) {
|
|
138
|
+
wrapped.push(current);
|
|
139
|
+
current = ` ${word}`;
|
|
140
|
+
} else {
|
|
141
|
+
current = current ? `${current} ${word}` : word;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (current) wrapped.push(current);
|
|
145
|
+
return wrapped.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A small stateful pool that emitSelect below uses to hoist arguments into
|
|
149
|
+
// fresh temp variables before ever calling an ef-cmp-* helper -- see the
|
|
150
|
+
// file header for why that's required, not optional. Reset once per
|
|
151
|
+
// top-level emitExpr call (one per let-binding, one per output field, one
|
|
152
|
+
// for a select-free body) so temp declarations only need to cover what
|
|
153
|
+
// that one statement actually produced -- but the NAME counter itself is
|
|
154
|
+
// shared (passed in, not owned) across every pool created in one
|
|
155
|
+
// emitFunction call. Confirmed the hard way: an earlier version gave each
|
|
156
|
+
// pool its own counter starting at 0, so two different let-bindings could
|
|
157
|
+
// each mint an "ef-tmp-0", and GnuCOBOL correctly rejected the resulting
|
|
158
|
+
// duplicate WORKING-STORAGE declaration as "ambiguous; needs
|
|
159
|
+
// qualification".
|
|
160
|
+
class TempPool {
|
|
161
|
+
constructor(counter) {
|
|
162
|
+
this.counter = counter;
|
|
163
|
+
this.lines = [];
|
|
164
|
+
this.decls = [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Spills `valueStr` into a freshly named temp, recording both the
|
|
168
|
+
// COMPUTE that sets it and the 01-level declaration it'll need, and
|
|
169
|
+
// returns the bare name -- always safe to pass to a user-defined
|
|
170
|
+
// FUNCTION-ID call, unlike valueStr itself.
|
|
171
|
+
spill(valueStr) {
|
|
172
|
+
const name = `ef-tmp-${this.counter.next++}`;
|
|
173
|
+
this.decls.push(name);
|
|
174
|
+
this.lines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
|
|
175
|
+
return name;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class CobolEmitter extends Emitter {
|
|
180
|
+
emitFunction(fn) {
|
|
181
|
+
const { collectLets } = require("../ast.js");
|
|
182
|
+
const { bindings, body } = collectLets(fn.body);
|
|
183
|
+
const counter = { next: 0 };
|
|
184
|
+
|
|
185
|
+
const letLines = [];
|
|
186
|
+
const letDecls = [];
|
|
187
|
+
for (const { name, node } of bindings) {
|
|
188
|
+
checkReservedNames([name]);
|
|
189
|
+
this._pool = new TempPool(counter);
|
|
190
|
+
const valueStr = this.emitExpr(node);
|
|
191
|
+
letLines.push(...this._pool.lines);
|
|
192
|
+
letDecls.push(...this._pool.decls, name);
|
|
193
|
+
letLines.push(wrapLine(` COMPUTE ${name} = ${valueStr}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (body.type === "outputs") {
|
|
197
|
+
if (!this.formatSuiteImpl) {
|
|
198
|
+
throw new Error(`emitter for .${this.ext}: no formatSuite configured -- multi-output suites aren't supported for this target yet`);
|
|
199
|
+
}
|
|
200
|
+
const outputStrs = {};
|
|
201
|
+
const outputLines = [];
|
|
202
|
+
for (const [name, node] of Object.entries(body.fields)) {
|
|
203
|
+
this._pool = new TempPool(counter);
|
|
204
|
+
outputStrs[name] = this.emitExpr(node);
|
|
205
|
+
outputLines.push(...this._pool.lines);
|
|
206
|
+
letDecls.push(...this._pool.decls);
|
|
207
|
+
}
|
|
208
|
+
return this.formatSuiteImpl(fn, outputStrs, letLines, letDecls, outputLines);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this._pool = new TempPool(counter);
|
|
212
|
+
const bodyStr = this.emitExpr(body);
|
|
213
|
+
const bodyLines = this._pool.lines;
|
|
214
|
+
letDecls.push(...this._pool.decls);
|
|
215
|
+
return this.formatFunctionImpl(fn, bodyStr, letLines, letDecls, bodyLines);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// JS renders very small/large magnitudes in exponential notation
|
|
220
|
+
// (String(1e-9) === "1e-9"), and COBOL numeric literals don't accept that
|
|
221
|
+
// syntax at all -- confirmed against a real compiler ("'1e-9' is not
|
|
222
|
+
// defined"). Expanded to plain decimal digit-by-digit instead of via
|
|
223
|
+
// toFixed(): toFixed(20) reveals a binary float's true (imprecise) decimal
|
|
224
|
+
// expansion for values like 4.2 ("4.20000000000000017764"), where this
|
|
225
|
+
// instead shifts the SAME shortest-round-trip digits String(v) already
|
|
226
|
+
// picked, so e.g. 1e-9 becomes exactly "0.000000001", nothing more.
|
|
227
|
+
function expandExponential(s) {
|
|
228
|
+
const m = s.match(/^(-?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/i);
|
|
229
|
+
if (!m) return s;
|
|
230
|
+
const [, sign, intPart, fracPart = "", expStr] = m;
|
|
231
|
+
const digits = intPart + fracPart;
|
|
232
|
+
const pointPos = intPart.length + Number(expStr);
|
|
233
|
+
let result;
|
|
234
|
+
if (pointPos <= 0) {
|
|
235
|
+
result = `0.${"0".repeat(-pointPos)}${digits}`;
|
|
236
|
+
} else if (pointPos >= digits.length) {
|
|
237
|
+
result = `${digits}${"0".repeat(pointPos - digits.length)}`;
|
|
238
|
+
} else {
|
|
239
|
+
result = `${digits.slice(0, pointPos)}.${digits.slice(pointPos)}`;
|
|
240
|
+
}
|
|
241
|
+
return sign + result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const emitter = new CobolEmitter({
|
|
245
|
+
ext: "cob",
|
|
246
|
+
formatNumber: (v) => {
|
|
247
|
+
const s = String(v);
|
|
248
|
+
return /e/i.test(s) ? expandExponential(s) : s;
|
|
249
|
+
},
|
|
250
|
+
calls: {
|
|
251
|
+
sqrt: fn1("SQRT"), abs: fn1("ABS"), sin: fn1("SIN"), cos: fn1("COS"), tan: fn1("TAN"),
|
|
252
|
+
asin: fn1("ASIN"), acos: fn1("ACOS"), atan: fn1("ATAN"),
|
|
253
|
+
exp: fn1("EXP"), log: fn1("LOG"), log10: fn1("LOG10"),
|
|
254
|
+
min: fn2("MIN"), max: fn2("MAX"),
|
|
255
|
+
pow: ([x, y]) => `(${x} ** ${y})`,
|
|
256
|
+
// No LOG2 intrinsic -- derive it (nesting two intrinsics is fine;
|
|
257
|
+
// only nesting a call inside a USER-DEFINED function's argument
|
|
258
|
+
// was the confirmed problem -- see the file header).
|
|
259
|
+
log2: ([x]) => `(FUNCTION LOG(${x}) / FUNCTION LOG(2.0))`,
|
|
260
|
+
// No HYPOT intrinsic -- derive it the same way.
|
|
261
|
+
hypot: ([a, b]) => `FUNCTION SQRT((${a}) ** 2 + (${b}) ** 2)`,
|
|
262
|
+
// FUNCTION INTEGER is floor (greatest integer <= x, confirmed
|
|
263
|
+
// against a real compiler, including for negatives). COMPUTE's
|
|
264
|
+
// automatic numeric conversion hands it back as COMP-2 with no
|
|
265
|
+
// explicit cast needed, unlike Fortran's REAL(..., 8) wrap.
|
|
266
|
+
floor: fn1("INTEGER"),
|
|
267
|
+
// No CEILING intrinsic -- negate, floor, negate back. Confirmed:
|
|
268
|
+
// ceil(2.2)=3, ceil(-2.2)=-2.
|
|
269
|
+
ceil: ([x]) => `(0 - FUNCTION INTEGER(0 - (${x})))`,
|
|
270
|
+
// FUNCTION INTEGER-PART truncates toward zero directly -- confirmed
|
|
271
|
+
// against a real compiler (2.7->2, -2.7->-2), no derivation needed.
|
|
272
|
+
trunc: fn1("INTEGER-PART"),
|
|
273
|
+
// No ROUND-as-an-expression intrinsic (COBOL's ROUNDED is a
|
|
274
|
+
// COMPUTE/ADD statement modifier, not composable inline). Built
|
|
275
|
+
// from FUNCTION SIGN and FUNCTION INTEGER instead -- both purely
|
|
276
|
+
// intrinsic, so unlike select() this doesn't need the hoisting
|
|
277
|
+
// machinery at all. GnuCOBOL's FUNCTION SIGN is 1-argument
|
|
278
|
+
// (SIGN(x) -> -1/0/1), NOT Fortran's 2-argument SIGN(A,B)
|
|
279
|
+
// "magnitude of A, sign of B" -- confirmed the hard way (a first
|
|
280
|
+
// version of this formula copied Fortran's 2-arg convention here
|
|
281
|
+
// by mistake and got "FUNCTION 'SIGN' has wrong number of
|
|
282
|
+
// arguments" from a real compiler). Also unlike Fortran's SIGN,
|
|
283
|
+
// confirmed zero-safe (SIGN(0.0) == 0.0 for real, not just by
|
|
284
|
+
// accident of a multiplied-away wrong case), so this needs no
|
|
285
|
+
// separate correction the way Fortran's round() does. Rounds ties
|
|
286
|
+
// away from zero, matching every other target here.
|
|
287
|
+
round: ([x]) => `(FUNCTION SIGN(${x}) * FUNCTION INTEGER(FUNCTION ABS(${x}) + 0.5))`,
|
|
288
|
+
// Confirmed zero-safe against a real compiler (SIGN(0.0) == 0.0,
|
|
289
|
+
// unlike Fortran's identically-named but 2-argument intrinsic --
|
|
290
|
+
// see round() above) -- no hoisting/spilling machinery needed,
|
|
291
|
+
// unlike every other emitter here that has to hand-build this.
|
|
292
|
+
sign: fn1("SIGN"),
|
|
293
|
+
// No FUNCTION ATAN2 -- the standard quadrant-corrected formula,
|
|
294
|
+
// built from ef-cmp-* the same way select() itself composes them
|
|
295
|
+
// (see emitSelect below). EVERY argument to EVERY ef-cmp-* call
|
|
296
|
+
// must be a bare, already-spilled name -- including ones built
|
|
297
|
+
// from ANOTHER ef-cmp-* call's result -- since a nested
|
|
298
|
+
// `FUNCTION ef-cmp-x(...)` used directly as an argument to another
|
|
299
|
+
// `FUNCTION ef-cmp-y(...)` hits the exact same confirmed bug as a
|
|
300
|
+
// nested intrinsic call would (see the file header); the first
|
|
301
|
+
// version of this formula got that wrong (nested an ef-cmp-lt
|
|
302
|
+
// call straight into ef-cmp-gt's argument list) and silently
|
|
303
|
+
// computed garbage, caught only by actually compiling and running
|
|
304
|
+
// it. So: absolutely nothing here is inlined -- every intermediate
|
|
305
|
+
// result, including literals, gets its own spill() first.
|
|
306
|
+
//
|
|
307
|
+
// Arrow function, not a plain one: base.js's emitExpr calls
|
|
308
|
+
// `this.calls[node.name](args)` unbound (unlike emitSelectImpl,
|
|
309
|
+
// which the Emitter constructor explicitly .bind(this)s) -- an
|
|
310
|
+
// arrow here closes over the `emitter` const below by reference
|
|
311
|
+
// instead, which is fully assigned by the time this ever actually
|
|
312
|
+
// runs (during some later emitFunction call), even though it's
|
|
313
|
+
// referenced before that `const` is declared in this same object
|
|
314
|
+
// literal.
|
|
315
|
+
atan2: (args) => {
|
|
316
|
+
const [yRaw, xRaw] = args;
|
|
317
|
+
const pool = emitter._pool;
|
|
318
|
+
const y = pool.spill(yRaw);
|
|
319
|
+
const x = pool.spill(xRaw);
|
|
320
|
+
const atanYX = pool.spill(`FUNCTION ATAN(${y} / ${x})`);
|
|
321
|
+
const zero = pool.spill("0.0");
|
|
322
|
+
const piOver2 = pool.spill("1.5707963267948966");
|
|
323
|
+
const negPiOver2 = pool.spill("-1.5707963267948966");
|
|
324
|
+
const atanPlusPi = pool.spill(`(${atanYX} + 3.141592653589793)`);
|
|
325
|
+
const atanMinusPi = pool.spill(`(${atanYX} - 3.141592653589793)`);
|
|
326
|
+
// x == 0 case: sign of y (0 conventionally maps to 0.0, same
|
|
327
|
+
// convention JS's/Python's atan2(0,0) use).
|
|
328
|
+
const yNegSubcase = pool.spill(`FUNCTION ef-cmp-lt(${y}, ${zero}, ${negPiOver2}, ${zero})`);
|
|
329
|
+
const xZeroCase = pool.spill(`FUNCTION ef-cmp-gt(${y}, ${zero}, ${piOver2}, ${yNegSubcase})`);
|
|
330
|
+
// x < 0 case: quadrant-corrected by the sign of y.
|
|
331
|
+
const xNegCase = pool.spill(`FUNCTION ef-cmp-ge(${y}, ${zero}, ${atanPlusPi}, ${atanMinusPi})`);
|
|
332
|
+
const xNegOrZeroCase = pool.spill(`FUNCTION ef-cmp-lt(${x}, ${zero}, ${xNegCase}, ${xZeroCase})`);
|
|
333
|
+
return `FUNCTION ef-cmp-gt(${x}, ${zero}, ${atanYX}, ${xNegOrZeroCase})`;
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
// See the file header and TempPool above for why this spills into
|
|
337
|
+
// temps instead of nesting inline: a user-defined FUNCTION-ID call
|
|
338
|
+
// (ef-cmp-*) confirmed miscomputes when given a complex argument, so
|
|
339
|
+
// every one of L/R/then/else gets its own COMPUTE into a fresh temp
|
|
340
|
+
// first, and the picker call itself only ever receives bare names.
|
|
341
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
342
|
+
const { suffix } = CMP_HELPERS[condNode.op];
|
|
343
|
+
const L = this._pool.spill(this.emitExpr(condNode.left));
|
|
344
|
+
const R = this._pool.spill(this.emitExpr(condNode.right));
|
|
345
|
+
const thenTmp = this._pool.spill(thenStr);
|
|
346
|
+
const elseTmp = this._pool.spill(elseStr);
|
|
347
|
+
return `FUNCTION ef-cmp-${suffix}(${L}, ${R}, ${thenTmp}, ${elseTmp})`;
|
|
348
|
+
},
|
|
349
|
+
// Both the scalar and suite cases use the SAME convention: a callable
|
|
350
|
+
// PROGRAM-ID with every output (the single return value, or every
|
|
351
|
+
// outputs() field) as a trailing BY REFERENCE parameter -- COBOL's
|
|
352
|
+
// default parameter-passing mode, confirmed reliable (BY VALUE is
|
|
353
|
+
// explicitly flagged "unfinished" by a real GnuCOBOL compile). This
|
|
354
|
+
// also sidesteps a second confirmed rough edge: calling a FUNCTION-ID
|
|
355
|
+
// module by name (`FUNCTION word(...)`) breaks if that name contains
|
|
356
|
+
// an underscore (confirmed against a real compiler -- see e.g.
|
|
357
|
+
// samples/spline-frame.js's "wy_wire" param), while CALL "name" takes
|
|
358
|
+
// the program name as a plain string literal, immune to that.
|
|
359
|
+
formatFunction: (fn, body, letLines, letDecls, bodyLines) => {
|
|
360
|
+
checkReservedNames([fn.name, ...fn.params]);
|
|
361
|
+
const linkageParams = [...fn.params, "ef-result"];
|
|
362
|
+
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
363
|
+
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
364
|
+
return ` >>SOURCE FORMAT FREE\n` +
|
|
365
|
+
` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
366
|
+
CMP_HELPER_SOURCE + "\n" +
|
|
367
|
+
` IDENTIFICATION DIVISION.\n` +
|
|
368
|
+
` PROGRAM-ID. ${fn.name}.\n` +
|
|
369
|
+
` ENVIRONMENT DIVISION.\n` +
|
|
370
|
+
` CONFIGURATION SECTION.\n` +
|
|
371
|
+
CMP_REPOSITORY +
|
|
372
|
+
` DATA DIVISION.\n` +
|
|
373
|
+
(wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
374
|
+
` LINKAGE SECTION.\n` +
|
|
375
|
+
paramDecls + "\n" +
|
|
376
|
+
` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
377
|
+
[...letLines, ...bodyLines].join("\n") + (letLines.length || bodyLines.length ? "\n" : "") +
|
|
378
|
+
wrapLine(` COMPUTE ef-result = ${body}`) + "\n" +
|
|
379
|
+
` GOBACK.\n` +
|
|
380
|
+
` END PROGRAM ${fn.name}.\n`;
|
|
381
|
+
},
|
|
382
|
+
formatSuite: (fn, outputStrs, letLines, letDecls, outputLines) => {
|
|
383
|
+
const outputNames = Object.keys(outputStrs);
|
|
384
|
+
checkReservedNames([fn.name, ...fn.params, ...outputNames]);
|
|
385
|
+
const linkageParams = [...fn.params, ...outputNames];
|
|
386
|
+
const paramDecls = linkageParams.map((p) => ` 01 ${p} USAGE COMP-2.`).join("\n");
|
|
387
|
+
const wsDecls = letDecls.map((n) => ` 01 ${n} USAGE COMP-2.`).join("\n");
|
|
388
|
+
const assigns = outputNames.map((n) => wrapLine(` COMPUTE ${n} = ${outputStrs[n]}`)).join("\n");
|
|
389
|
+
return ` >>SOURCE FORMAT FREE\n` +
|
|
390
|
+
` *> AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
391
|
+
CMP_HELPER_SOURCE + "\n" +
|
|
392
|
+
` IDENTIFICATION DIVISION.\n` +
|
|
393
|
+
` PROGRAM-ID. ${fn.name}.\n` +
|
|
394
|
+
` ENVIRONMENT DIVISION.\n` +
|
|
395
|
+
` CONFIGURATION SECTION.\n` +
|
|
396
|
+
CMP_REPOSITORY +
|
|
397
|
+
` DATA DIVISION.\n` +
|
|
398
|
+
(wsDecls ? ` WORKING-STORAGE SECTION.\n${wsDecls}\n` : "") +
|
|
399
|
+
` LINKAGE SECTION.\n` +
|
|
400
|
+
paramDecls + "\n" +
|
|
401
|
+
` PROCEDURE DIVISION USING ${linkageParams.join(" ")}.\n` +
|
|
402
|
+
(letLines.length ? letLines.join("\n") + "\n" : "") +
|
|
403
|
+
(outputLines.length ? outputLines.join("\n") + "\n" : "") +
|
|
404
|
+
assigns + "\n" +
|
|
405
|
+
` GOBACK.\n` +
|
|
406
|
+
` END PROGRAM ${fn.name}.\n`;
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
module.exports = emitter;
|