Skip to content

fix(cjs-wrap): stop HIR from dropping built-in require bindings in wrapped modules - #8343

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
jdalton:fix/cjs-wrap-builtin-require-hir-drop
Aug 18, 2026
Merged

proggeramlug merged 2 commits into
PerryTS:mainfrom
jdalton:fix/cjs-wrap-builtin-require-hir-drop

Conversation

@jdalton

@jdalton jdalton commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #8341. sdxgen's ReferenceError: node_process is not defined is eliminated. The HIR was dropping the built-in require binding before the wrap's runtime createRequire path could run.

Root cause (mechanism (a))

#8341 made the CJS wrap route built-in requires (require("process")) through the synthetic require's createRequire arm instead of the hoisted static import binding, and skipped alias adoption/blanking for built-ins. That logic is correct, but the binding was being dropped before the wrap's runtime path could use it.

The HIR's destructuring var/let/const pass (register_native_fetch_and_streams / register_destructured_stream_ctors) rewrites let node_process = require("process") into a native-module namespace binding (register_require_namespace_binding → remove_local_binding), mirroring import * as node_process from "process". This pass runs before call lowering, so the lookup_local("require") guard in try_require_literal (which would otherwise bail because the wrap's synthetic function require shadows the global) never fires. The codegen does not initialize native-module import bindings inside CJS-wrapped modules, so node_process resolved to nothing at runtime — the ReferenceError.

So the mechanism is (a): the HIR drops the built-in require binding via the destructuring native-require fast path, which is out of sync with the wrap fix — it matches the bare require ident even when shadowed by the wrap's synthetic function require, unlike try_require_literal which checks lookup_local("require"). This is NOT (b) (the synthetic require's createRequire fallback is reached and works) and NOT (c) (no separate blanking path — #8341's blanking skip is effective).

Fix (three parts)

  1. HIR (var_decl_sources.rs / native_fetch.rs): gate the destructuring native-require fast paths on require being the bare global (not shadowed by the wrap's synthetic function require), via a new require_is_shadowed_by_local helper that mirrors try_require_literal's guard. When shadowed, the require("<builtin>") call flows through to the synthetic require, which resolves builtins via createRequire.

  2. wrap (wrap.rs): stop emitting import _req_N from '<builtin>' for built-in specs — the binding is never initialized in a CJS-wrapped module and is now unreferenced.

  3. wrap (wrap.rs): the per-spec require case for builtins never references the (now nonexistent) import local — always go through the createRequire-backed required_value, including the try-site branch (the typeof {local} === 'boolean' sentinel guard does not apply to builtins).

Verification

  • ReferenceError: node_process is not defined is gone — confirmed by running the compiled sdxgen.
  • Minimal CJS witnesses compile, link, and print darwin:
    • const p = require("process"); console.log(p.platform) (and os/path)
    • the exact rolldown __toESM shape (let node_process = require("process"); node_process = __toESM(node_process, 1), with the real Object.create(Object.getPrototypeOf(mod)) helper)
    • the destructured const { platform } = require("process")
  • New regression tests: wrap-level (cjs_wrap_builtin_require_not_hoisted_as_static_import) and end-to-end (cjs_wrap_builtin_require.rs — 3 tests, all green).
  • cargo test for touched crates green, measured: perry-hir lib 315 passed; perry cjs_wrap unit 112 passed; createrequire_builtin_modules integration 5 passed (no regression in the createRequire path).

Separate newly-revealed blocker (not this PR's scope)

With the ReferenceError fixed, sdxgen now reveals a separate downstream runtime error: TypeError: Object prototype may only be an Object or null: -2 at external-pack.js (from __toESM's Object.create(Object.getPrototypeOf(mod)), where Object.getPrototypeOf returns -2 for some reified CJS chunk exports object). This is a different Perry runtime bug (Object prototype reification / getPrototypeOf returning a tagged sentinel for a specific reified object), not the CJS-wrap/HIR-drop mechanism this PR fixes. It was masked by the ReferenceError and needs its own follow-up. The assigned blocker (ReferenceError: node_process is not defined) is resolved.

Loading
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.

2 participants