Skip to content

fix(hir): queueMicrotask/structuredClone/atob/btoa as first-class function values (#5015)#5019

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-react-reconciler-5015
Jun 11, 2026
Merged

fix(hir): queueMicrotask/structuredClone/atob/btoa as first-class function values (#5015)#5019
proggeramlug merged 1 commit into
mainfrom
worktree-fix-react-reconciler-5015

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5015 — react-reconciler 0.33 production build threw TypeError: value is not a function inside updateContainerSync on the first render, blocking every custom React renderer (ink #348, react-three-fiber, react-pdf, …).

Root cause

Four callable global helpers — queueMicrotask, structuredClone, atob, btoa — were callable directly (queueMicrotask(fn) is intercepted in expr_call/globals.rs) but were never added to is_builtin_global_value_name in crates/perry-hir/src/analysis/builtins.rs.

So a bare value read — const m = queueMicrotask, or the object-literal property { scheduleMicrotask: queueMicrotask } — fell through ident lowering to the GlobalGet(0) sentinel and evaluated to the number 0. typeof reported "number" and calling the stored value threw "value is not a function". (The typeof-of-bare-ident fold and the globalThis.<name> thunk table already covered these names; only the value-read list was missing them — a gap left over from #3986.)

react-reconciler's host config stores scheduleMicrotask: queueMicrotask into a module var and later invokes it from scheduleImmediateRootScheduleTask, reached via updateContainerSync → updateContainerImpl → scheduleUpdateOnFiber → ensureRootIsScheduled. The stored 0 is exactly the non-callable the issue's native backtrace pinpointed (…__96 = scheduleImmediateRootScheduleTask).

Fix

Add the four names to is_builtin_global_value_name so a bare value read lowers to PropertyGet { GlobalGet(0), <name> }, resolving through the existing globalThis thunk — the same path fetch/parseInt/eval already use. This fixes both the bare value read and the object-literal property form, and is value-read-only (direct calls are untouched, intercepted earlier).

Validation

  • New gap test test-files/test_gap_global_fn_values_5015.ts — matches node --experimental-strip-types byte-for-byte (typeof, object-property extraction + call, direct calls).
  • Existing global-builtin gap tests (test_gap_global_builtins_2905_2889, test_gap_escape_unescape_global_4511, test_globalthis_builtins) still match Node.
  • The minimal repro from the issue: updateContainerSync no longer throws ([C1] updateContainerSync ok).
$ /tmp/t5015gap     (perry)        $ node test_gap_global_fn_values_5015.ts
function function function function   function function function function
function function function function   function function function function
{"x":1,"y":[2,3]}                     {"x":1,"y":[2,3]}
aGk= hi                               aGk= hi
...                                   ...   (identical)

Follow-up wall (separate bug — not in this PR)

With this fix updateContainerSync succeeds; the repro's subsequent flushSyncWork() now reaches commitRoot, where flushPendingEffects — a forward-referenced, hoisted sibling function declaration captured by commitRoot inside the huge module.exports = function($$$config){…} factory — reads back as undefined. This is a distinct closure-capture/box issue, independent of queueMicrotask (it reproduces with an arrow scheduleMicrotask too). It is the next wall on the ink-end-to-end path and is filed as #5020.

🤖 Generated with Claude Code

…ction values (#5015)

Bare value reads of these four callable globals (const m = queueMicrotask,
or the object-literal property { scheduleMicrotask: queueMicrotask }) fell
through ident lowering to the GlobalGet(0) sentinel and evaluated to the
number 0 -- typeof reported "number" and calling the stored value threw
"value is not a function". They were callable directly but missing from
is_builtin_global_value_name (a gap left from #3986).

react-reconciler's host config stores scheduleMicrotask: queueMicrotask and
invokes it from scheduleImmediateRootScheduleTask, so updateContainerSync
threw on the first render -- blocking every custom React renderer.

Add them to is_builtin_global_value_name so the value read lowers to
PropertyGet { GlobalGet(0), name }, resolving through the existing globalThis
thunk (same path as fetch/parseInt/eval). Value-read-only; direct calls
unaffected. Regression test matches Node byte-for-byte.
@proggeramlug proggeramlug merged commit f0430bb into main Jun 11, 2026
10 of 13 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-react-reconciler-5015 branch June 11, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant