Skip to content

fix(hir): global new MessageChannel() routes to always-linked runtime constructor (#4873)#4875

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4873-messagechannel-new
Jun 10, 2026
Merged

fix(hir): global new MessageChannel() routes to always-linked runtime constructor (#4873)#4875
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4873-messagechannel-new

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes #4873.

Problem

Bare new MessageChannel() (and new globalThis.MessageChannel() / BroadcastChannel) was lowered in HIR to a worker_threads NativeMethodCall, which codegen maps to the stdlib-only js_worker_threads_message_channel_new. That symbol is only anchored when node:worker_threads is otherwise in the module graph, so:

  • standalone binaries failed to link (Undefined symbols: _js_worker_threads_message_channel_new), and
  • the codegen arm that calls the always-linked global constructor (lower_builtin_newjs_message_channel_new) was unreachable for the global forms.

React's scheduler runs exactly this shape at module init (typeof MessageChannel !== "undefined"new MessageChannel()), so every React / react-reconciler / ink app (#348) died before user code.

Fix

crates/perry-hir/src/lower/expr_new.rs: the two global-form interception sites (bare ident with no worker_threads import; globalThis. member) now lower as plain Expr::New { class_name }, so codegen emits the perry-runtime js_message_channel_new / js_broadcast_channel_new — always linked.

No behavior is lost: those runtime globals delegate to the full worker_threads factory whenever the stdlib registers it (js_register_worker_threads_messaging_constructors in dispatch.rs), and fall back to the Web-shaped stub otherwise. The genuinely-imported forms (import { MessageChannel } from "node:worker_threads", new wt.MessageChannel()) keep their stdlib routing — the import anchors the symbol.

Validation (all byte-identical to node --experimental-strip-types)

  • Issue repro mc.ts standalone: links, prints object object object ✔️
  • Scheduler-shaped port1.onmessage + port2.postMessage(null) flush with stdlib linked: flushed work ✔️ (standalone stub: clean exit, no hang/crash)
  • Paired-port delivery through the global form when node:worker_threads is linked: receiveMessageOnPort returns {"message":{"n":7}} ✔️
  • test-files/test_gap_worker_channels_3157plus.ts and test-files/test_parity_worker_threads.ts: diff-clean vs node (no regression in imported forms) ✔️
  • New e2e regression tests crates/perry/tests/issue_4873_message_channel_global_new.rs (standalone link + worker_threads delegation): pass ✔️
  • cargo test -p perry-hir: green — including the updated node_named_export_hygiene test, which previously asserted the buggy worker_threads routing for global forms and now asserts Expr::New (plus a new companion test pinning the imported-form routing).

No version bump / changelog per maintainer flow.

…ays-linked runtime constructors (#4873)

The *global* constructor forms — bare `new MessageChannel()` with no
worker_threads import, and `new globalThis.MessageChannel()` — were
lowered to a worker_threads NativeMethodCall, which codegen maps to the
stdlib-only `js_worker_threads_message_channel_new`. That symbol is
`#[used]`-anchored inside `node:worker_threads`, so any binary that
never imports the module failed to link (`Undefined symbols:
_js_worker_threads_message_channel_new`). React's scheduler runs
exactly this shape at module init (`typeof MessageChannel !==
"undefined"` then `new MessageChannel()`), killing every
React/react-reconciler/ink app (#348).

Lower the global forms as `Expr::New` instead, so codegen's
`lower_builtin_new` emits the perry-runtime `js_message_channel_new` /
`js_broadcast_channel_new` — always linked. Those runtime globals
delegate to the full worker_threads factory whenever the stdlib has
registered it (paired-port delivery, receiveMessageOnPort, onmessage
all verified working through the global form), and fall back to the
Web-shaped stub otherwise. The genuinely-imported forms
(`import { MessageChannel } from "node:worker_threads"`) keep their
stdlib routing — the import anchors the symbol in the link.

Validated against Node byte-for-byte: standalone link + run, the
scheduler-shaped onmessage flush, globalThis member form,
BroadcastChannel, and the #3157 imported-form gap test (no regression).
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.

Codegen: new MessageChannel() global constructor unlinked/non-constructible — routes to stdlib symbol, breaks React scheduler init

1 participant