Skip to content

comp.ts: a fact stales only the units that read it absent - #1601

Open
nicolas-abril wants to merge 2 commits into
bendlang:mainfrom
nicolas-abril:comp-deslop
Open

nicolas-abril wants to merge 2 commits into
bendlang:mainfrom
nicolas-abril:comp-deslop

Conversation

@nicolas-abril

@nicolas-abril nicolas-abril commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

The compile-speed half of #1601, on top of #1817 (this PR shows both commits until #1817 merges; the second, 188bfa9, is this one's).

compile_book emitted every def again until a pass added no fact. Now a fact read as absent records its reader, and adding the fact marks only those units stale; a sweep emits the stale units callee-first (unit_order, an explicit stack, so a 200,000-def chain still sorts). A flat def, a def with one call site and a spin become units only once a segment names them (unit_lazy), so unreachable ones are never emitted.

comp.ts is 66,489 ttok, 280 more than #1817 and 489 over the 66,000 cap, so the repo gate fails (53/54). Test gate: 1715/1715 on the cluster.

Compile time (C and JS emit, median of 5, M4 Max)

main #1817 this PR
boss_3d 1,127 ms 587 ms 391 ms
raytrace bench 50 ms 35 ms 20 ms
35 demos and benches that build, total 3.04 s 1.89 s 1.38 s

Output: JS is byte-identical to #1817's on all 1,820 programs. C matches up to the order of segments, spins, tables and static nodes, except 17 programs where a def nothing reaches no longer adds facts (a constructor marked hot, a lend that kept a parameter borrowed, dead static nodes); those 17 pass on all three lanes on both PRs.

This PR's earlier description covered both halves; the simplification half, with the review fixes and its tests, is now #1817. Main 9bc7df3 is in both: #1542's ty_holds comes over whole, and #1805's rule (only a box roots) sits in val_new, as roots travel on values here.

🤖 Generated with Claude Code

@nicolas-abril nicolas-abril changed the title comp.ts deslop: typed facts, explicit tables, fewer layers comp.ts: typed facts, explicit tables, fewer layers Oct 16, 2026
@nicolas-abril
nicolas-abril marked this pull request as ready for review October 19, 2026 13:22
@nicolas-abril nicolas-abril changed the title comp.ts: typed facts, explicit tables, fewer layers comp.ts: facts settle per unit, nothing reads emitted text, pure mains print through Bend Oct 19, 2026
@Lorenzobattistela Lorenzobattistela self-assigned this Oct 19, 2026
@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Reviewed merged onto main f5cee56. All gates pass: repo 54/54, ping 63/63, test 1670/1670, perf 124/124, safe 1090 agree / 0 false reject.

Reproduced (PR vs main, built in both trees):

  1. JS miscompile, op_bare (comp.ts ~3166). A constant array literal now counts as bare, so it is pasted into a template that uses $0 twice and Array.set writes into a throwaway copy:
    def pick(p: Array<U32> & U32) -> U32:
      (a, x) = p
      x
    def main() -> U32:
      pick(Array.get(U32, Array.set(U32, ALeaf{5}, 0, 7), 0))
    
    JS prints 5 (const _x_0 = ([5][0 % [5].length] = 7, [5]);); main prints 7 (const _x_0 = [5];). The C lane prints 7.
  2. Pure main String overflows the JS stack. String.quote.go (base.bend) recurses once per character and is not a tail call: a pure main() -> String of 10,000 chars gives bend: memory fault (machine stack overflow?); main prints 200,000.
  3. Pure main memory on C. main_pure builds the whole output as a Bend String before IO.print, where the old C printer streamed it: a 3M-element List<U32> peaks at 1.19 GB vs 49 MB on main (8M: 3.17 GB vs 130 MB), so big outputs can hit "out of memory" where main printed.

From reading, not run:

  1. FL.folds lives for the file but fuel is per scope (comp.ts ~2522). A null cached by a unit that ran out of fuel blocks the fold for later units with full fuel, so output can depend on emit order (and lose folds that matter, e.g. merkle GPU).
  2. fact_when('ctr', …) in emit_ctr (~2497) stores a closure holding the whole Scope per emission of a non-hot constructor, with no dedup, for the life of the file. A key per (def, ctr), like FL.heat, would keep one.
  3. mat_lits passes cov = null (~1208) when the parent default reads its fields; main passed the parent's instantiation, so deeper replay defaults are no longer merged (one default row per bit level).
  4. ra / emit_intr array case (~1917, ~2448) keep args[0].rs[0] right after val_own(args[0]), which marks that root owned, so once facts settle the kept root is always null.
  5. C host Nat check (~6031) rejects only above 2^53 but nat_chk's message says "past the largest immediate 2^48-1", and differs from JS's nat_in message.

