From cd7b50a441fafb4a9e5210b60fc123c782e4d046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 23 Sep 2026 22:08:57 +0000 Subject: [PATCH 1/4] fix(hir): a per-evaluation class declaration's members see its evaluation, not the template (#11157) --- crates/perry-hir/src/analysis.rs | 9 +++ crates/perry-hir/src/lower/context_new.rs | 2 + .../perry-hir/src/lower/lowering_context.rs | 13 ++++ crates/perry-hir/src/lower_decl/body_stmt.rs | 20 +++++- .../body_stmt/class_self_binding.rs | 61 ++++++++++++++++ crates/perry-hir/src/lower_decl/class_decl.rs | 24 ++++++- .../class_decl/decl_self_binding.rs | 71 +++++++++++++++++++ 7 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 crates/perry-hir/src/lower_decl/body_stmt/class_self_binding.rs create mode 100644 crates/perry-hir/src/lower_decl/class_decl/decl_self_binding.rs diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 0b62440897..8c0eb1420d 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -493,6 +493,7 @@ pub fn substitute_lexical_this_in_expr(expr: &mut Expr, replacement: &Expr) { Expr::Closure { body, captures_this, + captures, params, .. } => { @@ -507,6 +508,14 @@ pub fn substitute_lexical_this_in_expr(expr: &mut Expr, replacement: &Expr) { // slot so the closure-cache key doesn't include a stale // implicit-this snapshot. *captures_this = false; + // #11157: a local replacement (a per-evaluation class's + // self-binding) is read from the closure body now, so the + // closure must capture it like any other outer local. + if let Expr::LocalGet(id) = replacement { + if !captures.contains(id) { + captures.push(*id); + } + } } } _ => crate::walker::walk_expr_children_mut(expr, &mut |child| { diff --git a/crates/perry-hir/src/lower/context_new.rs b/crates/perry-hir/src/lower/context_new.rs index 7bf9399924..bc92f20bc6 100644 --- a/crates/perry-hir/src/lower/context_new.rs +++ b/crates/perry-hir/src/lower/context_new.rs @@ -91,6 +91,8 @@ impl LoweringContext { current_class_inner_name: None, pending_class_inner_name: None, class_expr_self_bindings: Vec::new(), + class_decl_self_binding_wanted: false, + class_decl_self_binding: None, current_class_member_is_static: false, private_scopes: Vec::new(), object_super_home_stack: Vec::new(), diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 3c44999141..8b3f5d1bad 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -386,6 +386,19 @@ pub struct LoweringContext { /// while lowering that ClassBody, while nested classes can still capture /// an outer class expression's evaluated value. pub(crate) class_expr_self_bindings: Vec<(String, usize, LocalId)>, + /// #11157: set by the function-body class-DECLARATION arm right before it + /// calls `lower_class_decl`, and consumed there. When the declaration is + /// already known to take the per-evaluation `ClassExprFresh` path (dynamic + /// heritage or private elements), `lower_class_decl` then registers a + /// `class_expr_self_bindings` entry for the class name — exactly what a + /// named class expression gets — so the members' own references to the + /// class resolve to this evaluation instead of the shared template. + pub(crate) class_decl_self_binding_wanted: bool, + /// #11157: the compiler-private self-binding local `lower_class_decl` + /// registered for the declaration it just lowered (see above), handed + /// back to the declaration arm so it can become the `ClassExprFresh` + /// evaluation owner. + pub(crate) class_decl_self_binding: Option, /// True while lowering a static class member body. pub(crate) current_class_member_is_static: bool, /// Lexical stack of private-name scopes — one entry per enclosing class diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index 60c448d03a..49170705a1 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -17,11 +17,14 @@ use super::class_computed::push_deduped_class_computed_keys; use super::helpers::{async_iterator_method_call, is_filehandle_readlines_for_await_target}; use super::*; +mod class_self_binding; mod detect; mod for_await; pub(crate) mod gen_capture_scan; mod nested_fn_decl; +use class_self_binding::{decl_self_binding_owner, lower_body_class_decl}; + use gen_capture_scan::nested_generator_references_outer_locals; use detect::{ @@ -338,7 +341,7 @@ fn lower_body_stmt_impl(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result = if fresh_binding { class .static_fields @@ -447,6 +452,15 @@ fn lower_body_stmt_impl(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result Result<(Class, Option)> { + let mark = ctx.class_expr_self_bindings.len(); + ctx.class_decl_self_binding_wanted = true; + let class = super::lower_class_decl(ctx, class_decl, false); + ctx.class_decl_self_binding_wanted = false; + ctx.class_expr_self_bindings.truncate(mark); + let self_binding = ctx.class_decl_self_binding.take(); + Ok((class?, self_binding)) +} + +/// The `ClassExprFresh` evaluation owner for a declaration: its self-binding, +/// when anything the evaluation runs reads it. The owner local is registered +/// with the enclosing body so it is declared (and shadow-rooted) at body +/// entry, before codegen stores the evaluated class object in it — the same +/// contract a named class expression's self-binding has (`arm_class.rs`). +pub(super) fn decl_self_binding_owner( + ctx: &mut LoweringContext, + self_binding: Option, + class_name: &str, + captured_exprs: &[Expr], + named_statics: &[(String, Expr)], + computed_keys: &[(String, Expr)], + computed_statics: &[(String, Expr)], +) -> Option { + let self_id = self_binding?; + let uses_self = |expr: &Expr| { + let mut refs = Vec::new(); + let mut visited = std::collections::HashSet::new(); + crate::analysis::collect_local_refs_expr(expr, &mut refs, &mut visited); + refs.contains(&self_id) + }; + let used = captured_exprs.iter().any(&uses_self) + || named_statics.iter().any(|(_, value)| uses_self(value)) + || computed_keys.iter().any(|(_, key)| uses_self(key)) + || computed_statics.iter().any(|(_, value)| uses_self(value)); + if !used { + return None; + } + let ids = ctx + .lookup_class_captures(class_name) + .map(<[_]>::to_vec) + .unwrap_or_default(); + ctx.body_class_expr_captures.push((self_id, ids)); + Some(self_id) +} diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 05de5e433c..c107f718fb 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -48,6 +48,7 @@ fn is_genuine_node_stream_parent(ctx: &LoweringContext, name: &str) -> bool { } mod class_heritage; +mod decl_self_binding; mod from_ast; mod member_helpers; mod member_registration; @@ -70,6 +71,10 @@ pub fn lower_class_decl( // Resolve through any active scope-local rename so a disambiguated // duplicate class registers (and self-references) under its unique name. let name = ctx.resolve_class_name(class_decl.ident.sym.as_str()); + // #11157: consume the declaration arm's request before anything below can + // lower a nested class declaration. + let self_binding_wanted = std::mem::take(&mut ctx.class_decl_self_binding_wanted); + ctx.class_decl_self_binding = None; validate_legacy_decorator_surface(&class_decl.class, &name)?; validate_class_element_early_errors(&class_decl.class, &name)?; let class_id = match ctx.lookup_class(&name) { @@ -443,6 +448,14 @@ pub fn lower_class_decl( (None, None, None, None) }; + // #11157: a function-body declaration with a runtime heritage value or + // private elements is evaluated per evaluation (`ClassExprFresh`). Give + // its members the evaluated class, not the template, for its own name. + let class_self_binding = (self_binding_wanted + && (extends_expr.is_some() + || decl_self_binding::class_body_has_private_names(&class_decl.class))) + .then(|| decl_self_binding::push_decl_self_binding(ctx, class_decl.ident.sym.as_ref(), &name)); + // Issue #10486: the branches above deliberately leave `extends_name` // None when the heritage identifier resolves to a lexically-scoped // local (`locally_shadowed`) or a fully dynamic expression, to avoid @@ -1193,9 +1206,18 @@ pub fn lower_class_decl( // unpatched capture slot (test262 static-field-init-this-inside-arrow). for sf in &mut static_fields { if let Some(init) = &mut sf.init { - crate::analysis::substitute_lexical_this_in_expr(init, &Expr::ClassRef(name.clone())); + match class_self_binding { + Some(self_id) => decl_self_binding::substitute_static_this_with_self(init, self_id), + None => crate::analysis::substitute_lexical_this_in_expr( + init, + &Expr::ClassRef(name.clone()), + ), + } } } + if let Some(self_id) = class_self_binding { + decl_self_binding::pop_decl_self_binding(ctx, self_id); + } // Exit type parameter scope ctx.exit_type_param_scope(); diff --git a/crates/perry-hir/src/lower_decl/class_decl/decl_self_binding.rs b/crates/perry-hir/src/lower_decl/class_decl/decl_self_binding.rs new file mode 100644 index 0000000000..afc190881d --- /dev/null +++ b/crates/perry-hir/src/lower_decl/class_decl/decl_self_binding.rs @@ -0,0 +1,71 @@ +//! #11157: the lexical self-binding of a per-evaluation class DECLARATION. +//! +//! A class declared in a function body (including the body of a CommonJS +//! module wrapper) whose heritage is a runtime value or that has private +//! elements lowers to a `ClassExprFresh` object per evaluation. Its static +//! fields live on that object. Before this module, the class's own members +//! resolved its name to `ClassRef(