diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index a191d8420..092588a6e 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -70,6 +70,9 @@ interface ClassReference { name: string; } +/** The static field storage of a flag without a class. */ +const NO_STATIC_GLOBALS: ReadonlySet = new Set(); + /** Registers the initialization flags after global collection. Bindings * whose early access has no exact representation are refused: a `var` * (Node reads `undefined` there, but module storage only starts when the @@ -492,7 +495,8 @@ function rewriteFunction( for (const [className, flag] of lowerer.classInitFlags) { if (!runsEarly(flag) || fn.name.startsWith(`%${className}.`)) continue; classes.set(className, flag); - for (const id of flag.staticGlobals ?? []) statics.set(id, [...(statics.get(id) ?? []), flag]); + for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS) + statics.set(id, [...(statics.get(id) ?? []), flag]); } if (active.size === 0 && classes.size === 0) return; const flagOf = (localId: string, loc: SrcLoc): CycleInitFlag | undefined => @@ -660,7 +664,7 @@ function rewriteFunction( for (const [className, flag] of [...classes]) { if (flag.module !== initOf || !setsFlag(s, flag)) continue; classes.delete(className); - for (const id of flag.staticGlobals ?? []) { + for (const id of flag.staticGlobals ?? NO_STATIC_GLOBALS) { const rest = statics.get(id)!.filter((other) => other !== flag); if (rest.length > 0) statics.set(id, rest); else statics.delete(id); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a31a62e61..c33134488 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5408,8 +5408,18 @@ export function lowerStaticFieldRead( } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the - // chain above can never shadow it in TypeScript sources). + // chain above can never shadow it in TypeScript sources). A class of an + // import cycle reads it from its class object instead, whose reference + // checks the class's initialization (checkCycleBindingAccesses). if (expr.name.text === "name" && info.def.jsName !== undefined) { + if (lowerer.classInitFlags.get(info.def.name)?.cycle === true) + return { + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, info, expr.expression)], + type: STRING, + loc, + }; return { kind: "strLit", value: info.def.jsName, type: STRING, loc }; } if (info.callableBase) diff --git a/tests/corpus/6441-cycle-class-early-access/main.ts b/tests/corpus/6441-cycle-class-early-access/main.ts index 60555b264..5a87865cc 100644 --- a/tests/corpus/6441-cycle-class-early-access/main.ts +++ b/tests/corpus/6441-cycle-class-early-access/main.ts @@ -7,6 +7,8 @@ import { earlyMethod, earlyInherited, earlyInheritedField, + earlyName, + earlyRenamedName, earlyRenamed, earlyCap, } from "./stats.ts"; @@ -15,7 +17,9 @@ console.log("early:", early); console.log("early method:", earlyMethod); console.log("early inherited:", earlyInherited); console.log("early inherited field:", earlyInheritedField); +console.log("early name:", earlyName); +console.log("early renamed name:", earlyRenamedName); console.log("early renamed:", earlyRenamed); console.log("early renamed constant:", earlyCap); console.log("probed:", probed); -console.log("late:", new Sink("late").tag, Sink.describe(), Sink.hello(), Sink.count); +console.log("late:", new Sink("late").tag, Sink.describe(), Sink.hello(), Sink.count, Sink.name); diff --git a/tests/corpus/6441-cycle-class-early-access/stats.ts b/tests/corpus/6441-cycle-class-early-access/stats.ts index f1b012855..f41fa8c17 100644 --- a/tests/corpus/6441-cycle-class-early-access/stats.ts +++ b/tests/corpus/6441-cycle-class-early-access/stats.ts @@ -10,8 +10,8 @@ function attempt(run: () => string): string { // Each early access throws Node's ReferenceError, naming the binding the // access spells: construction, a static method read as a value, static -// members inherited from a base class, the class and a constant through -// renamed imports. +// members inherited from a base class, the class's name, the class and a +// constant through renamed imports. export const early = attempt(() => new Sink("early").tag); export const earlyMethod = attempt(() => { const describe = Sink.describe; @@ -19,9 +19,11 @@ export const earlyMethod = attempt(() => { }); export const earlyInherited = attempt(() => Sink.hello()); export const earlyInheritedField = attempt(() => String(Renamed.count)); +export const earlyName = attempt(() => Sink.name); +export const earlyRenamedName = attempt(() => Renamed.name); export const earlyRenamed = attempt(() => new Renamed("renamed").tag); export const earlyCap = attempt(() => String(cap)); export function probe(): string { - return `${new Sink("probe").tag} ${Sink.describe()} ${Sink.hello()} ${Sink.count} ${new Renamed("probe").tag} ${cap}`; + return `${new Sink("probe").tag} ${Sink.describe()} ${Sink.hello()} ${Sink.count} ${Sink.name} ${new Renamed("probe").tag} ${cap}`; }