Skip to content

Barrel re-export of a namespace value (export { X } from "mod") never gets a __perry_wrap_perry_fn wrapper — linker failure #5916

Description

@proggeramlug

Summary

crates/perry-codegen/src/codegen/artifacts.rs has two existing wrapper-emission loops (closing #836/#837) that walk hir.exports and emit __perry_wrap_perry_fn_<module>__<name> closure-wrapper symbols for exports that need one — but both loops explicitly pattern-match only Export::Named { local, exported }:

let perry_hir::Export::Named { local, exported } = export else { continue; };

perry_hir::ir::decl::Export has two other variants that can equally end up needing a wrapper — ReExport { source, imported, exported } (export { X } from "mod") and NamespaceReExport { source, name } (export * as X from "mod") — and neither loop handles them. When a namespace-shaped value (e.g. an export * as Foo from "./foo" self-reference) flows through a further export { Foo } from "./that-module" re-export, and a consumer accesses it via Foo.method(...) (lowered to StaticMethodCall), codegen expects a __perry_wrap_perry_fn_<reexporting-module>__Foo symbol that is never emitted anywhere — link failure.

Minimal repro

// selfns.ts
export * as Token from "./selfns"
const CHARS_PER_TOKEN = 4
export const estimate = (input: string) => Math.max(0, Math.round(input.length / CHARS_PER_TOKEN))

// reexport_selfns.ts
export { Token, estimate } from "./selfns"

// main_selfns.ts
import { Token, estimate } from "./reexport_selfns"
console.log(Token.estimate("hello world"))
console.log(estimate("hello world"))
$ perry compile main_selfns.ts -o out
Undefined symbols for architecture arm64:
  "___perry_wrap_perry_fn_reexport_selfns_ts__Token", referenced from:
      _main in main_selfns_ts.o
ld: symbol(s) not found for architecture arm64

Removing the indirection (importing directly from ./selfns instead of through ./reexport_selfns) compiles and runs fine — confirming the bug is specifically about the wrapper not surviving a ReExport/NamespaceReExport hop, not about namespace re-exports in general.

Found via

Real-world source compile of opencode (sst/opencode) with perry.compilePackages: ["*"]. Confirmed on 12+ distinct barrel files across the app (Effect Schema/Context.Tag values re-exported across workspace package boundaries) plus in zod's v4/index.ts (export * from "./classic/index.js", re-exporting z) and effect's NodeSocket.ts (export * as NodeWS from "ws"). App-level workaround (rewrite export { X } from "mod" to import { X } from "mod"; export { X }) fixes every instance tried, but this is exactly the kind of thing real-world barrel-file-heavy TypeScript codebases do pervasively, so it's likely one of the more impactful compiler-side fixes available right now.

Suggested fix direction

Extend the two loops around crates/perry-codegen/src/codegen/artifacts.rs:768 and :887 (or add a third, analogous loop) to also handle Export::ReExport and Export::NamespaceReExport, emitting a forwarding wrapper that calls through to the resolved source module's own wrapper/accessor — recursing through further re-export chains if necessary (the repro above is a 2-hop chain: mainreexport_selfnsselfns, and selfns's NamespaceReExport of itself is also presumably unhandled — likely needs its own wrapper emission first, with ReExport's wrapper then forwarding to it).

Not attempting a fix in this issue — flagging with a precise, minimal, isolated repro (compiles/fails in seconds, no dependencies) for whoever picks it up, since a correct fix needs to handle the cross-module forwarding chain carefully rather than being a blind mechanical addition.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions