From fe9b9f196fef3ccfc42bbb62d07e771de33bbe35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 16 May 2026 08:12:14 +0200 Subject: [PATCH] =?UTF-8?q?fix(compile):=20#684=20=E2=80=94=20type-only=20?= =?UTF-8?q?imports=20must=20not=20load=20as=20runtime=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `crates/perry/src/commands/compile/collect_modules.rs` iterated every `hir_module.imports` entry through cached_resolve_import and queued them all as runtime modules — ignoring `import.type_only`. The HIR layer at `crates/perry-hir/src/lower.rs:4151` already preserves the type-only annotation per specifier so class metadata can flow into `imported_classes` for method dispatch (the #446 fix), but the collector treated the annotation as informational only and loaded the underlying package anyway. The deterministic Effect repro from the issue: bun add effect@3.21.2 echo 'import {} from "effect"; console.log("ok")' > test.ts perry compile test.ts -o /tmp/out /tmp/out # before: TypeError: Cannot read properties of undefined (reading '_tag') Effect's `Schema.ts` has exactly one `@standard-schema/spec` reference: import type { StandardSchemaV1 } from "@standard-schema/spec" The package ships an empty `var src_exports = {}` at runtime — it's type-only by design. Perry's collector saw the import and queued the package for V8 fallback (only JS module among 363 modules), so any `._tag` read inside Effect's compiled code threw V8's exact wording. That uncaught TypeError was the #684 symptom in its current form — the original `(number).slice` message dissolved once intervening Schema/Effect landings advanced init past the old crash point. Fix is one guard at the top of the import-processing loop: skip entries with `type_only: true`. Class-metadata flow is unaffected because HIR lowering already captured it before this point; the collector only governs whether the module's `.o` (or JS bundle) participates in linking and runtime init. Validation: - `import type { Foo } from "./does_not_exist"` now compiles cleanly (was rejected at the resolve step). - Mixed `import { x }` + `import type { T }` from the same file still resolves the value side correctly. - `cargo test --release -p perry-hir -p perry-codegen -p perry` — all green (223 + 29 + 4 + 41 etc., 0 failed). - 7-test class+import smoke set byte-identical to `node --experimental-strip-types`. Follow-up: the Effect repro now advances past the `_tag` crash and exposes a separate link-time gap — `js_readable_stream_*` symbols are unresolved because Effect uses the global `ReadableStream` constructor but `compute_required_features` only triggers `bundled-streams` on a literal `import "streams"`. Auto-enabling the streams feature on global `new ReadableStream()` / `new WritableStream()` / `new TransformStream()` is a separate Effect-e2e (#321) follow-up. Closes #684 (the type-only-runtime-load root cause; the renamed `_tag` symptom this PR was reproducing). --- .../perry/src/commands/compile/collect_modules.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 66a90b567a..de6b844df5 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -334,6 +334,21 @@ pub(super) fn collect_modules( // Process imports and update their resolved paths and module kinds for import in &mut hir_module.imports { + // Skip type-only imports — they were recorded for class-metadata flow + // (see lower.rs's #446 comment: a per-specifier `import { type Foo }` + // is preserved so Foo's class info reaches `imported_classes` for + // method dispatch) but they MUST NOT be loaded as runtime modules. + // Without this skip, `import type { StandardSchemaV1 } from + // "@standard-schema/spec"` (Effect's only `@standard-schema` use, + // a type-only reference) queued the package's V8 fallback. The + // spec ships an empty `src_exports = {}` at runtime, so any + // `something._tag` from the import binding then threw + // `TypeError: Cannot read properties of undefined (reading '_tag')` + // during Effect's module init. Refs #321, #684. + if import.type_only { + continue; + } + // Apply package alias (e.g., @parse/node-apn → perry-push from perry.packageAliases) if let Some(alias) = ctx.package_aliases.get(import.source.as_str()).cloned() { import.source = alias;