Skip to content

Link failure: needs_stdlib not set when compiled-package code (Effect) references ReadableStream FFIs #835

@proggeramlug

Description

@proggeramlug

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparityNode.js compatibility / parity gaps

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions