Skip to content

fix(runtime): release tail-drop frames before the last child - #1438

Merged
nicolas-abril merged 18 commits into
bendlang:mainfrom
oxura:fix/1307-stable-wrapped-erasure
Oct 19, 2026
Merged

nicolas-abril merged 18 commits into
bendlang:mainfrom
oxura:fix/1307-stable-wrapped-erasure

Conversation

@oxura

@oxura oxura commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1307.

term_drop frees a node's frame before descending into its last live child, so recursive spines release cell by cell. Every child is still dropped once.

This keeps the maintainer's 6f7d462d commit and merges main through b869cca4 (including #1581); no history was replaced. The no-live case uses the ordinary frame, cid_spine is gone, and spine metadata comes from the constructor's own type. lay_holds reuses lay_pack instead of maintaining a second word-by-word union algorithm; the one-use spine calculation lives beside its CID row. Wide-node distances still use the actual layout width, not the encoded arity.

The frame-invariant comment remains, tightened to describe the shortened-wide-end case. The BEND_POISON note is unchanged. Related C declarations and address calculations are simplified; no code moved out of comp.ts.

Current Linux checks

Linux x86-64, Clang 22.1.8:

  • Five existing RFC fixtures (drop_spine, drop_array_spine, drop_spine_flat, drop_spine_wide, drop_spine_wordless), each compiled at -O3 with and without BEND_POISON, then executed with 1 and 4 threads: 20 exact-output native checks passed on the integrated head.
  • Constructor metadata compared against the exact pre-simplification merged head on a bounded 200-fixture sample: 141 identical CID tables, 59 inputs refused C emission on both sides, no differences or timeouts. This is not an exhaustive repository comparison.
  • bun gates/repo.ts: PASS: 54 / 54. comp.ts: 63,998 / 64,000 ttok, including main's new host declaration. Caps unchanged; no WONTFIX or release changelog edits.

The full test gate was attempted but could not resolve the private cluster SSH host, before any shard ran. The installer gate reports SKIP because the sibling site checkout is absent. No current-head macOS, CUDA/Metal, hub, cluster perf/safe, sanitizer or performance result is claimed. Earlier measurements and the maintainer's Mac qualification remain in the review history; they are not fresh Linux evidence.

AI-assisted without subagents.

@nicolas-abril nicolas-abril self-assigned this Oct 9, 2026
nicolas-abril added a commit to oxura/bend that referenced this pull request Oct 9, 2026
…ld order

compile_book puts each constructor's spine in CID_T: the word of its
last field whose type holds its own datatype (255 when none). term_drop
swaps the spine into the last slot when it opens the node, so the last
child's early free of bendlang#1438 fires on the spine whatever the field
order: a list with its tail first, before a word or behind a List drops
in constant frames. The inner loop has one pop for the last and the
exhausted child, and skips the dead counter and cursor writes there.
tests/rfc/drop_spine covers the three shapes, owned and shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nicolas-abril added a commit to oxura/bend that referenced this pull request Oct 11, 2026
…ld order

compile_book puts each constructor's spine in CID_T: the word of its
last field whose type holds its own datatype (255 when none). term_drop
swaps the spine into the last slot when it opens the node, so the last
child's early free of bendlang#1438 fires on the spine whatever the field
order: a list with its tail first, before a word or behind a List drops
in constant frames. The inner loop has one pop for the last and the
exhausted child, and skips the dead counter and cursor writes there.
tests/rfc/drop_spine covers the three shapes, owned and shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nicolas-abril
nicolas-abril force-pushed the fix/1307-stable-wrapped-erasure branch from d3bc283 to d757f53 Compare October 11, 2026 19:44
nicolas-abril added a commit to oxura/bend that referenced this pull request Oct 12, 2026
…ld order

compile_book puts each constructor's spine in CID_T: the word of its
last field whose type holds its own datatype (255 when none). term_drop
swaps the spine into the last slot when it opens the node, so the last
child's early free of bendlang#1438 fires on the spine whatever the field
order: a list with its tail first, before a word or behind a List drops
in constant frames. The inner loop has one pop for the last and the
exhausted child, and skips the dead counter and cursor writes there.
tests/rfc/drop_spine covers the three shapes, owned and shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nicolas-abril
nicolas-abril force-pushed the fix/1307-stable-wrapped-erasure branch from d757f53 to ad071bf Compare October 12, 2026 17:03
oxura and others added 4 commits October 14, 2026 08:58
…ld order

