Repository navigation
cjs: a conditional require() of a target with no default export is never evaluated, in every conditional shape #10754
Description
Activity
- added a commit that references this issue
on Sep 19, 2026 Correcting my own framing: this issue's title and table are wrong. The discriminator is the require target's export shape, not the call-site syntax.
My reproducer varied two things at once and I attributed the difference to the wrong one. The
ifcase requireddep.cjs, which hasmodule.exports = 1. The other five requireda.cjs..e.cjs, which onlyconsole.log. So "theifshape works, the other five don't" was an artifact of my fixture, not a property of Perry.Crossing the two axes properly — 6 call-site shapes × {side-effect-only target,
module.exportstarget} × {taken, not taken}, 24 cells, two release arms each built in one invocation withPERRY_RUNTIME_DIRpinned:shape main, side-effect-only target main, exporting target if (go) require(t)never loads loads go && require(t)never loads loads go ? require(t) : 0never loads loads try { if (go) require(t) }never loads loads switch (go) { case true: … }never loads loads for (…; go && …;) require(t)never loads loads All six shapes are broken, and all six work — depending on the target. Not-taken is correct in all 24 cells. Node loads in all 12 taken cells.
The mechanism, which the corrected framing makes visible
There is exactly one evaluation site per specifier, not one per shape —
wrap.rsemits a singleif (specifier === 'S')arm inside the syntheticrequire()shim, reached whenever control flow reaches the call regardless of surrounding syntax. So "a site forifand not the others" was never structurally possible, and my framing sent people looking for one.What varies is what that arm does. On main it reads the deferred import binding, and
dyn_extern_i18n.rsfires<S>__init()at that read only when the binding is inctx.import_function_prefixes— i.e. only when the target has a recognized default export. The site exists in every shape and fires only for some targets.Two further findings
#10285 fixes all six shapes but introduces a wrong value. Routing through the path-module registry makes init export-shape-independent, but the registry only holds exports for CJS-wrapped modules. A file with no CommonJS marker isn't wrapped, so it registers an initializer and never exports:
cond ? require('./side-effect-only.cjs') : 0returnsundefinedunder #10285 where Node returns{}and main returns an object. Nothing in #10285's own 11-test suite caught it.Main also still has a residual #10437 eager hoist. Three value-returning shapes (
if (c) x = require(S),c && (x = require(S)), and the same in aforbody) load before the program's first statement — shapes the text scanner can't see.Fix in #10756, which takes #10285 and adds a registry-miss fallback discriminated by
__perry_has_path_module. Against the Node oracle: 12 lines differ on unfixed main, byte-for-byte match with the fix, in both default and--no-auto-optimizemodes. Verified through the gate rather than only by hand —run_parity_tests.sh --filter test_gap_ --filter 10754givesparity_failand harness exit 1 on main,PASSwith the fix.Retitling would be reasonable: the accurate description is "a conditional
require()of a target with no default export is never evaluated, in every conditional shape".- changed the title
[-]cjs: a conditional require() in short-circuit, ternary, try, switch or loop position is silently never evaluated[/-][+]cjs: a conditional require() of a target with no default export is never evaluated, in every conditional shape[/+]on Sep 19, 2026 - added a commit that references this issue
on Sep 19, 2026
Summary
On
main(v0.5.1605,91a566c8af), a top-level CommonJSrequire()in short-circuit, ternary,try,switchor loop-body position is never evaluated — the module does not load even when the branch is taken. Node loads it.The
ifshape is correct. Only the other conditional positions are affected.Reproduction
Same for a ternary:
Combined fixture covering all five shapes — short-circuit, ternary,
tryblock,switchcase, loop body — with--goset:All five are dropped. Compiled with the release compiler at
91a566c8af,PERRY_RUNTIME_DIRpinned to the matching artifact set.Why this is worse than the bug it replaced
The original defect (#10437) was that a conditional
requirewas hoisted into an eager synthetic import, so the target initialized even when the branch was never taken — wrong order, but the module did load. #10674 fixed that by deferring instead of hoisting, and itsif-shape handling is correct.In these five positions the deferral appears to have no evaluation site, so the require is silently discarded. A program that conditionally loads a module now behaves as though the module does not exist, with no error. That is a silent wrong answer rather than a visible failure.
Relationship to #10285
#10285 is open and explicitly claims these shapes — its description names "a branch, ternary, short-circuit operand, logical assignment,
tryblock,switchcase or loop body". It merges cleanly against currentmainand addscjs_wrap/deferred_requires.rs(254 lines) plus a 377-line integration suite, neither of which is onmain.It appears to have stalled because #10674 landed a different implementation of overlapping intent (train #10716, v0.5.1598) and #10285 was treated as superseded. It isn't: #10674 covers the
ifshape, #10285 claims all of them. Whether #10285 actually fixes these five is unverified here and is the next thing to check.Not verified
Whether this is a regression introduced by #10674 or a pre-existing gap it did not cover — establishing that needs a compiler built before that train, which this report did not do.