Surfaced trying to compile the new canonical Effect e2e program (`test-files/compat-e2e/effect/main.ts`, added under #802) with v0.5.914.
Symptom
```
Linking (runtime-only)...
Undefined symbols for architecture arm64:
"_js_readable_stream_controller_close"
"_js_readable_stream_controller_enqueue"
"_js_readable_stream_controller_error"
"_js_readable_stream_new"
"_js_stream_unwrap_handle"
ld: symbol(s) not found for architecture arm64
Error: Linking failed
```
Root cause
These FFIs are exported with `#[no_mangle] pub unsafe extern "C" fn` from `crates/perry-stdlib/src/streams.rs` — they exist in `libperry_stdlib.a`. The link step picked the "runtime-only" path ("Linking (runtime-only)...") because `ctx.needs_stdlib` is false.
`needs_stdlib` is currently set only from explicit imports of `node:` modules (`crates/perry/src/commands/compile/collect_modules.rs:430-451`, via `perry_hir::requires_stdlib`). When a compiled-package (Effect) uses `ReadableStream` internally, codegen emits calls to `js_readable_stream_new` / `js_readable_stream_controller_` / `js_stream_unwrap_handle` (`crates/perry-codegen/src/lower_call/builtin.rs:571`, `lower_call.rs:5036`, `runtime_decls.rs:1256`), but `needs_stdlib` never flips, so `libperry_stdlib.a` is not linked → undefined symbols.
Why this matters
This is a class of bug, not a single missing symbol. Anytime codegen lowers something to an FFI call without a corresponding stdlib-import detection step, the binary link-fails when the call site is inside a compiled package (and works when the call site is reachable via a `node:*` import, because the import sets the flag).
Fix sketch
Either:
- Per-FFI side-effect flag: when codegen lowers a call that targets a `js_*` symbol exported only from `libperry_stdlib` (not `libperry_runtime`), record `ctx.needs_stdlib = true` at the codegen call site.
- Belt-and-suspenders link strategy: always link `libperry_stdlib` (relying on `strip_dedup` / GC-sections to keep binary size bounded). Simpler but cedes some size budget.
Option 1 is the cleaner long-term fix and aligns with how `needs_thread` and `needs_plugins` are tracked.
Repro
```bash
cd test-files/compat-e2e/effect
npm install
perry main.ts -o out
```
Surfaces from any compiled-package use of `new ReadableStream({ ... })`; not specific to Effect.
Part of #793 + #321. Surfaced by the #802 e2e scaffold.
Surfaced trying to compile the new canonical Effect e2e program (`test-files/compat-e2e/effect/main.ts`, added under #802) with v0.5.914.
Symptom
```
Linking (runtime-only)...
Undefined symbols for architecture arm64:
"_js_readable_stream_controller_close"
"_js_readable_stream_controller_enqueue"
"_js_readable_stream_controller_error"
"_js_readable_stream_new"
"_js_stream_unwrap_handle"
ld: symbol(s) not found for architecture arm64
Error: Linking failed
```
Root cause
These FFIs are exported with `#[no_mangle] pub unsafe extern "C" fn` from `crates/perry-stdlib/src/streams.rs` — they exist in `libperry_stdlib.a`. The link step picked the "runtime-only" path ("Linking (runtime-only)...") because `ctx.needs_stdlib` is false.
`needs_stdlib` is currently set only from explicit imports of `node:` modules (`crates/perry/src/commands/compile/collect_modules.rs:430-451`, via `perry_hir::requires_stdlib`). When a compiled-package (Effect) uses `ReadableStream` internally, codegen emits calls to `js_readable_stream_new` / `js_readable_stream_controller_` / `js_stream_unwrap_handle` (`crates/perry-codegen/src/lower_call/builtin.rs:571`, `lower_call.rs:5036`, `runtime_decls.rs:1256`), but `needs_stdlib` never flips, so `libperry_stdlib.a` is not linked → undefined symbols.
Why this matters
This is a class of bug, not a single missing symbol. Anytime codegen lowers something to an FFI call without a corresponding stdlib-import detection step, the binary link-fails when the call site is inside a compiled package (and works when the call site is reachable via a `node:*` import, because the import sets the flag).
Fix sketch
Either:
Option 1 is the cleaner long-term fix and aligns with how `needs_thread` and `needs_plugins` are tracked.
Repro
```bash
cd test-files/compat-e2e/effect
npm install
perry main.ts -o out
```
Surfaces from any compiled-package use of `new ReadableStream({ ... })`; not specific to Effect.
Part of #793 + #321. Surfaced by the #802 e2e scaffold.