diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index d5ee5d0c8..42d8aba4d 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -490,7 +490,33 @@ pub(super) fn compile_method( } } - if method.is_async { + // ECMAScript TDZ-on-`this`: a DERIVED constructor whose body never calls + // `super()` leaves `this` uninitialized, so the implicit `return this` + // throws ReferenceError. The inline `new` path enforces this in + // `lower_new`; mirror it here for the standalone constructor-symbol path + // — the DEFAULT when `force_ctor_call` routes `new C(...)` through the + // shared `_constructor` symbol instead of inlining. Without this, + // `class A extends Array { constructor() {} }; new A()` constructs + // silently instead of throwing. The predicate combination matches the + // inline path verbatim (closure-`super()` without a direct `this` use + // suppresses; a value-bearing `return` takes the return-override path). + // Refs class/subclass/builtin-objects/*/super-must-be-called. + let ctor_no_super_throw = is_constructor_method + && (class.extends.is_some() + || class.extends_name.is_some() + || class.native_extends.is_some() + || class.extends_expr.is_some()) + && class.constructor.as_ref().is_some_and(|ctor| { + !crate::lower_call::ctor_body_calls_super(&ctor.body) + && !(crate::lower_call::ctor_body_closure_calls_super(&ctor.body) + && !crate::lower_call::ctor_body_uses_this(&ctor.body)) + && !crate::lower_call::ctor_body_has_value_return(&ctor.body) + }); + if ctor_no_super_throw { + ctx.block() + .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); + ctx.block().unreachable(); + } else if method.is_async { stmt::lower_async_rejecting_stmts(&mut ctx, &method.body).with_context(|| { format!( "lowering async body of method '{}::{}'", diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index a5aafc0b7..24e7b1f1a 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -103,6 +103,15 @@ pub(crate) use native::lower_native_method_call; // etc. keep resolving after the split. pub(crate) use field_init::{apply_field_initializers_recursive, FieldInitMode}; pub(crate) use new::{bind_inline_constructor_params, lower_new, restore_inline_constructor_scope}; +// The derived-ctor no-super static-throw predicates (shared with the +// standalone-ctor-symbol path in `codegen/method.rs`, which is the default +// `new` lowering when `force_ctor_call` redirects construction to the shared +// symbol instead of inlining). Refs class/subclass/builtin-objects/*/ +// super-must-be-called. +pub(crate) use new_helpers::{ + ctor_body_calls_super, ctor_body_closure_calls_super, ctor_body_has_value_return, + ctor_body_uses_this, +}; // `extract_options_fields` is consumed by `expr.rs` as // `crate::lower_call::extract_options_fields` — keep that path stable. pub(crate) use options::extract_options_fields; diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index ae0df04e1..4f3b5b41f 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -100,7 +100,7 @@ const NO_STMT_PRED: &dyn Fn(&perry_hir::Stmt) -> bool = &|_| false; /// 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 { +pub(crate) fn ctor_body_calls_super(body: &[perry_hir::Stmt]) -> bool { ctor_body_any(body, &expr_calls_super, NO_STMT_PRED) } @@ -123,7 +123,7 @@ fn expr_calls_super(expr: &Expr) -> bool { /// 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 { +pub(crate) fn ctor_body_closure_calls_super(body: &[perry_hir::Stmt]) -> bool { ctor_body_any(body, &expr_calls_super_incl_closures, NO_STMT_PRED) } @@ -148,7 +148,7 @@ fn expr_calls_super_incl_closures(expr: &Expr) -> bool { /// 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 { +pub(crate) fn ctor_body_uses_this(body: &[perry_hir::Stmt]) -> bool { ctor_body_any(body, &expr_uses_this_direct, NO_STMT_PRED) } @@ -177,7 +177,7 @@ fn expr_uses_this_direct(expr: &Expr) -> bool { /// 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 { +pub(crate) fn ctor_body_has_value_return(body: &[perry_hir::Stmt]) -> bool { ctor_body_any( body, &|_| false,