Tracks #5373. A bare/computed `require(expr)` in a `compilePackages`-compiled module throws `ReferenceError: require is not defined` because no ambient `require` binding is emitted. Only a **literal** `require('pkg')` is rewritten to an import at compile time; a non-literal callee falls through the HIR ident-read path to `js_global_get_or_throw_unresolved("require")`. This issue scopes the fix into two tiers. **Tier 1 closes #5373 as filed.** Tier 2 extends coverage to const-foldable computed specifiers and is explicitly bounded by the AOT model. ## Background Three independent layers rewrite **literal** require/createRequire to imports at compile time, so the runtime closure is never consulted for them: - HIR `try_require_literal` — `crates/perry-hir/src/lower/expr_call/intrinsics.rs:40-111` (bails on a non-literal arg, returns `Ok(None)`). - var-decl destructuring — `crates/perry-hir/src/destructuring/var_decl_sources.rs`. - source-text createRequire transform — `crates/perry/src/commands/compile/collect_modules/create_require_transform.rs`. The runtime `require` closure that `createRequire(...)` produces (`crates/perry-runtime/src/module_require.rs`, `require_thunk`) **only supports Node builtins** — any package/relative specifier throws `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE` ("supports built-in modules only"). So "createRequire works" in #5373 is true only because the repro's computed specifier resolves to the builtin `node:os`. No ambient global `require` binding exists: `require` is absent from `is_known_global_identifier_name` (`crates/perry-hir/src/lower/lower_expr.rs:57-96`), so a bare `require(expr)` lowers to the throwing `js_global_get_or_throw_unresolved("require")` (`lower_expr.rs:666-689`). ## The hard architectural boundary A `require(expr)` in an ahead-of-time native binary can only resolve to a module that was **discovered and linked at compile time** — a runtime-computed string cannot pull in code that isn't in the executable. Perry already enforces exactly this for dynamic `import(expr)`: a specifier the const-folder/globber can't reduce to a finite literal set is rejected (strict mode: compile error; default mode: a deferred throw at the call site — "dynamic import() of a runtime-computed path cannot run in an ahead-of-time compiled binary"; see `crates/perry/src/commands/compile/collect_modules.rs:698-841` and `crates/perry-codegen/src/expr/dyn_extern_i18n.rs:116-369`). Therefore "dynamic require" can never mean "resolve arbitrary runtime strings." Both tiers respect this boundary. ## Tier 1 — ambient `require` backed by `createRequire` (closes #5373) Stop emitting `ReferenceError`. In the HIR ident-read fall-through (`crates/perry-hir/src/lower/lower_expr.rs:666-689`), add an arm so a bare unshadowed `require` lowers to a real `createRequire`-backed binding instead of `js_global_get_or_throw_unresolved("require")`. Effects: - Computed `require(builtin)` (e.g. the `node:os` repro in #5373) works out of the box — `require_thunk` already resolves builtins by string. - Computed `require(package)` changes failure mode from a confusing `ReferenceError` to the existing descriptive `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`. - Runtime backing already exists (`module_require.rs`); no new infra. **Scope guard:** gate to external/compilePackages modules (`ctx.is_external_module`). In *user* source, the bare-`require` compile-time error is deliberate ("Closes #668", points users at static imports) and must not regress into a silent runtime path. Tests: `crates/perry/tests/create_require_package.rs`, `crates/perry/tests/module_import_forms.rs:526-586` (currently asserts the "unsupported interop" diagnostic). ## Tier 2 — const-foldable / globbable computed require (follow-on) Extend coverage from builtins to **statically-resolvable** package/relative computed specifiers — e.g. `require(\`./plugins/${name}\`)`, `require(cond ? 'a' : 'b')`. Recommended approach reuses the proven dynamic-`import()` pipeline: - const-fold/glob the arg to a finite specifier set at compile time (machinery exists: `collect_modules.rs:698-841`, `expand_dynamic_import_glob`, 64-target cap), - add each target to the module graph as a dynamic edge (`per_module_dyn_import_targets`), - emit a `js_string_equals` dispatch chain → `<prefix>__init()` + load `@__perry_ns_<prefix>`, - **synchronous + CJS namespace shape** (require returns the value directly, not a promise; the one real divergence from `import()` — needs care around `module.exports` vs ESM namespace), - genuinely-computed specifiers fall through to `require_thunk` → builtin-or-throw (unchanged graceful behavior). **Out of scope (by design):** genuinely runtime-computed package strings (e.g. `['n','o',…].join('')` resolving to a package, network/dynamic specifiers). Same boundary as dynamic `import()`. **Rejected alternative:** a net-new runtime spec→`{init_fn, ns_global}` registry looked up in `require_thunk`. It resolves the *same* set of modules as the approach above (the AOT limit is unchanged), at the cost of net-new infra in `artifacts.rs`/`compile.rs`/`module_require.rs`. Only revisit if `require.cache` identity semantics force a single canonical namespace store. ## Plan - [ ] Tier 1: ambient `require` → createRequire-backed closure, gated to `is_external_module` (closes #5373) - [ ] Tier 2: extend the dynamic-import resolver to the synchronous CJS require shape for const-foldable/globbable specifiers