Skip to content

fix(codegen): #678 — re-export rename resolves to origin export name#785

Merged
proggeramlug merged 3 commits into
mainfrom
worktree-agent-ae80fd8bc334d518d
May 15, 2026
Merged

fix(codegen): #678 — re-export rename resolves to origin export name#785
proggeramlug merged 3 commits into
mainfrom
worktree-agent-ae80fd8bc334d518d

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Closes the symbol-name-mismatch path of #678. When a consumer imports a name that traverses a re-export rename (import { Box } from \"ink\" where ink/build/index.js does export { default as Box } from './components/Box.js'), the codegen was forming the cross-module extern as perry_fn_<components_Box_js>__Box. The origin module emits the symbol under its own export name (default), so the linker failed with Undefined symbols: _perry_fn_..._Box.

Every compile-package shaped as export { default as <Name> } from \"./<Name>.js\" (ink components, hono adapters, drizzle subpackages, react sub-bundles, ...) hit this on any named import. The original issue's hypothesis pointed at V8-fallback demotion, but reduction to a single-package fixture shows the actual root cause is the lost rename across the re-export hop, not the V8 fallback path.

Root cause

all_module_exports[ink/index] carried \"Box\" → components/Box.js path but not the second piece of information needed — what name does Box.js export it under?. Every codegen call/wrapper/getter site that built perry_fn_<src>__<suffix> reused the consumer's imported name (Box) verbatim. The origin module emits the symbol under its own local name (default, because Box.js does const Box = ...; export default Box).

Fix approach

Track origin-name alongside origin-path across re-export chains. Threaded through end-to-end:

  1. HIR-to-driver: new parallel map all_module_export_origin_names: BTreeMap<String, BTreeMap<String, String>> populated alongside all_module_exports during the export propagation loop. Sparse — only populated when origin_name differs from export_name. ReExport, Named, ExportAll, and NamespaceReExport branches all walk one extra hop to surface the deepest origin name through any chain depth.

  2. Driver-to-codegen: new CompileOptions::import_function_origin_names field, copied into CrossModuleCtx, exposed on FnCtx. Cache key in object_cache.rs includes it so re-shape changes invalidate cached .o.

  3. Codegen sites: new import_origin_suffix(map, name) helper returns the override-or-identity suffix. Every perry_fn_<src>__<name> construction site routes through it — lower_call.rs (direct extern call + namespace member call), expr.rs (ExternFuncRef value path, getter path for imported vars, namespace static method dispatch, property access on imported namespace), codegen.rs (FuncRef-as-value wrapper target).

  4. var-vs-function classification: the imported_vars lookup now probes BOTH (origin, exported_name) AND (origin, origin_name) so a const X = ...; export default X re-exported as a different name still routes through the closure-getter call path instead of a direct function call.

Validation

  • New regression fixture test-files/test_issue_678_reexport_default.ts + test-files/fixtures/issue_678_pkg/ covers three shapes (default-as-rename of const closure, default-as-rename of function decl, named-as-rename of function decl). Byte-for-byte parity with `node --experimental-strip-types`.
  • `cargo test --release -p perry -p perry-codegen` — 157 + 29 tests pass.
  • `/tmp/run_gap_tests.sh` — 34/36, same two pre-existing failures (`test_gap_console_methods`, `test_gap_regexp_advanced` — both in CLAUDE.md's known-categorical-gaps list).
  • Existing `test_issue_310_namespace_reexport.ts` still passes (ExportAll / NamespaceReExport branches still work).
  • The minimal ink repro from Linker: unresolved _perry_fn_..._render for V8-fallback modules referenced from main #678 now resolves Box / render correctly; remaining link failures in that repro (Text — `export default function`-body not lowered, react.development.js compile error in a closure, ink/devtools namespace global) are pre-existing bugs in unrelated code paths, not addressed here.

Test plan

When a TypeScript module imports a name that traverses a re-export
rename — `import { Box } from "ink"` where `ink/build/index.js` does
`export { default as Box } from './components/Box.js'` — the codegen
was forming the cross-module extern as
`perry_fn_<components_Box_js>__Box`. The origin module emits the symbol
under its own export name (`default`), so the linker failed with
`Undefined symbols: _perry_fn_..._Box`. Every compile-package that
shapes its barrel as `export { default as <Name> } from "./<Name>.js"`
(ink, hono adapters, drizzle subpackages, react sub-bundles, ...) hit
this on any named import.

The fix tracks the *origin name* alongside the origin path across
re-export chains and threads it through codegen so every
`perry_fn_<src>__<suffix>` construction site picks the right suffix:

* New `import_function_origin_names: HashMap<String, String>` on
  `CompileOptions` parallels `import_function_prefixes`. Entries are
  inserted only when origin_name != consumer_name (sparse map, identity
  fallback at the call site).
* The CLI driver's per-module loop builds the map by consulting
  `all_module_export_origin_names` (a parallel to `all_module_exports`
  populated during the export-propagation loop). Named, ReExport,
  ExportAll, and NamespaceReExport branches all surface deeper origin
  names through any number of transitive hops.
* `import_origin_suffix()` helper in `perry-codegen` returns the
  override-or-identity suffix. Every `perry_fn_<src>__<name>`
  construction site (lower_call.rs, expr.rs ExternFuncRef + namespace
  member dispatch, codegen.rs FuncRef-as-value wrapper emission) now
  routes through it.
* `imported_vars` classification probes BOTH `(origin, exported_name)`
  AND `(origin, origin_name)` so a `const X = ...; export default X`
  re-exported under a different name still gets recognized as a
  variable and routed through the closure-getter call path instead of
  being treated as a direct function call.
* Object cache key includes the new map so two builds with the same
  modules but different re-export shapes don't share cached `.o`.

Validation:

* New regression fixture `test-files/test_issue_678_reexport_default.ts`
  + `test-files/fixtures/issue_678_pkg/` exercises all three shapes
   (default-as-rename of a const closure, default-as-rename of a
   function declaration, named-as-rename of a function declaration);
  byte-for-byte parity with `node --experimental-strip-types`.
* `cargo test --release -p perry -p perry-codegen` — 157 + 29 tests pass.
* `/tmp/run_gap_tests.sh` — 34/36 (same two pre-existing failures:
  `test_gap_console_methods`, `test_gap_regexp_advanced`).
* Existing `test_issue_310_namespace_reexport.ts` still passes (the
  ExportAll/NamespaceReExport branches keep working).
* The minimal ink repro from #678 now resolves Box / render correctly;
  remaining link failures (Text — `export default function`-body not
  lowered, react.development.js compile error, ink/devtools namespace
  global) are pre-existing bugs in separate code paths, untouched by
  this PR and not introduced by it.

Refs: #678
@proggeramlug proggeramlug merged commit 3235273 into main May 15, 2026
9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-ae80fd8bc334d518d branch May 15, 2026 14:52
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