Also: #1386 does the same per-unit fact invalidation; one of the two should close.

nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 20, 2026
- JS: a constant array stays out of op templates (op_bare), so Array.set
  writes the array it returns (tests/run/const_array_op_js).
- A pure main streams: a show def of a word is a String, any other is
  IO writes, so big outputs no longer build the whole String first.
- String.quote walks its String with a tail call per char.
- A fold that ran out of fuel is not cached for later units.
- emit_ctr registers its ctr facts once per (def, ctr, type).
- mat_lits compares a replayed default with its parent's instantiation.
- arr_op and array intrinsics keep no root they just owned.
- A host Nat past 2^53 fails with the host-range message on C, as on JS.
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 20, 2026
…s print through Bend

The 68 commits of bendlang#1601 (ai/1601-prerebase, 053163d) squashed onto
642d8e1; bendlang#1786's comment and code cuts kept where they overlap.
@nicolas-abril

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. All 8 items are addressed. The PR has been rebased onto main 642d8e1 as three commits (see the description). Results on the current head, da7942c, compared with main 642d8e1:

  1. JS op_bare array copy: a constant array is held in a local before it goes into an op template, as on main. Your program now prints 7 on both lanes, and tests/run/const_array_op_js.bend checks it.
  2. String overflowing the JS stack: String.quote in base.bend walks the string with a tail call. A 200,000-character pure main() -> String prints the same bytes as main on C and JS.
  3. Pure main memory on C: the generated printer writes as it goes (each run of text is one IO.write) instead of building one String. The 3M-element List<U32> peaks at 49.6 MB, against 49.4 MB on main, and takes 0.23 s user time against 0.19 s. The output is byte-identical. The JS lane prints it too; on main the JS lane hits RangeError: Maximum call stack size exceeded.
  4. FL.folds and fuel: a fold that ran out of fuel is no longer cached, so a later unit with full fuel still folds.
  5. fact_when('ctr', …) closures: registered once per (def, constructor, type).
  6. mat_lits cov = null: a replayed default is compared against its parent's default again, as on main. Undoing this fix leaves every emitted program unchanged, so no test catches it.
  7. ra / emit_intr kept roots: removed. With them gone every emitted output stayed the same, so they were dead code.
  8. C host Nat check: a host Nat past 2^53 now fails on C with the same message as JS's nat_in, not the "2^48-1" one.

Each fix was checked by emitting every program under tests/, bench/ and demos/ (C and JS). Only the pure-main printers, the JS array case and the runtime error text changed.

Gates on an M4 Max: repo 54/54, ping 62/62, test 1689/1689, perf 121–122/124. Main 642d8e1 gets 122/124 on the same machine and fails the same mandelbrot SEQ-CPU cell. CUDA and Linux have not been run.

🤖 Generated with Claude Code

@Lorenzobattistela Lorenzobattistela left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all 8 hold up on da7942c (on main 642d8e1). The array program prints 7 on both lanes, a 200k-char String and a 3M-element list print the same bytes as main, and the list peaks at 49.7 MB against main's 49.5 MB. Gates: repo 54/54, ping 63/63, test 1689/1689, perf 124/124, safe 1098 agree / 0 false reject. Pure-main output matches main on 15 value shapes on C and JS, and a 1M-element list now prints on JS (main overflows).

One new break, two follow-ups:

  1. A pure main returning an imported type doesn't build (comp.ts ~2140). main_pure writes arms with the raw constructor id, so the generated source has case lib2:Circ{f0}: and fails to parse:
    # lib2.bend
    import Base
    type Shape is Data:
      Circ{r: U32}
      Sq{s: U32, t: Shape}
    
    # main.bend
    import Base
    import ./lib2.bend as L
    def main() -> L.Shape:
      L.Sq{4, L.Circ{5}}
    
    bend main.bend -o x and -o x.js both fail; main prints lib2.Sq{4, lib2.Circ{5}}. The Array.to_list(~${term_show(E)}) splice looks exposed the same way (Array<L.Shape>). A test with an imported type would cover it.
  2. JS printing is ~15x slower on large values (~2091). Each piece is its own IO.write, so one fs.writeSync through the scheduler: a depth-20 tree (17.8 MB) prints in 5.73 s against 0.37 s on main (C 0.65 s vs 0.50 s). Buffering the writes would avoid it.
  3. C host Nat (~6020): the check is now > 2^53 with JS's message, but a C Nat tops out at 2^48-1, so an answer in 2^48..2^53 still enters unchecked. NAT_IMM is the C bound.

