Skip to content
Merged
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
131 changes: 131 additions & 0 deletions crates/perry-hir/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2319,9 +2319,140 @@ pub fn lower_module_full(
}
}

// Post-pass: infer `extends_name` from `extends_expr` for the bare-factory
// shape `class Sub extends makeFactory() {}` where `makeFactory` is a
// top-level function whose body trivially returns a static `ClassRef`.
// Without this, the codegen chain walks
// (`apply_field_initializers_recursive` + the keys-array generator) walk
// by `extends_name` only, see `None`, and skip the factory class's
// field initializers entirely — `new Sub().kind` reads `undefined`
// instead of the parent's `kind = "bare"` literal. Surfaced by the
// #806 mixin harness (bare-factory section).
infer_dynamic_extends_names(&mut module);

Ok((module, ctx.next_class_id))
}

/// Fill in `Class::extends_name` for classes whose parent is the result of
/// calling a statically-resolvable factory function — but ONLY when the
/// parent's field initializers are closure-free (no `LocalGet` reads). The
/// post-pass runs after every function and class is in `module`, so
/// forward-references work (e.g. `class Sub extends makeBare() {}` ahead of
/// `function makeBare() …` hoisting).
///
/// The closure-free guard exists because the field-init pass at codegen
/// (`apply_field_initializers_recursive`) inlines each chained class's
/// init expressions directly into the subclass's constructor. That's
/// correct for pure-literal initializers like `kind = "bare"` but wrong
/// for `_tag = tag` where `tag` is the factory's parameter — the inlined
/// `LocalGet(tag)` would re-resolve in the subclass's scope (where `tag`
/// doesn't exist) and produce garbage. Conservatively skip those: the
/// subclass's static parent stays None and field-init inheritance only
/// works for the literal-initialized parents that #806's bare-factory
/// section needs.
fn infer_dynamic_extends_names(module: &mut Module) {
use std::collections::HashMap;
// Build a map of `function_id → returned ClassRef name` for every
// function whose body returns a static ClassRef. Only the LAST `Return`
// is examined — bodies with multiple Returns to different classes
// don't resolve uniquely, and the canonical factory shape has exactly
// one Return as its last statement.
let mut factory_returns: HashMap<u32, String> = HashMap::new();
for func in &module.functions {
if let Some(name) = trailing_return_classref(&func.body) {
factory_returns.insert(func.id, name);
}
}
// Index classes by name so we can re-resolve transitively (a chain like
// `Sub extends A() {}` where `A` returns `__anon_N` and `__anon_N` is
// a class we own — we only set `extends_name` for `Sub` here; chain
// walks at codegen step through `__anon_N.extends_name` normally).
let class_field_inits_pure: HashMap<String, bool> = module
.classes
.iter()
.map(|c| (c.name.clone(), fields_are_pure(c)))
.collect();
for class in &mut module.classes {
if class.extends_name.is_some() {
continue;
}
let Some(expr) = class.extends_expr.as_deref() else {
continue;
};
let Expr::Call { callee, .. } = expr else {
continue;
};
let Expr::FuncRef(func_id) = callee.as_ref() else {
continue;
};
let Some(parent_name) = factory_returns.get(func_id) else {
continue;
};
// Only inherit field-init machinery when the parent's fields are
// pure (no `LocalGet`). Methods on the parent are unaffected —
// those dispatch through the runtime CLASS_REGISTRY which is
// populated by the #826 RegisterClassParentDynamic side effect.
if class_field_inits_pure
.get(parent_name)
.copied()
.unwrap_or(false)
{
class.extends_name = Some(parent_name.clone());
}
}
}

/// True when none of the class's field initializers contain a `LocalGet`
/// (the canonical sign that an initializer closes over its surrounding
/// scope — function parameters, outer-block lets, etc.).
fn fields_are_pure(class: &Class) -> bool {
for field in &class.fields {
if let Some(init) = &field.init {
if expr_reads_local(init) {
return false;
}
}
if let Some(key) = &field.key_expr {
if expr_reads_local(key) {
return false;
}
}
}
true
}

fn expr_reads_local(expr: &Expr) -> bool {
if matches!(expr, Expr::LocalGet(_)) {
return true;
}
let mut found = false;
crate::walker::walk_expr_children(expr, &mut |child| {
if !found && expr_reads_local(child) {
found = true;
}
});
found
}

/// Return `Some(name)` if `body`'s last `Return` statement yields a static
/// `Expr::ClassRef` (directly or as the last element of an `Expr::Sequence`).
fn trailing_return_classref(body: &[Stmt]) -> Option<String> {
for stmt in body.iter().rev() {
if let Stmt::Return(Some(expr)) = stmt {
return classref_name(expr);
}
}
None
}

fn classref_name(expr: &Expr) -> Option<String> {
match expr {
Expr::ClassRef(name) => Some(name.clone()),
Expr::Sequence(parts) => parts.last().and_then(classref_name),
_ => None,
}
}

/// Post-lowering pass that widens every `Expr::Closure`'s `mutable_captures`
/// to include any capture that is assigned to inside a sibling closure in the
/// same lexical scope. Then recurses into each closure body so nested scopes
Expand Down
Loading