Skip to content

comp.ts: nothing reads emitted text, pure mains print through Bend - #1817

Merged
nicolas-abril merged 4 commits into
bendlang:mainfrom
nicolas-abril:comp-deslop-a
Oct 27, 2026
Merged

nicolas-abril merged 4 commits into
bendlang:mainfrom
nicolas-abril:comp-deslop-a

Conversation

@nicolas-abril

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

Copy link
Copy Markdown
Collaborator

The simplification half of #1601, split out so it can be reviewed apart from the compile-speed change (#1601, which goes on top of this). compile_book keeps main's loop: it emits every def again until a pass adds no fact.

Rebased on main (34c7abb). comp.ts is 65,974 ttok, under the 66,000 cap (main: 65,996). Gates on a Mac: repo 54/54, test 1747/1747, perf 124/124, ping 80/81 (the one failure, the BendHub hyphenated-entry check, fails on main too).

The rebase conflicted only in comp.ts. #1792's shared-allocation hoisting in emit_chain is kept as main has it, so one place still reads emitted text: a regex finds each arm's leading heap_alloc. Replacing it structurally is left for a follow-up.

Fitting the cap (second commit, no change to what a program does)

  • A spin is declared before the spins are defined, so the C file needs no callee-first order and unit_order goes. The prototype comes from the spin's own Seg.head, not from its emitted text.
  • unit_emit alone fills a unit; main_pure opens its own file; def_answer is inlined at its one call; a Nat match pushes each row where it is built; fact_add drains its implications in one call.
  • JS: a $$k entry is a const arrow in both cases; a write of 64 KiB or more joins the held output and is flushed with it instead of going out alone.
  • Comments from the first commit are tightened.
  • Checked by emitting all 1,791 tests and demos with both commits: C differs only by the spin prototypes and the order of spins; JS differs only in the three changes above.

Facts are one set

  • own, stat, ctr, type, fam, arg and open are strings in one set read through fact(), replacing Hot, FL.stat, FL.own, the brws memo and the hand-written reset of ten fields per pass. A fact that only leads to other facts is an implication (fact_when), not a dependency.
  • Each def and spin keeps its own output (segments, lends, closures), so a pass replaces a def's output instead of clearing the file's.

Nothing reads the text it emitted

  • A value carries the borrowed root of each box (Val.rs), replacing a lookup of the word's string in a scope map. A read-only walk owns its list when a sum type puts an unboxed field where another constructor has a boxed one #1805's rule (only a box roots) sits in val_new.
  • A Nat's Succs are counted on the term (nat_peel), not by regexes over emitted nat_chk(... + n).
  • A table's rows are words on both lanes, folded on values: an F32 constant keeps its bits, and an op over rows runs its JS on their values only if it calls nothing past Math.fround and Math.imul. The C lane no longer runs JS text that the JS lane printed.
  • A local is a name name_local made, not a name's shape. An op's argument is held in a local unless it is a variable or a constant (op_bare), replacing the bracket counting.
  • A JS call that waits on its callee calls the callee's entry ($$k), declared after the last def, instead of a marker rewritten by a regex over the finished program. A JS closure captures the variables free in its term. A line is indented as it is pushed.

Match arms that rebuild their scrutinee read it

  • A U32, F32, Nat or Char default arm often rebuilds its scrutinee from the bits and fields it knows. arm_self reads each such rebuild as the scrutinee itself; an arm that no longer reads its fields binds none.
  • The JS lane then needs no views (FL.views, js_view, word_u32 go).

A pure main prints through Bend code

  • It printed three ways: a descriptor of main's type built by the compiler, a C printer and a JS printer. Now a pure main of type T builds as a Bend printer of main's value: main_pure writes it as Bend source, parses it into a copy of the book and checks it. The printer writes as it goes, so a 3M-element List<U32> peaks at 49.6 MB on C as on main (49.4 MB); an Array of Data is read in place by index (Array.write on C, Array.show on JS). A pure main prints imported types too.
  • 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.

Smaller things

  • The term caches live as long as the file; the fold cache is cleared per def, as a hit costs no fuel.
  • A spin is one per def. lay_eq walks the layout DAG instead of comparing JSON. Reserved names are refused before anything else, on both lanes.

Compile time (C and JS emit, median of 5, M4 Max): boss_3d 1,127 ms on main, 587 ms here; the 35 demos and benches that build, 3.04 s → 1.89 s in total.

Verification

  • JS output is byte-identical to comp.ts: a fact stales only the units that read it absent #1601's on all 1,820 programs; C matches it up to the order of segments, spins, tables and static nodes, except 17 programs where a def nothing reaches adds a fact here (comp.ts: a fact stales only the units that read it absent #1601 never emits such defs); those 17 pass on all three lanes.
  • Tests added: f32_table_math_op, f32_neg_const_js, const_array_op_js, array_elements, imported_adt, imported_adt_alias, stdout_file_order.
  • Test added: once_box_return. Review found that the first commit had dropped main's once guard (ret !== BOX || sc.seg.ret === BOX, from 30e5b93). Without it, a once-called def with an index-dependent return fused into a caller typed at one index, and the C build crashed (TypeError on v.lay.arms). The third commit restores the guard.
  • Perf gate on the cluster: 123/124, every runtime and checker cell within 1.15x. The one miss is merkle's compiler time (1.18x; 1.15x on an earlier run). Locally, merkle builds in 778 ms here against 780 ms on main (median of 7), so it reads as node noise. An earlier run lost 8 SEQ-CPU cells to exit 127 (the timing wrapper never ran); a rerun measured them all, matching main.

🤖 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.

Reviewed 46e954e merged onto main 378be5f. Gates: repo 54/54, ping 81/81, perf 124/124, test 1735/1735, safe 1129 agree / 0 false reject. comp.ts is 65,953 ttok (main 66,000). C output matches main on tests/run, compile and show.

Two follow-ups, neither blocking:

  1. The restored once guard only catches a return layout that is exactly BOX. A box inside a pair (F(b) & U32) still fuses into a caller typed at one index, and the C build dies with TypeError ... v.lay.arms. Main does the same, so it is not a regression. Fusing only when lay_eq(ret, sc.seg.ret) || sc.seg.ret === BOX would cover it. I'll open an issue with the repro.
  2. ca301db drops the direct path for JS writes of 64 KiB or more, so io_sync now copies each one with Buffer.concat. If you can fit it, keep that path or write io_held[0] as is when it is the only chunk held.

nicolas-abril and others added 4 commits October 27, 2026 16:15
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>
- Spins are declared before they are defined, so the C file needs no
  callee-first order and unit_order goes. The prototype is the spin's
  own head (Seg.head), not read back from its text.
- A unit is filled by unit_emit alone; main_pure opens its own file.
- def_answer is inlined at its one call; a Nat match's rows are pushed
  where they are built; fact_add drains its implications in one call.
- JS: a $$k entry is a const in both cases; a write of 64 KiB or more
  joins the held output instead of going out alone.
- Comments the earlier commit wrote are tightened.

comp.ts is 65,959 ttok.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first commit dropped main's `(ret !== BOX || sc.seg.ret === BOX)`
guard on `once` (30e5b93). Without it a def whose return depends on an
index (layout BOX) fused into a caller typed at one index, and the C
lane died laying the other index's value into the caller's arms
(TypeError on `v.lay.arms`). once_box_return builds that case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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