Skip to content

fix(codegen): #6039 over-filtered module_local_types — 17 test262 regressions from swallowed receiver-typed throws - #6096

Merged
proggeramlug merged 1 commit into
mainfrom
fix/t262-regr-cleanup
Jul 6, 2026
Merged

fix(codegen): #6039 over-filtered module_local_types — 17 test262 regressions from swallowed receiver-typed throws#6096
proggeramlug merged 1 commit into
mainfrom
fix/t262-regr-cleanup

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

#6039 correctly 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 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_artifactsFnCtx.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)
  • 4 private-async-method, 1 Proxy set, 2 Temporal Instant limits — 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; leave module_local_types (the receiver oracle passed to emit_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 from typed_abi_local_types, so typed_closure_capture_reps returns None). So #5982's win holds — for(let i…){const c=i; fns.push(()=>c)} still returns 0,1,2,3,4, not 0,0,0,0,0.

Verification (internal Linux sweep host)

Scope note

7 of the 24 flagged cases (annexB createdynfn, Function/GeneratorFunction instance-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-out PERRY_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

  • Bug Fixes
    • Improved closure handling for values defined at module scope, so they can be captured and specialized more reliably.
    • Fixed cases where typed closures could be missed or treated inconsistently when they referenced module-level values.
    • Strengthened support for optimized closure behavior across several data types, with more consistent runtime results.

…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.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 92946a00-3b25-48ad-b1fd-9d9019402975

📥 Commits

Reviewing files that changed from the base of the PR and between 759f947 and 07ce0ea.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/codegen/mod.rs

📝 Walkthrough

Walkthrough

In 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.

Changes

Typed-ABI Closure Specialization Scoping

Layer / File(s) Summary
Introduce typed_abi_local_types and update typed closure checks
crates/perry-codegen/src/codegen/mod.rs
Stops removing module-global entries from module_local_types in place; instead creates a filtered typed_abi_local_types map and switches typed f64/i1/i32/string closure rejection-reason and capture-representation checks to use it.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • PerryTS/perry#5949: Both PRs modify compile_module's handling of module_local_types-derived data feeding typed-ABI closure specialization logic.
  • PerryTS/perry#6039: Both PRs adjust typed-ABI closure capture specialization in compile_module to prevent module-global values from being treated as unboxed capture slots.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main fix: scoping the module_local_types filter to typed-ABI closure specialization.
Description check ✅ Passed The description is detailed and largely complete, covering summary, root cause, fix, and verification, though it omits explicit related-issue and checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/t262-regr-cleanup

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit ae3ef59 into main Jul 6, 2026
46 of 48 checks passed
@proggeramlug
proggeramlug deleted the fix/t262-regr-cleanup branch July 6, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant