Skip to content

A read-only walk borrows its list when a word shares a slot with a box (#1805) - #1806

Merged
Lorenzobattistela merged 2 commits into
bendlang:mainfrom
viridale:fix-1805-word-borrow
Oct 21, 2026
Merged

Lorenzobattistela merged 2 commits into
bendlang:mainfrom
viridale:fix-1805-word-borrow

Conversation

@viridale

Copy link
Copy Markdown
Contributor

Fixes #1805.

What changes

A word that is not a box holds no reference, so the borrow analysis now ignores it in the two places the issue names:

  • val_arms merges an unboxed match's arms into one set of out slots, and each slot collects one borrow label per arm. An arm now adds a label only where that arm has a box in the slot. Before, Flag's Bool added "" next to Hit's hits~0, the labels disagreed, and hits~0 became owned.
  • val_own looks a word's label up only when the word's kind is box. With the first change alone, the merged slot keeps Hit's label, so Flag's Bool, read back out of that slot, carries it. Passing that Bool to a call (ons in the new test) made its root owned again.

The val_own change also covers a case beyond the issue. lay_node lays out a generic constructor once, so Con's head is a box field, and node_fields labels it. A List<&2, U32> then reads that head as a U32 that still carries the label. So a walk that kept its head across its own call (h + sum(tail)) took its list.

The comment above val_new now says a non-box word neither roots nor owns. comp.ts goes from 65,807 to 65,872 ttok (cap 66,000).

What it reaches

I built every runnable test, bench and demo to C and JS with main's compiler and with this one:

  • JS: identical everywhere. The JS lane doesn't use this analysis.
  • Benches: identical C.
  • Five programs' C changes. In each, takes become peeks. The term_keeps that shared the value go away too, and so do the rfc_seals of the types that sharing heated.
    • demos/pure_par_sort: Tree(d) keeps Nats in Sigma's generic fields.
    • tests/check/data_table_inferred: the sum above, so main.out no longer keeps xs.
    • tests/run/family_arg_hot, tests/run/family_field_hot and tests/rfc/drop_spine: see below.

demos/pure_par_sort on an M3 Max (medians of 7 interleaved runs after a warm-up; both print sorted):

main this PR
16 threads 0.090 s 0.064 s
1 thread 0.160 s 0.122 s
GPU (Metal) 0.148 s 0.138 s
16 threads, -falign-functions=64 0.087 s 0.066 s
1 thread, -falign-functions=64 0.158 s 0.120 s

Three tests needed an owner

family_arg_hot, family_field_hot and drop_spine each need a value to be shared, so that its type is hot. Each got that sharing from a reader that owned its argument only because of a stray label. With this PR those readers borrow and nothing is shared: each test's C has no term_keep and no rfc_seal. The tests still pass, but they no longer test what they were written for. To show this, I removed each test's guard from scratch copies of both compilers:

guard removed test main this PR, test as it was this PR, with give
facts_hot walks a stuck family's arguments family_arg_hot (want 29) 2198905222 29 2198905222
emit_ctr heats a hot build's fields (#853) family_field_hot (want 5) 1 5 1

Each test now passes the shared value through give, an identity def that owns what it returns:

  • family_arg_hot: heat is first(give(c)) + first(c).
  • family_field_hot: total(s, build(give(s))).
  • drop_spine: the matcher is renamed head, and first(xs) is head(give(xs)), so each sum owns its copy as before.

Each test's C now has main's keeps, seals, takes and CID_T. I found no removed guard that drop_spine catches: zeroing CID_T's drop column (#1438) passes on main too, with and without BEND_POISON. Its header says it shares one copy, and with give it does again. Each header gets a line naming its owner.

Tests

tests/reg/brw_mixed_word_slot.bend is a borrow witness with three readers: the issue's hits; ons, which passes the Bool to a call; and sum over a List<&2, U32>. main reads both lists after them. On main all three take; with this PR all three peek. Neither change alone makes ons peek.

I ran the gate locally on four shards: PASS: 1692 / 1694. The two failures, gfx_clicks and gfx_window, expect "no display", and they fail on main here too because this Mac has a desktop session. Repo gate: PASS: 54 / 54. tsc reports the same 11 errors as on main.

bendlang#1805)

A word that is not a box holds no reference. val_arms now collects an
arm's borrow label for a slot only where that arm boxes it, and val_own
looks a label up only for a box word. Before, a Bool sharing a slot with
a String made the slot's labels disagree, and a word read through a box
slot (that Bool, or a List<&2, U32>'s head, which the generic Con lays
out as a box) owned its root at an owned position.

family_arg_hot, family_field_hot and drop_spine shared a value through
such a reader. Each now shares it through an owning give, so its C keeps
and seals as on main.
@Lorenzobattistela Lorenzobattistela self-assigned this Oct 21, 2026
val_box handed a non-box word back under its own name, so a List<&2,
U32> head put back into a list ([h]) found the label node_fields gave
the generic Con head, and the box built from it owned the walk's list.
The word now gets a fresh name when it carries a label. lens in
brw_mixed_word_slot witnesses it; drop_spine's header is rewrapped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Lorenzobattistela
Lorenzobattistela merged commit a5bae35 into bendlang:main Oct 21, 2026
1 check passed
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.

A read-only walk owns its list when a sum type puts an unboxed field where another constructor has a boxed one

2 participants