You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
runtime: every built-in iterator step allocates a "next" key string to prove nothing was patched (the guard that should prevent it is dead after the first iterator) #9846
Every step of every built-in iterator allocates a 4-byte "next" key string.
object/iterator_prototypes.rs::call_overridden_iterator_next is the per-step
probe that lets a user replacement of %ArrayIteratorPrototype%.next (and the
Map / Set / String family prototypes) drive for…of, spread, Array.from and
manual .next(). It ends in a by-name prototype lookup:
let key = scope.root_raw_const_ptr(crate::string::js_string_from_bytes(b"next".as_ptr(),4));let method = ...js_object_get_field_by_name(proto, key) ...
so on the overwhelmingly common path — nothing patched — it mints a fresh
string per iteration step purely to conclude that nothing was patched.
Why the existing guard does not prevent it
The function carries an early-out meant for exactly this:
"the tower was never materialized, so no override can exist". But every
iterator allocator calls attach_iterator_prototype → ensure_iterator_prototypes,
which materializes the tower. The guard is true exactly once per program and
false forever after, so it protects nothing on any program that has allocated
a single iterator.
Impact
Third-ranked allocation site by count in a claude-code allocation census
(2026-09-06): ~122,880 × 32 B per 400-character reply, 17.1 % of the top-30
count. Caller walk in the shipped binary:
It is not specific to any one iterator: one allocation per step of every array,
Map, Set and string iterator in the program.
Fix
An allocation-free proof of "not overridden" on the common path: the
prototype's OWN next slot still holds a closure whose native entry is the
canonical thunk (#9480's certified non-allocating own-field read), AND no
accessor descriptor is recorded for "next" on it (#6759 C2's per-key accessor
Bloom bit — required because defineProperty(proto,"next",{get}) leaves the
old closure in the data slot). Anything else — replaced, deleted, an accessor,
a bound copy of the original — takes the by-name path unchanged.
Measured
Counter on a relinked claude-code binary (the fix plus a measurement-only
hit/miss counter). Before the fix every probe allocated, so hits + byname is
the pre-fix count and byname is what survives:
reply
probes = pre-fix "next" strings
byname = post-fix
400 chars, run A
144,189
0
400 chars, run B
144,303
0
3300 chars
887,076
0
byname = 0 on every one of the 173 per-minor reports across the three runs:
the allocation-free proof answers 100 % of probes on a real program. At 32 B
that is 4.6 MB (400-char) and 28.4 MB (3300-char) of allocation removed per
process.
Fixed by #9848, landed on main via merge train #9875. The train merged as its own branch, so the close-keyword never fired — closing manually, verified on main.
Activity
chore: renumber the changelog fragment to the filed issue (#9846)
proggeramlug commentedon Sep 6, 2026
Fixed by #9848, landed on
mainvia merge train #9875. The train merged as its own branch, so the close-keyword never fired — closing manually, verified on main.chore: renumber the changelog fragment to the filed issue (#9846)
docs: point the segment-view references at the issue that exists (Per…
docs: point the segment-view references at the issue that exists (#9843)