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
1 change: 1 addition & 0 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mod native;
mod native_module_dispatch;
mod native_table;
mod new;
mod new_helpers;
mod options;
mod property_get;
mod ui_styling;
Expand Down
284 changes: 22 additions & 262 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,213 +9,14 @@ use perry_hir::{Expr, Param};
use perry_types::Type as HirType;

use super::lower_builtin_new;
use super::new_helpers::{
collect_decl_local_ids, ctor_body_calls_super, ctor_body_closure_calls_super,
ctor_body_has_value_return, ctor_body_uses_this, node_stream_parent_kind,
};
use crate::expr::{lower_expr, lower_js_args_array, nanbox_pointer_inline, FnCtx};
use crate::nanbox::{double_literal, POINTER_MASK_I64};
use crate::types::{DOUBLE, I32, I64, I8, PTR};

/// Generic "does any statement in this ctor body satisfy `stmt_pred` or
/// contain an expression satisfying `expr_pred`" walker, shared by the
/// no-super static-throw heuristics below.
fn ctor_body_any(
body: &[perry_hir::Stmt],
expr_pred: &dyn Fn(&Expr) -> bool,
stmt_pred: &dyn Fn(&perry_hir::Stmt) -> bool,
) -> bool {
body.iter().any(|s| stmt_any(s, expr_pred, stmt_pred))
}

fn stmt_any(
stmt: &perry_hir::Stmt,
expr_pred: &dyn Fn(&Expr) -> bool,
stmt_pred: &dyn Fn(&perry_hir::Stmt) -> bool,
) -> bool {
use perry_hir::Stmt;
if stmt_pred(stmt) {
return true;
}
match stmt {
Stmt::Let { init, .. } => init.as_ref().is_some_and(expr_pred),
Stmt::Expr(e) | Stmt::Throw(e) => expr_pred(e),
Stmt::Return(opt) => opt.as_ref().is_some_and(expr_pred),
Stmt::If {
condition,
then_branch,
else_branch,
} => {
expr_pred(condition)
|| ctor_body_any(then_branch, expr_pred, stmt_pred)
|| else_branch
.as_ref()
.is_some_and(|b| ctor_body_any(b, expr_pred, stmt_pred))
}
Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => {
expr_pred(condition) || ctor_body_any(body, expr_pred, stmt_pred)
}
Stmt::For {
init,
condition,
update,
body,
} => {
init.as_deref()
.is_some_and(|s| stmt_any(s, expr_pred, stmt_pred))
|| condition.as_ref().is_some_and(expr_pred)
|| update.as_ref().is_some_and(expr_pred)
|| ctor_body_any(body, expr_pred, stmt_pred)
}
Stmt::Labeled { body, .. } => stmt_any(body, expr_pred, stmt_pred),
Stmt::Try {
body,
catch,
finally,
} => {
ctor_body_any(body, expr_pred, stmt_pred)
|| catch
.as_ref()
.is_some_and(|c| ctor_body_any(&c.body, expr_pred, stmt_pred))
|| finally
.as_ref()
.is_some_and(|f| ctor_body_any(f, expr_pred, stmt_pred))
}
Stmt::Switch {
discriminant,
cases,
} => {
expr_pred(discriminant)
|| cases.iter().any(|c| {
c.test.as_ref().is_some_and(expr_pred)
|| ctor_body_any(&c.body, expr_pred, stmt_pred)
})
}
Stmt::Break
| Stmt::Continue
| Stmt::LabeledBreak(_)
| Stmt::LabeledContinue(_)
| Stmt::PreallocateBoxes(_) => false,
}
}

const NO_STMT_PRED: &dyn Fn(&perry_hir::Stmt) -> bool = &|_| false;

/// True when a DIRECT `super(...)` call appears in this constructor body
/// (`walk_expr_children` does not descend into `Expr::Closure` bodies). A
/// derived constructor that never calls `super()` leaves `this`
/// uninitialized — ECMAScript then throws ReferenceError at the implicit
/// `return this`. We detect the static no-super case at compile time so
/// `new Sub()` throws instead of returning a half-built object.
fn ctor_body_calls_super(body: &[perry_hir::Stmt]) -> bool {
ctor_body_any(body, &expr_calls_super, NO_STMT_PRED)
}

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

/// True when a closure (arrow) created in the ctor body contains a
/// `super(...)` call. Such an arrow can run DURING construction (e.g.
/// stored on an iterator and invoked from its `return()` while the ctor's
/// for-of is still iterating), so the static no-super throw must not fire —
/// unless the body also dereferences `this` directly (see the call site).
/// Refs class/subclass/derived-class-return-override-{for-of,finally-super}-arrow.
fn ctor_body_closure_calls_super(body: &[perry_hir::Stmt]) -> bool {
ctor_body_any(body, &expr_calls_super_incl_closures, NO_STMT_PRED)
}

fn expr_calls_super_incl_closures(expr: &Expr) -> bool {
if matches!(expr, Expr::SuperCall(_) | Expr::SuperCallSpread(_)) {
return true;
}
if let Expr::Closure { body, .. } = expr {
return ctor_body_any(body, &expr_calls_super_incl_closures, NO_STMT_PRED);
}
let mut found = false;
perry_hir::walker::walk_expr_children(expr, &mut |child| {
if !found && expr_calls_super_incl_closures(child) {
found = true;
}
});
found
}

/// True when the ctor body dereferences `this` OUTSIDE nested closures.
/// Combined with `ctor_body_closure_calls_super`: a direct `this` access in
/// a no-direct-super derived ctor throws ReferenceError per spec before any
/// closure could run `super()`, so the static entry throw stays correct
/// (test262 class/elements/privatefieldset-evaluation-order-1).
fn ctor_body_uses_this(body: &[perry_hir::Stmt]) -> bool {
ctor_body_any(body, &expr_uses_this_direct, NO_STMT_PRED)
}

