Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ impl LoweringContext {
unresolved_ident_as_global: false,
with_env_stack: Vec::new(),
var_hoisted_ids: HashSet::new(),
annexb_block_fn_var_ids: HashMap::new(),
annexb_block_fn_names_all: HashSet::new(),
lexical_forward_decls: HashMap::new(),
functions_index: HashMap::new(),
classes_index: HashMap::new(),
Expand Down
63 changes: 63 additions & 0 deletions crates/perry-hir/src/lower/expr_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,13 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul
let is_strict = outer_strict || block_has_use_strict(fn_expr.function.body.as_ref());
ctx.current_strict = is_strict;

// Annex B B.3.3 (#5297): this function-expression body owns its own
// block-nested-function `var` map; take the enclosing one aside and restore
// it on exit (mirrors `lower_fn_body_block_stmt`). The nested-var pass below
// repopulates it for this body.
let saved_annexb_block_fn_var_ids = std::mem::take(&mut ctx.annexb_block_fn_var_ids);
let saved_annexb_block_fn_names_all = std::mem::take(&mut ctx.annexb_block_fn_names_all);

// Generate Let statements for destructuring patterns BEFORE lowering body
let mut destructuring_stmts = Vec::new();
for (param_id, pat) in &destructuring_params {
Expand Down Expand Up @@ -895,6 +902,60 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul
}
}
}
// Annex B B.3.3 (#5297): a block-nested `function f(){}` in this sloppy
// function-expression body also gets an enclosing-scope `var f`
// (undefined until the declaration runs). Register one hoisted slot per
// such name and record name -> slot so the block-nested declaration
// writes the closure into it while keeping its block-local binding
// independent. The IIFE wrapper `(function(){ { function f(){} } f(); }())`
// is exactly the test262 `annexB/.../function-code` shape. The legacy
// `var` is skipped when the name collides with a parameter or a lexical
// binding (it would make `var f` an early error) — `forbidden` carries
// the parameter names, the body's top-level lexical names, and
// `arguments`; nested blocks add their own lexical names as we descend.
if !ctx.current_strict {
let mut forbidden: std::collections::HashSet<String> =
params.iter().map(|p| p.name.clone()).collect();
crate::lower_decl::collect_lexical_decl_names(&block.stmts, &mut forbidden);
forbidden.insert("arguments".to_string());

let mut all_names = Vec::new();
let mut names = Vec::new();
crate::lower_decl::collect_annexb_block_fn_decl_names(
&block.stmts,
&forbidden,
&mut all_names,
&mut names,
);
ctx.annexb_block_fn_names_all.extend(all_names);
names.sort();
names.dedup();
for name in names {
// Reuse an existing in-scope `var` (parameters are excluded by
// `forbidden`, so any same-name binding here is a hoisted
// `var`); otherwise mint a fresh hoisted slot. Either way emit
// an undefined-init entry slot: a direct top-level `var f = …`
// in a function expression has no entry `Let` (only its source-
// position one), but the block's B.3.3 write runs BEFORE that
// position, so the slot must already exist (#5297
// `existing-var-update`).
let id =
if let Some(pos) = ctx.locals.lookup_index_in_scope(&name, outer_locals_len) {
ctx.locals[pos].1
} else {
ctx.define_local(name.clone(), Type::Any)
};
nested_var_prologue.push(Stmt::Let {
id,
name: name.clone(),
ty: Type::Any,
mutable: true,
init: Some(Expr::Undefined),
});
ctx.var_hoisted_ids.insert(id);
ctx.annexb_block_fn_var_ids.insert(name, id);
}
}
Comment on lines +905 to +958

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 | 🟠 Major | ⚡ Quick win

Do not drop the Annex B prologue in using function-expression bodies.

The Annex B pass records hoisted slots in nested_var_prologue, but the has_using branch returns lower_stmts_using_aware(...) directly. A block-nested function in a function expression containing using can then emit LocalSet(outer_id, ...) without the undefined entry Stmt::Let for outer_id.

