Skip to content

fix(c): write a shared word back to the fork frame slot that holds it - #1615

Merged
Lorenzobattistela merged 1 commit into
bendlang:mainfrom
viridale:fork-frame-keep
Oct 16, 2026
Merged

Lorenzobattistela merged 1 commit into
bendlang:mainfrom
viridale:fork-frame-keep

Conversation

@viridale

Copy link
Copy Markdown
Contributor

Split out of #1596, as asked in its review.

What changes

In sequence, a fork's middle steps read the words they hold from the frame without popping it (STK(-k)). When such a step shares one of them, term_keep may wrap a raw root in a redirect and return a new word, but the frame slot kept the old one. The step that pops the frame then read the raw word, and the join's sink freed the cell the redirect still pointed at.

In Push below, Xs.cnt borrows the list and Xs.push stores it. At --threads 1 the freed cell loops the free list and the program hangs on main:

def Push(+xs: Xs) -> U32:
  a b = Xs.cnt(xs, 0) Xs.push(xs)
  (a + Xs.cnt(b, 0) : U32)

bind_pop now writes the kept word back to the slot it came from:

STK(-1) = _xs_1 = term_keep(e, _xs_1, 1);

Witness

tests/reg/brw_fork_push.bend. The gate runs each binary at the default thread count, where a direct Push(Xs.build(100n, 1)) takes the parallel path and passes. So the test runs Push at the leaves of a depth-12 fork tree, and the leaves run in sequence at any thread count. On main it hangs at --threads 1, 2, 8, 128 and the default. On this branch it prints 823296 at all of them, as do the JS lane and the interpreter.

Checks

  • A/B against main over the 809 programs that build to C and JS (tests, benches, demos). The code changes in 4 programs and no bench:

    • brw_fork_push gains 1 write-back;
    • brw_fork_cross gains 1;
    • compile/family_two_indices gains 3, for the quadrants that Mat.sq shares, whose second use read the stale raw word;
    • the app_slash_boss_3d demo gains 10, in Cloth.mesh and Scene.build.

    Otherwise, C and JS output lose only the cut runtime comments listed below.

  • brw_fork_cross prints 199 and family_two_indices 64 and 512, as on main. SLASH_PROBE=600 on the slash demo prints the same game state as main at --threads 1 and 16. Only the audio queue and voice counts and the frame timings differ, and those vary between runs of either build.

  • gates/test.ts, run locally: PASS 1638 / 1640. The two failures, gfx_clicks and gfx_window, expect "no display" and fail the same way on main here, because this Mac has a desktop session.

  • tsc -p bend2/pack/tsconfig.json (TypeScript 5.9.3): the same 11 errors as main, none in comp.ts.

  • bun gates/repo.ts: 54 / 54.

  • The A/B, the program runs and the test gate above were done before the last five comment cuts (ring_rewind, io_park_add, the Heap and Blk comments, show_main). Against that build, this branch changes only comment lines: in the C of all 809 programs, and in no JS.

comp.ts tokens

Main is at 64,092 / 64,000 by itself, so the repo gate currently fails on every open PR. This PR brings comp.ts to 63,990. To fit, these comments are cut, each one a twin or a restatement of code:

  • C runtime:
    • the twin of the JS io_list comment, "Bytes cross as they are…" (the JS one says it in full);
    • io_park_add's double-link note (io_park_cut);
    • ring_rewind's comment (its body);
    • the Heap comment's bump sentence (the sentence before it lists the misses in order);
    • the Blk comment's blk_node and blk_copy mappings (their call sites).
  • JS runtime: the show_val comment (the C twin says it in full).
  • Emitter:
    • seg_new's comment. It restated seg_take and the runtime's Segments comment, and said a segment pops its frame, which a fork's middle steps do not.
    • show_main's last sentence (the refuse error and the io_type check below it).

#1596, rebuilt on main, makes a disjoint set of cuts. The two merge cleanly, and with both merged comp.ts is 63,886.

In sequence, a fork's middle steps read the words they hold from the
frame without popping it (STK(-k)). When such a step shares one,
term_keep may wrap a raw root in a redirect and return a new word, but
the slot kept the old one. The step that pops the frame then read the
raw word, and the join's sink freed the cell the redirect still pointed
at. In Push (Xs.cnt borrows the list, Xs.push stores it) the freed cell
looped the free list and --threads 1 hung. bind_pop now writes
the kept word back: STK(-1) = _xs_1 = term_keep(e, _xs_1, 1).

The code of 4 programs changes and of no bench: brw_fork_push,
brw_fork_cross, family_two_indices (the quadrants Mat.sq shares, whose
second use read the stale raw word) and the slash_boss_3d demo
(Cloth.mesh and Scene.build), whose SLASH_PROBE run prints the same
state. Otherwise C and JS output lose only the cut runtime comments.

New witness: tests/reg/brw_fork_push.bend. A direct Push takes the
parallel path at the gate's default thread count and passes, so Push
runs at the leaves of a depth-12 fork tree, which run in sequence at
any thread count; before this commit it hangs at 1 to 128 threads.

comp.ts: 64,092 -> 63,990 / 64,000 tokens (main is 92 over its cap).
To fit, comments are cut, each one a twin or a restatement of code:
the C runtime's io_list comment (the JS twin says it in full), the JS
runtime's show_val comment (the C twin says it in full), seg_new's
(it restated seg_take and said a segment pops its frame, which a
fork's middle steps do not), show_main's last sentence (the refuse
error and the io_type check below), io_park_add's double-link note
(io_park_cut), ring_rewind's comment (its body), the Heap comment's
bump sentence (the one before it lists the misses in order) and the
Blk comment's blk_node and blk_copy mappings (their call sites).
@Lorenzobattistela
Lorenzobattistela merged commit 5d6c87a into bendlang:main Oct 16, 2026
1 check passed
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 19, 2026
comp.ts keeps this branch's structure, with upstream's changes ported in:
bendlang#1615's frame-slot write-back in bind_pop; bendlang#1617's named memo functions
(open_of, spine_of, uses_of), per-file literal steps keyed by string, and
lay_at memoized per type key (FL.ty_opens, keyed by ty_key); bendlang#1625's
F32.read regex; bendlang#1628's string_contains.

bendlang#1617's per-def PROBES reset is left out: here probes and the term caches
live as long as the file and units emit again across sweeps, so a reset
would reuse probe indices still held in FL.opens and FL.uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Lorenzobattistela Lorenzobattistela mentioned this pull request Oct 20, 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