Smaller:

  • e6c2c5f drops turn_fits; turn_set still writes 8 words from H_GROUPS, and nothing guards that against H_ROOT_DONE now.
  • A pure main's C is ~2.5x bigger (82 KB → 204 KB for a 3-type main) from the printer defs, and compile_book builds a file_new it throws away before file_new(main_pure(book)).

nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 20, 2026
…s print through Bend

The 68 commits of bendlang#1601 (ai/1601-prerebase, 053163d) squashed onto
642d8e1; bendlang#1786's comment and code cuts kept where they overlap.
@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

I pushed two commits on top of the buffering:

  • 1275ad1: a File.write to /dev/stdout came out before the prints held above it on JS (piped: two one three; C and main: one two three). tests/io/stdout_file_order pins it.
  • 33a0dcd: narrows that to a sync in file_write_buffer, as C (whose loop flushes before it waits), so a print next to IO.now, Chan or IO.sleep stays buffered (200k print + IO.now: 0.13 s vs 0.33 s on main). The test opens /dev/stdout with "a", as "w" truncates a redirected stdout on Linux.

Three small things left on your side:

  1. process.once("exit", io_sync) in io_std (comp.ts ~6350) looks redundant: io_run's finally already syncs on every return and throw. Each in-process run (--checkup runs one per import, the test gate one per test) adds a listener that keeps that program's runtime alive until bend exits. Dropping it, or registering it once in io_exit, avoids that.
  2. main_pure's _ns0, _ns1, ... aliases (~2085) aren't made fresh like pre: a main-file def or constructor named _ns0.Circ next to an imported Circ fails the build with "an unambiguous name (the alias _ns0 shadows _ns0.Circ)".
  3. io_sync (~6363) concatenates every held chunk into a new buffer, even when one large IO.write alone crosses 64 KiB, which copies it once more. Writing the chunks in order, or a lone chunk directly, avoids the copy.

Note on 73525e6: since JS now builds the whole text, a 200k-element List<U32> pure main overflows on JS again (bend: memory fault (machine stack overflow?)), as on main; the streaming version printed 1M. Fine by me if parity with main is the goal.

@Lorenzobattistela Lorenzobattistela left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed on 33a0dcd merged onto main 10256a7, and it's OK: ping 73/73, test 1696/1696, perf 123/124, safe 1101 agree / 0 false reject. The only blocker is repo 53/54: comp.ts is 66,598 against the 66k cap after #1530 landed on main, so it needs ~600 tokens cut before it can merge.

@nicolas-abril

Copy link
Copy Markdown
Collaborator Author

I pushed three commits on top of 33a0dcd. They make comp.ts shorter and don't change what programs print:

  • 1b44649: each generated JS show function returns the text it prints, joined with ++, and no longer takes the text that follows it as an accumulator. Import aliases are built in one expression.
  • 6e804c0: term_uses sets bound names to zero instead of deleting them, and a closure lists only the names it uses at least once. This removes pmap_del. arm_self now uses term_same for its constructor check.
  • 67d3e7b: seq merges adjacent text the same way for C and JS. Show-function variables are named by field id, which removes the per-arm alias map. One template helper now writes the WL_LOAD/WL_LAST/WL_SAVE/WL_TAKE macros, one line each. The C they expand to is unchanged.

comp.ts is now 65,472 tokens (it was 65,917 at 73525e6; the cap is 66,000).

Checks on 67d3e7b (macOS):

  • Gates: repo 54/54, ping 62/62, test 1695/1695.
  • Generated code: I compiled all 1,752 .c/.js outputs under tests, bench and demos, before and after 67d3e7b. Ignoring whitespace, the only differences are local variable names, in 2 .c files and some .js files. 6e804c0 alone produced no differences at all.
  • Depth-20 tree pure main on JS: same output, 0.37 s, the same as 73525e6 (main: 0.27 s).
  • Not run: the perf gate, CUDA, Linux.

