diff --git a/crates/perry-hir/src/destructuring/var_decl.rs b/crates/perry-hir/src/destructuring/var_decl.rs index 330eaf38a1..46c2d49879 100644 --- a/crates/perry-hir/src/destructuring/var_decl.rs +++ b/crates/perry-hir/src/destructuring/var_decl.rs @@ -136,7 +136,24 @@ pub(crate) fn lower_var_decl_with_destructuring( ctx.async_generator_func_names.insert(name.clone()); } } - let id = if let Some(pid) = pre_id { + // Annex B B.3.4: a `var = init` whose name shadows a live + // `catch ()` parameter assigns to the catch binding, so the + // function-scoped hoisted `var` keeps its pre-catch value. Target the + // catch parameter (innermost `lookup_local`) instead of reusing the + // hoisted id. Only a simple `var x = init` (a plain ident declarator + // with an initializer); a bare `var x;` re-declaration is inert and a + // destructuring pattern never names the catch param directly. + let shadows_catch_param = is_var_decl + && init.is_some() + && matches!(&decl.name, ast::Pat::Ident(_)) + && ctx + .catch_param_scopes + .iter() + .any(|scope| scope.contains(&name)); + let id = if shadows_catch_param { + ctx.lookup_local(&name) + .unwrap_or_else(|| ctx.define_local(name.clone(), ty.clone())) + } else if let Some(pid) = pre_id { pid } else if ctx.scope_depth == 0 && ctx.inside_block_scope == 0 diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 28ba06ca21..35b8a1d454 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -114,6 +114,7 @@ impl LoweringContext { unresolved_ident_as_global: false, with_env_stack: Vec::new(), var_hoisted_ids: HashSet::new(), + catch_param_scopes: Vec::new(), annexb_block_fn_var_ids: HashMap::new(), annexb_block_fn_names_all: HashSet::new(), lexical_forward_decls: HashMap::new(), diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 59ac06e918..a6319e128f 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -374,6 +374,15 @@ pub struct LoweringContext { /// continue to lexically shadow the object environment. pub(crate) with_env_stack: Vec, pub(crate) var_hoisted_ids: HashSet, + /// Names bound by an enclosing `catch (e)` parameter that is currently in + /// scope (a stack, innermost last). Annex B B.3.4: a `var e = init;` whose + /// name collides with a live catch parameter assigns to that *catch + /// parameter* binding, not the function-scoped hoisted `var` — so the outer + /// `var e` keeps its pre-catch value (test262 `annexB/language/statements/ + /// try/catch-redeclared-var-statement`). `lower_var_decl_with_destructuring` + /// consults this to target the shadowing catch binding via `lookup_local` + /// instead of reusing the hoisted id. Pushed/popped around the catch body. + pub(crate) catch_param_scopes: Vec>, /// Annex B B.3.3 (#5297): for the function/program scope currently being /// lowered, maps each name declared by a *block-nested* `function f(){}` /// (legacy sloppy-mode block-level function declaration) to the enclosing- diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index b81446bdfe..b7e655deae 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -1570,7 +1570,22 @@ pub(crate) fn lower_stmt( None }; + // Annex B B.3.4: track the (simple-ident) catch parameter name so + // a `var = init` inside the catch body targets the catch + // binding, not the function-scoped hoisted `var`. + let mut pushed_catch_scope = false; + if let Some((_, ref pname)) = param { + if matches!(catch_clause.param, Some(ast::Pat::Ident(_))) { + let mut set = std::collections::HashSet::new(); + set.insert(pname.clone()); + ctx.catch_param_scopes.push(set); + pushed_catch_scope = true; + } + } let mut catch_body = lower_block_stmt(ctx, &catch_clause.body)?; + if pushed_catch_scope { + ctx.catch_param_scopes.pop(); + } for (i, stmt) in binding_stmts.into_iter().enumerate() { catch_body.insert(i, stmt); }