diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index 1bff31bf9..99789357c 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -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; diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 84ec473df..ca7c287f8 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -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, local_types: std::collections::HashMap, @@ -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) { - 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, @@ -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); } diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs new file mode 100644 index 000000000..ae0df04e1 --- /dev/null +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -0,0 +1,278 @@ +//! Constructor-body analysis helpers for `new ClassName(args…)` lowering. +//! +//! Pure predicate walkers (no codegen side effects) split out of `new.rs` +//! to keep that file under the file-size gate. They classify a constructor +//! body — does it call `super()`, dereference `this`, value-return, etc. — +//! to drive `lower_new`'s static no-super-throw / inline-vs-call decisions, +//! plus `node_stream_parent_kind` and `collect_decl_local_ids`. + +use perry_hir::Expr; + +use crate::expr::FnCtx; + +/// 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. +pub(super) 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. +pub(super) 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). +pub(super) 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. +pub(super) 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)), + ) +} + +pub(super) 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 +} + +/// 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`. +pub(super) fn collect_decl_local_ids( + stmts: &[perry_hir::Stmt], + out: &mut std::collections::HashSet, +) { + 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) + } + _ => {} + } + } +}