Your three items (the process.once("exit", io_sync) listener, the non-fresh _ns0 aliases, and the io_sync copy) are not addressed in these commits. The C Nat bound (2^48-1 in C vs 2^53 in JS) is also still open.

@nicolas-abril

Copy link
Copy Markdown
Collaborator Author

@Lorenzobattistela thanks. 524050d handles all three:

  1. Exit listener: removed. io_run's finally already writes what's held, so io_std no longer adds a process.once("exit", ...) on each run.
  2. _ns aliases: now made fresh like pre. One fresh helper checks every def and constructor name, so a main-file _ns1.Circ next to an imported Circ builds. tests/show/imported_adt_alias covers this. It failed with "the alias _ns1 shadows _ns1.Circ" before the fix.
  3. io_sync copy: a write of 64 KiB or more now goes out as is, after whatever is held, so it isn't copied. Smaller writes are still held and joined as before, so at most about 128 KiB gets copied at a time. A program that mixes prints with 128 KiB and 80 KiB IO.writes gives the same bytes on JS and C.

On the 200k List<U32> overflow: we're keeping parity with main for now. We found no option that's simply better: streaming prints more before it overflows, but it's slower.

Checked: repo 54/54, ping 62/62, test 1696/1696. JS depth-20 tree is unchanged (0.36 s, same md5). comp.ts is 65,502 tok (cap 66,000). Perf, CUDA and Linux not run.

@nicolas-abril nicolas-abril left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of 524050d against its merge-base 60ca80a. Everything below was measured or reproduced on an M4 Max unless marked otherwise. Items 1–4 should be fixed before merging.

1. A pure main's Array should be printed in place, not through Array.to_list

main_pure prints an Array<T> as show(Array.to_list(~T, x)). That copies the whole array into a linked list before printing anything, and on JS the list printer recurses once per element.

main() -> Array<U32> = Array.new(U32, d, 7) main PR
C, 2^20, peak RSS 5.4 MB 29.7 MB
C, 2^22, peak RSS / time 17.3 MB / 225 ms 113.7 MB / 287 ms
JS, 2^16 prints bend: memory fault (machine stack overflow?)
JS, 2^22 prints (147 MB) overflows

Proposal: the generated printer should read the array directly. I wrote both candidates by hand and measured them:

  • Index loop with Array.get (first/loop over Array.size, one IO.write per element on C, a text-so-far accumulator on JS): C at 2^20 is 5.5 MB and at 2^22 is 17.5 MB / 237 ms, the same as main. Output is byte-identical to main's. On JS it prints 2^22 (499 MB, against main's 147 MB; the extra is the string ropes). Array.get needs T: Data, though, and Array<Array<U32>> prints today on both sides.
  • Tree walk matching ALeaf/ANode, with a "first" and a "rest" function for the separators. It works for any T, and its recursion depth is only the tree depth. It costs more than the index loop: C at 2^20 is 13.7 MB and at 2^22 is 49.7 MB / 281 ms.

I'd use the index loop when the element type is Data and the tree walk otherwise.

2. Pure-main Lists still overflow the JS stack, and the description's 3M claim doesn't hold

Since 1b44649, the JS show functions return ", " ++ show(h) ++ show_tail(t), which isn't a tail call. Even before that, the accumulator held the text after the element, so the recursion still wasn't a tail call (33a0dcd also overflows).

List<U32> length, JS 10k 30k 50k 3M
main prints stack overflow stack overflow stack overflow
PR prints prints memory fault memory fault

The fix is to carry the text printed so far: tail(t, acc ++ ", " ++ U32.show(h)), with Nil returning acc ++ "]". That compiles to a loop. Written by hand, it prints a 3M-element list on JS in 0.19 s (389 MB peak). The same form fixes the JS side of item 1.

3. unit_order overflows on long def chains (C)

visit recurses once per unit. Main folds such chains into a handful of functions; the PR makes a unit per def and walks them all recursively. Measured with compile_book, after load and check pass:

Chain main C PR C
each def calls the next, one call site builds up to 200k overflows in unit_order from 10k
lend chain builds at 50k overflows in visit at 20k
two call sites per def builds up to 100k overflows from 10k

This is a regression against main, unlike the JS lane's existing deep-recursion limits:

  • The JS lane never calls unit_order.
  • On JS, a one-call-site chain already overflows at about 5k on main (term_higher under open_of) and between 5k and 9k on the PR (the loop-detection visit at comp.ts:1365, which main has too).

