Skip to content

fix(compile): #684 — type-only imports must not load as runtime modules#828

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-684-schema-slice
May 16, 2026
Merged

fix(compile): #684 — type-only imports must not load as runtime modules#828
proggeramlug merged 1 commit into
mainfrom
worktree-fix-684-schema-slice

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

The module collector iterated every hir_module.imports entry through cached_resolve_import and queued them all as runtime modules, ignoring import.type_only. So import type { … } from \"…\" triggered V8 fallback for type-only packages, and any access to the (non-existent) named imports during init threw TypeError.

Deterministic repro

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 (type-only by design). The collector saw the import and routed the package through V8 (only JS module among 363), so any <StandardSchemaV1>._tag read inside Effect's compiled code threw V8's exact wording. That uncaught TypeError is the #684 symptom in its current form — the original (number).slice message dissolved once intervening landings advanced Effect's init past the old crash point.

Fix

One guard at the top of the import-processing loop in collect_modules.rs — skip entries with type_only: true. Class-metadata flow is unaffected: the HIR layer at lower.rs:4151 already captures type-only specifiers into imported_classes for method dispatch (#446 path). The collector only governs whether the module's .o (or JS bundle) participates in linking and runtime init.

Test plan

  • 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 + 1 + 40 + 10 + 6, 0 failed).
  • 7-test class+import smoke set byte-identical to node --experimental-strip-types.
  • cargo fmt --all -- --check — clean.

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 (no literal import \"streams\"), and compute_required_features only triggers bundled-streams on the named import. Auto-enabling the streams feature on global new ReadableStream() / new WritableStream() / new TransformStream() is a separate Effect-e2e (#321) follow-up.

Closes #684.

`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
`<StandardSchemaV1>._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).
@proggeramlug proggeramlug merged commit 8e69bc6 into main May 16, 2026
9 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-684-schema-slice branch May 16, 2026 06:22
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.

perry-codegen: Effect — (number).slice is not a function during Schema.ts__init (#680 follow-up, ~310th init)

1 participant