Merge train: #9728, #9729, #9730, #9731, #9732, #9733 - #9735
Merged
Conversation
added 9 commits
September 4, 2026 16:24
…isplay A dynamically dispatched `x["toString"]()` on a number reached three arms of the native-method tower that formatted with a bare `f64::to_string()`. That is Rust's Display: it never switches to scientific notation and spells the infinities `inf`, so `2.2e-308` printed ~308 decimal digits and `Infinity` printed `inf` — while the same value's four static renderings were correct in the same program. The three arms are the plain-number and boxed-`Number` `toString` in `dispatch_common` and the boxed-`Number` `toString`/`toLocaleString` in `dispatch_primitive`. All now call `js_number_to_string`, which carries the spec's `|n| >= 1e21 || |n| < 1e-6` switch and its own integer fast path. This is the same mistake #3987 fixed in the string-concat fast paths; these arms were not part of that sweep. A neighbouring defect in the same arms rides along: a boxed receiver dropped an explicit radix, so `new Number(255).toString(16)` answered "255". Both boxed arms now route an explicit radix through `js_jsvalue_to_string_radix`, as the unboxed arm already did. `toLocaleString`'s argument is a locale, not a radix, so it keeps ignoring it. Closes #9713
…9708) Every inline-cache site owned a `[12 x i64] zeroinitializer` global, 96 B of __bss per site whether or not the program executed it; on the Claude Code bundle that was 262k caches and 18.7 MB dirty at idle. A site now owns `@perry_ic_N = private global ptr null`; the runtime allocates the cache words from an arena on the site's first priming miss and publishes them into the slot with a CAS. Hit paths load the slot, fold `!= null` into the receiver guard they already evaluate, and read the words through the pointer; every miss entry takes the slot's address. Cache layout and prime/evict policy are unchanged; IC hit counters are identical. Claude-Session: https://claude.ai/code/session_014RVEmbrpNKHwaQdrcMgMHc
A `break label` / `continue label` that targets an outer loop from inside a
nested loop threw `TypeError: Cannot read properties of undefined (reading
'done')` (break) or silently produced nothing (continue), in sync
generators, async generators and async functions alike.
Generator linearization gives each loop one break sentinel and one continue
sentinel, so a completion can only name the loop it sits in.
`rewrite_labeled_bc_in_stmts` converts labeled completions to plain ones at
the labeled loop's own body level and stops at nested loops — a plain
completion there would bind to the nested loop. The escape that was left
survived into a state body, where the dispatch lowering has no sentinel for
it and dropped it. The code noted the gap ("the single-sentinel scheme
can't yet distinguish targets").
Unwind it through a carrier local instead, so every completion the
linearizer sees is plain: the escape sets the carrier and plain-breaks out
of its loop, each intermediate loop propagates with `if (carrier != 0)
break`, and the labeled loop turns the carrier back into the real
`break`/`continue`. A switch carrying an escape is desugared to `if`s
first, since a plain `break` inside a switch binds to the switch.
The issue's own repro was already fixed by #9189; the surviving hole is the
cross-loop target, where the switch turns out to be incidental.
Closes #9199
…assifier (#9717) A `#private` array pushed past its inline capacity leaves a permanent forwarding stub at the pre-grow address, and the reference pointing at it is never rewritten (#6228/#233), so a live slot can keep naming the stub. A synchronous full trace handles this: its exact census (`record_arena_header`) admits every arena object, stubs included, so `mark_field_into_worklist` marks the stub and `trace_one_worklist_header` follows it to the live array. A budgeted full trace — what the idle-time reducer (`PERRY_GC_IDLE_RECLAIM`) runs when a server goes quiet — resolves membership through the page-metadata classifier instead. `classifier_valid_object_start` rejected every FORWARDED header (a dead metadata key's recycled bytes can set the bit, #8040), so the field->stub edge was dropped: the stub was never marked, the FORWARDED-follow never ran, and the array reachable only through it was swept. The private field then resolved to reused memory as an empty array, so every hono route `match()` returned 404 for the life of the process — but only when the first request arrived ~10-20s after startup while background work allocated. The classifier is documented as a census superset; for growth stubs it was not. It now admits a plausible forwarded arena stub (`GC_FLAG_ARENA` set, valid obj_type/size), the shape a real growth stub has. The forwarding target is still validated in the follow, so a garbage target stops the walk. A `PERRY_GC_DIAG` counter (`forwarded_stub_recoveries=`) reports recoveries. Regression: gc::tests::forwarded_stub_membership plants the edge, asserts the pre-fix gate would have rejected the stub, drives a budgeted full cycle, and checks the stub-reached array survives; a synchronous control keeps it without recovery. The budgeted test fails without the fix and passes with it. Claude-Session: https://claude.ai/code/session_01GkugRUwRCCjfYYNfzyyvQv
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train: #9728, #9729, #9730, #9731, #9732, #9733, plus one gate verdict. Validated once as a tree, rebase-merged so each commit keeps its author.
Three of these are GC changes that interact — when idle reclaim runs (#9731), what a budgeted cycle will trace (#9732), and when a descriptor may be retired (#9733) — which is why they were validated together rather than landed separately.
#9732 — a hono server 404s forever on a late first request (#9717).
hono/router/smart-routerreplays not-yet-installed routes from a#privatearray on the firstmatch(). Growing that array past its inline capacity leaves a permanent forwarding stub, and the live field keeps pointing at the stub. A synchronous full trace copes: its exact census admits every arena object, stubs included. A budgeted full trace — what the idle reducer runs when the server goes quiet — resolves membership through the page-metadata classifier, which rejected everyGC_FLAG_FORWARDEDheader by design (#8040). So the field→stub edge was dropped, the array reachable only through the stub was swept, and every route returned 404. Only reproduces on a late first request, because an early one builds the router before any idle collection.The classifier now admits a plausible forwarded arena stub —
GC_FLAG_ARENAplus a validobj_type/size. That is precisely what separates it from #8040's case, whose recycled bytes carriedobj_type = 104(noGcTypeInforecognises it, percopying.rs:335); the pre-existing #8040 tests still pass. The forwarding target is still validated in the follow, so a garbage target stops the walk. The new regression asserts the pre-fix gate would have rejected the stub and fails without the fix, verified by disabling the branch.#9731 — right-size idle arena capacity. Adds no new env knob (reuses
PERRY_GC_DIAG), so there is no new mode needing a CI arm to exercise its OFF state. Its tests discriminate in both directions:!owed()for a single low collection,owed()for two consecutive — the negative assertion is what proves the hysteresis gates rather than always firing.#9733 — retire uncarried shape descriptors (#9726). Stacked on #9724 (landed in #9734); only its top commit is picked here. Retirement is gated on a full-trace census of reachable objects, complete by construction, with per-record carrier flags set at each stamp site covering ids held outside object headers — better than a central enumeration that must be kept in sync. Minor and budgeted cycles stay conservative. It preserves all 17 of
shape_descriptor_census.py's pinned patterns (13 on main before #9724).#9730 — labeled escape out of nested loops (#9199).
break Ofrom a nested loop producedCannot read properties of undefined (reading 'done');continue Oproduced nothing at all, with thefor awaitnever yielding and never settling. HandlesStmt::Try/finallyalong the unwind path, and keeps the rows that already worked via the switch-wrapped route (#9186/#9189) as regression guards.#9729 — inline caches per used site (#9708). 262k caches on the Claude Code bundle, 25 MB of zero-fill, 18.7 MB dirty resident at idle, replaced by an 8-byte slot per site with words allocated on first priming miss and published by CAS. GC-neutral: the cache word is
(ShapeId << 32) | class_id— packed integer ids, never a pointer — andperry_icappears nowhere undergc/, so these were never traced roots on main either.#9728 — dynamic
x["toString"]()on a number (#9713). Formatted with bare Rustf64::to_string(), which never switches to scientific notation and spells the infinitiesinf, while the same value's four static renderings were correct. All three arms now route throughjs_number_to_string, and the radix argument the old code ignored is honoured. Gap test pins-0,1e21,1e-6/1e-7,2.2e-308,1e-310,Infinity,NaN.Gate verdict (mine). #9732 adds a
PERRY_GC_DIAGcounter,FORWARDED_STUB_MEMBERSHIP_RECOVERIES, without classifying it, sogc_runtime_root_holderswent red. It is aCell<u64>tally holding no address; recordednot_a_gc_pointer.Validation
64/64 lint gates; release build;
perry-runtime,perry-transform,perry-codegen(allRUST_TEST_THREADS=1) — all green.