An explicit stack gives the same order. It produced byte-identical C and JS on 133 programs, a temporary check found no differences from the recursive order, and it builds the 200k / 50k / 100k chains above. It costs 116 ttok, which brings comp.ts to 65,618:

@@ -2016,14 +2016,28 @@
   FL.units.forEach((u) => u.segs.forEach((s) => at.set(s.fid, u)));
   const seen = new Set<Unit>();
   const out: Unit[] = [];
+  // a stack of units and the callees left to visit, reversed: a chain of
+  // calls longer than the JS stack still sorts
+  const todo: [Unit, (Unit | undefined)[]][] = [];
   const visit = (u: Unit | undefined) => {
     if (u !== undefined && !seen.has(u)) {
       seen.add(u);
-      u.segs.forEach((s) => s.refs.forEach((r) => visit(at.get(r))));
-      out.push(u);
+      todo.push([u, u.segs.flatMap((s) => [...s.refs].map((r) => at.get(r)))
+        .reverse()]);
     }
   };
-  FL.units.forEach(visit);
+  FL.units.forEach((u) => {
+    visit(u);
+    while (todo.length > 0) {
+      const [v, rs] = todo.at(-1)!;
+      if (rs.length > 0) {
+        visit(rs.pop());
+      } else {
+        out.push(v);
+        todo.pop();
+      }
+    }
+  });
   return out;
 }

4. op_bare: a negative F32 constant breaks the JS output

op_bare treats every constant as safe to inline in JS. js_word prints an F32 with its sign bit set as -1.5 or -0, so f32_neg's template (-$0) becomes (--1.5). Main's ATOM regex sent anything starting with - to a local.

Repro: F32.neg of an F32 built from bits 0xBFC00000, written as 32 nested WCon{…}.

main C main JS PR C PR JS
bits of -1.5 1.5 1.5 1.5 (--1.5) → SyntaxError at load
bits of -0 0.0 0.0 0.0 (--0) → same

Only the JS lane breaks, since C constants are …ull words. Source literals can't be negative, so the constructor form is the only way in. Fix, deciding from the term rather than the emitted text:

-  return s.$ === "Var"
-    || term_const(s) && !(js && s.$ === "Ctr" && s.k in OPTIMIZED.Array);
+  return s.$ === "Var" || term_const(s) && !(js && s.$ === "Ctr"
+    && (s.k in OPTIMIZED.Array || Bend.u32_from_term(s, "F32")! >= 2 ** 31));

With this, both repros print correctly on both lanes, and the 66 buildable tests that mention F32 emit byte-identical JS.

5. The fold cache lets one unit inline far past FOLD_FUEL

FL.folds lives for the whole file, and a cache hit costs no fuel. So a unit gets for free calls that an earlier unit already folded, and the cap no longer holds per segment.

Synthetic test: 200 flat defs g{j}(+x: U32) with roughly 80-node bodies, plus a recursive a that sums g{j}(5) and a recursive b that sums g{j}(7). The table counts the g calls each def leaves unfolded (fewer means more inlined):

a (of 200) b with a present (of 400) b alone
PR 163 385 371
PR, fold cache cleared per unit 185 385 385
main 189 385 393

On the PR, a inlines 37 bodies against main's 11, and its C spin is 3,826 lines instead of 2,154. b's code also changes when an unrelated a is added.

