Skip to content

fix(hir): skip AnnexB B.3.3 legacy var on for-head/destructuring-catch early errors (#5346)#5356

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5346-b33-bindings
Jun 18, 2026
Merged

fix(hir): skip AnnexB B.3.3 legacy var on for-head/destructuring-catch early errors (#5346)#5356
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5346-b33-bindings

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Partial fix for #5346 — targets the largest cited cluster, the 94× "An initialized binding is not created" failures, which the issue flags as a likely #5319 B.3.3 residual.

These are test262 annexB/language/eval-code/direct cases that verify the AnnexB B.3.3 legacy block-function var is not created when the equivalent var would be an early error. The #5319 collection (collect_annexb_block_fn_decl_names) seeded its forbidden set only from block/switch/parameter lexical names and missed two enclosing-binding sources, so the legacy var was wrongly created and the function name leaked to the outer scope:

  • for / for-in / for-of lexical headsfor (let f; …) { function f(){} }, for (let f in/of …) { … }. The loop binding scopes the body; a same-named var in the body is an early error (14.7.4.1 / 14.7.5.1).
  • destructuring CatchParametercatch ({ f }) { function f(){} }. B.3.5 makes a same-named var an early error unless the catch param is a simple catch (e) BindingIdentifier (which stays exempt — the no-skip case).

The fix adds the for-head lexical names and pattern catch-param bound names to the forbidden set before descending into those bodies. A simple-identifier catch param is deliberately left untouched so its allowed var is still created.

Verification (node v26 differential, cache-disabled)

annexB/language/eval-code/direct skip-early-err cluster: 42/96 → 72/96 passing (+30)

early-error construct baseline fail fixed fail
try (destructuring catch) 16 4
for 10 4
for-in 10 4
for-of 10 4
block 4 4
switch 4 4

The residual (4 per suffix = 24) are all switch-case-prefixed templates (a block function declared directly in a switch case) — a separate, pre-existing root cause, left for follow-up.

No regressions:

  • The 24 no-skip cases (where the var should be created) fail identically on baseline and fixed binaries (12 = 12, same files), confirming no over-suppression.
  • cargo test -p perry-hir passes.

Strict bodies and ES modules are unaffected (the AnnexB var only exists in sloppy mode).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed variable scoping behavior in nested functions within block statements to correctly comply with JavaScript specifications.
    • Improved handling of lexical bindings in loop constructs and catch clauses.
    • Enhanced edge-case handling for complex variable shadowing scenarios where block functions interact with enclosing scope bindings.

…h early errors (#5346)

The #5319 AnnexB B.3.3 collection (`collect_annexb_block_fn_decl_names`)
suppresses the legacy enclosing-scope `var` for a block-nested sloppy
function whenever the equivalent `var` would be an early error, but it
seeded the `forbidden` set only from block/switch/param lexical names. It
missed two enclosing-binding sources, so the legacy `var` was wrongly
created — leaking the function name to the outer scope:

- **`for`/`for-in`/`for-of` lexical heads** (`for (let f; …) { function f(){} }`,
  `for (let f in/of …) { … }`): the loop binding scopes the body, and a
  same-named `var` in the body is an early error (14.7.4.1 / 14.7.5.1).
- **destructuring CatchParameter** (`catch ({ f }) { function f(){} }`): B.3.5
  makes a same-named `var` an early error unless the catch param is a simple
  `catch (e)` BindingIdentifier, which stays exempt (no-skip).

Add the for-head lexical names and pattern catch-param bound names to the
`forbidden` set before descending into those bodies. A simple identifier
catch param is left untouched so its no-skip `var` is still created.

test262 annexB/language/eval-code/direct skip-early-err cluster (node v26
differential): 42/96 -> 72/96 passing (+30). The residual failures are the
`switch`-case-prefixed templates (a block function declared directly in a
switch case), a separate pre-existing issue. Verified no regression: the
no-skip cases fail identically on both binaries (cache-disabled), and
`cargo test -p perry-hir` passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

annexb_nested_stmt in lower_decl/block.rs is updated to extend the Annex B forbidden set with lexical head bindings when descending into for/for-in/for-of loop bodies, and to only augment the forbidden set for catch clauses that use destructuring patterns. Three new internal helpers (var_decl_lexical_names, for_head_lexical_names, annexb_nested_loop_body) are added to support these changes.

Changes

Annex B B.3.3/B.3.5 Forbidden-Set Refinements

Layer / File(s) Summary
New helper functions for lexical name extraction and loop descent
crates/perry-hir/src/lower_decl/block.rs
Adds var_decl_lexical_names, for_head_lexical_names, and annexb_nested_loop_body as private helpers. The first two extract let/const binding names from VarDecl nodes and for-loop heads respectively; the third centralizes the forbidden-set extension and loop-body descent logic.
annexb_nested_stmt: refined forbidden-set propagation for loops and catch
crates/perry-hir/src/lower_decl/block.rs
for/for-in/for-of arms now call annexb_nested_loop_body instead of passing the unchanged forbidden set into the body. The try/catch arm now clones and augments the forbidden set only when the catch parameter is a destructuring pattern; a simple identifier catch parameter leaves the forbidden set unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5319: Modifies the same annexb_nested_stmt / forbidden-set bookkeeping path in lower_decl/block.rs, making it a direct prerequisite for the loop and catch refinements in this PR.

Poem

🐇 Hoppity-hop through the for-loop head,
I gather the lexicals, add them to dread —
The forbidden set grows with each let and const,
Destructured catch bindings? Also get tossed!
Simple identifiers, left well alone.
Annex B is tidy — this bunny has known. 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing AnnexB B.3.3 legacy var handling for for-head and destructuring-catch early error scenarios.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, changes, verification results, and includes all required checklist items. It exceeds template expectations with detailed explanations and test data.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 worktree-fix-5346-b33-bindings

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/lower_decl/block.rs`:
- Around line 718-726: The `for_head_lexical_names` function currently only
handles `ast::ForHead::VarDecl` and returns an empty vector for all other
variants, including `UsingDecl`. However, `using` declarations in for-of/for-in
loops also introduce lexically scoped bindings that should be extracted. Add a
new match arm for `ast::ForHead::UsingDecl` that extracts the binding names from
the using declaration by iterating through its declarations (following the same
pattern used in `fn_ctor_env.rs` at lines 957-961). Return the collected binding
names instead of returning an empty vector for this variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b71f6b64-02b5-4024-abf9-2f7b1cb5bf19

📥 Commits

Reviewing files that changed from the base of the PR and between e5fbd2e and c487ba7.

📒 Files selected for processing (1)
  • crates/perry-hir/src/lower_decl/block.rs

Comment on lines +718 to +726
/// Lexical binding names of a `for-in` / `for-of` head (`for (let f in …)`).
/// A `var` head or a bare assignment-target pattern introduces no lexical
/// binding here and yields an empty list.
fn for_head_lexical_names(head: &ast::ForHead) -> Vec<String> {
match head {
ast::ForHead::VarDecl(vd) => var_decl_lexical_names(vd),
_ => Vec::new(),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if ForHead enum has a UsingDecl variant in swc_ecma_ast
rg -n 'enum ForHead' --type rust -A 10 | head -30

# Also check if there are any for-of/for-in tests involving `using` in the test suite
rg -n 'for.*using.*of' --type rust -C 2 | head -30

Repository: PerryTS/perry

Length of output: 999


🏁 Script executed:

# Check the imports in block.rs to identify what 'ast' refers to
head -50 crates/perry-hir/src/lower_decl/block.rs | grep -E '^use|^extern'

Repository: PerryTS/perry

Length of output: 314


🏁 Script executed:

# Find where ForHead is defined (could be swc_ecma_ast or local)
fd -t f '*.rs' | xargs rg 'enum ForHead' -B 2 -A 15

Repository: PerryTS/perry

Length of output: 1945


🏁 Script executed:

# Check if UsingDecl appears anywhere in ForHead-related code
rg 'UsingDecl' crates/perry-hir/src/ -B 2 -A 2

Repository: PerryTS/perry

Length of output: 483


🏁 Script executed:

# Read the full for_head_lexical_names function and surrounding context in block.rs
sed -n '705,745p' crates/perry-hir/src/lower_decl/block.rs

Repository: PerryTS/perry

Length of output: 1546


🏁 Script executed:

# Check how var_decl_lexical_names is defined
sed -n '685,720p' crates/perry-hir/src/lower_decl/block.rs

Repository: PerryTS/perry

Length of output: 1408


🏁 Script executed:

# Check the fn_ctor_env.rs to see full UsingDecl handling for comparison
rg -n 'UsingDecl' crates/perry-hir/src/lower/fn_ctor_env.rs -B 5 -A 10

Repository: PerryTS/perry

Length of output: 565


Handle ForHead::UsingDecl in for_head_lexical_names.

The function currently returns an empty list for any ForHead variant other than VarDecl, including UsingDecl. However, using declarations in for-of/for-in loops introduce lexically scoped bindings (per the explicit resource management proposal), similar to let and const. These bindings should be extracted and added to the forbidden set to correctly apply AnnexB legacy var restrictions.

Compare with fn_ctor_env.rs (line 957–961), which explicitly handles UsingDecl by iterating through its declarations. The fix should extract binding names from UsingDecl in the same way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower_decl/block.rs` around lines 718 - 726, The
`for_head_lexical_names` function currently only handles `ast::ForHead::VarDecl`
and returns an empty vector for all other variants, including `UsingDecl`.
However, `using` declarations in for-of/for-in loops also introduce lexically
scoped bindings that should be extracted. Add a new match arm for
`ast::ForHead::UsingDecl` that extracts the binding names from the using
declaration by iterating through its declarations (following the same pattern
used in `fn_ctor_env.rs` at lines 957-961). Return the collected binding names
instead of returning an empty vector for this variant.

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