compile_book puts each constructor's spine in CID_T: the word of its
last field whose type holds its own datatype (255 when none). term_drop
swaps the spine into the last slot when it opens the node, so the last
child's early free of bendlang#1438 fires on the spine whatever the field
order: a list with its tail first, before a word or behind a List drops
in constant frames. The inner loop has one pop for the last and the
exhausted child, and skips the dead counter and cursor writes there.
tests/rfc/drop_spine covers the three shapes, owned and shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pushed frame drops its trailing trivial slots, so a constructor, closure,
task, Array or wide node frees before its last live child whatever words
follow it; an Array or wide node whose live end is under 256 becomes a
plain frame at its block's class. The tail column of CID_T is computed
where the table is printed: the last box of the last field that holds the
node's own datatype (a Maybe's pointer, not its tag), so a spine declared
first or wrapped frees as early as one declared last. drop_spine adds a
list whose spine sits in a flat Maybe; drop_array_spine runs a spine
through a 4-cell and a 512-cell Array.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ctr_spine picks the last box of a field that holds the node's own
datatype. When that field has no box (Succ's Nat word, a phantom
parameter), the old offset arithmetic gave -1, which Metal rejects as a
narrowing in the CID_T initializer, so any such program with a `!` call
failed its GPU build. The trailing-trivial trim runs one loop for plain
and Array frames. comp.ts goes from 64,048 to 63,990 ttok.
tests/rfc/drop_spine_wordless covers the wordless field under `!`.
@Lorenzobattistela
Lorenzobattistela force-pushed the fix/1307-stable-wrapped-erasure branch from 40f9cc3 to 5a1ba75 Compare October 14, 2026 11:59
nicolas-abril and others added 2 commits October 14, 2026 16:24
…omp.ts back under its cap

- CID_T's spine column is the spine's distance from the node's end (0: no
  swap), as u32, so a wide node past 255 words gets its swap too.
  ctr_spine reads lay_node's own arms and counts a box by the type it
  stores, so a phantom or a sibling box in a flat field is not a spine.
- A node with no live cell frees at once, without a frame; the walker
  no longer checks for an empty frame.
- An Array or wide node whose live end is 256 or more keeps that end in
  its last cell, so the walker stops there and frees early instead of
  scanning the trailing trivial cells a second time.
- heap_free fills a freed block with a bad shared pointer under
  -DBEND_POISON (CCC_OVERRIDE_OPTIONS=+-DBEND_POISON), so a read of a
  freed block fails.
- Tests: a 300-field node with its spine at word 200, and a flat field
  with a phantom of the node's type.
- comp.ts is 63,970 ttok: the File type comes from file_new, and smaller
  simplifications in the C runtime, the IO layer and the JS runtime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nicolas-abril

nicolas-abril commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

I pushed follow-up commits (2287053, then 7509c88) on top of a merge of current main. They fix the review findings. Here is what changed compared to your head:

  • The spine column is now a distance from the end of the node. CID_T[k][2] holds how far the spine slot is from the node's last word, and 0 means no swap. So there is one "none" value instead of 255, and the PR description's "255 when there is none" is now 0. The column is u32 and the C code reads it through a new cid_spine accessor. Wide constructors, past 255 words, get the swap too.
  • ctr_spine reads lay_node(k)'s own arms. It no longer rebuilds the layout: a new lay_holds walks each field's slots, recursing into flat layouts. It judges each box by the type stored in it, not by its field's type, so a phantom parameter or a sibling box in a flat field is no longer picked. For example, RCon{rest: RL, p: P2<Ph<RL>, List<U32>>} now picks rest.
  • A node with no live cell is freed right away, without pushing a frame, so the walker no longer needs j >= n.
  • A large node keeps its live end in its last cell. When an Array or wide node's live end is 256 or more, it is stored in the block's last cell, which is trivial by then. The walker stops at that end, so the node frees early and its trailing trivial cells are no longer scanned twice.
  • Opt-in poisoning in heap_free. Built with -DBEND_POISON, it fills a freed block, past its free-list link, with a bad shared pointer, so a read of a freed block fails. To turn it on for a normal build, set CCC_OVERRIDE_OPTIONS=+-DBEND_POISON. I planted a read-after-free in term_drop to check it: the spine tests fail with poisoning on and pass without it. CI does not build with it.
  • New tests: tests/rfc/drop_spine_wide.bend (a 300-field node with its spine at word 200) and tests/rfc/drop_spine_flat.bend (the phantom flat-field case).
  • comp.ts is over its cap: 64,778 ttok. 2287053 cut it to 63,970 with simplifications elsewhere in the file. 7509c88 undid them to keep this PR to the drop change, so repo-gate fails here, and the cap is being handled separately.

Tested on macOS (M-series):

  • C lane: rfc, run, reg, base, io, state, eval, halt and stats, both plain and with poisoning. This PR passes 374 and main passes 369. The same 14 tests fail on both; those failures come from my local harness, not from this change.
  • JS lane: the five drop tests pass.
  • Benches: tree-bitonic, tree-radix, merkle and hashmap are within noise of main.
  • Not run: CUDA, Linux, and the test/perf gates on the minis.

🤖 Generated with Claude Code

…h lay_holds

The File type, the ternaries and the other simplifications outside
term_drop are back as they were on main; comp.ts goes over its cap again
(64,778 ttok), to be cut separately. ctr_spine walks each field's slots
with lay_holds, which recurses into flat lays with plain loops, and
term_drop has a comment on its frames and the live end a large node keeps
in its last cell.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@oxura

oxura commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for pushing the follow-ups and for separating the drop correctness changes from the cap work. I have read the updated 7509c88 notes, including the remaining 64,778-ttok failure. I am not treating the macOS runs as Linux/CUDA qualification or trimming unrelated comments to hide the cap. I will update the old 255-sentinel wording when qualifying this head.

@oxura

oxura commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Completed the promised Linux qualification at 7509c88 and corrected the stale PR description. CID_T spine metadata is now documented as an unsigned distance from the last word, with 0 meaning no swap; wide/phantom-flat and live-end follow-ups are preserved. All five fixtures passed emitted JS plus normal/poisoned native C at 1 and 4 threads: 25 exact-output checks. Native GPU execution was explicitly disabled, and captured Clang output confirms -DBEND_POISON was applied. No source changes or force push. The known 64,778/64,000 comp.ts cap failure remains an explicit merge hold; I did not rerun it, alter caps or trim unrelated code. No CUDA/Metal, current-head sanitizer, fresh performance or full-cluster gate claim; your macOS/poison control evidence remains attributed to you.

…hrough lay_holds, a node with no live cell goes through the frame like any other, and the two new comments are gone
@nicolas-abril

Copy link
Copy Markdown
Collaborator

This comment and commit 6f7d462 were written by an AI agent (Claude), unsupervised. No human has reviewed them.

What 6f7d462 changes (on top of 7509c88 plus a merge of main):

  • The two comments the PR added are gone (above lay_holds and ctr_spine). The term_drop comment and the BEND_POISON note stay.
  • ctr_spine reads the spine off the constructor's own type through lay_holds, so no separate spine logic is needed.
  • term_drop: a node with no live cell goes through the frame like any other node. The special early heap_free branch is gone.
  • The cid_spine macro is gone.

