diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index cebf4bc31f..a8800d61cb 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -680,7 +680,22 @@ pub(super) fn init_static_fields_late( if init_references_out_of_scope_local(init_expr) { continue; } - let v = crate::expr::lower_expr(ctx, init_expr)?; + // `this` in a static field initializer is the class + // constructor (`static g = this.f + '262'`). Seed the same + // class-ref NaN-box a static method binds (see + // `compile_static_method`) for the init's duration. + let seeded_this = ctx.class_ids.get(&c.name).copied().map(|cid| { + let bits = crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF); + let class_ref_lit = crate::nanbox::double_literal(f64::from_bits(bits)); + let this_slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store(DOUBLE, &class_ref_lit, &this_slot); + ctx.this_stack.push(this_slot); + }); + let v = crate::expr::lower_expr(ctx, init_expr); + if seeded_this.is_some() { + ctx.this_stack.pop(); + } + let v = v?; let g_ref = format!("@{}", global_name); crate::expr::emit_root_nanbox_store_on_block(ctx.block(), &v, &g_ref); } diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 9e3f5722ba..02e391cccb 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -13,34 +13,43 @@ 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}; -/// True when any `super(...)` call appears (anywhere) in this constructor -/// body. 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 { - body.iter().any(stmt_calls_super) +/// 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_calls_super(stmt: &perry_hir::Stmt) -> bool { +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_calls_super), - Stmt::Expr(e) | Stmt::Throw(e) => expr_calls_super(e), - Stmt::Return(opt) => opt.as_ref().is_some_and(expr_calls_super), + 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_calls_super(condition) - || ctor_body_calls_super(then_branch) + expr_pred(condition) + || ctor_body_any(then_branch, expr_pred, stmt_pred) || else_branch .as_ref() - .is_some_and(|b| ctor_body_calls_super(b)) + .is_some_and(|b| ctor_body_any(b, expr_pred, stmt_pred)) } Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - expr_calls_super(condition) || ctor_body_calls_super(body) + expr_pred(condition) || ctor_body_any(body, expr_pred, stmt_pred) } Stmt::For { init, @@ -48,30 +57,34 @@ fn stmt_calls_super(stmt: &perry_hir::Stmt) -> bool { update, body, } => { - init.as_deref().is_some_and(stmt_calls_super) - || condition.as_ref().is_some_and(expr_calls_super) - || update.as_ref().is_some_and(expr_calls_super) - || ctor_body_calls_super(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_calls_super(body), + Stmt::Labeled { body, .. } => stmt_any(body, expr_pred, stmt_pred), Stmt::Try { body, catch, finally, } => { - ctor_body_calls_super(body) + ctor_body_any(body, expr_pred, stmt_pred) || catch .as_ref() - .is_some_and(|c| ctor_body_calls_super(&c.body)) - || finally.as_ref().is_some_and(|f| ctor_body_calls_super(f)) + .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_calls_super(discriminant) + expr_pred(discriminant) || cases.iter().any(|c| { - c.test.as_ref().is_some_and(expr_calls_super) || ctor_body_calls_super(&c.body) + c.test.as_ref().is_some_and(expr_pred) + || ctor_body_any(&c.body, expr_pred, stmt_pred) }) } Stmt::Break @@ -82,6 +95,18 @@ fn stmt_calls_super(stmt: &perry_hir::Stmt) -> bool { } } +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(_)) { return true; @@ -95,6 +120,74 @@ fn expr_calls_super(expr: &Expr) -> bool { 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(_)) { + 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; @@ -920,7 +1013,16 @@ pub(crate) fn lower_new(ctx: &mut FnCtx<'_>, class_name: &str, args: &[Expr]) -> || class.extends_name.is_some() || class.native_extends.is_some() || class.extends_expr.is_some(); - if is_derived_class && !ctor_body_calls_super(&ctor.body) { + // A closure-captured `super()` may run during construction, so it + // suppresses the static throw — but only when the body never touches + // `this` directly (a direct `this` in a no-direct-super derived ctor + // throws before any closure could fire). A value-bearing `return` + // takes the return-override path instead of the implicit `return + // this`, so it suppresses the throw too. + let no_super_throw_statically = !ctor_body_calls_super(&ctor.body) + && !(ctor_body_closure_calls_super(&ctor.body) && !ctor_body_uses_this(&ctor.body)) + && !ctor_body_has_value_return(&ctor.body); + if is_derived_class && no_super_throw_statically { ctx.block() .call(DOUBLE, "js_throw_reference_error_this_before_super", &[]); ctx.block().unreachable(); diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 0b11b95bc3..ba442ccee9 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1180,6 +1180,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_throw_symbol_constructor_type_error", DOUBLE, &[]); module.declare_function("js_throw_bigint_constructor_type_error", DOUBLE, &[]); module.declare_function("js_throw_strict_eval_arguments_syntax_error", DOUBLE, &[]); + module.declare_function("js_throw_eval_syntax_error", DOUBLE, &[DOUBLE]); module.declare_function( "js_throw_restricted_function_property_assignment", DOUBLE, diff --git a/crates/perry-hir/src/lower/const_fold_fn.rs b/crates/perry-hir/src/lower/const_fold_fn.rs index 1f919fbcba..03cbb87663 100644 --- a/crates/perry-hir/src/lower/const_fold_fn.rs +++ b/crates/perry-hir/src/lower/const_fold_fn.rs @@ -502,10 +502,45 @@ fn resolve_fn_ctor_arg( /// the *unshadowed* `eval` builtin, a single non-spread argument, and a /// constant body that trims to exactly `this` / `globalThis`. Anything /// else returns `None`. -pub(crate) fn try_indirect_eval_globalthis( - ctx: &LoweringContext, - call: &ast::CallExpr, -) -> Option { +/// Re-parse a direct-eval body that failed the plain script parse inside an +/// object-method wrapper so SuperProperty forms parse, and return the method +/// body statements. Only used when the eval call site provides a super +/// capability (class member / object method); returns None otherwise so the +/// caller keeps the parse-failure SyntaxError. +fn reparse_eval_body_with_super(ctx: &LoweringContext, body_src: &str) -> Option> { + let super_capable = ctx.current_class.is_some() + || ctx.in_constructor_class.is_some() + || !ctx.object_super_home_stack.is_empty(); + if !super_capable || !body_src.contains("super") { + return None; + } + let wrapped = format!("({{ __perry_eval_m() {{\n{body_src}\n}} }});"); + let module = perry_parser::parse_typescript(&wrapped, ".cjs").ok()?; + let ast::ModuleItem::Stmt(ast::Stmt::Expr(expr_stmt)) = module.body.into_iter().next()? else { + return None; + }; + let mut e = *expr_stmt.expr; + loop { + match e { + ast::Expr::Paren(p) => e = *p.expr, + ast::Expr::Object(obj) => { + let prop = obj.props.into_iter().next()?; + let ast::PropOrSpread::Prop(prop) = prop else { + return None; + }; + let ast::Prop::Method(method) = *prop else { + return None; + }; + return Some(method.function.body?.stmts); + } + _ => return None, + } + } +} + +/// Match the `(0, eval)('')` indirect-eval shape and return the +/// constant body source. +fn indirect_eval_const_body(ctx: &LoweringContext, call: &ast::CallExpr) -> Option { if call.args.len() != 1 || call.args[0].spread.is_some() { return None; } @@ -533,7 +568,42 @@ pub(crate) fn try_indirect_eval_globalthis( { return None; } - let body = const_string_of(&call.args[0].expr)?; + const_string_of(&call.args[0].expr) +} + +/// Indirect eval evaluates as *global* code: any `super()` / `super.x` in the +/// body is a SyntaxError at the eval call, and private names are only valid +/// when a class inside the eval source declares them. +fn try_indirect_eval_super_private(ctx: &LoweringContext, call: &ast::CallExpr) -> Option { + let body = indirect_eval_const_body(ctx, call)?; + let module = match perry_parser::parse_typescript(&body, ".cjs") { + Ok(m) => m, + // SWC rejects some super / new.target forms at parse time (`super.x` + // at script top level). Global code can never contain either, so a + // body that mentions one and fails to parse is the same SyntaxError; + // other parse failures stay on the existing fallthrough path. + Err(_) => { + if body.contains("super") || body.contains("new.target") { + return Some(super::eval_super_scan::throw_eval_super_unexpected_expr()); + } + return None; + } + }; + let mut stmts: Vec = Vec::with_capacity(module.body.len()); + for item in module.body { + match item { + ast::ModuleItem::Stmt(s) => stmts.push(s), + _ => return None, + } + } + super::eval_super_scan::check_indirect_eval_super_private(&stmts) +} + +pub(crate) fn try_indirect_eval_globalthis( + ctx: &LoweringContext, + call: &ast::CallExpr, +) -> Option { + let body = indirect_eval_const_body(ctx, call)?; let trimmed = body.trim().trim_end_matches(';').trim(); if matches!(trimmed, "this" | "globalThis") { if eval_diag_enabled() { @@ -752,6 +822,9 @@ pub(crate) fn try_eval_function_call_fold( if let Some(expr) = try_indirect_eval_globalthis(ctx, call) { return Ok(Some(expr)); } + if let Some(expr) = try_indirect_eval_super_private(ctx, call) { + return Ok(Some(expr)); + } if let Some(expr) = try_direct_eval_this_fold(ctx, call) { return Ok(Some(expr)); } @@ -885,6 +958,23 @@ fn extract_fn_expr(module: &ast::Module) -> Option<&ast::FnExpr> { } } +/// Owning arrow analog of [`extract_fn_expr_owned`] — pull the `ArrowExpr` +/// out of a synthesized `(() => { ... });` module. +fn extract_arrow_expr_owned(module: ast::Module) -> Option { + let item = module.body.into_iter().next()?; + let ast::ModuleItem::Stmt(ast::Stmt::Expr(expr_stmt)) = item else { + return None; + }; + let mut e = *expr_stmt.expr; + loop { + match e { + ast::Expr::Paren(p) => e = *p.expr, + ast::Expr::Arrow(arrow) => return Some(arrow), + _ => return None, + } + } +} + /// Owning variant of [`extract_fn_expr`] — consumes the module so the caller /// can mutate the function body before lowering. fn extract_fn_expr_owned(module: ast::Module) -> Option { @@ -1042,31 +1132,60 @@ fn try_const_fold_eval( // Parse the eval body as sloppy-mode statements (`.cjs` → script, not // module, so `with` is allowed). A parse failure is a runtime SyntaxError. - let body_module = match perry_parser::parse_typescript(&body_src, ".cjs") { - Ok(m) => m, - Err(_) => return synth_function_syntax_error(ctx, EvalSurface::Eval, span).map(Some), - }; - let mut body_stmts: Vec = Vec::with_capacity(body_module.body.len()); - for item in body_module.body { - match item { - ast::ModuleItem::Stmt(s) => body_stmts.push(s), - // `import` / `export` inside eval is a SyntaxError. - _ => return synth_function_syntax_error(ctx, EvalSurface::Eval, span).map(Some), + let mut body_stmts: Vec; + match perry_parser::parse_typescript(&body_src, ".cjs") { + Ok(body_module) => { + body_stmts = Vec::with_capacity(body_module.body.len()); + for item in body_module.body { + match item { + ast::ModuleItem::Stmt(s) => body_stmts.push(s), + // `import` / `export` inside eval is a SyntaxError. + _ => { + return synth_function_syntax_error(ctx, EvalSurface::Eval, span).map(Some) + } + } + } } + Err(_) => { + // SWC rejects SuperProperty at script top level, but direct eval + // inside a class member may legally contain `super.x` (PerformEval + // runs the eval code with the member's home object). Re-parse the + // body inside an object-method wrapper — which admits super — and + // splice the method body out; the super-scan below still rejects + // SuperCall. Contexts with no super capability keep the plain + // parse-failure SyntaxError. + match reparse_eval_body_with_super(ctx, &body_src) { + Some(stmts) => body_stmts = stmts, + None => return synth_function_syntax_error(ctx, EvalSurface::Eval, span).map(Some), + } + } + } + + // PerformEval early errors: `super()` / `super.x` outside a context that + // provides them, and private names with no declaring class in scope, are + // SyntaxErrors thrown when the eval call evaluates. + if let Some(throw) = super::eval_super_scan::check_direct_eval_super_private(ctx, &body_stmts) { + return Ok(Some(throw)); } // Wrapper template: stmts == [var __perry_cv = undefined; // __perry_cv = undefined; (reset/assign template) // return __perry_cv;] - let template_src = "(function () {\nvar __perry_cv = undefined;\n__perry_cv = undefined;\nreturn __perry_cv;\n});\n"; + // An ARROW wrapper, not a plain function: direct eval code runs with the + // caller's `this` / `arguments` / `super` / `new.target` bindings, and the + // arrow gets all of those lexically. A plain-function wrapper rebound + // `this` to undefined, so `eval("this.#x")` inside a method brand-checked + // against the wrong receiver and `eval("this.prop")` read undefined. + let template_src = + "(() => {\nvar __perry_cv = undefined;\n__perry_cv = undefined;\nreturn __perry_cv;\n});\n"; let template_module = match perry_parser::parse_typescript(template_src, ".cjs") { Ok(m) => m, Err(_) => return Ok(None), }; - let Some(mut fn_expr) = extract_fn_expr_owned(template_module) else { + let Some(mut arrow) = extract_arrow_expr_owned(template_module) else { return Ok(None); }; - let Some(body) = fn_expr.function.body.as_mut() else { + let ast::BlockStmtOrExpr::BlockStmt(body) = arrow.body.as_mut() else { return Ok(None); }; if body.stmts.len() != 3 { @@ -1082,10 +1201,10 @@ fn try_const_fold_eval( insert_at += 1; } - // Lower the wrapper (sloppy) and immediately call it: `(function(){…})()`. + // Lower the wrapper (sloppy) and immediately call it: `(() => {…})()`. let outer_strict = ctx.current_strict; ctx.current_strict = false; - let lowered = lower_fn_expr(ctx, &fn_expr); + let lowered = lower_expr(ctx, &ast::Expr::Arrow(arrow)); ctx.current_strict = outer_strict; let closure = match lowered { Ok(l) => l, diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index bdc3485118..37dec471c8 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -130,6 +130,8 @@ impl LoweringContext { proxy_revoke_locals: HashMap::new(), class_expr_aliases: HashMap::new(), in_constructor_class: None, + current_class_is_derived: false, + in_class_field_init: false, current_class_super_ident: None, mixin_funcs: HashMap::new(), anon_shape_classes: HashMap::new(), diff --git a/crates/perry-hir/src/lower/eval_super_scan.rs b/crates/perry-hir/src/lower/eval_super_scan.rs new file mode 100644 index 0000000000..a84253b0af --- /dev/null +++ b/crates/perry-hir/src/lower/eval_super_scan.rs @@ -0,0 +1,576 @@ +//! Additional early error rules for eval code (spec: PerformEval). +//! +//! Direct eval applies extra early errors to its parsed ScriptBody based on +//! the lexical context of the eval call site: +//! +//! - `ScriptBody Contains SuperCall` is a SyntaxError unless the eval occurs +//! directly inside the constructor of a derived class. +//! - `ScriptBody Contains SuperProperty` is a SyntaxError unless the eval +//! occurs inside a class member (method / constructor / field initializer) +//! or an object-literal method (anywhere with a [[HomeObject]]). +//! - `AllPrivateIdentifiersValid`: every `#name` referenced by the eval code +//! must be declared by an enclosing class (of the call site, lexically) or +//! by a class inside the eval code itself. +//! +//! Indirect eval is global code: any SuperCall / SuperProperty is a +//! SyntaxError, and private names are only valid when declared by a class +//! inside the eval source. +//! +//! The errors are *runtime* SyntaxErrors thrown when the eval call evaluates +//! (the surrounding script must still compile and run up to that point), so +//! the check emits a `js_throw_eval_syntax_error` call expression rather than +//! failing the build. `Contains` semantics: arrow functions are transparent, +//! ordinary function bodies / class member bodies / object-literal method +//! bodies are opaque (their `super` belongs to their own home object). +//! Private-name collection ignores those boundaries — private names are +//! lexically scoped through nested functions. + +use std::collections::HashSet; + +use perry_types::Type; +use swc_ecma_ast as ast; + +use super::LoweringContext; +use crate::ir::Expr; + +#[derive(Default)] +struct Scan { + super_call: bool, + super_prop: bool, + /// `arguments` referenced in the eval's own context (arrows transparent, + /// function bodies opaque — a nested plain function has its own). + arguments_ref: bool, + /// `new.target` referenced in the eval's own context (same transparency). + new_target: bool, + /// Referenced private names, `#name` form. + private_refs: Vec, + /// Private names declared by class bodies inside the eval source. + private_decls: HashSet, +} + +/// The "super is illegal here" throw, exposed for the indirect-eval +/// parse-failure path in `const_fold_fn.rs`. +pub(crate) fn throw_eval_super_unexpected_expr() -> Expr { + throw_eval_syntax_error_expr("'super' keyword unexpected here") +} + +fn throw_eval_syntax_error_expr(msg: &str) -> Expr { + Expr::Call { + callee: Box::new(Expr::ExternFuncRef { + name: "js_throw_eval_syntax_error".to_string(), + param_types: vec![Type::String], + return_type: Type::Any, + }), + args: vec![Expr::String(msg.to_string())], + type_args: vec![], + } +} + +/// Direct eval: check the parsed body against the call site's capabilities. +/// Returns the throw expression to substitute for the eval call on violation. +pub(crate) fn check_direct_eval_super_private( + ctx: &LoweringContext, + stmts: &[ast::Stmt], +) -> Option { + let mut scan = Scan::default(); + for s in stmts { + scan_stmt(&mut scan, s, true); + } + // SuperCall capability: directly inside a derived-class constructor. + let super_call_ok = ctx.in_constructor_class.is_some() && ctx.current_class_is_derived; + // SuperProperty capability: inside any class member or object method. + let super_prop_ok = ctx.current_class.is_some() + || ctx.in_constructor_class.is_some() + || !ctx.object_super_home_stack.is_empty(); + if scan.super_call && !super_call_ok { + return Some(throw_eval_syntax_error_expr( + "'super' keyword unexpected here", + )); + } + if scan.super_prop && !super_prop_ok { + return Some(throw_eval_syntax_error_expr( + "'super' keyword unexpected here", + )); + } + // Class field initializers have no arguments object — `arguments` in a + // direct eval body there is a SyntaxError at the eval call. + if scan.arguments_ref && ctx.in_class_field_init { + return Some(throw_eval_syntax_error_expr( + "'arguments' is not allowed in class field initializer", + )); + } + check_private_refs(&scan, |name| { + ctx.private_scopes + .iter() + .any(|s| s.members.contains_key(name)) + }) +} + +/// Indirect eval: global code — no super of either kind, and private names +/// must be declared inside the eval source itself. +pub(crate) fn check_indirect_eval_super_private(stmts: &[ast::Stmt]) -> Option { + let mut scan = Scan::default(); + for s in stmts { + scan_stmt(&mut scan, s, true); + } + if scan.super_call || scan.super_prop { + return Some(throw_eval_syntax_error_expr( + "'super' keyword unexpected here", + )); + } + // Indirect eval is global code — it has no new.target binding. + if scan.new_target { + return Some(throw_eval_syntax_error_expr( + "new.target expression is not allowed here", + )); + } + check_private_refs(&scan, |_| false) +} + +fn check_private_refs(scan: &Scan, outer_declares: impl Fn(&str) -> bool) -> Option { + for name in &scan.private_refs { + if !scan.private_decls.contains(name) && !outer_declares(name) { + return Some(throw_eval_syntax_error_expr(&format!( + "Private field '{name}' must be declared in an enclosing class" + ))); + } + } + None +} + +/// `transparent` — whether SuperCall/SuperProperty found here belongs to the +/// eval's own context (arrows keep it; function/method bodies clear it). +/// Private-name refs are collected regardless. +fn scan_stmt(scan: &mut Scan, stmt: &ast::Stmt, transparent: bool) { + use ast::Stmt as S; + match stmt { + S::Block(b) => { + for s in &b.stmts { + scan_stmt(scan, s, transparent); + } + } + S::With(w) => { + scan_expr(scan, &w.obj, transparent); + scan_stmt(scan, &w.body, transparent); + } + S::Return(r) => { + if let Some(arg) = &r.arg { + scan_expr(scan, arg, transparent); + } + } + S::Labeled(l) => scan_stmt(scan, &l.body, transparent), + S::If(i) => { + scan_expr(scan, &i.test, transparent); + scan_stmt(scan, &i.cons, transparent); + if let Some(alt) = &i.alt { + scan_stmt(scan, alt, transparent); + } + } + S::Switch(sw) => { + scan_expr(scan, &sw.discriminant, transparent); + for case in &sw.cases { + if let Some(test) = &case.test { + scan_expr(scan, test, transparent); + } + for s in &case.cons { + scan_stmt(scan, s, transparent); + } + } + } + S::Throw(t) => scan_expr(scan, &t.arg, transparent), + S::Try(t) => { + for s in &t.block.stmts { + scan_stmt(scan, s, transparent); + } + if let Some(handler) = &t.handler { + for s in &handler.body.stmts { + scan_stmt(scan, s, transparent); + } + } + if let Some(fin) = &t.finalizer { + for s in &fin.stmts { + scan_stmt(scan, s, transparent); + } + } + } + S::While(w) => { + scan_expr(scan, &w.test, transparent); + scan_stmt(scan, &w.body, transparent); + } + S::DoWhile(w) => { + scan_expr(scan, &w.test, transparent); + scan_stmt(scan, &w.body, transparent); + } + S::For(f) => { + match &f.init { + Some(ast::VarDeclOrExpr::VarDecl(v)) => scan_var_decl(scan, v, transparent), + Some(ast::VarDeclOrExpr::Expr(e)) => scan_expr(scan, e, transparent), + None => {} + } + if let Some(test) = &f.test { + scan_expr(scan, test, transparent); + } + if let Some(update) = &f.update { + scan_expr(scan, update, transparent); + } + scan_stmt(scan, &f.body, transparent); + } + S::ForIn(f) => { + if let ast::ForHead::VarDecl(v) = &f.left { + scan_var_decl(scan, v, transparent); + } + scan_expr(scan, &f.right, transparent); + scan_stmt(scan, &f.body, transparent); + } + S::ForOf(f) => { + if let ast::ForHead::VarDecl(v) = &f.left { + scan_var_decl(scan, v, transparent); + } + scan_expr(scan, &f.right, transparent); + scan_stmt(scan, &f.body, transparent); + } + S::Decl(decl) => scan_decl(scan, decl, transparent), + S::Expr(e) => scan_expr(scan, &e.expr, transparent), + S::Empty(_) | S::Debugger(_) | S::Break(_) | S::Continue(_) => {} + } +} + +fn scan_decl(scan: &mut Scan, decl: &ast::Decl, transparent: bool) { + match decl { + ast::Decl::Class(class_decl) => scan_class(scan, &class_decl.class, transparent), + ast::Decl::Fn(fn_decl) => scan_function(scan, &fn_decl.function), + ast::Decl::Var(v) => scan_var_decl(scan, v, transparent), + ast::Decl::Using(u) => { + for d in &u.decls { + if let Some(init) = &d.init { + scan_expr(scan, init, transparent); + } + } + } + _ => {} + } +} + +fn scan_var_decl(scan: &mut Scan, var: &ast::VarDecl, transparent: bool) { + for d in &var.decls { + if let Some(init) = &d.init { + scan_expr(scan, init, transparent); + } + } +} + +/// Function bodies are opaque to `Contains` for super; private names still +/// collected. +fn scan_function(scan: &mut Scan, func: &ast::Function) { + for p in &func.params { + scan_pat(scan, &p.pat, false); + } + if let Some(body) = &func.body { + for s in &body.stmts { + scan_stmt(scan, s, false); + } + } +} + +fn scan_class(scan: &mut Scan, class: &ast::Class, transparent: bool) { + // Heritage and computed keys evaluate in the outer context. + if let Some(sc) = &class.super_class { + scan_expr(scan, sc, transparent); + } + for member in &class.body { + match member { + ast::ClassMember::Constructor(ctor) => { + if let Some(body) = &ctor.body { + for s in &body.stmts { + scan_stmt(scan, s, false); + } + } + } + ast::ClassMember::Method(m) => { + if let ast::PropName::Computed(c) = &m.key { + scan_expr(scan, &c.expr, transparent); + } + scan_function(scan, &m.function); + } + ast::ClassMember::PrivateMethod(m) => { + scan.private_decls.insert(format!("#{}", m.key.name)); + scan_function(scan, &m.function); + } + ast::ClassMember::ClassProp(p) => { + if let ast::PropName::Computed(c) = &p.key { + scan_expr(scan, &c.expr, transparent); + } + if let Some(value) = &p.value { + scan_expr(scan, value, false); + } + } + ast::ClassMember::PrivateProp(p) => { + scan.private_decls.insert(format!("#{}", p.key.name)); + if let Some(value) = &p.value { + scan_expr(scan, value, false); + } + } + ast::ClassMember::StaticBlock(b) => { + for s in &b.body.stmts { + scan_stmt(scan, s, false); + } + } + _ => {} + } + } +} + +fn scan_pat(scan: &mut Scan, pat: &ast::Pat, transparent: bool) { + match pat { + ast::Pat::Array(arr) => { + for elem in arr.elems.iter().flatten() { + scan_pat(scan, elem, transparent); + } + } + ast::Pat::Object(obj) => { + for p in &obj.props { + match p { + ast::ObjectPatProp::KeyValue(kv) => { + if let ast::PropName::Computed(c) = &kv.key { + scan_expr(scan, &c.expr, transparent); + } + scan_pat(scan, &kv.value, transparent); + } + ast::ObjectPatProp::Assign(a) => { + if let Some(value) = &a.value { + scan_expr(scan, value, transparent); + } + } + ast::ObjectPatProp::Rest(r) => scan_pat(scan, &r.arg, transparent), + } + } + } + ast::Pat::Assign(a) => { + scan_pat(scan, &a.left, transparent); + scan_expr(scan, &a.right, transparent); + } + ast::Pat::Rest(r) => scan_pat(scan, &r.arg, transparent), + ast::Pat::Expr(e) => scan_expr(scan, e, transparent), + ast::Pat::Ident(_) | ast::Pat::Invalid(_) => {} + } +} + +fn scan_expr(scan: &mut Scan, expr: &ast::Expr, transparent: bool) { + use ast::Expr as E; + match expr { + E::SuperProp(sp) => { + if transparent { + scan.super_prop = true; + } + if let ast::SuperProp::Computed(c) = &sp.prop { + scan_expr(scan, &c.expr, transparent); + } + } + E::Call(call) => { + if matches!(call.callee, ast::Callee::Super(_)) && transparent { + scan.super_call = true; + } + if let ast::Callee::Expr(callee) = &call.callee { + scan_expr(scan, callee, transparent); + } + for arg in &call.args { + scan_expr(scan, &arg.expr, transparent); + } + } + E::New(new) => { + scan_expr(scan, &new.callee, transparent); + if let Some(args) = &new.args { + for arg in args { + scan_expr(scan, &arg.expr, transparent); + } + } + } + E::Member(m) => { + scan_expr(scan, &m.obj, transparent); + match &m.prop { + ast::MemberProp::PrivateName(p) => { + scan.private_refs.push(format!("#{}", p.name)); + } + ast::MemberProp::Computed(c) => scan_expr(scan, &c.expr, transparent), + ast::MemberProp::Ident(_) => {} + } + } + E::OptChain(oc) => match oc.base.as_ref() { + ast::OptChainBase::Member(m) => { + scan_expr(scan, &m.obj, transparent); + match &m.prop { + ast::MemberProp::PrivateName(p) => { + scan.private_refs.push(format!("#{}", p.name)); + } + ast::MemberProp::Computed(c) => scan_expr(scan, &c.expr, transparent), + ast::MemberProp::Ident(_) => {} + } + } + ast::OptChainBase::Call(c) => { + scan_expr(scan, &c.callee, transparent); + for arg in &c.args { + scan_expr(scan, &arg.expr, transparent); + } + } + }, + E::Bin(bin) => { + // `#x in obj` — the left operand of `in` may be a private name. + if let E::PrivateName(p) = bin.left.as_ref() { + scan.private_refs.push(format!("#{}", p.name)); + } else { + scan_expr(scan, &bin.left, transparent); + } + scan_expr(scan, &bin.right, transparent); + } + E::PrivateName(p) => { + scan.private_refs.push(format!("#{}", p.name)); + } + E::Ident(id) => { + if transparent && id.sym.as_ref() == "arguments" { + scan.arguments_ref = true; + } + } + E::MetaProp(mp) => { + if transparent && mp.kind == ast::MetaPropKind::NewTarget { + scan.new_target = true; + } + } + E::Unary(u) => scan_expr(scan, &u.arg, transparent), + E::Update(u) => scan_expr(scan, &u.arg, transparent), + E::Assign(a) => { + match &a.left { + ast::AssignTarget::Simple(simple) => match simple { + ast::SimpleAssignTarget::Member(m) => { + scan_expr(scan, &m.obj, transparent); + match &m.prop { + ast::MemberProp::PrivateName(p) => { + scan.private_refs.push(format!("#{}", p.name)); + } + ast::MemberProp::Computed(c) => scan_expr(scan, &c.expr, transparent), + ast::MemberProp::Ident(_) => {} + } + } + ast::SimpleAssignTarget::SuperProp(sp) => { + if transparent { + scan.super_prop = true; + } + if let ast::SuperProp::Computed(c) = &sp.prop { + scan_expr(scan, &c.expr, transparent); + } + } + _ => {} + }, + ast::AssignTarget::Pat(pat) => match pat { + ast::AssignTargetPat::Array(arr) => { + scan_pat(scan, &ast::Pat::Array(arr.clone()), transparent) + } + ast::AssignTargetPat::Object(obj) => { + scan_pat(scan, &ast::Pat::Object(obj.clone()), transparent) + } + ast::AssignTargetPat::Invalid(_) => {} + }, + } + scan_expr(scan, &a.right, transparent); + } + E::Cond(c) => { + scan_expr(scan, &c.test, transparent); + scan_expr(scan, &c.cons, transparent); + scan_expr(scan, &c.alt, transparent); + } + E::Seq(s) => { + for e in &s.exprs { + scan_expr(scan, e, transparent); + } + } + E::Paren(p) => scan_expr(scan, &p.expr, transparent), + E::Array(arr) => { + for elem in arr.elems.iter().flatten() { + scan_expr(scan, &elem.expr, transparent); + } + } + E::Object(obj) => { + for prop in &obj.props { + match prop { + ast::PropOrSpread::Spread(s) => scan_expr(scan, &s.expr, transparent), + ast::PropOrSpread::Prop(p) => match p.as_ref() { + ast::Prop::KeyValue(kv) => { + if let ast::PropName::Computed(c) = &kv.key { + scan_expr(scan, &c.expr, transparent); + } + scan_expr(scan, &kv.value, transparent); + } + ast::Prop::Assign(a) => scan_expr(scan, &a.value, transparent), + // Object methods/accessors have their own home + // object — opaque for super, still scanned for + // private names. + ast::Prop::Getter(g) => { + if let ast::PropName::Computed(c) = &g.key { + scan_expr(scan, &c.expr, transparent); + } + if let Some(body) = &g.body { + for s in &body.stmts { + scan_stmt(scan, s, false); + } + } + } + ast::Prop::Setter(s) => { + if let ast::PropName::Computed(c) = &s.key { + scan_expr(scan, &c.expr, transparent); + } + if let Some(body) = &s.body { + for st in &body.stmts { + scan_stmt(scan, st, false); + } + } + } + ast::Prop::Method(m) => { + if let ast::PropName::Computed(c) = &m.key { + scan_expr(scan, &c.expr, transparent); + } + scan_function(scan, &m.function); + } + ast::Prop::Shorthand(_) => {} + }, + } + } + } + E::Fn(fn_expr) => scan_function(scan, &fn_expr.function), + E::Arrow(arrow) => { + for p in &arrow.params { + scan_pat(scan, p, transparent); + } + match arrow.body.as_ref() { + ast::BlockStmtOrExpr::BlockStmt(b) => { + for s in &b.stmts { + scan_stmt(scan, s, transparent); + } + } + ast::BlockStmtOrExpr::Expr(e) => scan_expr(scan, e, transparent), + } + } + E::Class(class_expr) => scan_class(scan, &class_expr.class, transparent), + E::Tpl(tpl) => { + for e in &tpl.exprs { + scan_expr(scan, e, transparent); + } + } + E::TaggedTpl(tt) => { + scan_expr(scan, &tt.tag, transparent); + for e in &tt.tpl.exprs { + scan_expr(scan, e, transparent); + } + } + E::Yield(y) => { + if let Some(arg) = &y.arg { + scan_expr(scan, arg, transparent); + } + } + E::Await(a) => scan_expr(scan, &a.arg, transparent), + E::TsAs(t) => scan_expr(scan, &t.expr, transparent), + E::TsNonNull(t) => scan_expr(scan, &t.expr, transparent), + E::TsTypeAssertion(t) => scan_expr(scan, &t.expr, transparent), + E::TsConstAssertion(t) => scan_expr(scan, &t.expr, transparent), + E::TsSatisfies(t) => scan_expr(scan, &t.expr, transparent), + _ => {} + } +} diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index 0b6559ce93..80beb8c5a5 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -462,6 +462,11 @@ pub(crate) fn lower_fn_expr(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> fn_expr.function.is_async, ); let scope_mark = ctx.enter_scope(); + // A plain function has its own `arguments` object, so a direct `eval` + // inside its body may reference `arguments` even when the function sits + // 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; // Track which locals exist before entering the closure scope let outer_locals: Vec<(String, LocalId)> = ctx @@ -774,6 +779,7 @@ pub(crate) fn lower_fn_expr(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> ctx.exit_strict_mode(); ctx.exit_scope(scope_mark); + ctx.in_class_field_init = saved_field_init; let (captures, mutable_captures) = compute_closure_captures(ctx, &body, &outer_locals, ¶ms); diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 8c69d1ba28..84adb76d41 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -431,6 +431,17 @@ pub struct LoweringContext { /// Used to resolve `new.target` to a placeholder object whose `.name` /// returns the class name. None outside any constructor. pub(crate) in_constructor_class: Option, + /// True while lowering inside a class declaration/expression whose + /// heritage clause is present (`class C extends ... {}`). Combined with + /// `in_constructor_class` to decide whether a direct `eval` body may + /// contain `super()` (spec: PerformEval early errors). Saved/restored + /// alongside `current_class` at both class lowering entry points. + pub(crate) current_class_is_derived: bool, + /// True while lowering a class field initializer expression. A direct + /// `eval` body containing `arguments` in this context is a SyntaxError at + /// the eval call (field initializers have no arguments object). Set in + /// `lower_class_prop` / `lower_private_prop`. + pub(crate) in_class_field_init: bool, /// Issue #562 — set to the parent class identifier (e.g. `"WritableStream"`, /// `"ReadableStream"`, `"TransformStream"`, or any ident from `class X /// extends Y`) when lowering inside a class declaration. Used by the diff --git a/crates/perry-hir/src/lower/mod.rs b/crates/perry-hir/src/lower/mod.rs index 3fb1270e40..edd3896a6b 100644 --- a/crates/perry-hir/src/lower/mod.rs +++ b/crates/perry-hir/src/lower/mod.rs @@ -63,6 +63,7 @@ mod pre_scan; pub(crate) use pre_scan::*; mod closure_analysis; mod const_fold_fn; +mod eval_super_scan; mod fn_ctor_env; pub(crate) use closure_analysis::*; mod decorators; diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 208a18b67e..2fa0ef5c62 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -151,6 +151,8 @@ pub fn lower_class_decl( // Set current class for arrow function `this` capture tracking let old_class = ctx.current_class.take(); ctx.current_class = Some(name.clone()); + let old_is_derived = ctx.current_class_is_derived; + ctx.current_class_is_derived = class_decl.class.super_class.is_some(); // Push the private-name scope for this class body so `obj.#name` accesses // brand-check against the declaring class and reject illegal read/write @@ -1014,6 +1016,7 @@ pub fn lower_class_decl( // Restore previous current_class ctx.current_class = old_class; + ctx.current_class_is_derived = old_is_derived; ctx.pop_private_scope(); // Issue #562: restore the prior super-ident slot. ctx.current_class_super_ident = old_super_ident; @@ -1103,6 +1106,8 @@ pub fn lower_class_from_ast( let old_class = ctx.current_class.take(); ctx.current_class = Some(name.to_string()); + let old_is_derived = ctx.current_class_is_derived; + ctx.current_class_is_derived = class.super_class.is_some(); // Private-name scope for this class-expression body (see lower_class_decl). ctx.push_private_scope(super::build_private_scope(class, name)); @@ -1409,6 +1414,7 @@ pub fn lower_class_from_ast( ctx.register_class_native_extends(name.to_string(), module.clone(), class.clone()); } ctx.current_class = old_class; + ctx.current_class_is_derived = old_is_derived; ctx.pop_private_scope(); // Issue #562: restore prior super-ident slot. ctx.current_class_super_ident = old_super_ident; diff --git a/crates/perry-hir/src/lower_decl/class_members.rs b/crates/perry-hir/src/lower_decl/class_members.rs index db6ceb4b4f..8a3cc2029e 100644 --- a/crates/perry-hir/src/lower_decl/class_members.rs +++ b/crates/perry-hir/src/lower_decl/class_members.rs @@ -519,27 +519,31 @@ pub fn lower_class_method_with_name( destructuring_params.push((param_id, inner_pat.clone())); } } - for (param, pat) in params.iter_mut().zip(default_param_pats.iter()) { - param.default = get_param_default(ctx, pat)?; - } - - // #677: synthesize `arguments` if the method body references it. + // #677: synthesize `arguments` if the method body — or any parameter + // DEFAULT expression (`method(x = arguments[2]) {}`) — references it. + // Appended BEFORE the defaults are lowered below so `arguments` inside a + // default resolves to the synthetic local instead of an unknown global. let user_has_arguments_param = method .function .params .iter() .any(|p| get_pat_name(&p.pat).ok().as_deref() == Some("arguments")); let needs_arguments_synth = !user_has_arguments_param - && method + && (method .function .body .as_ref() .map(|b| body_uses_arguments(&b.stmts)) - .unwrap_or(false); + .unwrap_or(false) + || params_use_arguments(&method.function.params)); if needs_arguments_synth { append_synthetic_arguments_param(ctx, &mut params, true, false, true, Vec::new()); } + for (param, pat) in params.iter_mut().zip(default_param_pats.iter()) { + param.default = get_param_default(ctx, pat)?; + } + // Extract return type (with context). Phase 4: when the method has no // explicit annotation, fall back to body-based inference after body // lowering so parameters and locals are visible to `infer_type_from_expr`. @@ -898,12 +902,14 @@ pub fn lower_class_prop(ctx: &mut LoweringContext, prop: &ast::ClassProp) -> Res .unwrap_or(Type::Any), }; - // Lower initializer expression if present - let init = prop - .value - .as_ref() - .map(|e| lower_expr(ctx, e)) - .transpose()?; + // Lower initializer expression if present. Mark the field-initializer + // context so a direct `eval` in the initializer rejects `arguments` + // (PerformEval early error — field initializers have no arguments object). + let saved_field_init = ctx.in_class_field_init; + ctx.in_class_field_init = true; + let init = prop.value.as_ref().map(|e| lower_expr(ctx, e)).transpose(); + ctx.in_class_field_init = saved_field_init; + let init = init?; Ok(ClassField { name, diff --git a/crates/perry-hir/src/lower_decl/fn_decl.rs b/crates/perry-hir/src/lower_decl/fn_decl.rs index b4b6599114..07d7e35a1d 100644 --- a/crates/perry-hir/src/lower_decl/fn_decl.rs +++ b/crates/perry-hir/src/lower_decl/fn_decl.rs @@ -78,12 +78,13 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result ctx.enter_strict_mode(strict); let simple_parameters = params_are_simple_arguments_list(&fn_decl.function.params); let needs_arguments_synth = !user_has_arguments_param - && fn_decl + && (fn_decl .function .body .as_ref() .map(|b| body_uses_arguments(&b.stmts)) - .unwrap_or(false); + .unwrap_or(false) + || params_use_arguments(&fn_decl.function.params)); // Lower parameters with type extraction (using context for type param resolution) // @@ -124,11 +125,9 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result destructuring_params.push((param_id, inner_pat.clone())); } } - for (param, pat) in params.iter_mut().zip(default_param_pats.iter()) { - param.default = get_param_default(ctx, pat)?; - } - - // If the body references `arguments`, append the hidden raw-arguments input. + // If the body (or a parameter default) references `arguments`, append the + // hidden raw-arguments input — BEFORE the defaults are lowered below so + // `arguments` inside a default expression resolves to the synthetic local. if needs_arguments_synth { let mapped = !strict && simple_parameters; let mapped_parameter_ids = if mapped { @@ -146,6 +145,10 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result ); } + for (param, pat) in params.iter_mut().zip(default_param_pats.iter()) { + param.default = get_param_default(ctx, pat)?; + } + // Register parameters with known native types as native instances for param in ¶ms { if let Type::Named(type_name) = ¶m.ty { diff --git a/crates/perry-hir/src/lower_decl/helpers.rs b/crates/perry-hir/src/lower_decl/helpers.rs index 3e0b9f019d..ae53e4e8f5 100644 --- a/crates/perry-hir/src/lower_decl/helpers.rs +++ b/crates/perry-hir/src/lower_decl/helpers.rs @@ -569,6 +569,31 @@ pub fn mapped_argument_parameter_ids(params: &[Param]) -> Vec<(u32, LocalId)> { mapped } +/// True when any parameter's DEFAULT expression references `arguments` +/// (`method(x = arguments[2], y) {}`). Parameter defaults evaluate in the +/// function's own scope, so they see the same arguments object as the body — +/// the synthetic-arguments check must include them (test262 +/// class/params-dflt-meth-ref-arguments). +pub fn params_use_arguments(params: &[ast::Param]) -> bool { + params.iter().any(|p| param_pat_uses_arguments(&p.pat)) +} + +fn param_pat_uses_arguments(pat: &ast::Pat) -> bool { + match pat { + ast::Pat::Assign(a) => expr_uses_arguments(&a.right) || param_pat_uses_arguments(&a.left), + ast::Pat::Array(arr) => arr.elems.iter().flatten().any(param_pat_uses_arguments), + ast::Pat::Object(obj) => obj.props.iter().any(|p| match p { + ast::ObjectPatProp::Assign(a) => { + a.value.as_deref().map(expr_uses_arguments).unwrap_or(false) + } + ast::ObjectPatProp::KeyValue(kv) => param_pat_uses_arguments(&kv.value), + ast::ObjectPatProp::Rest(r) => param_pat_uses_arguments(&r.arg), + }), + ast::Pat::Rest(r) => param_pat_uses_arguments(&r.arg), + _ => false, + } +} + /// Synthesize a hidden raw-arguments parameter. Call after lowering the user's /// parameters, before lowering the body, when the body references `arguments` /// and the user hasn't already bound it explicitly. diff --git a/crates/perry-hir/src/lower_decl/mod.rs b/crates/perry-hir/src/lower_decl/mod.rs index 416fe00a2e..8b7359c42a 100644 --- a/crates/perry-hir/src/lower_decl/mod.rs +++ b/crates/perry-hir/src/lower_decl/mod.rs @@ -49,7 +49,7 @@ pub(crate) use helpers::{ append_synthetic_arguments_param, body_has_use_strict, body_uses_arguments, build_default_param_stmts, collect_let_decls_in_stmt, init_is_webassembly_instantiate, is_inspect_custom_key, is_symbol_iterator_key, mapped_argument_parameter_ids, - params_are_simple_arguments_list, symbol_well_known_key, + params_are_simple_arguments_list, params_use_arguments, symbol_well_known_key, }; pub(crate) use interface_decl::lower_interface_decl; pub(crate) use private_members::{ diff --git a/crates/perry-hir/src/lower_decl/private_members.rs b/crates/perry-hir/src/lower_decl/private_members.rs index 2755b361f1..9b77087618 100644 --- a/crates/perry-hir/src/lower_decl/private_members.rs +++ b/crates/perry-hir/src/lower_decl/private_members.rs @@ -366,12 +366,13 @@ pub fn lower_private_prop( .unwrap_or(Type::Any), }; - // Lower initializer expression if present - let init = prop - .value - .as_ref() - .map(|e| lower_expr(ctx, e)) - .transpose()?; + // Lower initializer expression if present — field-initializer context for + // the direct-eval `arguments` early error (see `lower_class_prop`). + let saved_field_init = ctx.in_class_field_init; + ctx.in_class_field_init = true; + let init = prop.value.as_ref().map(|e| lower_expr(ctx, e)).transpose(); + ctx.in_class_field_init = saved_field_init; + let init = init?; Ok(ClassField { name, diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 4c8e6da182..b756ba5a92 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -658,6 +658,20 @@ pub extern "C" fn js_throw_strict_eval_arguments_syntax_error() -> f64 { crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } +/// PerformEval early errors (super outside its context / undeclared private +/// name in eval code) — a SyntaxError thrown when the eval call evaluates. +#[no_mangle] +pub extern "C" fn js_throw_eval_syntax_error(message: f64) -> f64 { + let message = value_to_lossy_string(message); + let msg = js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = js_syntaxerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + +// #1561-style force-keep: only generated IR calls this. +#[used] +static KEEP_JS_THROW_EVAL_SYNTAX_ERROR: extern "C" fn(f64) -> f64 = js_throw_eval_syntax_error; + #[no_mangle] pub extern "C" fn js_throw_restricted_function_property_assignment() -> f64 { crate::fs::validate::throw_type_error_with_code(