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
45 changes: 29 additions & 16 deletions crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3516,6 +3516,28 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) ->
} else {
None
};
// Issue #740: synthesized `__perry_cap_<id>` ctor params (added by
// `lower_class_decl` when a class declared inside a function captures
// outer-scope locals) must be visible to field initializers, since
// those field initializers were rewritten to read the captured value
// via `LocalGet(fresh_param_id)`. Bind ALL ctor params (own + cap)
// before `apply_field_initializers_recursive` so the soft-fallback at
// `LocalGet` codegen doesn't return 0.0. Locals/local_types are
// saved-and-restored around the whole inlined ctor flow below; we
// mirror that here so the ctor params don't leak out of `new`.
let mut saved_locals_for_ctor: Option<std::collections::HashMap<u32, String>> = None;
let mut saved_local_types_for_ctor: Option<std::collections::HashMap<u32, HirType>> = None;
if let Some(ctor) = &class.constructor {
saved_locals_for_ctor = Some(ctx.locals.clone());
saved_local_types_for_ctor = Some(ctx.local_types.clone());
for (param, arg_val) in ctor.params.iter().zip(lowered_args.iter()) {
let slot = ctx.func.alloca_entry(DOUBLE);
ctx.block().store(DOUBLE, arg_val, &slot);
ctx.locals.insert(param.id, slot);
ctx.local_types.insert(param.id, param.ty.clone());
}
}

