diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index 4d57f008e..ff350dc2a 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -449,7 +449,115 @@ pub(super) fn lower_arrow(ctx: &mut LoweringContext, arrow: &ast::ArrowExpr) -> }) } +/// #5126: a named function expression binds its own name as a read-only +/// local *inside its own body* (the FunctionExpression name scope per +/// spec) — `const fact = function f(n){ return n<=1?1:n*f(n-1); }` must +/// see `f` from within the body even though the outer binding is `fact`. +/// +/// We model this without a new HIR node by wrapping the (otherwise +/// anonymous) function in an immediately-invoked arrow that binds the +/// name to the function value: +/// (() => { let f = ; return f; })() +/// The `let f = ` shape is exactly the +/// self-recursive-`const` pattern that `collect_boxed_vars` already +/// boxes (step 5: a `Stmt::Let` whose `Closure` init references the +/// Let's own id). So `f` resolves to the function through its heap box, +/// reusing the proven recursion machinery. pub(crate) fn lower_fn_expr(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Result { + // A named function expression with a non-empty ident may reference its + // own name from within its body. Lower it through the self-binding path, + // which keeps the IIFE wrapper only when the body actually captures the + // name (so plain `function f(){...}` and the synthetic `Function(...)` + // body stay a bare `Closure`). + if let Some(ident) = &fn_expr.ident { + let own_name = ident.sym.to_string(); + if !own_name.is_empty() { + return lower_named_fn_expr(ctx, fn_expr, own_name); + } + } + lower_fn_expr_anon(ctx, fn_expr) +} + +/// Lower a *named* function expression, binding its own name inside the +/// body. We put the name in scope, lower the function anonymously, and +/// inspect whether the lowered closure actually captured the name. If it +/// didn't (the common case — no recursive self-reference), we discard the +/// scaffolding and return the bare closure unchanged. If it did, we wrap +/// it in an immediately-invoked arrow that binds the name to the function +/// value: +/// (() => { let f = ; return f; })() +/// The `let f = ` shape is exactly the +/// self-recursive-`const` pattern that `collect_boxed_vars` already boxes +/// (a `Stmt::Let` whose `Closure` init references the Let's own id), so the +/// name resolves to the function through its heap box — reusing the proven +/// recursion machinery without a dedicated HIR node. +fn lower_named_fn_expr( + ctx: &mut LoweringContext, + fn_expr: &ast::FnExpr, + own_name: String, +) -> Result { + // Wrapper scope: holds just the self-binding local. Collect the + // enclosing scope's locals first so they (not the self-binding) are + // what the wrapper itself captures and threads through to the inner + // function. + let wrapper_scope = ctx.enter_scope(); + let outer_locals: Vec<(String, LocalId)> = ctx + .locals + .iter() + .map(|(name, id, _)| (name.clone(), *id)) + .collect(); + let self_id = ctx.define_local(own_name.clone(), Type::Any); + + // Lower the function itself as an anonymous closure. With `self_id` + // already in scope, any reference to the name inside the body resolves + // to it (correctly shadowing any outer binding of the same name) and is + // captured. + let inner = lower_fn_expr_anon(ctx, fn_expr)?; + + let self_referenced = + matches!(&inner, Expr::Closure { captures, .. } if captures.contains(&self_id)); + if !self_referenced { + // No recursive self-reference — drop the scaffolding. + ctx.exit_scope(wrapper_scope); + return Ok(inner); + } + + let wrapper_func_id = ctx.fresh_func(); + let body = vec![ + Stmt::Let { + id: self_id, + name: own_name, + ty: Type::Any, + mutable: false, + init: Some(inner), + }, + Stmt::Return(Some(Expr::LocalGet(self_id))), + ]; + let (captures, mutable_captures) = compute_closure_captures(ctx, &body, &outer_locals, &[]); + ctx.exit_scope(wrapper_scope); + + Ok(Expr::Call { + callee: Box::new(Expr::Closure { + func_id: wrapper_func_id, + params: Vec::new(), + return_type: Type::Any, + body, + captures, + mutable_captures, + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: true, + is_async: false, + is_generator: false, + is_strict: ctx.current_strict, + }), + args: Vec::new(), + type_args: Vec::new(), + }) +} + +fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Result { // Lower function expression to a closure (similar to arrow but // without `this` capture — function expressions have their own // `this` binding determined by how they're called). diff --git a/crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs b/crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs new file mode 100644 index 000000000..bb3756e59 --- /dev/null +++ b/crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs @@ -0,0 +1,100 @@ +//! Regression test for #5126: a named function expression could not +//! reference its own name from inside its body — a recursive self-call threw +//! `ReferenceError: f is not defined`. +//! +//! Per spec, the identifier of a `NamedFunctionExpression` is bound (read-only) +//! within the function's own body, independent of the binding it is later +//! assigned to: +//! +//! ```ts +//! const fact = function f(n) { return n <= 1 ? 1 : n * f(n - 1); }; +//! fact(5); // 120 — `f` resolves to the function itself +//! ``` +//! +//! Fix: when a named function expression's body actually captures its own +//! name, lower it through an immediately-invoked arrow that binds the name to +//! the function value (`(() => { let f = ; return f; })()`). That `let f = +//! ` shape is exactly the self-recursive-`const` +//! pattern codegen already boxes, so `f` resolves through its heap box. A +//! named function expression that does NOT reference its own name keeps its +//! bare closure (and its `.name`). + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn compile_and_run(dir: &std::path::Path, source: &str) -> String { + let entry = dir.join("main.ts"); + let output = dir.join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .current_dir(dir) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed (pre-fix: 'ReferenceError: f is not defined')\n\ + status: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +/// The canonical repro from the issue plus a second self-recursive case and a +/// non-self-referential named function expression (whose `.name` must survive). +#[test] +fn named_fn_expr_can_reference_its_own_name() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + r#" +const fact = function f(n: number): number { return n <= 1 ? 1 : n * f(n - 1); }; +console.log(fact(5)); +const fib = function fb(n: number): number { return n < 2 ? n : fb(n - 1) + fb(n - 2); }; +console.log(fib(10)); +const dbl = function named(x: number) { return x * 2; }; +console.log(dbl(21), dbl.name); +"#, + ); + assert_eq!(stdout, "120\n55\n42 named\n"); +} + +/// The self-binding must shadow an outer binding of the same name, capture +/// enclosing-scope variables, and work for generators and IIFEs. +#[test] +fn named_fn_expr_self_ref_edge_cases() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + r#" +const mul = 3; +const step = function step(n: number): number { return n <= 0 ? 0 : mul + step(n - 1); }; +console.log(step(4)); +const gen = function* g(n: number): Generator { if (n > 0) { yield n; yield* g(n - 1); } }; +console.log([...gen(3)].join(",")); +console.log((function fac(n: number): number { return n <= 1 ? 1 : n * fac(n - 1); })(6)); +"#, + ); + assert_eq!(stdout, "12\n3,2,1\n720\n"); +}