From d48116e2144160e26fbda99a8ed9534b05a3d5b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 17:30:23 +0000 Subject: [PATCH] eval: SyntaxError for new.target in direct eval outside non-arrow function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ES2025 §15.2.1.1 makes `new.target` in a direct eval body a SyntaxError unless the eval is contained in function code that is not an ArrowFunction. Perry's const-fold path folded eval("new.target") into a completion IIFE without checking this rule, so at global scope or inside an arrow it returned undefined instead of throwing. Fix: add `in_nonarrow_fn: bool` to `LoweringContext`, set it to `true` at every non-arrow function boundary (fn declarations, fn expressions, methods, constructors, getters, setters, private members, static blocks, object-literal methods/accessors), and check it in `check_direct_eval_super_private` — the same pattern already used by `check_indirect_eval_super_private` for its unconditional new.target rejection. Fixes two test262 cases tracked in #5579: language/eval-code/direct/new.target.js language/eval-code/direct/new.target-arrow.js --- crates/perry-hir/src/lower/context.rs | 1 + crates/perry-hir/src/lower/eval_super_scan.rs | 8 ++++++++ crates/perry-hir/src/lower/expr_function.rs | 3 +++ crates/perry-hir/src/lower/expr_object.rs | 6 ++++++ crates/perry-hir/src/lower/lowering_context.rs | 8 ++++++++ .../src/lower_decl/body_stmt/nested_fn_decl.rs | 3 +++ crates/perry-hir/src/lower_decl/class_decl.rs | 6 ++++++ crates/perry-hir/src/lower_decl/class_members.rs | 12 ++++++++++++ crates/perry-hir/src/lower_decl/fn_decl.rs | 3 +++ crates/perry-hir/src/lower_decl/private_members.rs | 9 +++++++++ 10 files changed, 59 insertions(+) diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index fcfb8bf5e5..77d125133b 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 93ffcdc3e6..a985038771 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 0041389abd..55cf433c4c 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 aa2eee0c51..c849a57c81 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 6970bc76ad..34f0df8a57 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 6e0bcc27d2..3c1fe5abab 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 8c74e63c84..2b964ddb5a 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 5870d60718..ed75751c1d 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 c22d9c6e3b..fefef0bfe0 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 2138364c21..71b5b600bb 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(),