if let Some(stop_at) = inherited_ctor_class.clone() {
apply_field_initializers_recursive(ctx, class_name, FieldInitMode::UpToInclusive(stop_at))?;
} else {
Expand All @@ -3532,25 +3554,16 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) ->
// the constructor's bindings scoped to its body — they don't leak
// back into the enclosing function.
if let Some(ctor) = &class.constructor {
let saved_locals = ctx.locals.clone();
let saved_local_types = ctx.local_types.clone();

for (param, arg_val) in ctor.params.iter().zip(lowered_args.iter()) {
// Ctor params become ctx.locals for the inlined body;
// closures inside the ctor may capture them, so hoist
// to the entry block.
let slot = ctx.func.alloca_entry(DOUBLE);
ctx.block().store(DOUBLE, arg_val, &slot);
ctx.locals.insert(param.id, slot);
ctx.local_types.insert(param.id, param.ty.clone());
}

// Issue #740: ctor params were already bound above so field
// initializers could read them. Don't re-bind (the slots already
// hold the lowered arg values); just lower the body.
let _ = ctor;
// Lower the constructor body. Errors propagate.
crate::stmt::lower_stmts(ctx, &ctor.body)?;
crate::stmt::lower_stmts(ctx, &class.constructor.as_ref().unwrap().body)?;

// Restore the enclosing function's local scope.
ctx.locals = saved_locals;
ctx.local_types = saved_local_types;
ctx.locals = saved_locals_for_ctor.take().unwrap_or_default();
ctx.local_types = saved_local_types_for_ctor.take().unwrap_or_default();
} else {
// No own constructor — walk the parent chain to find an
// inherited constructor and inline it. TypeScript semantics:
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-hir/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1575,7 +1575,7 @@ fn remap_local_ids_in_stmt(stmt: &mut Stmt, map: &std::collections::HashMap<Loca
/// `perry_hir::walker`. Pre-refactor this fn carried its own ad-hoc walker
/// with a `_ => {}` catch-all that silently skipped any new variant added to
/// `Expr` (issue #212 partial-fix lineage).
fn remap_local_ids_in_expr(expr: &mut Expr, map: &std::collections::HashMap<LocalId, LocalId>) {
pub fn remap_local_ids_in_expr(expr: &mut Expr, map: &std::collections::HashMap<LocalId, LocalId>) {
match expr {
Expr::LocalGet(id) => {
if let Some(&new_id) = map.get(id) {
Expand Down
23 changes: 23 additions & 0 deletions crates/perry-hir/src/destructuring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2138,6 +2138,29 @@ pub(crate) fn lower_var_decl_with_destructuring(
} else {
ctx.define_local(name.clone(), ty.clone())
};
// Issue #740: track `let/const/var <name> = ClassRef(...)` so
// `new <name>(...)` can resolve captures via the alias chain.
// Also follow LocalGet aliases for `const B = A` style chains.
if let Some(init_expr) = &init {
match init_expr {
Expr::ClassRef(class_name) => {
ctx.register_let_class_alias(name.clone(), class_name.clone());
}
Expr::LocalGet(src_id) => {
if let Some((src_name, _, _)) =
ctx.locals.iter().rev().find(|(_, lid, _)| lid == src_id)
{
let src_name = src_name.clone();
if let Some(resolved) = ctx.resolve_class_alias(&src_name) {
ctx.register_let_class_alias(name.clone(), resolved);
} else if ctx.classes_index.contains_key(&src_name) {
ctx.register_let_class_alias(name.clone(), src_name);
}
}
}
_ => {}
}
}
result.push(Stmt::Let {
id,
name,
Expand Down
48 changes: 48 additions & 0 deletions crates/perry-hir/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,15 @@ pub struct LoweringContext {
/// here so the `Expr::New { class_name }` lowering can append
/// `LocalGet(id)` for each captured id at every construction site.
pub(crate) class_captures: Vec<(String, Vec<LocalId>)>,
/// Issue #740: `let_name → class_name` for `let/const/var <name> = <ClassRef>`
/// initializers. Lets `Expr::New { class_name }` (where `class_name` is
/// the source-level identifier of an alias binding) resolve to the
/// underlying class so its `class_captures` (if any) get appended as
/// ctor args at the `new` site. Mirrors codegen's `local_class_aliases`,
/// but built at HIR-lowering time so the captured-arg LocalGets land in
/// the HIR (where codegen consumes them) rather than being patched in
/// after lowering.
pub(crate) let_class_aliases: Vec<(String, String)>,
/// Issue #444: true when this module is the user-supplied entry file.
/// Drives `import.meta.main` — Node 24+ / Bun semantics where the entry
/// module reports `true` and every imported module reports `false`. Set
Expand Down Expand Up @@ -378,6 +387,7 @@ impl LoweringContext {
next_anon_shape_id: 0,
class_method_return_types: Vec::new(),
class_captures: Vec::new(),
let_class_aliases: Vec::new(),
is_entry_module: false,
is_external_module: false,
}
Expand Down Expand Up @@ -762,6 +772,44 @@ impl LoweringContext {
.map(|(_, c)| c.as_slice())
}

/// Issue #740: register a `let/const/var <let_name> = <ClassRef>` alias
/// so `Expr::New { class_name: <let_name> }` can resolve to the
/// underlying class for capture-forwarding purposes.
pub(crate) fn register_let_class_alias(&mut self, let_name: String, class_name: String) {
if let Some(entry) = self
.let_class_aliases
.iter_mut()
.find(|(n, _)| *n == let_name)
{
entry.1 = class_name;
} else {
self.let_class_aliases.push((let_name, class_name));
}
}

/// Look up the underlying class name for a let/const/var alias. Walks
/// the alias chain (`const B = A; const C = B` → C resolves to A's
/// underlying class) up to a small depth to avoid runaway loops.
pub(crate) fn resolve_class_alias(&self, name: &str) -> Option<String> {
let mut cur = name.to_string();
for _ in 0..8 {
let next = self
.let_class_aliases
.iter()
.find(|(n, _)| n == &cur)
.map(|(_, c)| c.clone());
match next {
Some(n) if n != cur => cur = n,
_ => break,
}
}
if cur != name {
Some(cur)
} else {
None
}
}

pub(crate) fn register_class_statics(
&mut self,
class_name: String,
Expand Down
15 changes: 14 additions & 1 deletion crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,8 +636,21 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
// constructor with one synthesized param per captured id;
// pass each as `LocalGet(id)` here so the outer scope's
// current value is snapshotted onto the new instance.
//
// Issue #740: when `class_name` is the name of a `let/const`
// alias (`const C = Inner` or `const C = makeChild(...)`
// where the returned class is statically known via a
// `ClassRef` chain), resolve through the alias before
// looking up captures. Plain function-return aliases
// (`const C = makeChild("foo")`) can't be resolved at HIR
// time — those flow through the closure mechanism in
// `compile_function` (the function body inlines `new`
// with the captures forwarded correctly).
let lookup_name = ctx
.resolve_class_alias(&class_name)
.unwrap_or_else(|| class_name.clone());
let class_captures: Vec<LocalId> = ctx
.lookup_class_captures(&class_name)
.lookup_class_captures(&lookup_name)
.map(|c| c.to_vec())
.unwrap_or_default();
for cid in class_captures {
Expand Down
Loading
Loading