fix(codegen): don't refresh local_types for a box-captured local - #10747
proggeramlug wants to merge 2 commits into
Conversation
#10488 added a `ctx.local_types` refresh to the redeclaration branch of `lower_let` so `is_numeric_expr` and `static_type_of` would stop disagreeing about a hoisted `var`. A captured local reaches that same branch without any redeclaration in the source: `Stmt::PreallocateBoxes` registers the id up front, so its one real `Stmt::Let` finds `ctx.locals` already populated and lands there. For such an id the refined type describes the VALUE while the slot holds a box pointer. The `local_types` readers then lower reads as raw local loads instead of `js_box_get_bits`, so the declaring scope read `undefined` while a closure over the same binding, holding the box directly, still saw the real value -- `peek() === dest` was false. It fixed that desync for hoisted `var` and introduced a new one for box-captured locals. Skip the refresh when `ctx.boxed_vars` holds the id; a hoisted `var` is unboxed unless separately captured, so #10488 keeps its fix. Reached only once `Array.prototype` has carried an indexed property, which arms the monotone array-index deopt and routes element stores through the generic runtime-key path. The trigger is rare; an array local captured by a closure is ordinary code.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe fix prevents boxed captured locals from receiving value types for pointer storage. Hoisted unboxed variables retain the type refresh. A regression fixture covers captured arrays, control cases, mutations, and hoisted redeclarations. ChangesCaptured local fix
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 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 |
|
Landed in merge train 226 (#10751), released as v0.5.1605 — main is now Closing rather than merging is how trains work here: the four PRs were cherry-picked onto one tree, validated together, and landed under the train's own commit, so GitHub cannot mark this one merged even though your change is on main. The workspace count triple was re-derived on the assembled tree rather than taken from any PR's recorded value: 78 members / externalize=29 / keep=44. Both #10679 and #10691 correctly recorded 78/29/44 against Validation: all nine cheap gates, |
What went wrong
0ed806587c(#10488) added actx.local_typesrefresh to the redeclaration branch oflower_let, sois_numeric_exprandstatic_type_ofwould stop disagreeing about a hoistedvar.It fixed that desync for hoisted
varand introduced a new one for box-captured locals.A captured local reaches that same branch without any redeclaration in the source:
Stmt::PreallocateBoxesregisters the id up front, so its one realStmt::Letfindsctx.localsalready populated and lands there. For such an id the refined type describes the value, while the slot holds a box pointer. Thelocal_typesreaders then lower reads as raw local loads instead of going throughjs_box_get_bits.Result: the declaring scope read
undefinedwhile a closure over the same binding, which holds the box directly, still saw the real array.Node prints
object 12 true. Before this change Perry printedundefined 12 false.The trigger is rare — something must have put an indexed property on
Array.prototype, which arms a monotone deopt latch that deleting the property does not clear. The affected shape is not rare: an array local captured by a closure is ordinary code, and the symptom is a silently empty-looking variable rather than a crash.The fix
One condition, in the branch that introduced the bug:
ctx.boxed_varsis an existingFnCtxfield already in scope here — documented as the set whoseLocalGet"reads the slot, unboxes, and callsjs_box_get_bits", andprealloc_boxesids are added to it automatically. No plumbing was needed.A hoisted
varis unboxed unless separately captured, so #10488 keeps its fix.Why not a revert
Measured, not assumed. Building HEAD with the added line simply deleted:
test_gap_10727_captured_array_local_proto_indextest_gap_array_side_mask_covers_a_pointer_stored_at_a_late_indextest_gap_10488_var_array_void_compareA revert trades one red gap test for another and leaves
pr-gatebroken either way.What this costs, and what it does not
A box-captured local now keeps whatever type the predefine recorded, normally
Any, so it can lose a fast path. Three things about that:0ed806587cthese locals got no refinement at all; after it they got one that described the value while the slot held a box pointer. This declines to record a type that was wrong for this storage class.What this does NOT touch
No change to capture lowering,
PreallocateBoxes,boxed_varscollection, the box runtime helpers,array/indexing*.rs,array/splice_slice.rs, or the collector. The array was never corrupted and the GC was never involved — the fault was entirely in which storage a read was lowered against.How it was found
The fixture that exposed it is named
test_gap_array_side_mask_covers_a_pointer_stored_at_a_late_indexand its companions live ingc/tests/, so it read as a GC side-mask bug. It is not one. Readingdestination[10]before thegc()showed it alreadyundefined, which removed the collector from the picture in one probe.slicepassing whilesplicefailed turned out to be an ordering artefact — the first invocation in a process passes because the latch is not yet armed.Dated and attributed by bisect, six builds, each with its runtime stamp checked against the commit under test:
60922041cd08bf655af7e821e10a8b8f1ad83a8f324b8ad0bf0ed806587c8cbf5bef094715bc2fa1Verification
v0.5.1598 | git:4715bc2fa1) it fails at line 3 withcaptured typeof: undefined.src:1b6c72be…) — new fixture,test_gap_10488_var_array_void_compare, andtest_gap_array_side_mask....cargo test --release -p perry-codegen: 2145 passed, 0 failed, including bothlet_stmt_var_redeclare_tests— the unit-level guards on codegen:arr[i] === void 0is always false for an out-of-bounds read of avar-declared number array (numericfcmpon the undefined tag) #10488's behaviour.cargo check --release -p perry-codegen --all-targetsclean (--all-targetsbecause--libcompiles nocfg(test)code).cargo fmt --all -- --checkclean.HARNESS_EXIT=0, 1h37m):All 7 failures are pre-existing and none is attributable to this change: the six standing
gap_snapshot.jsonentries (2159_defineproperty_class_prototype,2514_settracesigint,json_lazy_defineproperty_index,perfhooks_3088_3008_3010_3011,prop_plan_cache_invalidation,v8_2_3680plus) all failing in exactly their recorded state, plus9592_child_timeout_threads, which is #10730 — a macOS fixture-portability bug where the fixture hardcodes/bin/trueand the oracle is the broken half. The snapshot gate is bidirectional, so a listed test that started passing would also be a divergence; none did.The three tests that matter here came from that same invocation as the other 868, not from separate runs:
test_gap_10488_var_array_void_compareat position 46,test_gap_10727_captured_array_local_proto_indexat 59, andtest_gap_array_side_mask_covers_a_pointer_stored_at_a_late_indexat 353 — all PASS. The first two are the two sides of the trade a plain revert could not satisfy, green together in one pass.The new fixture includes the #10488 hoisted-
var-redeclare shape as its own case, so it guards against a future change re-breaking what0ed806587cfixed.Note for the next person in this file
crates/perry-codegen/src/stmt/let_stmt.rsis now at exactly 2000 lines, thecheck_file_size.shcap. It was at 1999 before this change; the combined#10488/#10727comment was condensed to fit the guard in at net +1 line. Anything further added here needs the file split first.Closes #10727
Summary by CodeRabbit
Bug Fixes
undefinedfrom their declaring scope after certain array prototype changes.Tests