Fix (matches main's per-def memo_gc for folds):

 function unit_emit(u: Unit): void {
   const up = FL.unit;
   FL.stale.delete(u);
+  FL.folds.clear();

With this fix, all 856 C and 898 JS programs in tests/, bench/, demos/ and evals/ emit byte-identical output. Emit time is within noise on boss_3d, hvm5_mini, walkers_test and par_sort. I wouldn't fix this by not caching fuel-limited results instead: that runs the compiler out of memory on the synthetic test.

6. Smaller things

  • The m.args.length === 0 guard (comp.ts:2641) should come back. Nothing reaches the case it guarded:

    • A non-Ref head with live arguments is always CLO_APPLY, and anf lifts it into a let.
    • Flat bodies, the only ones emit_unfold copies, can't contain one.
    • I made the branch exit and built all 1,801 .bend files under tests, demos, bench and evals: no hits.

    But if a future path reaches it, the PR silently compiles f(x) to f, where main stopped with a compiler error. Restoring the guard changes no output:

    -        ?? (m.t.$ !== "Ref" ? m.h : null);
    +        ?? (m.t.$ !== "Ref" && m.args.length === 0 ? m.h : null);
  • own facts are derived while some units are still stale (comp.ts:~3061). This is safe today, and main has the same pattern.

    • It happens in 846 programs, but the only fact read on a path that records a lend is own, and adding it only removes lends. So stale lends are a superset of the fresh ones, and the own facts derived from them are a subset of the correct ones.
    • The last derivation always runs on fresh data.
    • Deriving only when nothing is stale (if (FL.stale.size > 0) continue;) gives byte-identical output if you want the guard.
  • Compiler memory isn't a regression. The PR uses less than main everywhere I measured. Each cell is peak RSS / compile time / heap still held after compile:

    Program Lane main PR
    boss_3d C 584 MB / 1004 ms / 131 MB 344 MB / 361 ms / 83 MB
    boss_3d JS 354 MB / 202 ms / 76 MB 255 MB / 112 ms / 43 MB
    hvm5_mini C 268 / 261 / 42 200 / 156 / 37
    walkers_test C 222 / 263 / 24 166 / 101 / 20
    netcode server C 204 / 149 / 38 159 / 80 / 22
    17 perf benches C 114–148 MB peak 114–143 MB peak
    • Every program settles in 3 sweeps, with 1.4–2× re-emission, so cache growth stays bounded in practice.
    • Clearing the term caches and PROBES per unit, as main's memo_gc did, saves about 11 MB of held heap on boss_3d (6 MB on hvm5_mini) for 0–6% compile time, with identical C. Optional.
    • Narrowing the fact_when callbacks to def saves nothing measurable: boss_3d ends with 591 callbacks. It's still cleaner, since facts_hot and facts_ctr only read sc.def.
    • Separately, the C lane never resets PROBES after compile_book (js_lib does), which only matters for in-process users like the loader.
  • The description is stale:

    • It says the C lane checks foreign Nat answers via Cid.nat, but d680a47 removed that check and ERRS[10] is now JS-only.
    • The 3M-element List on JS claim fails (item 2).
    • The commit list describes the earlier three-commit rebase onto 642d8e1, not the current 11 commits on 60ca80a.

nicolas-abril and others added 2 commits October 21, 2026 19:12
The simplification half of bendlang#1601, on main's pass loop: compile_book still
emits every def again until a pass adds no fact.

- Facts are one set of strings (own, stat, ctr, type, fam, arg, open)
  read through fact(), replacing Hot, FL.stat, FL.own, the brws memo and
  the reset of ten fields per pass; a fact that only leads to others is
  an implication (fact_when). Each def and spin keeps its own output.
- A value carries the borrowed root of each box (Val.rs); a Nat's Succs
  are counted on the term (nat_peel); a table's rows are words on both
  lanes, folded on values, and an op's JS runs on them only if it calls
  nothing past Math.fround and Math.imul; a local is a name name_local
  made; an op's argument is held unless it is a variable or a constant
  (op_bare); a JS call that waits on its callee calls its entry ($$k); a
  JS closure captures what is free in it.
- A match arm that rebuilds its scrutinee reads it (arm_self), so JS
  needs no views.
- A pure main prints through a Bend printer of main's value (main_pure),
  instead of a descriptor and a C and a JS printer; Base gains
  Char.quote, String.quote, F32.show.dot, U32.hex, Array.write and
  Array.show. JS holds stdout off a terminal and writes it before a wait,
  an error, an exit, a File.write or at 64 KiB.
- The term caches live as long as the file; the fold cache is cleared per
  def. Reserved names are refused before anything else, on both lanes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
compile_book emitted every def again until a pass added no fact. Now a
fact read as absent records its reader, and adding it marks only those
units stale; a sweep emits the stale ones callee-first (unit_order). A
flat def, a def with one call site and a spin are units only once a
segment names them (unit_lazy), so unreachable ones are never emitted.

C and JS emit time (C and JS, median of 5, M4 Max): boss_3d 587 ms ->
391 ms; the 35 demos and benches that build, 1.89 s -> 1.38 s in total
(main: 1.13 s and 3.04 s). JS is byte-identical; C matches up to the
order of segments, spins, tables and static nodes, except where a def
no segment reaches no longer adds facts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nicolas-abril nicolas-abril changed the title comp.ts: facts settle per unit, nothing reads emitted text, pure mains print through Bend comp.ts: a fact stales only the units that read it absent Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants