What happened
When user code does import { X } from "pkg" and the package's entry point re-exports X via the namespace form export * as X from "./X.js", Perry's module collector does not traverse the re-export. The compile succeeds, the binary links cleanly, and every property access on X (e.g. X.someFn(...)) silently returns 0 — Perry's "unknown identifier" lowering fallback. There is no warning that X.someFn was unresolved.
Reproduced with Effect but the bug is general: any npm package whose index.ts uses export * as Foo from "./Foo.js" exhibits the same silent-zero behavior under import { Foo } from "pkg".
What you expected
Either:
- Preferred: traverse the re-export and pull
./X.js into the module graph the same way import * as X from "pkg/X" does.
- At minimum: emit a hard error or loud warning ("namespace re-export
X from pkg/index.ts not traversed; calls on X will return 0"). Today the user gets a silently-broken binary.
Minimal reproduction
mkdir nsre-zero && cd nsre-zero
cat > package.json <<'JSON'
{
"name": "nsre-zero",
"version": "0.0.0",
"type": "module",
"perry": { "compilePackages": ["effect"] }
}
JSON
bun add effect
// test_minimal.ts
import { Effect } from "effect";
const program = Effect.succeed(42);
const result = Effect.runSync(program);
console.log("result:", result);
Command:
perry compile test_minimal.ts -o /tmp/test_minimal
/tmp/test_minimal
Expected output: result: 42
Actual output: result: 0 — exit 0, no errors, no warnings about Effect being unresolved
Diagnostic output
perry compile -v test_minimal.ts ... surfaces the smoking gun:
Collecting modules...
Compile package: effect
Found 3 module(s): 3 native, 0 JavaScript
Module init order (2 modules):
[0] node_modules/effect/src/Function.ts
[1] node_modules/effect/src/index.ts
Only 2 of Effect's 152 modules were collected. effect/src/Effect.ts (29,102 lines, the actual implementation) was never visited despite being explicitly named in the index.ts re-export. By contrast, import * as Effect from "effect/Effect" correctly pulls 152 modules (then OOMs on a separate Perry-side bug — filed as #309).
Relevant source
effect/src/index.ts line 229:
export * as Effect from "./Effect.js"
This is a standard ES2020 namespace re-export and is the recommended pattern for "deep stdlib" packages (Effect, lodash-fp, ramda, etc.) — Perry should support it for compilePackages resolution.
Environment
- Perry version: 0.5.399
- Host OS: macOS 26.4 (arm64, Apple Silicon)
- Target: native
- Installed via: from source
- Effect version: 3.21.2
Notes for triage
- Likely fix site:
crates/perry/src/commands/compile/collect_modules.rs and / or crates/perry/src/commands/compile/resolve.rs (the v0.5.340 split). The import { Foo } from "pkg" code path needs to consult the resolved pkg's exports for Foo — and when Foo is itself a namespace re-export pointing at another file, recurse into that file's module graph.
- A scoped workaround that doesn't fix the silent-zero but stops the false success: when a user-side
PropertyGet lowers to "unknown identifier X" AND X came from an import { X } from "pkg" binding, upgrade from "warn-and-lower-to-0" to a hard error. Today's "warn at lowering site, lower to 0" is the right default for genuinely-runtime-resolved globals (Intl, globalThis.foo) but completely wrong for an import that the resolver gave up on.
Why this matters
This is a silent-correctness bug — the binary produces wrong output and exits 0. Users with even modestly large dependency graphs will hit this without realizing it; the failure mode is "my Effect program prints all zeros" not "my Effect program won't compile." That is the worst possible failure mode for a compiler.
What happened
When user code does
import { X } from "pkg"and the package's entry point re-exportsXvia the namespace formexport * as X from "./X.js", Perry's module collector does not traverse the re-export. The compile succeeds, the binary links cleanly, and every property access onX(e.g.X.someFn(...)) silently returns 0 — Perry's "unknown identifier" lowering fallback. There is no warning thatX.someFnwas unresolved.Reproduced with Effect but the bug is general: any npm package whose
index.tsusesexport * as Foo from "./Foo.js"exhibits the same silent-zero behavior underimport { Foo } from "pkg".What you expected
Either:
./X.jsinto the module graph the same wayimport * as X from "pkg/X"does.Xfrompkg/index.tsnot traversed; calls onXwill return 0"). Today the user gets a silently-broken binary.Minimal reproduction
Command:
Expected output:
result: 42Actual output:
result: 0— exit 0, no errors, no warnings about Effect being unresolvedDiagnostic output
perry compile -v test_minimal.ts ...surfaces the smoking gun:Only 2 of Effect's 152 modules were collected.
effect/src/Effect.ts(29,102 lines, the actual implementation) was never visited despite being explicitly named in theindex.tsre-export. By contrast,import * as Effect from "effect/Effect"correctly pulls 152 modules (then OOMs on a separate Perry-side bug — filed as #309).Relevant source
effect/src/index.tsline 229:This is a standard ES2020 namespace re-export and is the recommended pattern for "deep stdlib" packages (Effect, lodash-fp, ramda, etc.) — Perry should support it for
compilePackagesresolution.Environment
Notes for triage
crates/perry/src/commands/compile/collect_modules.rsand / orcrates/perry/src/commands/compile/resolve.rs(the v0.5.340 split). Theimport { Foo } from "pkg"code path needs to consult the resolvedpkg's exports forFoo— and whenFoois itself a namespace re-export pointing at another file, recurse into that file's module graph.PropertyGetlowers to "unknown identifier X" ANDXcame from animport { X } from "pkg"binding, upgrade from "warn-and-lower-to-0" to a hard error. Today's "warn at lowering site, lower to 0" is the right default for genuinely-runtime-resolved globals (Intl,globalThis.foo) but completely wrong for an import that the resolver gave up on.Why this matters
This is a silent-correctness bug — the binary produces wrong output and exits 0. Users with even modestly large dependency graphs will hit this without realizing it; the failure mode is "my Effect program prints all zeros" not "my Effect program won't compile." That is the worst possible failure mode for a compiler.