diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index fcfb8bf5e..77d125133 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -175,6 +175,7 @@ impl LoweringContext { fn_ctor_env: super::fn_ctor_env::FnCtorEnv::default(), expr_lower_depth: 0, prelowered_member_receiver: None, + in_nonarrow_fn: false, } } diff --git a/crates/perry-hir/src/lower/eval_super_scan.rs b/crates/perry-hir/src/lower/eval_super_scan.rs index 93ffcdc3e..a98503877 100644 --- a/crates/perry-hir/src/lower/eval_super_scan.rs +++ b/crates/perry-hir/src/lower/eval_super_scan.rs @@ -183,6 +183,14 @@ pub(crate) fn check_direct_eval_super_private( "'arguments' is not allowed in class field initializer", )); } + // ES2025 §15.2.1.1: `new.target` in a direct eval body is only legal when + // the eval is contained in function code that is NOT an ArrowFunction. + // At module/script top-level and inside arrow functions, it is a SyntaxError. + if scan.new_target && !ctx.in_nonarrow_fn { + return Some(throw_eval_syntax_error_expr( + "new.target expression is not allowed here", + )); + } check_private_refs(&scan, |name| { ctx.private_scopes .iter() diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index 0041389ab..55cf433c4 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -578,6 +578,8 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul // in a class field initializer. Cleared here, restored at the end. let saved_field_init = ctx.in_class_field_init; ctx.in_class_field_init = false; + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; // Lower parameters and collect destructuring info. // @@ -1217,6 +1219,7 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); ctx.in_class_field_init = saved_field_init; + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Scope popped: `ctx.locals.id_set()` is now the enclosing scope's locals. let (captures, mutable_captures) = diff --git a/crates/perry-hir/src/lower/expr_object.rs b/crates/perry-hir/src/lower/expr_object.rs index aa2eee0c5..c849a57c8 100644 --- a/crates/perry-hir/src/lower/expr_object.rs +++ b/crates/perry-hir/src/lower/expr_object.rs @@ -203,6 +203,8 @@ fn lower_method_prop( .collect(); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; // Object-literal methods are NOT implicitly strict (unlike class bodies): // strictness is inherited from the enclosing code or introduced by the // method's own directive prologue. A blanket `true` here made every @@ -343,6 +345,7 @@ fn lower_method_prop( } ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Capture analysis (same pattern as arrow/function expressions) let mut all_refs = Vec::new(); @@ -465,6 +468,8 @@ fn lower_accessor_prop( .collect(); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; // Accessors in object literals inherit strictness (see lower_method_prop). let accessor_strict = ctx.current_strict_mode() || body @@ -508,6 +513,7 @@ fn lower_accessor_prop( }; ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Capture analysis — identical pattern to `lower_method_prop`. let mut all_refs = Vec::new(); diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 6970bc76a..34f0df8a5 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -783,4 +783,12 @@ pub struct LoweringContext { /// idempotent w.r.t. the value produced (the fluent-success path already /// reuses the same lowered receiver), so reusing it is semantics-preserving. pub(crate) prelowered_member_receiver: Option<((u32, u32), Expr)>, + /// True when the eval call site is lexically inside a non-arrow function + /// (ordinary `function` declaration/expression, method, constructor, getter, + /// setter, or static block). Used by `check_direct_eval_super_private` to + /// enforce the spec rule: `new.target` in a direct eval body is only legal + /// when the eval is contained in function code that is NOT an ArrowFunction + /// (ES2025 §15.2.1.1, early error for `new.target` in eval). ArrowFunction + /// bodies and module/script top-level both leave this false. + pub(crate) in_nonarrow_fn: bool, } diff --git a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs index 6e0bcc27d..3c1fe5aba 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs @@ -62,6 +62,8 @@ pub(super) fn lower_nested_fn_decl( }; let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; // Lower parameters. Skip the TypeScript `this:` annotation — // it has no runtime existence (see the sibling site above for @@ -194,6 +196,7 @@ pub(super) fn lower_nested_fn_decl( } ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Detect captured variables let mut all_refs = Vec::new(); diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 8c74e63c8..2b964ddb5 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -1045,8 +1045,11 @@ pub fn lower_class_decl( // such method right after static field init, so they // run once at module startup. let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; let body = lower_block_stmt(ctx, &block.body)?; ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; let block_idx = static_methods .iter() @@ -1814,8 +1817,11 @@ pub fn lower_class_from_ast( } ast::ClassMember::StaticBlock(block) => { let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; let body = lower_block_stmt(ctx, &block.body)?; ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; let block_idx = static_methods .iter() diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index 5870d6071..ed75751c1 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -15,6 +15,8 @@ pub fn lower_constructor( ctor: &ast::Constructor, ) -> Result { let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); // Track that we're inside a constructor body so `new.target` can resolve @@ -210,6 +212,7 @@ pub fn lower_constructor( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; ctx.in_constructor_class = saved_ctor_class; Ok(Function { @@ -469,6 +472,8 @@ pub fn lower_class_method_with_name( ctx.enter_type_param_scope(&type_params); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); // Add 'this' for instance methods @@ -641,6 +646,7 @@ pub fn lower_class_method_with_name( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Exit method's type param scope ctx.exit_type_param_scope(); @@ -710,6 +716,8 @@ pub fn lower_getter_method_with_name( name: String, ) -> Result { let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); // Add 'this' for instance getters @@ -747,6 +755,7 @@ pub fn lower_getter_method_with_name( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; Ok(Function { id: ctx.fresh_func(), @@ -788,6 +797,8 @@ pub fn lower_setter_method_with_name( name: String, ) -> Result { let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); // Add 'this' for instance setters @@ -870,6 +881,7 @@ pub fn lower_setter_method_with_name( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; Ok(Function { id: ctx.fresh_func(), diff --git a/crates/perry-hir/src/lower_decl/fn_decl.rs b/crates/perry-hir/src/lower_decl/fn_decl.rs index c22d9c6e3..fefef0bfe 100644 --- a/crates/perry-hir/src/lower_decl/fn_decl.rs +++ b/crates/perry-hir/src/lower_decl/fn_decl.rs @@ -68,6 +68,8 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result ctx.enter_type_param_scope(&type_params); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; // Pre-scan body for `arguments` references. If the function references // `arguments`, we synthesize a hidden raw-arguments parameter so @@ -372,6 +374,7 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; // Exit type parameter scope ctx.exit_type_param_scope(); diff --git a/crates/perry-hir/src/lower_decl/private_members.rs b/crates/perry-hir/src/lower_decl/private_members.rs index 2138364c2..71b5b600b 100644 --- a/crates/perry-hir/src/lower_decl/private_members.rs +++ b/crates/perry-hir/src/lower_decl/private_members.rs @@ -80,6 +80,8 @@ pub fn lower_private_method( ctx.enter_type_param_scope(&type_params); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); // Add 'this' for instance methods @@ -190,6 +192,7 @@ pub fn lower_private_method( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; ctx.exit_type_param_scope(); let func_id = ctx.fresh_func(); @@ -232,6 +235,8 @@ pub fn lower_private_getter( ) -> Result { let name = format!("get_#{}", method.key.name); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); ctx.define_local("this".to_string(), Type::Any); @@ -250,6 +255,7 @@ pub fn lower_private_getter( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; Ok(Function { id: ctx.fresh_func(), @@ -276,6 +282,8 @@ pub fn lower_private_setter( ) -> Result { let name = format!("set_#{}", method.key.name); let scope_mark = ctx.enter_scope(); + let saved_in_nonarrow_fn = ctx.in_nonarrow_fn; + ctx.in_nonarrow_fn = true; ctx.enter_strict_mode(true); ctx.define_local("this".to_string(), Type::Any); @@ -326,6 +334,7 @@ pub fn lower_private_setter( ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_nonarrow_fn = saved_in_nonarrow_fn; Ok(Function { id: ctx.fresh_func(),