Suggested fix
         if has_using {
-            crate::lower_decl::lower_stmts_using_aware(ctx, &block.stmts)?
+            let mut lowered = crate::lower_decl::lower_stmts_using_aware(ctx, &block.stmts)?;
+            if !nested_var_prologue.is_empty() {
+                let mut combined = std::mem::take(&mut nested_var_prologue);
+                combined.append(&mut lowered);
+                lowered = combined;
+            }
+            lowered
         } else {

Also applies to: 1019-1042

🤖 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/expr_function.rs` around lines 905 - 958, The
Annex B prologue statements stored in nested_var_prologue must be preserved when
lowering function-expression bodies that contain using statements. When the
has_using branch returns the result of lower_stmts_using_aware directly, the
nested_var_prologue statements are being dropped. Fix this by prepending the
nested_var_prologue statements to the result of lower_stmts_using_aware so that
the undefined-init Stmt::Let entries are present before the using-aware
statements are executed, ensuring block-nested function declarations have their
hoisted slots initialized.

// Forward-captured `let`/`const` (incl. destructuring) referenced by an
// EARLIER closure than their declaration. The `#4973` pass above only
// covers `Pat::Ident` and only when the body has a `function`
Expand Down Expand Up @@ -1007,6 +1068,8 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul
Vec::new()
};
ctx.current_strict = outer_strict;
ctx.annexb_block_fn_var_ids = saved_annexb_block_fn_var_ids;
ctx.annexb_block_fn_names_all = saved_annexb_block_fn_names_all;
ctx.forward_class_names = saved_forward_class_names;

// Prepend destructuring statements to body
Expand Down
57 changes: 57 additions & 0 deletions crates/perry-hir/src/lower/lower_module_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,63 @@ pub fn lower_module_full(
}
}

// Annex B B.3.3 (#5297): in sloppy (non-strict) global code, a block-nested
// `function f(){}` also creates a global `var f` (undefined until the
// declaration runs). Mirror the function-body pre-pass: register one hoisted
// slot per such name, emit an undefined-initialised entry, and record name
// -> slot in `annexb_block_fn_var_ids` so the block-nested declaration
// (lowered via `lower_nested_fn_decl`) writes the closure into it while
// keeping its block-local binding independent.
if !ctx.module_strict {
let body_stmts: Vec<ast::Stmt> = ast_module
.body
.iter()
.filter_map(|item| match item {
ast::ModuleItem::Stmt(stmt) => Some(stmt.clone()),
_ => None,
})
.collect();
// Forbidden: the program's own top-level lexical names make `var f` an
// early error; `arguments` is excluded. There are no parameters at
// program scope. Nested blocks add their own lexical names while
// descending.
let mut forbidden = std::collections::HashSet::new();
crate::lower_decl::collect_lexical_decl_names(&body_stmts, &mut forbidden);
forbidden.insert("arguments".to_string());

let mut all_names = Vec::new();
let mut names = Vec::new();
crate::lower_decl::collect_annexb_block_fn_decl_names(
&body_stmts,
&forbidden,
&mut all_names,
&mut names,
);
ctx.annexb_block_fn_names_all.extend(all_names);
names.sort();
names.dedup();
for name in names {
// Reuse an existing global `var`, else mint a fresh hoisted slot;
// either way emit an undefined-init entry slot so the block's B.3.3
// write (which runs before any source-position `var f = …`) has
// storage to target.
let id = if let Some(existing) = ctx.lookup_local(&name) {
existing
} else {
ctx.define_local(name.clone(), Type::Any)
};
module.init.push(Stmt::Let {
id,
name: name.clone(),
ty: Type::Any,
mutable: true,
init: Some(Expr::Undefined),
});
ctx.var_hoisted_ids.insert(id);
ctx.annexb_block_fn_var_ids.insert(name, id);
}
}

// Pre-register all class declarations so that static method calls between
// classes declared in the same file resolve correctly regardless of declaration order.
// Without this, SqrtPriceMath.getAmount0Delta calling FullMath.mulDivRoundingUp
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-hir/src/lower/lowering_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,23 @@ pub struct LoweringContext {
/// continue to lexically shadow the object environment.
pub(crate) with_env_stack: Vec<WithEnvFrame>,
pub(crate) var_hoisted_ids: HashSet<LocalId>,
/// 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-
/// scope `var`-style binding it must also write at its declaration point.
/// `lower_nested_fn_decl` consults this (only when `inside_block_scope > 0`
/// and the enclosing scope is sloppy) to keep the block-local binding
/// independent of the hoisted outer `var`, then assigns the closure into the
/// outer slot. Saved/restored across nested function bodies.
pub(crate) annexb_block_fn_var_ids: HashMap<String, LocalId>,
/// Annex B (#5297): names of ALL block-nested function declarations in the
/// scope currently being lowered (superset of `annexb_block_fn_var_ids`
/// keys — includes those whose legacy `var` is suppressed by a parameter or
/// lexical conflict). Every block-level function declaration is block-scoped,
/// so `lower_nested_fn_decl` gives one a fresh block-local instead of
/// reusing an enclosing same-named binding (e.g. a parameter). Saved/restored
/// across nested function bodies alongside `annexb_block_fn_var_ids`.
pub(crate) annexb_block_fn_names_all: HashSet<String>,
/// #4973: top-of-function-body `let`/`const` Ident bindings pre-registered
/// by the function-body hoist pass so hoisted sibling FUNCTIONS that
/// reference them before their lexical position bind the (boxed) local
Expand Down
Loading
Loading