exprforge 0.2.0 → 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 +288 -21
- package/emitters/cobol.js +478 -0
- package/emitters/exprsyntax.js +81 -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 +8 -0
- package/emitters/scheme.js +154 -0
- package/emitters/zig.js +126 -0
- package/evaluate.js +109 -0
- package/expr.js +313 -0
- package/fn.js +117 -0
- package/index.js +14 -2
- package/math/index.js +9 -1
- package/package.json +5 -2
- package/samples/math-demo.js +8 -4
package/README.md
CHANGED
|
@@ -10,13 +10,23 @@
|
|
|
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) — plus a native in-process evaluator and a
|
|
25
|
+
printer for exprforge's own readable syntax (see `fn`/`expr` below).
|
|
17
26
|
|
|
18
|
-
No
|
|
19
|
-
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.
|
|
20
30
|
|
|
21
31
|
## Why
|
|
22
32
|
|
|
@@ -27,6 +37,63 @@ mainstream languages. This exists for two things SymPy doesn't do:
|
|
|
27
37
|
- A conformance test harness that actually proves the emitted targets
|
|
28
38
|
agree numerically, not just that they compile.
|
|
29
39
|
|
|
40
|
+
Two shapes of real use this tends to fall into:
|
|
41
|
+
|
|
42
|
+
- **Keeping concurrent codebases in sync.** A client/server split (game
|
|
43
|
+
client prediction + authoritative server, or any two independently
|
|
44
|
+
deployed services) where both sides need to compute the *same* formula
|
|
45
|
+
and disagree — desync, or a cheat signal — the moment they drift. One
|
|
46
|
+
AST, not two hand-maintained implementations that quietly diverge.
|
|
47
|
+
- **De-risking a migration.** Replacing an older implementation (a COBOL
|
|
48
|
+
batch job, a Fortran numerical kernel) with a new one doesn't require
|
|
49
|
+
trusting a manual port — emit the same formula into both the legacy
|
|
50
|
+
target and the new one, and let the conformance suite prove they agree
|
|
51
|
+
before cutover, not after.
|
|
52
|
+
|
|
53
|
+
Neither is "translate my code for me" — it's "prove two independent
|
|
54
|
+
implementations of one formula actually match," which is a narrower,
|
|
55
|
+
checkable claim.
|
|
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
|
+
|
|
30
97
|
## Install
|
|
31
98
|
|
|
32
99
|
```
|
|
@@ -36,12 +103,12 @@ npm install exprforge
|
|
|
36
103
|
## Usage
|
|
37
104
|
|
|
38
105
|
```js
|
|
39
|
-
const {
|
|
106
|
+
const { expr, emitAll } = require("exprforge");
|
|
40
107
|
|
|
41
108
|
const fn = {
|
|
42
109
|
name: "lerp",
|
|
43
110
|
params: ["a", "b", "t"],
|
|
44
|
-
body:
|
|
111
|
+
body: expr`(b - a) * t + a`,
|
|
45
112
|
};
|
|
46
113
|
|
|
47
114
|
const outputs = emitAll(fn);
|
|
@@ -49,6 +116,13 @@ console.log(outputs.rust.source);
|
|
|
49
116
|
console.log(outputs.c.source);
|
|
50
117
|
```
|
|
51
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
|
+
|
|
52
126
|
## Samples
|
|
53
127
|
|
|
54
128
|
`samples/` has worked, non-trivial examples (also exported from the
|
|
@@ -130,6 +204,29 @@ Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
|
|
|
130
204
|
existing file as a template), then add one line to
|
|
131
205
|
`emitters/registry.js`. Nothing else changes — proven by the TypeScript
|
|
132
206
|
emitter, added with no changes to `base.js`, `build.js`, or `index.js`.
|
|
207
|
+
`Emitter` is a real class (not just a factory function), so a target that
|
|
208
|
+
needs to intercept how expressions themselves get rendered — not just
|
|
209
|
+
`calls`/`emitSelect`/`formatFunction`, all ordinary config — can subclass
|
|
210
|
+
it instead: Perl/PHP override `emitExpr`'s `"var"` case to add the `$`
|
|
211
|
+
sigil every reference needs, Scheme overrides the `"bin"` case for prefix
|
|
212
|
+
notation. See `emitters/scheme.js` and `emitters/perl.js`.
|
|
213
|
+
|
|
214
|
+
### Reserved-word collisions
|
|
215
|
+
|
|
216
|
+
Several emitters (QB64, Fortran, Zig, Scheme, COBOL) guard against a
|
|
217
|
+
generated variable/parameter/function name colliding with that language's
|
|
218
|
+
own reserved words or builtins — a `<LANG>_RESERVED` set checked at
|
|
219
|
+
emission time, throwing a clear error instead of producing code that fails
|
|
220
|
+
to compile somewhere downstream with no context (see e.g. `QB64_RESERVED`
|
|
221
|
+
in `emitters/qb64.js`). **These lists are not, and can't practically be,
|
|
222
|
+
exhaustive** — each covers the collisions that came up in this project's
|
|
223
|
+
own samples plus the obvious/common ones for that language, not every
|
|
224
|
+
reserved word in every language's full grammar. If you're naming your own
|
|
225
|
+
functions/params/`letIn` bindings, especially ones you know will target a
|
|
226
|
+
specific language, it's still on you to know that language's reserved
|
|
227
|
+
words — Perl/PHP mostly sidestep this (every variable is `$`-sigiled, so
|
|
228
|
+
it can't collide with a bareword keyword), but the sigil-free languages
|
|
229
|
+
above genuinely can't be fully guarded against in advance.
|
|
133
230
|
|
|
134
231
|
## Named subexpressions and conditional values
|
|
135
232
|
|
|
@@ -188,6 +285,98 @@ See [`docs/planned-additions.md`](./docs/planned-additions.md) for the
|
|
|
188
285
|
full design rationale, including why the naive "guard division with
|
|
189
286
|
select" pattern is wrong.
|
|
190
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
|
+
|
|
191
380
|
## Multiple named outputs
|
|
192
381
|
|
|
193
382
|
`outputs({ name: Node, ... })` computes several named values from ONE
|
|
@@ -214,11 +403,14 @@ multi-value idiom it has, since none of them agree:
|
|
|
214
403
|
| Target | Shape |
|
|
215
404
|
|---|---|
|
|
216
405
|
| JS | object literal |
|
|
217
|
-
| Go, Lua | native multiple return values |
|
|
218
|
-
| C
|
|
219
|
-
| C / Rust | a small `...Result` struct, returned by value |
|
|
406
|
+
| Go, Lua, Scheme | native multiple return values (`(values ...)` in Scheme) |
|
|
407
|
+
| C#, Julia | a native named value tuple / named tuple |
|
|
408
|
+
| C / Rust / Zig | a small `...Result` struct, returned by value |
|
|
220
409
|
| Java, Python | a nested/local `Result` class |
|
|
221
|
-
| QB64 | a `SUB` with the outputs as trailing by-reference parameters |
|
|
410
|
+
| QB64, Fortran | a `SUB`/`subroutine` with the outputs as trailing by-reference (`intent(out)`) parameters |
|
|
411
|
+
| Perl | a hash ref (`{ rx => ..., ry => ... }`) |
|
|
412
|
+
| PHP | an associative array (`['rx' => ..., 'ry' => ...]`) |
|
|
413
|
+
| 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
414
|
|
|
223
415
|
Go specifically does **not** use *named* return values (`(rx, ry float64)`)
|
|
224
416
|
even though Go supports them and it reads nicer: those are sugar for
|
|
@@ -244,30 +436,45 @@ pre-declared locals the way Go's named returns are.
|
|
|
244
436
|
npm test
|
|
245
437
|
```
|
|
246
438
|
|
|
247
|
-
Runs `node --test`. For each sample, that's
|
|
439
|
+
Runs `node --test`. For each sample, that's three kinds of check:
|
|
248
440
|
|
|
249
441
|
- Emitted JS vs. an independently hand-written reference implementation
|
|
250
442
|
(catches a wrong formula in the AST itself).
|
|
251
443
|
- Every other emitted target vs. that same JS, compiled (and, for
|
|
252
444
|
TypeScript, also type-checked under `--strict`) and run, with the sample
|
|
253
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.
|
|
254
460
|
|
|
255
461
|
The compiled/interpreted-language checks need their toolchain on `PATH`
|
|
256
462
|
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
|
-
|
|
463
|
+
on any one machine. Every one of `tsc`/`qb64pe`/`dotnet`/`python3`/`lua`/
|
|
464
|
+
`perl`/`php`/`julia`/`gfortran`/`zig`/`guile3.0`/`cobc` is treated exactly
|
|
465
|
+
like gcc/go/rustc/javac: looked up on `PATH`, never a project
|
|
466
|
+
dependency — exprforge only ever generates source text for these, it
|
|
467
|
+
doesn't execute or type-check any of it itself. `package.json` has zero
|
|
468
|
+
dependencies of any kind, matching this.
|
|
262
469
|
|
|
263
470
|
CI is one workflow file per target language (`.github/workflows/test-*.yml`),
|
|
264
471
|
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
|
|
472
|
+
reason to serialize installing sixteen different toolchains (QB64-PE
|
|
473
|
+
alone, built from source and cached by version, takes several minutes)
|
|
474
|
+
into one job, and splitting by file rather than by job within one file is
|
|
475
|
+
also what gets each language its own real status badge above, not just
|
|
476
|
+
one combined "did everything pass" badge. Each workflow installs only its
|
|
477
|
+
own toolchain and runs `EXPRFORGE_TEST_TARGETS=<Label> npm test`; that
|
|
271
478
|
environment variable (read once in `test/conformance.test.js`) filters
|
|
272
479
|
the target lists down to just that one language, plus the toolchain-
|
|
273
480
|
independent JS/reference checks, which every workflow repeats — cheap,
|
|
@@ -304,6 +511,66 @@ compiling/running against a real toolchain rather than assumed to work:
|
|
|
304
511
|
`math.atan2` (use two-argument `math.atan(y, x)`); there's no
|
|
305
512
|
`math.round` or `math.trunc` or `math.sign` at any version (manual
|
|
306
513
|
`floor(x+0.5)`, `math.modf(x)`, and an `and`/`or` chain respectively).
|
|
514
|
+
- **Perl / PHP**: every variable reference needs a `$` sigil, which
|
|
515
|
+
`base.js`'s shared `emitExpr` doesn't produce for anything — both
|
|
516
|
+
subclass `Emitter` to override just the `"var"` case (see "Adding a
|
|
517
|
+
language" above) rather than needing a new hook every other emitter
|
|
518
|
+
would have to ignore. Perl has no `log2()`/`trunc()`/`hypot()` in core
|
|
519
|
+
(POSIX supplies `trunc`/`hypot`, `log2` is derived); PHP has no
|
|
520
|
+
`trunc()` at all (`floor`/`ceil` picked by sign instead, not an `(int)`
|
|
521
|
+
cast, which would misbehave outside PHP's platform integer range).
|
|
522
|
+
- **Julia**: `round()` defaults to ties-to-even (banker's rounding), not
|
|
523
|
+
ties-away-from-zero like every other target here —
|
|
524
|
+
`round(x, RoundNearestTiesAway)` used explicitly to actually match,
|
|
525
|
+
not just avoid the untested case. `sign(-0.0)` returns `-0.0`, which is
|
|
526
|
+
numerically equal to `0.0` for the tolerance-based comparisons this
|
|
527
|
+
project uses, so it isn't a real divergence.
|
|
528
|
+
- **Fortran**: a literal without the `D0` exponent marker is parsed as
|
|
529
|
+
*single*-precision first, then widened — silently losing precision
|
|
530
|
+
before it reaches a `real(8)` variable, unlike every other target's
|
|
531
|
+
literals — so every literal gets it, not just ones already in
|
|
532
|
+
scientific notation. `FLOOR`/`CEILING` return the default `INTEGER`
|
|
533
|
+
kind, not `REAL`, wrapped back with `REAL(..., 8)`. No ternary, but
|
|
534
|
+
`MERGE(then, else, mask)` is a genuine expression-level conditional —
|
|
535
|
+
confirmed to evaluate both branches regardless of `mask`, matching
|
|
536
|
+
`select()`'s own contract exactly. The native 2-argument `SIGN(A, B)`
|
|
537
|
+
("magnitude of A, sign of B") is *not* this project's `sign(x)` —
|
|
538
|
+
`SIGN(1.0, 0.0)` returns `1.0`, not `0.0` — built from `MERGE` instead.
|
|
539
|
+
- **Zig**: `std.debug.print` writes to **stderr** by design, not
|
|
540
|
+
stdout — the conformance harness has to use
|
|
541
|
+
`std.io.getStdOut().writer()` instead, or every result silently comes
|
|
542
|
+
back empty. A fully-literal expression with no runtime operand (e.g.
|
|
543
|
+
`sqrt(2.0)` alone) gets evaluated at Zig's extended `comptime_float`
|
|
544
|
+
precision instead of truncated to an actual IEEE double, unless
|
|
545
|
+
explicitly `@as(f64, ...)`-cast — every literal gets that cast, not
|
|
546
|
+
just ones that would otherwise hit this.
|
|
547
|
+
- **Scheme (Guile)**: a bare integer literal like `2` is *exact* in
|
|
548
|
+
Scheme's reader syntax, and exact arithmetic that never touches an
|
|
549
|
+
inexact (float) operand stays exact — `(/ 1 3)` prints as the fraction
|
|
550
|
+
`1/3`, not `0.333...`. Every literal gets `.0` appended unless it
|
|
551
|
+
already has a decimal point or exponent, forcing inexactness by literal
|
|
552
|
+
syntax alone rather than relying on some other operand in the same
|
|
553
|
+
expression happening to already be a float.
|
|
554
|
+
- **COBOL (GnuCOBOL)**: has no expression-level conditional at all — no
|
|
555
|
+
ternary, no `MERGE`-equivalent. `select()` is built from six small
|
|
556
|
+
helper `FUNCTION-ID` modules (one per comparator), but confirmed
|
|
557
|
+
against a real compile+run that a user-defined `FUNCTION` call
|
|
558
|
+
*silently miscomputes* — no error, just a wrong number — when given a
|
|
559
|
+
complex argument (one containing its own nested call); every argument
|
|
560
|
+
to a helper gets spilled into its own `COMPUTE`d temp first, always,
|
|
561
|
+
not just when an argument "looks complex." `BY VALUE` parameter passing
|
|
562
|
+
is explicitly flagged "unfinished" by the compiler — every function
|
|
563
|
+
uses `BY REFERENCE` (the default) instead, which is also why COBOL is
|
|
564
|
+
the one target where even a *scalar* function's return value is a
|
|
565
|
+
trailing by-reference parameter (see the outputs table above), not a
|
|
566
|
+
`FUNCTION`-style return: calling a user `FUNCTION` by name breaks if
|
|
567
|
+
that name contains an underscore (confirmed against a real compiler),
|
|
568
|
+
while `CALL "name"` takes it as a plain string literal, immune to that.
|
|
569
|
+
Source lines have a real ~512-byte cap — long expressions (e.g.
|
|
570
|
+
`samples/kitchen-sink.js`'s summed call to all 22 functions) get
|
|
571
|
+
wrapped at word boundaries. The native `FUNCTION SIGN` is
|
|
572
|
+
1-argument (`SIGN(x)`), unlike Fortran's identically-named
|
|
573
|
+
2-argument intrinsic — and unlike Fortran's, is genuinely zero-safe.
|
|
307
574
|
|
|
308
575
|
One test (`normalizeX`) is deliberately excluded from the QB64 check
|
|
309
576
|
only: it exists specifically to demonstrate the "don't guard division
|