Checks (macOS):

  • CID_T tables: same md5 for all 675 test files before and after the change (the 152 files that don't compile to C fail on both sides).
  • tests/rfc/drop_*.bend (array_spine, spine, spine_flat, spine_wide, spine_wordless): C output matches the expected values with and without -DBEND_POISON.
  • Gates: test 1639/1639, ping 57/57, perf 124/124. gates/safe.ts tests (26 nodes): 1652 files, 1063 agree, 0 false rejects, 0 timeouts.
  • repo 53/54 FAIL: bend2/comp.ts is 64,296 > 64,000 ttok. This PR adds ~640 ttok over main, so it still needs about 300 tokens cut, or the cap raised.

Not run: Linux, CUDA, the hub corpus.

@oxura

oxura commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I am taking 6f7d462 as the base and keeping the term_drop and BEND_POISON notes. I will reduce the PR-added implementation rather than trim unrelated comments or ask to raise the cap, then qualify the result on Linux. Your macOS results remain the baseline; I will not rerun them just to confirm them.

AI-assisted; no subagents.

@oxura

oxura commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Kept 6f7d462 and merged current main, then pushed fcd72b6. Reusing lay_pack and simplifying the related frame code gets comp.ts down to 63,985 tokens; repo is 54/54 without changing the cap. The frame comment remains in shorter form, and the poison note is unchanged. Linux: all 20 native ownership checks pass, including BEND_POISON; the bounded 200-fixture CID comparison found no differences. Full cluster tests are still unavailable here, and I have not presented the Mac results as my own. The description now reflects this head instead of the obsolete 7509c88 implementation.

@oxura

oxura commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up after #1581 landed: merged b869cca and pushed 1059b08. The integrated compiler is 63,998/64,000 tokens, the 20 Linux native checks pass, and the new GitHub run for this exact head is green: https://github.com/bendlang/bend/actions/runs/38767613356 . The earlier red run was the 64,006-token merge; this is a new passing head, not a rerun of that failure. No cap change or removal of the maintainer notes.

nicolas-abril and others added 5 commits October 16, 2026 14:26
- term_drop reads a node's first word only when it has a live word, so
  a zero-arity task no longer drops its continuation.
- An array frame reads its live end once and keeps it beside the counter
  instead of reloading the last cell for every element.
- CID_T is u16[3] (6 bytes a row, was u32[3]); a wide node's spine
  distance needs 16 bits. Rows are built with each constructor's layout.
- ty_holds takes a family name, shared by lay_of and lay_holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each row names its fields (Fid: ar, resw, bangs, nofk; Cid: ar, hot,
spine). A Cid row is 4 bytes; FID_T's two flags take a byte each, as
Metal has no bitfields.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A field nested n deep in a two-armed inline datatype made lay_holds
visit 2^n arms: reg/show_shared_layout (24 deep) took 16-110 s to
compile and put the test gate past its cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nicolas-abril
nicolas-abril merged commit d194597 into bendlang:main Oct 19, 2026
1 check passed
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 19, 2026
bendlang#1438's tail-drop spine and bendlang#1447's device rounds come in as written. The
constructor table takes bendlang#1438's Cid struct with this branch's nat flag
(a foreign Nat answer checked as it comes in) as one more byte; lay_holds
keys by ty_key. tests/io/nat_host_neg takes bendlang#1656's JS-only form.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 20, 2026
Drops the fid_bangs, cid_hot and cid_nat macros, turn_fits and the CUDA
bend_dev exit; shortens main_pure, turn_groups, turn_set, map_msb_u,
map_bit and comments; io_run requeues through io_push. lay_holds keeps
its memo: without it fan-out types compile 7-13x slower (a 200-type
chain of 100-ctor types: 0.31 s vs 4.06 s).
Lorenzobattistela added a commit that referenced this pull request Oct 20, 2026
main's ty_holds now takes p as a datatype name too (#1438); a name
predicate keeps the first-met arguments, as lay_of's re-entry check did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 20, 2026
Drops the fid_bangs, cid_hot and cid_nat macros, turn_fits and the CUDA
bend_dev exit; shortens main_pure, turn_groups, turn_set, map_msb_u,
map_bit and comments; io_run requeues through io_push. lay_holds keeps
its memo: without it fan-out types compile 7-13x slower (a 200-type
chain of 100-ctor types: 0.31 s vs 4.06 s).
@Lorenzobattistela Lorenzobattistela mentioned this pull request Oct 20, 2026
paymog added a commit to paymog/bend-kit that referenced this pull request Oct 24, 2026
bendlang/bend#1438 ships in 2.0.38, which CI already pins. The minimal
reproduction now stays flat across all 24 repeats. Since 2.0.38, runtime
flags go inside +RTS ... -RTS, so the documented run command changes too.
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.

Native automatic erasure progressively slows repeated wrapped-list allocation; explicit traversal stays fast

3 participants