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 CHANGED
@@ -10,13 +10,23 @@
10
10
  [![Java](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-java.yml)
11
11
  [![Go](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-go.yml)
12
12
  [![Rust](https://github.com/theraccoonbear/exprforge/actions/workflows/test-rust.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-rust.yml)
13
+ [![Perl](https://github.com/theraccoonbear/exprforge/actions/workflows/test-perl.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-perl.yml)
14
+ [![PHP](https://github.com/theraccoonbear/exprforge/actions/workflows/test-php.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-php.yml)
15
+ [![Julia](https://github.com/theraccoonbear/exprforge/actions/workflows/test-julia.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-julia.yml)
16
+ [![Fortran](https://github.com/theraccoonbear/exprforge/actions/workflows/test-fortran.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-fortran.yml)
17
+ [![Zig](https://github.com/theraccoonbear/exprforge/actions/workflows/test-zig.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-zig.yml)
18
+ [![Scheme](https://github.com/theraccoonbear/exprforge/actions/workflows/test-scheme.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-scheme.yml)
19
+ [![COBOL](https://github.com/theraccoonbear/exprforge/actions/workflows/test-cobol.yml/badge.svg)](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, and Rust.
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 parser, no dependencies. You build the AST directly with plain JS
19
- functions; the same tree is walked once per target language.
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 { num, v, bin, mul, add, sub, emitAll } = require("exprforge");
106
+ const { expr, emitAll } = require("exprforge");
40
107
 
41
108
  const fn = {
42
109
  name: "lerp",
43
110
  params: ["a", "b", "t"],
44
- body: add(v("a"), mul(sub(v("b"), v("a")), v("t"))),
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# | a native named value tuple (`(double rx, double ry)`) |
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 two kinds of check:
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 like gcc/go/rustc/javac: looked up on `PATH`, never a
259
- project dependency exprforge only ever generates source text for these,
260
- it doesn't execute or type-check any of it itself. `package.json` has
261
- zero dependencies of any kind, matching this.
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 nine different toolchains (QB64-PE alone,
266
- built from source and cached by version, takes several minutes) into one
267
- job, and splitting by file rather than by job within one file is also
268
- what gets each language its own real status badge above, not just one
269
- combined "did everything pass" badge. Each workflow installs only its own
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