fix(hir): global new MessageChannel() routes to always-linked runtime constructor (#4873)#4875
Merged
Merged
Conversation
…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).
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4873.
Problem
Bare
new MessageChannel()(andnew globalThis.MessageChannel()/BroadcastChannel) was lowered in HIR to a worker_threadsNativeMethodCall, which codegen maps to the stdlib-onlyjs_worker_threads_message_channel_new. That symbol is only anchored whennode:worker_threadsis otherwise in the module graph, so:Undefined symbols: _js_worker_threads_message_channel_new), andlower_builtin_new→js_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 plainExpr::New { class_name }, so codegen emits the perry-runtimejs_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_constructorsindispatch.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)mc.tsstandalone: links, printsobject object object✔️port1.onmessage+port2.postMessage(null)flush with stdlib linked:flushed work✔️ (standalone stub: clean exit, no hang/crash)node:worker_threadsis linked:receiveMessageOnPortreturns{"message":{"n":7}}✔️test-files/test_gap_worker_channels_3157plus.tsandtest-files/test_parity_worker_threads.ts: diff-clean vs node (no regression in imported forms) ✔️crates/perry/tests/issue_4873_message_channel_global_new.rs(standalone link + worker_threads delegation): pass ✔️cargo test -p perry-hir: green — including the updatednode_named_export_hygienetest, which previously asserted the buggy worker_threads routing for global forms and now assertsExpr::New(plus a new companion test pinning the imported-form routing).No version bump / changelog per maintainer flow.