Skip to content

fix(hir): closes #461 — pre-register local for const f = (arrow) decls so nested closures resolve self-references (v0.5.600) - #524

Merged
proggeramlug merged 1 commit into
mainfrom
fix/461-effect-link-errors
May 6, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
fix/461-effect-link-errors

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Root cause

Effect's internal/stream.ts has both:

  1. import * as pull from "./stream/pull.js" (namespace import at module top)
  2. const pull = (state, ...) => { … pull(...) … } (local rebinding inside three exported functions)

Without pre-registration, the inner closures' pull(...) reference saw lookup_local("pull") == None → lookup_imported_func("pull") == Some("pull") → HIR lowered as Expr::ExternFuncRef { name: "pull" } → lower_call.rs::ExternFuncRef's "Native library functions" fallback emitted call double @pull(...) → linker: Undefined symbol: _pull.

Test plan

  • Multi-file repro (import * as pull + const pull rebind): returns 101 / 101 matching Node (was: _pull undefined at link)
  • Direct self-recursion: const fac = (n) => n < 2 ? 1 : n * fac(n-1) returns 120 / 3628800
  • Function-expression init: const fib = function(n) { ... } returns 55
  • Self-reference through nested arrow returning recursive helper: returns 15
  • Effect's bun add effect && perry compile test_minimal.ts: _pull no longer appears in undefined symbols (remaining errors are unrelated cross-module references — _perry_fn_…_{Cron,Duration,Graph}_ts__* — separate issue, not blocking perry-codegen: residual link errors on Effect — bare _pull + class-as-function references (Union/Emit/Order) #461)
  • Parity sweep: 209 pass / 3 known fails / 1 known compile-fail / 13 skipped (98.5%, no regressions)
  • Closure-focused tests (test_edge_closures, test_gap_closures, test_returning_closures) all PASS

Notes

The fix should also benefit any pre-bundled npm package that uses the var X = (...) => …; export { X }; pattern where X is recursive AND a same-named module is namespace-imported elsewhere — the Effect surface has dozens of such shapes.

…cls so nested closures resolve self-references (v0.5.600)

A `const f = (...) => ...` arrow declaration inside a function body was
not pre-registered as a local before its initializer was lowered, so any
nested closure inside that initializer that referenced `f` couldn't find
it via `lookup_local` and fell through to `lookup_imported_func`. When a
namespace import shared the same name (Effect's `import * as pull from
"./stream/pull.js"` + `const pull = (state) => { ... pull(...) ... }`),
the inner reference lowered to `ExternFuncRef { name: "pull" }` and
codegen's "Native library functions" fallback emitted a direct LLVM call
to the bare symbol `@pull`, surfacing as `_pull` undefined at link time.

Function declarations already pre-register their local id before lowering
the body. This commit applies the same treatment to `const f = arrow` /
`const f = function() {}` initializers in `lower_var_decl_with_destructuring`'s
`Pat::Ident` arm. Gated on function-expr init only, so `const x = x + 1`
keeps its (correct) TDZ-violation behavior.

Parity sweep: 209 pass / 3 known fails / 1 known compile-fail / 13
skipped — no regressions.
@proggeramlug
proggeramlug merged commit 1f45934 into main May 6, 2026
5 of 10 checks passed
@proggeramlug
proggeramlug deleted the fix/461-effect-link-errors branch May 6, 2026 12:51
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.

perry-codegen: residual link errors on Effect — bare _pull + class-as-function references (Union/Emit/Order)

1 participant