Skip to content

fix(hir): bind named function expression's own name in its body (#5126)#5156

Merged
proggeramlug merged 2 commits into
mainfrom
fix/5126-named-fn-expr-self-ref
Jun 15, 2026
Merged

fix(hir): bind named function expression's own name in its body (#5126)#5156
proggeramlug merged 2 commits into
mainfrom
fix/5126-named-fn-expr-self-ref

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #5126.

Problem

A named function expression could not reference its own name from inside its body, so a recursive self-call threw ReferenceError:

const fact = function f(n: number): number { return n <= 1 ? 1 : n * f(n - 1); };
console.log(fact(5)); // ReferenceError: f is not defined  (Node prints 120)

Per spec, a NamedFunctionExpression's identifier is bound (read-only) within the function's own body, independent of the binding it is later assigned to. Perry dropped that binding — recursion only worked when the outer binding name happened to match the inner name (so the outer capture resolved it).

Fix

In lower_fn_expr (crates/perry-hir/src/lower/expr_function.rs), a named function expression is now lowered with its own name in scope, then we inspect whether the lowered closure actually captured that name:

  • Captured (genuine recursive self-reference) → wrap the function in an immediately-invoked arrow that binds the name to the function value:
    (() => { let f = <function expr>; return f; })()
    The let f = <closure referencing 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 f resolves to the function through its heap box — reusing the proven recursion machinery with no new HIR node.
  • Not captured (the common case, including the synthetic Function(...) constructor body) → the scaffolding is discarded and the bare Closure is returned unchanged, preserving .name and HIR shape.

Detection is precise (based on the real capture analysis, not a text/AST heuristic), and the self-name correctly shadows any outer binding of the same name per spec.

Verification

  • New repro matches Node exactly: 120, 55 (fib), generator self-yield*, IIFE self-call 720, outer-variable capture, and .name preserved for non-self-referential named expressions.
  • New regression test: crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs (2 tests, both green).
  • cargo test -p perry-hir fully green (172 tests), including the Function(...)-constructor HIR-shape test that guards against over-wrapping.

No changelog/version bump per maintainer's release-at-merge workflow.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Named function expressions now correctly bind their own name within the function body, allowing self-recursive calls without ReferenceError. This also improves consistency of behavior when the name is shadowed or when the function interacts with captured variables.
  • Tests

    • Added regression tests covering self-reference, shadowing, captured outer variables, and generator recursion scenarios.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b7b91d6-29df-4aea-8574-57295beeecaf

📥 Commits

Reviewing files that changed from the base of the PR and between f13a978 and 1a33193.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs

📝 Walkthrough

Walkthrough

lower_fn_expr in the HIR lowering layer now dispatches named function expressions to a new lower_named_fn_expr helper, which inserts a self-binding local into a wrapper scope, lowers the closure anonymously, and — if capture is detected — wraps it in an immediately-invoked arrow closure binding the self name. A new regression test file verifies recursive self-reference, shadowing, outer captures, generator recursion, and IIFE cases.

Changes

Named function expression self-binding fix

Layer / File(s) Summary
HIR lowering: named fn expr self-binding
crates/perry-hir/src/lower/expr_function.rs
lower_fn_expr gains a branch that routes named function expressions to a new lower_named_fn_expr helper. That helper creates a wrapper scope with a self-binding local, lowers the closure via lower_fn_expr_anon, and either returns the bare closure (no capture) or an IIFE-wrapped Expr::Call that binds let <name> = <closure>; return <name>. The prior body is extracted into lower_fn_expr_anon.
Regression tests
crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs
Adds a compile_and_run harness and two #[test] cases covering self-recursive calls, non-self-referential .name preservation, self-shadowing, captured outer variables, recursive generator self-reference, and recursive named IIFE usage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A function once lost its own name in the night,
Called itself endlessly — but got no reply!
Now a wrapper sneaks in, binds the self with a bow,
let f = <closure>; return f — what a show!
The rabbit hops happy, the ReferenceError is gone~ ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(hir): bind named function expression's own name in its body' clearly and specifically describes the main change—implementing proper scoping for named function expression self-references.
Description check ✅ Passed The PR description comprehensively covers the problem statement, implementation details, and verification steps, though some optional template checkboxes are unchecked.
Linked Issues check ✅ Passed The PR directly addresses issue #5126 by implementing self-name binding in named function expressions, enabling recursive self-references and matching JavaScript spec behavior and Node.js output.
Out of Scope Changes check ✅ Passed All changes—HIR lowering logic, closure capture analysis, and regression tests—are directly scoped to fixing the named function expression self-reference bug.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/5126-named-fn-expr-self-ref

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs (1)

83-100: 💤 Low value

Consider adding an explicit outer-binding shadowing test case.

The comment mentions "self-shadowing" but the current step case doesn't have a separate outer binding with the same name to shadow. While the implementation handles this correctly, an explicit test would strengthen confidence:

const f = "outer";
const fn = function f() { return typeof f; };
console.log(fn()); // should print "function", not "string"

This is optional since the core self-reference behavior is well-tested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs` around lines 83 -
100, The test function named_fn_expr_self_ref_edge_cases has a comment
mentioning that "the self-binding must shadow an outer binding of the same
name," but the current test cases do not explicitly demonstrate this shadowing
behavior. Add a test case to the code string that creates an outer binding with
a given name (like a string or number) and then defines a named function
expression with the same name, verifying that references to that name inside the
function resolve to the function itself rather than the outer binding. This
strengthens the test coverage by explicitly validating the shadowing behavior
mentioned in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs`:
- Around line 83-100: The test function named_fn_expr_self_ref_edge_cases has a
comment mentioning that "the self-binding must shadow an outer binding of the
same name," but the current test cases do not explicitly demonstrate this
shadowing behavior. Add a test case to the code string that creates an outer
binding with a given name (like a string or number) and then defines a named
function expression with the same name, verifying that references to that name
inside the function resolve to the function itself rather than the outer
binding. This strengthens the test coverage by explicitly validating the
shadowing behavior mentioned in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a2e21be-2de9-49d4-8269-40f1fc2caa65

📥 Commits

Reviewing files that changed from the base of the PR and between c1da78e and f13a978.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry/tests/issue_5126_named_fn_expr_self_ref.rs

Ralph Küpper added 2 commits June 15, 2026 07:37
A named function expression could not reference its own name from inside
its body, so a recursive self-call threw `ReferenceError: f is not
defined`. Per spec, a NamedFunctionExpression's identifier is bound
(read-only) within the function's own body, independent of the binding it
is later assigned to.

Fix: in `lower_fn_expr`, lower a named function expression with the name
in scope and check whether the lowered closure actually captured it. When
it did (a genuine recursive self-reference), wrap the function in an
immediately-invoked arrow that binds the name to the function value:

    (() => { let f = <function expr>; return f; })()

The `let f = <closure referencing f>` shape is exactly the
self-recursive-const pattern `collect_boxed_vars` already boxes, so the
name resolves to the function through its heap box. When the body does not
reference its own name (the common case, including the synthetic
`Function(...)` body), the scaffolding is discarded and the bare closure
is returned unchanged so `.name` and HIR shape are preserved.
@proggeramlug proggeramlug force-pushed the fix/5126-named-fn-expr-self-ref branch from f13a978 to 1a33193 Compare June 15, 2026 05:38
@proggeramlug proggeramlug merged commit 7a1bdbb into main Jun 15, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/5126-named-fn-expr-self-ref branch June 15, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Named function expression cannot reference its own name (ReferenceError in recursive self-call)

1 participant