Merge train: #9697, #9698, #9699, #9700, #9702 + MERGE_GUIDE.md - #9711
Conversation
…ntry `js_native_call_method`'s handling of a *bare* receiver — a real GC pointer whose value was never NaN-boxed, so its top 16 bits are zero and it decodes as a positive subnormal double — was split between a late magnitude-only recovery and ~1200 lines of probes that ran first. Four defects, all closed by deciding once, at the entry, before the root and before the first probe. * The receiver was not a root. `root_nanbox_f64` parks it in a `RuntimeHandleSlot::Nanbox`, which `gc::try_mark_value` and `gc::try_rewrite_nanboxed_value` both screen on the tag being `POINTER_TAG`/`STRING_TAG`/`BIGINT_TAG`. A bare pointer's tag is zero, so it was neither marked (a mark-sweep inside dispatch can reap it) nor rewritten (an evacuating minor leaves the slot on a from-space address). #6910's hole, re-opened in the transient-handle registry. * `JSValue::pointer` stamps `POINTER_TAG` unconditionally, and `string_methods::dispatch_string` gates on `is_any_string()` — so a bare `GC_TYPE_STRING` receiver was reboxed into something that is not a string and `.slice` never reached the string arm. * No forwarding walk, so an already-moved receiver was used at its old address. * And the mirror image: a word that only LOOKED like an address was still read as one. `1e-310` is `0x1268_8b70_e62b` — above the handle band, inside `is_valid_obj_ptr`'s window, unmapped. The tower's probes are magnitude-gated (`try_read_gc_header` derefs `addr - 8` on address range alone, a contract written for a stale address, where the page is still mapped), so `(1e-310 as any).toString()` SIGSEGV'd in `url::search_params::shape_is_url_search_params`. `5e-324` is `0x1` and aliased the handle band, so the handle dispatcher answered it `undefined`. Node prints `1e-310` and `5e-324`. The entry now asks an owner before any probe runs. The allocator answers strongest (`try_read_tracked_gc_header` proves arena page membership or an exact malloc-registry hit); the address-keyed registries answer for headerless allocations (a `Symbol.for` symbol, an ArrayBuffer/Uint8Array backing store, a typed array) that have no `GcHeader` at all. A vouched receiver is reboxed under its true tag, through `resolve_forwarding`, with the `SYMBOL_MAGIC` content screen keeping a fresh `Symbol()` (also a `GC_TYPE_STRING` allocation) out of the string arm — and it is that value the tower roots. An unvouched word is dispatched as the number its bits spell, and never reaches a pointer-shaped probe. Fixing only the probe that happened to fault would be whack-a-mole: every magnitude-gated probe has the same exposure. Deciding at the chokepoint makes the class unreachable, and retires the magnitude-only tail recovery, which is deleted here. Measured by flipping the entry guard off: pre-fix, `.slice(1)` on a bare managed string receiver did not throw — it returned a `POINTER_TAG` value that `try_read_tracked_gc_header` does not recognise, at a different address every run. A wild pointer handed back to codegen. Closes #9675 Claude-Session: https://claude.ai/code/session_01AqKj15vbuTWf2KZb4Xc4Zi
… executor (#9587) `js_promise_new_with_executor` held `promise`, `resolve_closure` and `reject_closure` in bare Rust locals across `js_closure_call2`, which runs arbitrary user JS. An evacuating young collection inside the executor moves the Promise; the resolving closures' capture slots are rewritten by the collector, but the returned pointer was the pre-collection address. Awaiting that dead copy either fell through on a recycled header that decoded as Fulfilled, or parked the continuation on an object `resolve()` would never settle — a silent hang with no throw and no rejection. Claude Code's trust dialog wedged on it 100% of the time on a fresh HOME, which is also why onboarding never persisted `theme` / `hasCompletedOnboarding` (#9674). Root all three in a RuntimeHandleScope and re-read from the handles, matching `js_promise_subclass_init` / `new_promise_capability`. Also root `executor` (only when it is really a closure) and move `make_resolving_functions`'s `promise` rooting above the allocating arity registration. Adds crates/perry/tests/issue_9587_promise_executor_evacuation.rs and test-files/test_issue_9587_promise_executor_evacuation.ts.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (25)
📝 WalkthroughWalkthroughThe PR adds merge-train documentation and updates runtime behavior for shared stdin handling, bare receiver dispatch, Promise executor rooting, and labeled async loops. It also adds regression coverage for these changes and strengthens affine-index compiler assertions. ChangesMerge workflow documentation
Shared stdin reader
Bare receiver dispatch
Promise executor evacuation
Labeled async loop lowering
Affine indexing regression coverage
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant StdinReader
participant PerryRuntime
participant ReadlinePump
participant NodeStream
StdinReader->>PerryRuntime: Read fd-0 bytes or EOF
PerryRuntime->>ReadlinePump: Forward ordered input blocks
ReadlinePump->>NodeStream: Emit data and keypress events
NodeStream->>PerryRuntime: Remove or list stdin listeners
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Merge train: #9697, #9698, #9699, #9700, #9702, plus
MERGE_GUIDE.md.Cherry-picked onto one branch and validated once as a tree. Rebase-merged so
each commit keeps its author.
What's in it
#9700 —
new Promise(executor)returned a dead promise. The root cause ofClaude Code's onboarding hang (#9587/#9674). The executor is arbitrary user JS
running while
js_promise_new_with_executorstill owns the promise; an ink/Reactrender inside it triggers an evacuating minor that MOVES the promise, but
promise/resolve_closure/reject_closurelived in bare Rust locals acrossjs_closure_call2, so the caller got the pre-collection address. Fails two ways,both silent: the recycled header decodes
Fulfilledand theawaitresumes withgarbage, or it decodes
Pendingand the continuation parks on the dead copy whileresolve()settles the live one — a permanent hang with no throw. All three arerooted now and every address is re-read from its handle. A second hole closes with
it:
make_resolving_functionsrootedpromiseafterensure_native_resolving_arity_registered, which allocates on first call.#9698 — bare managed receivers decided at the dispatch tower's entry. A real
GC pointer that was never NaN-boxed was rooted in a
RuntimeHandleSlot::Nanbox,whose mark and rewrite paths both reject any word without a
POINTER/STRING/BIGINT tag — so the receiver was neither marked nor rewritten
(#6910's hole, reopened in the transient-handle registry). It was also reboxed as
an object even when it was a string, and never walked forwarding. Canonicalization
now runs as the first statement, gated on allocator ownership, not address
magnitude. Two bugs found while validating, both reproducing on unfixed main:
(1e-310 as any).toString()SIGSEGV'd (a subnormal double whose bits look like anaddress, dereferenced by a magnitude-gated probe), and
5e-324aliased the handleband and returned
undefined.#9697 — one physical fd-0 reader. perry-runtime becomes the sole stdin reader.
#9702 — labeled
for-ofacrossawait. TheArrayIterationPatchedguardwraps both generated loops, so neither saw the source label;
break label/continue labelbecame dispatch-loop completions and hung.#9699 — IR pin for #9248's matmul fast path. Asserts both affine receivers are
guarded once in the preheader and the accumulator stays a native double.
MERGE_GUIDE.md— the audit/merge-train process, referenced fromCLAUDE.md.Validation
64/64 lint gates; release build;
perry-runtime,perry-stdlib,perry-transform(allRUST_TEST_THREADS=1); andissue_9692_stdin_surface,issue_9253_affine_range_index,issue_9587_promise_executor_evacuation,issue_5868_switch_state_machine— all green.#9699's assertions were additionally checked against real emitted IR rather than
trusted from a green test: one
__matmul$specdefine, one packed fast-body block(unquoted, so no #9494-class blindspot), 2 guard calls, 3 raw loads feeding
fmul/fadd/store.Note
#9697's author pushed their own version of the raw-handle fix mid-validation; it
uses
across_mutaround the allocating call, which is the better idiom than thewith_mut_ptrI had written. Theirs is what landed.Summary by CodeRabbit
New Features
stdinhandling across runtime andnode:readline, including reliable keypress delivery, listener removal, and EOF processing.Bug Fixes
for...ofloops.new Promise(executor)returning invalid promises after garbage collection.Documentation