Skip to content

fix(hir): anonymous export default function () {} — emit __default symbol#885

Merged
proggeramlug merged 2 commits into
mainfrom
fix/anon-default-fn-export
May 16, 2026
Merged

fix(hir): anonymous export default function () {} — emit __default symbol#885
proggeramlug merged 2 commits into
mainfrom
fix/anon-default-fn-export

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • HIR lowerer dropped the body of export default function () { ... } (no name binding) entirely, so codegen never emitted perry_fn_<src>__default and consumers link-failed with Undefined symbols: _perry_fn_<src>__default. The __perry_wrap_perry_fn_<src>__default rename wrapper from uuid (v4) fails to compile: default export name not wired up under compilePackages #837 also had nothing to point at.
  • Fix: when fn_expr.ident == None and the function has a body, synthesize an ast::FnDecl with ident default and run it through the same lower-and-export path that ExportDecl::Fn uses (lower_fn_declis_exportedfunc_defaults + exported_functions + Export::Named { local: "default", exported: "default" }). The HIR function name default flows through sanitize() unchanged, so the LLVM symbol is perry_fn_<src>__default — what consumers ask for. Since f.name == exported_name, the alias-wrapper loop is a no-op and the undefined-stub fallback at codegen.rs:2310 skips emission because the real symbol exists.
  • Scope-narrow: only the fn_expr.ident == None branch changes. Named-default (export default function foo() {}) has a separate bug (body also dropped) that is left for a follow-up per the issue directive. Closes the remaining-1-symbol case from zod fails to compile under perry.compilePackages: cross-module class re-exports unresolved #836's closeout.

Test plan

  • Regression test added: test-files/test_issue_anonymous_default_export.ts + test-files/test_issue_anonymous_default_export_pkg/{producer,consumer}.ts (producer exports an anonymous default fn returning 42; consumer imports and invokes it; expected output 42).
  • Output matches node --experimental-strip-types byte-for-byte (42).
  • Zod link error from zod fails to compile under perry.compilePackages: cross-module class re-exports unresolved #836's closeout is gone — canonical repro (perry.compilePackages: ["zod"], import { z } from "zod"; console.log(z.object({a:z.string()}).parse({a:'hi'}).a);) compiles + links successfully (runtime divergence remains, separate).
  • Existing zod fails to compile under perry.compilePackages: cross-module class re-exports unresolved #836 regression test (test_issue_836_zod_class_reexports.ts) still passes.
  • cargo build --release -p perry-runtime -p perry-stdlib -p perry clean.
  • cargo test --release -p perry-hir -p perry-codegen clean.
  • cargo test --release --workspace (with the usual UI-crate excludes) exits 0.

Refs #793, #836, #837.

…` symbol

Pre-fix `export default function () { ... }` (no name binding) was
dropped entirely by the HIR lowerer in `ExportDefaultDecl::DefaultDecl::Fn`
when `fn_expr.ident == None`. No HIR Function was created, codegen never
emitted `perry_fn_<src>__default`, and any consumer link-failed with
`Undefined symbols: _perry_fn_<src>__default`. The
`__perry_wrap_perry_fn_<src>__default` rename wrapper from #837 had
nothing to point at either. This blocked zod (`v4/locales/en.ts`) and
vitest under `perry.compilePackages` — the remaining-1-symbol case from
the #836 closeout.

When `fn_expr.ident == None` and the function has a body, synthesize an
`ast::FnDecl` with ident `default`, then run it through the same flow
that `ExportDecl::Fn` uses: lower the body via `lower_fn_decl`, flip
`is_exported = true`, register defaults and `exported_functions`, push
`Export::Named { local: "default", exported: "default" }`. The HIR
function name is `default`, so the LLVM symbol is
`perry_fn_<src>__default` — exactly what consumers ask for. Since
`f.name == exported_name`, the alias-wrapper loop is a no-op and the
undefined-stub fallback at `codegen.rs:2310` skips emission because the
real symbol exists.

Scope-narrow: this only touches the `fn_expr.ident == None` branch.
Named-default (`export default function foo() {}`) has its own separate
bug — function body also dropped — that's left untouched per the issue's
scope-narrow directive.

Regression test: `test-files/test_issue_anonymous_default_export.ts` +
`test-files/test_issue_anonymous_default_export_pkg/{producer,consumer}.ts`.
Matches node `--experimental-strip-types` byte-for-byte. Zod link-error
from #836's closeout is gone (`perry main.ts -o out` succeeds against
the canonical zod repro).

Refs #793, #836, #837.
@proggeramlug proggeramlug merged commit f915f37 into main May 16, 2026
9 checks passed
@proggeramlug proggeramlug deleted the fix/anon-default-fn-export branch May 16, 2026 23:40
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.

1 participant