fix(codegen): #6039 over-filtered module_local_types — 17 test262 regressions from swallowed receiver-typed throws - #6096
Conversation
…er-typed throws in harness closures #6039 (#5982 fix) stripped every MODULE-GLOBAL captured local out of `module_local_types` so the typed-ABI closure specialization would not read an unset capture slot for a global read through `@perry_global_*`. That part was right, but `module_local_types` is used for TWO purposes in `compile_module`: 1. the typed-ABI closure-clone DECISION (`typed_{f64,i1,i32,string}_closure_rejection_reason_with_types`), and 2. the per-function-body RECEIVER-TYPE oracle — it is handed to `emit_module_artifacts` -> `FnCtx.local_types`, which drives `static_type_of` / `is_array_expr`. Removing a module-global's declared type from (2) mis-classified a captured array receiver as untyped INSIDE a closure. `arr.every()` with an undefined callbackfn then lowered to the generic dynamic method dispatch (`js_arraylike_*`) instead of the array-typed path that emits `js_validate_array_callback`, so the mandatory TypeError was never thrown. The test262 harness runs every negative case as `assert.throws(TypeError, function () { ... })` — a module-level closure that captures the module-global under test — so the swallowed throw surfaced as 24 conformance regressions: - 8 Array HOF callbackfn-not-callable cases (every/filter/forEach/map/reduce/reduceRight/some 15.4.4.*-4-1) - 3 symbol-strict [[Set]] cases (Object.defineProperty/freeze/seal *-strict: obj[sym]=2 on a non-writable prop must throw) - the private-async-method / Proxy / Temporal cases that reach the same harness-closure path. Fix: keep the module-globals filter scoped to the typed-ABI specialization only. Build a dedicated `typed_abi_local_types` (module-locals minus module-globals) and feed it to the four closure-clone decisions and the capture-rep probe; leave `module_local_types` (the receiver oracle passed to `emit_module_artifacts`) module-global-INCLUSIVE. The emission side (`compile_typed_*_closure`) is only reached for closures the decision accepted, and a closure capturing a module-global is always rejected by the decision (its capture type is absent from `typed_abi_local_types`), so decision and emission never disagree — #5982's `for(let i...){const c=i; fns.push(()=>c)}` still returns 0,1,2,3,4, not 0,0,0,0,0. Bisected to 09f2f24 (#6039) on an internal Linux sweep host; witness slice (8 Array HOF + 3 symbol-strict) restored to 10/10, #5982 guard held.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughIn compile_module, module_local_types is no longer mutated to strip module-global entries; a new filtered typed_abi_local_types map is introduced instead. Typed-ABI closure rejection and capture-representation checks for f64, i1, i32, and string closures now use this new map. ChangesTyped-ABI Closure Specialization Scoping
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 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 |
Summary
A consolidation test262 sweep found the recent mechanism-PR wave shipped 24 regressions (net was still +16, but 24 previously-passing cases now failed). Bisected the dominant clusters to a single culprit and forward-fixed it.
Culprit:
09f2f24895(#6039) — "fix(codegen): #5982 — module-global captures must not feed the typed-ABI closure specialization".Root cause
#6039correctly stopped a closure that captures a module-global local (read via@perry_global_*, not the capture array) from feeding the typed-ABI closure specialization — which otherwise read an unset capture slot and returned 0 (for(let i…){const c=i; fns.push(()=>c)}→0,0,0,0,0).It did so by stripping every module-global id out of
module_local_types. But that map is used for two distinct purposes incompile_module:typed_{f64,i1,i32,string}_closure_rejection_reason_with_types), andemit_module_artifacts→FnCtx.local_types, which drivesstatic_type_of/is_array_expr.Removing a module-global's declared type from (2) mis-classified a captured array receiver as untyped inside a closure.
arr.every()with an undefinedcallbackfnthen lowered to the generic dynamic method dispatch (js_arraylike_*) instead of the array-typed path that emitsjs_validate_array_callback, so the mandatoryTypeErrorwas never thrown.The test262 harness runs every negative case as
assert.throws(TypeError, function () { … })— a module-level closure that captures the module-global under test — so the swallowed throw surfaced as 24 conformance regressions:every/filter/forEach/map/reduce/reduceRight/some15.4.4.*-4-1)Object.defineProperty/freeze/seal*-strict:obj[sym]=2on a non-writable prop must throw)Instantlimits — all reach the same harness-closure path.Fix
Keep the module-globals filter scoped to the typed-ABI specialization only. Build a dedicated
typed_abi_local_types(module-locals minus module-globals) and feed it to the four closure-clone decisions and the capture-rep probe; leavemodule_local_types(the receiver oracle passed toemit_module_artifacts) module-global-inclusive.Decision and emission never disagree: the emission side (
compile_typed_*_closure) is only reached for closures the decision accepted, and a closure capturing a module-global is always rejected by the decision (its capture type is absent fromtyped_abi_local_types, sotyped_closure_capture_repsreturnsNone). So#5982's win holds —for(let i…){const c=i; fns.push(()=>c)}still returns0,1,2,3,4, not0,0,0,0,0.Verification (internal Linux sweep host)
8e303627e(GOOD, fix(runtime):inoperator throws TypeError on a non-object right operand #6024 baseline: witness 10/10) →c5246ece6(BAD: 0/10) to09f2f24895(fix(codegen): #5982 — module-global captures must not feed the typed-ABI closure specialization (#5466 regression) #6039). The originally-suspected[[Set]]PRs#6059/#6063were cleared: the witness already fails at their common parent.assert.throws-style closure capturing a module-globalarrnow throws again (CAUGHT);#5982guard held (0,1,2,3,4).built-ins/Array+Object+TypedArrayConstructors+Reflect+Proxy, jobs=4) vs the GOOD baseline:TypedArrayConstructors/internals/Set/…/key-is-valid-index-reflect-set.js(+ BigInt variant) mechanism-PR wins from fix(runtime): typed-array integer-indexed [[Set]] spec rejection + coercion #6059/fix(runtime): OrdinarySet rejects a receiver-owned accessor instead of firing its setter #6063 preserved.defineProperty/freeze/sealsymbol-strict + Array-HOF + Proxy-set witness cases all PASS.Scope note
7 of the 24 flagged cases (
annexB createdynfn,Function/GeneratorFunctioninstance-length,S15.3*) are a separate, intentional trade-off from#6031(new Function("")capability probe now honestly reports dynamic-codegen unavailable so zod 4 et al. take the interpreter fallback; opt-outPERRY_EVAL_CSP=0). Not attributable to the bisected culprit and out of scope here.Files
crates/perry-codegen/src/codegen/mod.rs— scope the module-globals filter to the typed-ABI specialization; keep the receiver oracle module-global-inclusive.Summary by CodeRabbit