diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index c3edafddcc..d72792ae7c 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -148,17 +148,16 @@ function truthinessOnly(node: ts.Expression): boolean { } /** Which of the module's exports bindings the file ever assigns (any - * nesting, compound operators included). */ -const assignmentsCache = new WeakMap(); - + * nesting, compound operators included), cached per file on the lowerer + * (Lowerer.cjsExportsAssignments). */ function exportsAssignments( lowerer: Lowerer, sf: ts.SourceFile, ): { exports: boolean; moduleExports: boolean } { - const cached = assignmentsCache.get(sf); + const cached = lowerer.cjsExportsAssignments.get(sf); if (cached !== undefined) return cached; const out = { exports: false, moduleExports: false }; - assignmentsCache.set(sf, out); + lowerer.cjsExportsAssignments.set(sf, out); ts.walkPreorder(sf, (node) => { // A fresh `exports = module.exports = {}` reset leaves both bindings // naming one (new, empty) exports object for the rest of the module. diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 75474565b3..7f31337f5b 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -25,7 +25,6 @@ import { cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, - isInlineRequireCycleCall, isModuleExportsAccess, locOf, npmStaticDepSf7, @@ -109,7 +108,7 @@ export function inlineNamespaceRequireOf( /** `value` evaluated after the required module's run-once %init (and its * module-registry link) at the require's own position. */ export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr { - if (isInlineRequireCycleCall(req.call)) { + if (lowerer.inlineRequireCycleCalls.has(req.call)) { lowerer.unsupported( "SC1090", req.call, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6da1ede274..55447e1e08 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -753,6 +753,8 @@ export interface LowerOptions { * resolution walk and named-import link checks): the program * compiles to that startup crash. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** LIBRARY mode's reachability roots: the profile-mapped exports of the * entry module. Executable builds root at the entry's top level alone * (an unreferenced export dead-strips); a library's exports are called from @@ -845,6 +847,8 @@ export interface LowererMode { * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** The build's outbound native FFI declarations. */ ffiImports?: readonly IrFfiImport[]; /** LowerOptions.libraryCallbacks (see there). */ @@ -958,6 +962,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -981,6 +986,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1011,6 +1017,7 @@ export function lowerToIr( reachable, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1069,6 +1076,7 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, targetPlatform, ffiImports, libraryCallbacks, @@ -1628,6 +1636,15 @@ export class Lowerer { readonly checker: ts.TypeChecker; readonly diags: ScrDiagnostic[] = []; readonly fnSigsBySymbol = new Map(); + /** Per file: whether it assigns `exports` and `module.exports` + * (cjs-exports-object.ts). */ + readonly cjsExportsAssignments = new Map< + ts.SourceFile, + { exports: boolean; moduleExports: boolean } + >(); + /** Inline require() calls whose module edge closes a require cycle + * (LoadResult.inlineRequireCycleCalls). */ + readonly inlineRequireCycleCalls: ReadonlySet; /** Immutable callable aliases and compiler-generated adapters. The map * is symbol-keyed, so shadowing and cross-module imports keep the same * exact binding discipline as globals/functions. */ @@ -2859,6 +2876,7 @@ export class Lowerer { this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); this.projectCheckJs = mode.projectCheckJs ?? false; this.ffiImports = mode.ffiImports ?? []; this.libraryCallbacks = mode.libraryCallbacks ?? false; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 48d7e45a04..f7ab33429e 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -593,6 +593,7 @@ function loadFrontend( ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, + inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls, projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 47543007f4..8433338d03 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -338,6 +338,12 @@ export interface LoadResult { * last. Empty until checkPreflight runs (and empty when preflight fails * structurally). */ moduleOrder: ts.SourceFile[]; + /** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the + * static export globals do not model; their lowering refuses them by name + * at run time instead of refusing the whole program. Filled by + * checkPreflight. */ + inlineRequireCycleCalls?: ReadonlySet; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that @@ -975,9 +981,10 @@ export function loadProgram( * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { - const { diags, moduleOrder, startupCrash } = preflight7(load); + const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; + load.inlineRequireCycleCalls = inlineRequireCycleCalls; return diags; } @@ -2447,17 +2454,6 @@ export interface CycleEdge { call?: ts.CallExpression; } -/** Inline require() calls whose edge closes a require cycle. Node answers - * them with the still-evaluating module's partial exports, which the static - * export globals do not model; their lowering refuses them by name at run - * time instead of refusing the whole program (the inline call kept its - * runtime fence before it became a module edge). */ -const inlineRequireCycleCalls = new WeakSet(); - -export function isInlineRequireCycleCall(call: ts.Node): boolean { - return inlineRequireCycleCalls.has(call); -} - /** True when the reference at `node` can only evaluate AFTER the module * graph finished initializing: inside a function-like BODY, inside a * parameter (default initializers evaluate at call time), or inside an @@ -4133,6 +4129,7 @@ function preflight7(load: LoadResult): { diags: ScrDiagnostic[]; moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; + inlineRequireCycleCalls: ReadonlySet; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; @@ -5118,6 +5115,7 @@ function preflight7(load: LoadResult): { return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; }); const order: ts.SourceFile[] = []; + const inlineRequireCycleCalls = new Set(); const state = new Map(); const stack: string[] = []; const visit = (sf: ts.SourceFile): void => { @@ -5214,7 +5212,12 @@ function preflight7(load: LoadResult): { esmNamedImportLinkCheck(program, entry, order, diags), ); - return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash }; + return { + diags, + moduleOrder: order, + startupCrash: resolveCrash ?? linkCrash, + inlineRequireCycleCalls, + }; } /* ── the CJS named-import link check ───────────────────────────────────── diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index 1b35465188..fbb1a65fcc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -1,12 +1,12 @@ -// @deferred-fences: 9 +// @deferred-fences: 1 // Function expressions lowered speculatively — the `.call(this)` and // `.apply` receivers of CommonJS wrappers, and a computed class base — keep -// every statement of their bodies. A statement with no static lowering -// (here `module.exports` read as a value) compiles to its runtime fence -// instead of vanishing; these never run, so the binary matches Node. -// Coverage counts the three fenced statements nine times: each lowering of -// a wrapper body counts its statements, and the `.call` and `.apply` -// wrapper bodies are each lowered four times. +// every statement of their bodies. The wrappers' `root === module.exports` +// and `typeof module.exports` compile; a statement with no static lowering +// (here `module.exports` passed as a value in the class base) compiles to +// its runtime fence instead of vanishing. It never runs, so the binary +// matches Node. Coverage counts that one fenced statement once: the class +// base's body is lowered once. const verbose = process.argv.length > 99; (function () { var root = this;