fn expr_uses_this_direct(expr: &Expr) -> bool {
if matches!(expr, Expr::This) {
return true;
}
if matches!(expr, Expr::Closure { .. }) {
return false;
}
let mut found = false;
perry_hir::walker::walk_expr_children(expr, &mut |child| {
if !found && expr_uses_this_direct(child) {
found = true;
}
});
found
}

/// True when the constructor body contains a value-bearing `return` in its
/// own body (closures excluded; a bare `return undefined` does NOT count —
/// spec falls back to the uninitialized `this` and still throws). The
/// return-override path initializes the `new` expression's value without
/// `super()`, so the static no-super ReferenceError must not fire —
/// `js_ctor_return_override` still enforces the derived-ctor rules on the
/// returned value at runtime. Refs
/// class/subclass/class-definition-null-proto-contains-return-override and
/// class/subclass/builtin-objects/Object/constructor-return-undefined-throws.
fn ctor_body_has_value_return(body: &[perry_hir::Stmt]) -> bool {
ctor_body_any(
body,
&|_| false,
&|s| matches!(s, perry_hir::Stmt::Return(Some(e)) if !matches!(e, Expr::Undefined)),
)
}

fn node_stream_parent_kind(ctx: &FnCtx<'_>, class: &perry_hir::Class) -> Option<&'static str> {
let mut cur = class.extends_name.as_deref();
let mut depth = 0usize;
while let Some(name) = cur {
match name {
"Readable" => return Some("readable"),
"Duplex" => return Some("duplex"),
"Transform" => return Some("transform"),
_ => {}
}
if ctx.imported_class_ctors.contains_key(name) {
return None;
}
let Some(parent) = ctx.classes.get(name).copied() else {
return None;
};
if parent.constructor.is_some() {
return None;
}
cur = parent.extends_name.as_deref();
depth += 1;
if depth > 32 {
break;
}
}
None
}

pub(crate) struct InlineConstructorScope {
locals: std::collections::HashMap<u32, String>,
local_types: std::collections::HashMap<u32, HirType>,
Expand Down Expand Up @@ -369,64 +170,6 @@ fn local_constructor_symbol_exists(ctx: &FnCtx<'_>, class: &perry_hir::Class) ->
.contains_key(&(class.name.clone(), ctor_method_name))
}

/// Collect every LocalId DECLARED (via `Stmt::Let`, incl. nested in compound
/// statements) within a constructor body. Used to detect the wall-44 inline
/// collision: a ctor local whose id is also a capture of the enclosing closure.
/// Mirrors `collect_let_ids` in `class_members.rs`.
fn collect_decl_local_ids(stmts: &[perry_hir::Stmt], out: &mut std::collections::HashSet<u32>) {
use perry_hir::Stmt;
for s in stmts {
match s {
Stmt::Let { id, .. } => {
out.insert(*id);
}
Stmt::If {
then_branch,
else_branch,
..
} => {
collect_decl_local_ids(then_branch, out);
if let Some(e) = else_branch {
collect_decl_local_ids(e, out);
}
}
Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => {
collect_decl_local_ids(body, out)
}
Stmt::For { init, body, .. } => {
if let Some(init_stmt) = init {
if let Stmt::Let { id, .. } = init_stmt.as_ref() {
out.insert(*id);
}
}
collect_decl_local_ids(body, out);
}
Stmt::Try {
body,
catch,
finally,
} => {
collect_decl_local_ids(body, out);
if let Some(c) = catch {
collect_decl_local_ids(&c.body, out);
}
if let Some(f) = finally {
collect_decl_local_ids(f, out);
}
}
Stmt::Switch { cases, .. } => {
for case in cases {
collect_decl_local_ids(&case.body, out);
}
}
Stmt::Labeled { body, .. } => {
collect_decl_local_ids(std::slice::from_ref(body.as_ref()), out)
}
_ => {}
}
}
}

fn call_local_constructor_symbol(
ctx: &mut FnCtx<'_>,
class: &perry_hir::Class,
Expand Down Expand Up @@ -1070,7 +813,24 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) ->
collect_decl_local_ids(&c.body, &mut ids);
ids.iter().any(|id| ctx.closure_captures.contains_key(id))
});
if ctx.class_stack.iter().any(|active| active == class_name) || ctor_alias_collision {
// [#bloat] Default: CALL the shared standalone-symbol constructor instead of
// inlining the constructor body at every `new` site. The inlined ctor body
// (field-init stores etc.) is the dominant per-`new`-site IR after the
// allocator (~136 lines/site); calling the shared ctor symbol emits it once.
// Measured win-win vs inlining: ~2.5x FASTER on an 8M construct-heavy loop
// AND much smaller IR. Opt back into inlining with PERRY_INLINE_CTOR=1.
// Restricted to classes with their OWN constructor: a no-own-ctor subclass
// (`class C extends B {}`) gets a synthesized symbol, but the symbol-call
// path doesn't reproduce the inline path's leaf-keys/shape setup, so by-name
// field reads on the instance return undefined. Own-ctor classes (incl. ones
// with `super(...)`/rest params) round-trip correctly through the call.
let force_ctor_call = std::env::var_os("PERRY_INLINE_CTOR").is_none()
&& class.constructor.is_some()
&& local_constructor_symbol_exists(ctx, class);
if ctx.class_stack.iter().any(|active| active == class_name)
|| ctor_alias_collision
|| force_ctor_call
{
call_local_constructor_symbol(ctx, class, &obj_box, &lowered_args);
return Ok(obj_box);
}
Expand Down
Loading
Loading