From 073ac634697fbfda314e6c7ba224f662ddf00cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 17 May 2026 01:07:28 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(hir):=20anonymous=20`export=20default?= =?UTF-8?q?=20function=20()=20{}`=20=E2=80=94=20emit=20`=5F=5Fdefault`=20s?= =?UTF-8?q?ymbol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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___default`, and any consumer link-failed with `Undefined symbols: _perry_fn___default`. The `__perry_wrap_perry_fn___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___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. --- CHANGELOG.md | 2 + CLAUDE.md | 2 +- Cargo.lock | 136 +++++++++--------- Cargo.toml | 2 +- crates/perry-hir/src/lower.rs | 62 ++++++++ .../test_issue_anonymous_default_export.ts | 14 ++ .../consumer.ts | 5 + .../producer.ts | 9 ++ 8 files changed, 162 insertions(+), 70 deletions(-) create mode 100644 test-files/test_issue_anonymous_default_export.ts create mode 100644 test-files/test_issue_anonymous_default_export_pkg/consumer.ts create mode 100644 test-files/test_issue_anonymous_default_export_pkg/producer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b49d929b4..b0a029869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.929 — fix(hir): anonymous `export default function () { ... }` lowering — emit the `__default` symbol so zod/vitest under `perry.compilePackages` link. **Symptom.** The `perry.compilePackages: ["zod"]` repro left over from #836 (`import { z } from "zod"; console.log(z.object({a:z.string()}).parse({a:'hi'}).a);`) still link-failed with `Undefined symbols: _perry_fn_node_modules_zod_src_v4_locales_en_ts__default`. The producer module `zod/src/v4/locales/en.ts` exports an anonymous default function: `export default function () { return { ... }; }`. vitest fails with the same shape, surfacing as `___perry_wrap_perry_fn_node_modules_vitest_dist_index_js__default` — the rename wrapper added in #837, missing because the underlying `perry_fn_..._default` was never emitted. **Root cause.** `crates/perry-hir/src/lower.rs` handled `ast::ModuleDecl::ExportDefaultDecl(ast::DefaultDecl::Fn(fn_expr))` by checking `fn_expr.ident` and, when present, pushing an `Export::Named` entry with no function lowering (an old TODO). The `fn_expr.ident == None` branch (the anonymous case) was a no-op — the function body was dropped entirely, no HIR `Function` was created, and codegen never emitted `perry_fn___default`. The `__perry_wrap_perry_fn___default` rename wrapper from #837 then had nothing to point at. The named-default form has its own separate bug (function body also dropped) that's left untouched here per the issue's scope-narrow directive. **Fix.** When `fn_expr.ident == None` and the function has a body, synthesize an `ast::FnDecl` with ident `default`, run it through the same `lower_fn_decl` → `is_exported = true` → `func_defaults` + `exported_functions` + `Export::Named { local: "default", exported: "default" }` flow that `ExportDecl::Fn` uses. The synthesized name `default` flows straight through `sanitize()` unchanged (it's `[a-z]+`), so the LLVM symbol is `perry_fn___default` — exactly what consumers ask for. Since `f.name == exported_name == "default"`, the alias-wrapper loop at `crates/perry-codegen/src/codegen.rs:2259` is a no-op (no indirection needed), and the undefined-stub fallback at line 2310 skips emission because `llmod.has_function("perry_fn___default")` is true. **Why this layer (vs. routing through `lower_fn_expr`).** `lower_fn_expr` returns `Expr::Closure` — a runtime closure value, not a top-level function. Top-level emission as `perry_fn___default` is what the consumer's `ExternFuncRef { name: "default" }` resolves against (`crates/perry-codegen/src/lower_call.rs:779-1054`), so the function must live in `module.functions` with `is_exported = true`. Synthesizing the `FnDecl` and reusing `lower_fn_decl` keeps every downstream invariant (default-param resolution, native-instance return-type registration, arguments-param synth, etc.) without reimplementing them at this site. **Out of scope.** Named-default form (`export default function foo() {...}`) still drops the function body — separate bug, separate PR. The remaining zod runtime undefined output from `z.object(...).parse(...).a` is also separate and tracks back to other lowering paths. **Validation.** The minimal repro (producer: `export default function () { return 42; }`; consumer: `import x from "./producer.ts"; console.log(x())`) now compiles and prints `42`, matching node `--experimental-strip-types` byte-for-byte. The zod link-error from #836's closeout is gone — `perry main.ts -o out` succeeds. `cargo build --release -p perry-runtime -p perry-stdlib -p perry` clean. New regression test `test-files/test_issue_anonymous_default_export.ts` + `test-files/test_issue_anonymous_default_export_pkg/{producer,consumer}.ts`. **Files touched.** `crates/perry-hir/src/lower.rs` (~60 lines in the `ExportDefaultDecl::Fn` branch), `test-files/test_issue_anonymous_default_export.ts` + `test-files/test_issue_anonymous_default_export_pkg/` (regression fixture). Refs #793 (Node.js + TypeScript compatibility roadmap), #836 (zod compilePackages closeout), #837 (default/null/undefined-named-exports wrapper). + ## v0.5.928 — fix(codegen): #836 — emit producer-side aliases for `$`-prefixed const re-exports + namespace-import re-export wrappers. **Symptom.** `perry main.ts -o out` against the canonical zod repro (`perry.compilePackages: ["zod"]`, `import { z } from "zod"; console.log(z.object({a:z.string()}).parse({a:'hi'}).a);`) link-failed with 82 undefined symbols, dominated by two patterns: `_perry_fn_node_modules_zod_src_v4_core_checks_ts__$ZodCheck` (and ~75 sibling `$Zod*`/`$constructor` shapes from `core/checks.ts` / `core/schemas.ts` / `core/errors.ts` / `core/core.ts`) plus `___perry_wrap_perry_fn_node_modules_zod_src_index_ts__z` (the top-level zod barrel). **Root cause — sub-bug A (sanitize mismatch).** Every producer-side symbol-name formation runs the exported name through `sanitize()` (`crates/perry-codegen/src/codegen.rs:6126` — replaces every non-`[A-Za-z0-9_]` char with `_`), so `export const $ZodCheck = …` lands at `perry_fn____ZodCheck` (one underscore from the `__` separator, the second from `$` → `_`). Every consumer-side reference, in contrast, runs through `import_origin_suffix()` (`crates/perry-codegen/src/expr.rs:62`) which returns the exported name VERBATIM — so consumer call sites read `perry_fn___$ZodCheck`. The mismatch was invisible until zod (with its `$`-prefixed identifier convention for internal/private-API marker) became the first compilePackages target with non-alphanumeric exports at scale. Same mismatch hits the closure wrapper: `__perry_wrap_perry_fn___$ZodCheck` referenced by the consumer's `js_closure_alloc_singleton(@__perry_wrap_…)` path was emitted at `__perry_wrap_perry_fn____ZodCheck` by the producer's `#837` rename loop. **Root cause — sub-bug B (`local == exported` namespace-import re-export).** zod's top-level `node_modules/zod/src/index.ts` is `import * as z from "./v4/classic/external.js"; export { z };`. The `Export::Named { local: "z", exported: "z" }` entry was skipped by the regular `__perry_wrap_` loop (`crates/perry-codegen/src/codegen.rs:2607-2641` — keys on `hir.functions`; `z` is a namespace alias, not a HIR function) and by the v0.5.916 `#837` renamed-export wrapper loop (filters on `local != exported`). The result: `__perry_wrap_perry_fn___z` had no definition anywhere in the producer object file. Any consumer that read the imported `z` binding as a value — including the user's `console.log(z)` from the repro and every `z.` access that lowered the receiver to a closure handle first — link-failed. **Fix.** Added a new emission block in `crates/perry-codegen/src/codegen.rs` right after the v0.5.916 `#837` renamed-wrapper loop (~line 2751) that walks `hir.exports` once and, for every `Export::Named { local, exported }`, emits two kinds of producer-side aliases: (1) **raw-name aliases when `sanitize(exported) != exported`**: `perry_fn___` forwards to `perry_fn___` (a thin call-and-return at the same arity, looked up via `func_by_local_name`; defaults to 0-arg for variable-shaped exports where the sanitized definition is a zero-arg getter), and `__perry_wrap_perry_fn___` forwards to the sanitized wrapper if one exists, falling back to a no-op `ret double ` matching the variable-shaped-rename branch in the existing `#837` loop. (2) **No-op wrapper for `local == exported` non-function exports**: `__perry_wrap_perry_fn___` defined as a no-op `ret double ` whenever `local == exported`, the local is not a HIR function (so the regular `__perry_wrap_` loop already skipped it), and no wrapper has been emitted by any earlier path. Both alias emissions are idempotent via a `HashSet` and skip if `llmod.has_function(&name)` already claims the symbol. LLVM IR allows `$` unquoted in identifiers (`[-a-zA-Z$._][-a-zA-Z$._0-9]*` per LangRef), so the raw-name aliases serialize correctly without quoting changes. **Why this layer (vs. fixing `sanitize` to preserve `$`).** Modifying `sanitize()` to allow `$` would change every class-name / method-name / global-name mangling across codegen (45+ call sites), risking regressions in unrelated paths that may rely on the conservative `[A-Za-z0-9_]` shape (debug-info munging, plugin-binding lookup, etc.). Producer-side alias emission keeps the existing canonical form and only adds the raw alternative at the link surface — the consumer-emitted reference resolves immediately, and DCE strips unused aliases from the final binary. **Validation.** The user's `perry.compilePackages: ["zod"]` repro now advances from 82 undefined symbols to 1: only `_perry_fn_node_modules_zod_src_v4_locales_en_ts__default` remains, from the anonymous `export default function () {…}` in `zod/src/v4/locales/en.ts`. That's a separate HIR-lowerer bug (lower.rs drops anonymous default-function bodies entirely; the symbol is never emitted) and is out of scope for this PR. New regression test `test-files/test_issue_836_zod_class_reexports.ts` + `test-files/fixtures/issue_836_pkg/{checks,external,index}.ts` mirrors the failing zod shape standalone: a producer `export const $ZodCheck = {…}` re-exported through a barrel, plus the `import * as z; export { z };` namespace-import re-export. Test exercises the property-read (`$ZodCheck.kind` / `.validate()`), the value-as-arg shape (`typeof $ZodCheck`), and the namespace-binding read. Compiles + runs byte-for-byte with node `--experimental-strip-types`. `cargo build --release -p perry-runtime -p perry-stdlib -p perry` clean. **Files touched.** `crates/perry-codegen/src/codegen.rs` (new emission block, ~150 lines), `test-files/test_issue_836_zod_class_reexports.ts` + `test-files/fixtures/issue_836_pkg/` (regression fixture). Closes #836 link surface. Refs #793 (Node.js + TypeScript compatibility roadmap), #805 (npm-sweep harness), #837 (the v0.5.916 default/null/undefined-named-exports fix that this PR completes). Remaining zod runtime work (anonymous-default function bodies + namespace-import-re-export propagation) tracked separately. ## v0.5.927 — fix(compile): #851 — Rollup-bundled CJS-inside-ESM modules no longer trigger `ImportExportInScript`. **Symptom.** `import { expect } from "vitest"` against a project with `perry.compilePackages: ["vitest"]` fails at parse time: `Failed to parse vitest/dist/chunks/test.DNmyFkvJ.js: Parse error: Error { error: (12817..12823, ImportExportInScript) }`. The vitest dist bundle is Rollup-produced — top-level ESM `import`/`export` statements with inlined CJS dependencies preserved as nested IIFE bodies (`(function (module, exports$1) { module.exports = factory(); })(...)`). **Root cause.** `crates/perry/src/commands/compile/cjs_wrap.rs::is_commonjs` returned true whenever the source contained any of `module.exports`, `exports.`, or `require(` — regardless of where in the file the token appeared. For Rollup hybrid output the inner CJS body trips the `module.exports` check and the entire file gets fed through `wrap_commonjs`, which moves the file body (including the top-level `import`/`export`) inside an IIFE. SWC then parses each function body as a script context, so the relocated `import` statement raises `ImportExportInScript`. **Fix.** Added a top-level ESM check that short-circuits `is_commonjs`: if any unindented line begins with `import ` / `import{` / `import"` / `import'` / `import(` / `export ` / `export{` / `export*` / `export"` / `export'`, the file is unambiguously ESM and the wrap is skipped. The `starts_with_esm_keyword` helper rejects identifier-continuation characters (so `exports.foo` doesn't match `export` and `importMap` doesn't match `import`), and indented occurrences are ignored because `import`/`export` statements are only valid at module top-level. **Validation.** Pre-fix: `cd /tmp/perry-vitest && perry main.ts -o out` fails at `Failed to parse .../test.DNmyFkvJ.js: ImportExportInScript`. Post-fix: the same command parses the bundle cleanly and advances to the link stage (where it then fails on missing `libperry_jsruntime.a` because vitest pulls in V8-fallback modules — a separate downstream issue tracked under #793/#805). Seven new unit tests in `crates/perry/src/commands/compile/cjs_wrap.rs` cover the Rollup hybrid shape, top-level `export`-wins-over-CJS-tokens, `export *`, the `exports.foo`/`importMap` non-match guards, the indented-import guard, and top-level dynamic `import('./x')` classification. Existing `is_commonjs`/`wrap_commonjs` tests unaffected (48/48 pass). New end-to-end smoke test `test-files/test_issue_851_rollup_cjs_in_esm.ts` compiles natively and produces `ok-851`, guarding against later-stage regressions when bare `module`/`exports`/`require` identifiers appear inside ESM function bodies. **Files touched.** `crates/perry/src/commands/compile/cjs_wrap.rs` (top-level ESM guard + 7 unit tests), `test-files/test_issue_851_rollup_cjs_in_esm.ts` (new regression test). Closes #851 (parser-mode half). Refs #793 (Node.js + TypeScript compatibility roadmap), #805 (npm sweep tier2 — vitest is the canonical example; the same Rollup-CJS-inlining shape affects an estimated 20%+ of top-1k npm packages including vite, modern React tooling, and anything built with `@rollup/plugin-commonjs`). diff --git a/CLAUDE.md b/CLAUDE.md index f166d5546..0869473a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.928 +**Current Version:** 0.5.929 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index f85b2e1f7..2580ebba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4790,7 +4790,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "base64", @@ -4845,14 +4845,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.928" +version = "0.5.929" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "log", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-hir", @@ -4874,7 +4874,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-hir", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-dispatch", @@ -4892,7 +4892,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-hir", @@ -4901,7 +4901,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "base64", @@ -4914,7 +4914,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-hir", @@ -4922,7 +4922,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.928" +version = "0.5.929" dependencies = [ "serde", "serde_json", @@ -4930,7 +4930,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.928" +version = "0.5.929" [[package]] name = "perry-doc-fixture-my-bindings" @@ -4941,7 +4941,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "clap", @@ -4956,7 +4956,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.928" +version = "0.5.929" dependencies = [ "argon2", "perry-ffi", @@ -4964,7 +4964,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "reqwest", @@ -4973,7 +4973,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.928" +version = "0.5.929" dependencies = [ "bcrypt", "perry-ffi", @@ -4981,7 +4981,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "rusqlite", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "scraper", @@ -4997,14 +4997,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.928" +version = "0.5.929" dependencies = [ "chrono", "cron", @@ -5013,7 +5013,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.928" +version = "0.5.929" dependencies = [ "chrono", "perry-ffi", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "rust_decimal", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "serde_json", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5045,21 +5045,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.928" +version = "0.5.929" dependencies = [ "bytes", "http-body-util", @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lazy_static", "perry-ffi", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.928" +version = "0.5.929" dependencies = [ "bytes", "http-body-util", @@ -5115,7 +5115,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lazy_static", "perry-ffi", @@ -5125,7 +5125,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.928" +version = "0.5.929" dependencies = [ "base64", "jsonwebtoken", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lru", "perry-ffi", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.928" +version = "0.5.929" dependencies = [ "chrono", "perry-ffi", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.928" +version = "0.5.929" dependencies = [ "bson", "futures-util", @@ -5164,7 +5164,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.928" +version = "0.5.929" dependencies = [ "chrono", "perry-ffi", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.928" +version = "0.5.929" dependencies = [ "nanoid", "perry-ffi", @@ -5183,7 +5183,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "rustls", @@ -5194,7 +5194,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lettre", "perry-ffi", @@ -5204,7 +5204,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "sqlx", @@ -5213,7 +5213,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.928" +version = "0.5.929" dependencies = [ "governor", "perry-ffi", @@ -5221,7 +5221,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.928" +version = "0.5.929" dependencies = [ "base64", "image", @@ -5230,14 +5230,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.928" +version = "0.5.929" dependencies = [ "lazy_static", "perry-ffi", @@ -5245,7 +5245,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "uuid", @@ -5253,7 +5253,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.928" +version = "0.5.929" dependencies = [ "perry-ffi", "regex", @@ -5263,7 +5263,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.928" +version = "0.5.929" dependencies = [ "futures-util", "lazy_static", @@ -5274,7 +5274,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.928" +version = "0.5.929" dependencies = [ "flate2", "perry-ffi", @@ -5282,7 +5282,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.928" +version = "0.5.929" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5291,7 +5291,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-api-manifest", @@ -5305,7 +5305,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "deno_core", @@ -5325,7 +5325,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-diagnostics", @@ -5337,7 +5337,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "base64", @@ -5361,7 +5361,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.928" +version = "0.5.929" dependencies = [ "aes", "aes-gcm", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "perry-hir", @@ -5439,7 +5439,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.928" +version = "0.5.929" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5447,11 +5447,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.928" +version = "0.5.929" [[package]] name = "perry-ui-android" -version = "0.5.928" +version = "0.5.929" dependencies = [ "itoa", "jni", @@ -5466,7 +5466,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.928" +version = "0.5.929" dependencies = [ "rand 0.8.6", "serde", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.928" +version = "0.5.929" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5495,7 +5495,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.928" +version = "0.5.929" dependencies = [ "block2", "libc", @@ -5510,7 +5510,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.928" +version = "0.5.929" dependencies = [ "block2", "libc", @@ -5528,11 +5528,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.928" +version = "0.5.929" [[package]] name = "perry-ui-tvos" -version = "0.5.928" +version = "0.5.929" dependencies = [ "block2", "libc", @@ -5547,7 +5547,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.928" +version = "0.5.929" dependencies = [ "block2", "libc", @@ -5562,7 +5562,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.928" +version = "0.5.929" dependencies = [ "block2", "libc", @@ -5575,7 +5575,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.928" +version = "0.5.929" dependencies = [ "libc", "perry-runtime", @@ -5589,7 +5589,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.928" +version = "0.5.929" dependencies = [ "base64", "ed25519-dalek", @@ -5603,7 +5603,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.928" +version = "0.5.929" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 0f0e07dc4..c39f7346d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.928" +version = "0.5.929" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-hir/src/lower.rs b/crates/perry-hir/src/lower.rs index 721b59aba..c87c16526 100644 --- a/crates/perry-hir/src/lower.rs +++ b/crates/perry-hir/src/lower.rs @@ -5351,6 +5351,68 @@ fn lower_module_decl( local: func_name, exported: "default".to_string(), }); + } else if fn_expr.function.body.is_some() { + // Anonymous-default-function (zod / vitest blocker): + // `export default function () { ... }` arrives as a + // `DefaultDecl::Fn(FnExpr)` with `fn_expr.ident == None`. + // Pre-fix this branch dropped the body entirely — + // codegen never saw the function, so + // `perry_fn___default` was never emitted and + // consumers link-failed with `Undefined symbols: + // _perry_fn___default`. The + // `__perry_wrap_perry_fn___default` rename + // wrapper added in #837 had nothing to point at + // either. + // + // Synthesize an `FnDecl` with ident `default` so + // the HIR function name (and therefore the LLVM + // symbol) is `perry_fn___default`, matching + // what consumers ask for. Run the resulting decl + // through the same flow as `ExportDecl::Fn`: lower + // the body, mark `is_exported`, register defaults + // and `exported_functions` so codegen's wrapper- + // emission machinery picks it up. + // + // Scope-narrow: this only changes the + // `fn_expr.ident == None` branch. The named-default + // case above keeps its existing behavior pending a + // separate fix. + let synth_ident = ast::Ident::new( + "default".to_string().into(), + swc_common::DUMMY_SP, + Default::default(), + ); + let synth_fn_decl = ast::FnDecl { + ident: synth_ident, + declare: false, + function: fn_expr.function.clone(), + }; + let mut func = lower_fn_decl(ctx, &synth_fn_decl)?; + func.is_exported = true; + let func_id = func.id; + // Defaults registration mirrors the `ExportDecl::Fn` + // path so call sites that pad missing args still + // resolve user-written defaults. + let defaults: Vec> = + func.params.iter().map(|p| p.default.clone()).collect(); + let param_ids: Vec = + func.params.iter().map(|p| p.id).collect(); + let rest_idx = func.params.iter().position(|p| p.is_rest); + ctx.func_defaults + .push((func.id, defaults, param_ids, rest_idx)); + module.functions.push(func); + // Both the named export entry (so the importer's + // namespace populator sees `default`) and the + // `exported_functions` registry (so codegen's + // alias / wrapper emission treats it as a real + // exported function) are required. + module.exports.push(Export::Named { + local: "default".to_string(), + exported: "default".to_string(), + }); + module + .exported_functions + .push(("default".to_string(), func_id)); } } ast::DefaultDecl::Class(class_expr) => { diff --git a/test-files/test_issue_anonymous_default_export.ts b/test-files/test_issue_anonymous_default_export.ts new file mode 100644 index 000000000..4a12e3208 --- /dev/null +++ b/test-files/test_issue_anonymous_default_export.ts @@ -0,0 +1,14 @@ +// Regression for the anonymous-default-function lowering gap that +// blocks zod (`v4/locales/en.ts`) and vitest under +// `perry.compilePackages`. Pre-fix `export default function () { ... }` +// (no name binding) was dropped entirely by the HIR lowerer — codegen +// never emitted `perry_fn___default`, so the consumer link-failed +// with `Undefined symbols: _perry_fn___default`. The +// `__perry_wrap_perry_fn___default` rename wrapper (added in #837) +// also had nothing to point at. +// +// Output must match `node --experimental-strip-types` byte-for-byte. + +import x from "./test_issue_anonymous_default_export_pkg/producer.ts"; + +console.log(x()); diff --git a/test-files/test_issue_anonymous_default_export_pkg/consumer.ts b/test-files/test_issue_anonymous_default_export_pkg/consumer.ts new file mode 100644 index 000000000..ee50a8d00 --- /dev/null +++ b/test-files/test_issue_anonymous_default_export_pkg/consumer.ts @@ -0,0 +1,5 @@ +// Consumer for the anonymous-default-function regression. Imports the +// anonymous default function from `producer.ts` and invokes it. Expected +// output is `42` byte-for-byte (matches `node --experimental-strip-types`). +import x from "./producer.ts"; +console.log(x()); diff --git a/test-files/test_issue_anonymous_default_export_pkg/producer.ts b/test-files/test_issue_anonymous_default_export_pkg/producer.ts new file mode 100644 index 000000000..1e5762ebc --- /dev/null +++ b/test-files/test_issue_anonymous_default_export_pkg/producer.ts @@ -0,0 +1,9 @@ +// Anonymous-default-function regression: `export default function () { ... }` +// (no name binding). Pre-fix the HIR lowerer dropped the body entirely, +// codegen never emitted `perry_fn___default`, and any consumer +// link-failed with `Undefined symbols: _perry_fn___default` — same +// shape that blocks zod's `v4/locales/en.ts` and vitest's bundled CJS +// under `perry.compilePackages`. +export default function () { + return 42; +} From abad5c3e5c70149c5fbadd18ab6d886bd42d74fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 17 May 2026 01:07:52 +0200 Subject: [PATCH 2/2] fix: cargo fmt --- crates/perry-hir/src/lower.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/perry-hir/src/lower.rs b/crates/perry-hir/src/lower.rs index c87c16526..b7a02e358 100644 --- a/crates/perry-hir/src/lower.rs +++ b/crates/perry-hir/src/lower.rs @@ -5395,8 +5395,7 @@ fn lower_module_decl( // resolve user-written defaults. let defaults: Vec> = func.params.iter().map(|p| p.default.clone()).collect(); - let param_ids: Vec = - func.params.iter().map(|p| p.id).collect(); + let param_ids: Vec = func.params.iter().map(|p| p.id).collect(); let rest_idx = func.params.iter().position(|p| p.is_rest); ctx.func_defaults .push((func.id, defaults, param_ids, rest_idx));