Skip to content

fix(codegen): re-export-only modules must populate their namespace / follow re-export hops (#6304, #5916)#6309

Merged
proggeramlug merged 1 commit into
mainfrom
fix/6304-reexport-chunk-namespace
Jul 13, 2026
Merged

fix(codegen): re-export-only modules must populate their namespace / follow re-export hops (#6304, #5916)#6309
proggeramlug merged 1 commit into
mainfrom
fix/6304-reexport-chunk-namespace

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #6304. Fixes #5916.

Two bugs with the same shape: Perry resolved an exported name to the module that mentions it rather than the module that defines it, and so never followed the re-export hop to the real binding.

#6304 — dynamic import() namespace of a pure re-export chunk was empty

esbuild/bun emit a shared chunk under --splitting as a two-statement re-export file:

// agent-VP4LHHJR.js
import { run } from "./chunk-XXXX.js";
export { run };

flatten_exports treated export { run } as a local export of the agent module. The driver then searched the agent's own HIR for a function/class/global named run, found nothing — it is an import binding, not a definition — and fell back to a ForeignVar getter on perry_fn_<agent>__run, a symbol the #461 stub loop claims with an undefined-returning stub.

Net effect: the namespace of a re-export-only chunk came out empty, and an unguarded ns.run() printed undefined rather than throwing — a silent wrong answer that quietly breaks every code-split bundle. A chunk containing real code was unaffected, which is why this hid for so long.

Fix: flatten_exports resolves an Export::Named whose local is an import binding to the module that actually defines it, following chains of import / re-export hops to the ultimate owner. Native imports and sources with no HIR in the graph are left alone (the resolver returns None, callers keep prior behaviour), so the change can only move an entry closer to a real definition — never invent one.

node perry before perry after
typeof ns.run function undefined function
ns.run() "[shared-util] agent" (missing) "[shared-util] agent"
chunk with real code works works works

#5916 — barrel re-export of a namespace value failed to LINK

// selfns.ts
export * as Token from "./selfns"
// reexport_selfns.ts
export { Token, estimate } from "./selfns"
// main.ts
import { Token } from "./reexport_selfns"; Token.estimate("hi")
Undefined symbols: ___perry_wrap_perry_fn_reexport_selfns_ts__Token

The consumer-side namespace detection only inspected the imported-from module's own exports for a NamespaceReExport. Across an export { X } from hop it saw a plain ReExport, fell through to the ordinary value path, and Token.estimate(...) lowered to a StaticMethodCall against a closure wrapper no module emits.

Fix: walk the re-export chain first, so the scan runs against the module that actually declares the namespace, under the name it declares it with. When no hop applies (the common case) the scan target is unchanged.

Correcting the issue's hypothesis

#5916 suggests the cause is the Export::Named-only destructuring in artifacts.rs's wrapper-emission loops. That hypothesis does not hold, and it is also not the cause of #6304:

So the two issues are different roots in the same family (export-origin resolution not following re-export hops), and are fixed independently here rather than by a single mechanical change to those loops.

Verification

test_gap_events_import_4995 and test_gap_module_const_local_shadow mismatch, but both were A/B-verified as failing identically on pristine main (the latter is already in known_failures.json).

Note: lint and conformance-smoke are already red on clean main (addr-class ratchet on map.rs; #6297 fixes it) — unrelated to this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed dynamic-import re-export/namespace materialization to resolve bindings to their true defining origin across multi-hop forwarding chains (including renamed and namespace-valued re-exports).
    • Improved handling of import-driven re-export cases so export { ... }-style redirects resolve correctly.
    • Added safeguards to stop pathological/cyclic re-export chains from looping, while preserving correct resolution.
  • Tests

    • Added regression tests and fixtures covering multi-chunk pure re-exports, renamed re-exports, namespace re-export barrels, and cycle termination behavior.

@coderabbitai

coderabbitai Bot commented Jul 12, 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: b1c9e8dd-964a-49d7-80da-21364fe43f01

📥 Commits

Reviewing files that changed from the base of the PR and between e280550 and a871399.

📒 Files selected for processing (10)
  • crates/perry-hir/src/dynamic_import.rs
  • crates/perry-hir/src/dynamic_import/tests.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • test-files/dynamic_import_chunk_agent.ts
  • test-files/dynamic_import_chunk_shared.ts
  • test-files/dynamic_import_chunk_worker.ts
  • test-files/namespace_reexport_barrel.ts
  • test-files/namespace_reexport_selfns.ts
  • test-files/test_gap_dynamic_import_reexport_chunk.ts
  • test-files/test_gap_namespace_reexport_barrel.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • test-files/dynamic_import_chunk_worker.ts
  • test-files/namespace_reexport_selfns.ts
  • test-files/dynamic_import_chunk_agent.ts
  • test-files/test_gap_dynamic_import_reexport_chunk.ts
  • test-files/dynamic_import_chunk_shared.ts
  • test-files/namespace_reexport_barrel.ts
  • crates/perry-hir/src/dynamic_import/tests.rs
  • crates/perry-hir/src/dynamic_import.rs
  • crates/perry/src/commands/compile/run_pipeline.rs

📝 Walkthrough

Walkthrough

The PR resolves binding origins across import and re-export chains, bounds namespace re-export traversal, normalizes non-native import sources, and adds regression coverage for pure dynamic-import chunks and namespace-valued barrel re-exports.

Changes

Re-export resolution

Layer / File(s) Summary
HIR binding-origin resolution
crates/perry-hir/src/dynamic_import.rs, crates/perry-hir/src/dynamic_import/tests.rs
flatten_exports follows import and re-export chains to defining bindings, preserves namespace origins, handles local definitions and native imports, and terminates cyclic chains with regression tests.
Pipeline re-export routing
crates/perry/src/commands/compile/run_pipeline.rs
The compile pipeline normalizes non-native import sources, limits re-export hops, and resolves namespace routing from the module that declares the namespace binding.
Runtime regression fixtures
test-files/dynamic_import_chunk_*.ts, test-files/namespace_reexport_*.ts, test-files/test_gap_*.ts
Adds shared, worker, pure re-export, and namespace-barrel fixtures covering renamed exports, dynamic imports, namespace access, and existing worker behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CompilePipeline
  participant HIR
  participant ModuleGraph
  participant DynamicImport
  CompilePipeline->>HIR: normalize import and export sources
  HIR->>ModuleGraph: follow import and re-export bindings
  ModuleGraph-->>HIR: resolved defining module and local binding
  HIR-->>CompilePipeline: flattened export origin
  DynamicImport->>CompilePipeline: load pure re-export chunk
  CompilePipeline-->>DynamicImport: expose resolved bindings
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: re-export-hop resolution for dynamic-import namespaces and namespace-valued re-exports.
Description check ✅ Passed The description covers the issues fixed, the behavioral change, and verification, even though it does not follow the template headings exactly.
Linked Issues check ✅ Passed The code changes and tests address both #6304 and #5916 by resolving export origins through re-export chains and preserving fallback behavior.
Out of Scope Changes check ✅ Passed The PR stays focused on export-origin resolution and regression tests, with no clearly unrelated code changes.
✨ 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/6304-reexport-chunk-namespace

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…#6304, #5916)

Two bugs with the same shape: Perry resolved an exported name to the module
that *mentions* it rather than the module that *defines* it, and so never
followed the re-export hop to the real binding.

#6304 — dynamic import() namespace of a pure re-export chunk was empty.

  esbuild/bun emit a shared chunk under `--splitting` as a two-statement
  re-export file:

      // agent-VP4LHHJR.js
      import { run } from "./chunk-XXXX.js";
      export { run };

  `flatten_exports` treated `export { run }` as a LOCAL export of the agent
  module. The driver then searched the agent's own HIR for a function/class/
  global named `run`, found nothing (it is an IMPORT binding, not a
  definition), and fell back to a `ForeignVar` getter on
  `perry_fn_<agent>__run` — a symbol the #461 stub loop claims with an
  undefined-returning stub. Net effect: the whole namespace of a re-export-only
  chunk came out `undefined`, and an unguarded `ns.run()` printed `undefined`
  instead of throwing — a SILENT wrong answer that quietly broke every
  code-split bundle. A chunk containing real code was unaffected, which is why
  this hid for so long.

  Fix: `flatten_exports` now resolves an `Export::Named` whose local is an
  import binding to the module that actually defines it, following chains of
  import/re-export hops to the ultimate owner. Native imports and sources with
  no HIR in the graph are left alone (the resolver returns `None` and callers
  keep their prior behaviour), so this can only move an entry CLOSER to a real
  definition, never invent one.

#5916 — barrel re-export of a namespace value failed to LINK.

      // selfns.ts
      export * as Token from "./selfns"
      // reexport_selfns.ts
      export { Token, estimate } from "./selfns"
      // main.ts
      import { Token } from "./reexport_selfns"; Token.estimate("hi")

      Undefined symbols: ___perry_wrap_perry_fn_reexport_selfns_ts__Token

  The consumer-side namespace detection only inspected the imported-from
  module's OWN exports for a `NamespaceReExport`. Across a `export { X } from`
  hop it saw a plain `ReExport`, fell through to the ordinary value path, and
  `Token.estimate(...)` lowered to a `StaticMethodCall` against a closure
  wrapper no module emits.

  Fix: walk the re-export chain first, so the scan runs against the module that
  actually DECLARES the namespace, under the name it declares it with. When no
  hop applies (the common case) the scan target is unchanged.

  Note: the issue's stated hypothesis — that the `Export::Named`-only
  destructuring in artifacts.rs's wrapper loops is the cause — does not hold. A
  `ReExport` of an ordinary function/const across the same hop already links and
  runs correctly today; it is specifically a NAMESPACE-valued binding that needs
  the namespace routing. The two issues are therefore different roots in the same
  family, and are fixed independently here.

Verification:
  * #6304 repro built with real `esbuild --bundle --splitting --format=esm`:
    perry output is now byte-identical to `node out/index.js`.
  * #5916 repro from the issue links and prints `3 / 3`, matching node.
  * New gap fixtures (parity vs `node --experimental-strip-types`):
    test_gap_dynamic_import_reexport_chunk.ts (re-export-only chunk in a
    dynamic-import namespace, incl. renamed re-export + a real-code control
    chunk) and test_gap_namespace_reexport_barrel.ts (#5916 link case).
  * 6 new flatten_exports unit tests (rename, multi-hop chain, local-definition
    precedence, native-import exclusion, cycle termination); perry-hir lib
    suite green (229 passed).
  * Static-import barrels through both shapes still work (no regression), and
    all 10 pre-existing dynamic-import gap tests still pass.
@proggeramlug proggeramlug force-pushed the fix/6304-reexport-chunk-namespace branch from e280550 to a871399 Compare July 12, 2026 18:46
@proggeramlug proggeramlug merged commit 325b0e4 into main Jul 13, 2026
25 checks passed
@proggeramlug proggeramlug deleted the fix/6304-reexport-chunk-namespace branch July 13, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant