From 95837b91c535c1d212de667dbc10ada4bef27a8b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 02:26:01 -0500 Subject: [PATCH] Check static members a cycle class inherits from its base classes --- .../frontend/lowering/cycle-initialization.ts | 52 ++++++++++++------- .../6441-cycle-class-early-access/log.ts | 10 +++- .../6441-cycle-class-early-access/main.ts | 13 ++++- .../6441-cycle-class-early-access/stats.ts | 9 ++-- 4 files changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index c31a1baef..a191d8420 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -47,9 +47,13 @@ export interface CycleInitFlag { * declaration initializes it (to undefined, which the pointer cannot * hold), so the flag alone decides its writes (markUninitializedWrites). */ uninitialized?: true; - /** A class binding: its references are the class's own IR forms and its - * static field storage (`global` is the flag itself). */ + /** A class binding: its references are the class's own IR forms and the + * static members it reaches, its own and its bases' (`global` is the flag + * itself). */ className?: string; + /** The class and its base classes, whose `%C.static:` functions a static + * member access through the class can name. */ + staticOwners?: readonly string[]; staticGlobals?: ReadonlySet; /** The source references to the class outside its own body, with the * local name each one spells (an import can rename the class; Node's @@ -172,6 +176,12 @@ function registerClassInitFlag( const flagId = `%g.class.${className}%initialized`; const global: IrGlobal = { id: flagId, name: "%initialized", type: BOOL, mutable: true }; lowerer.globalsList.push(global); + const staticOwners: string[] = []; + const staticGlobals = new Set(); + for (let c: ClassInfo | null = info; c; c = c.base) { + staticOwners.push(c.def.name); + for (const field of c.staticFields) staticGlobals.add(field.globalId); + } lowerer.classInitFlags.set(className, { global, flagId, @@ -180,7 +190,8 @@ function registerClassInitFlag( settled: binding.settled, cycle, className, - staticGlobals: new Set(info.staticFields.map((f) => f.globalId)), + staticOwners, + staticGlobals, references: classReferences(lowerer, binding.symbol, binding.name, cycle), }); } @@ -472,16 +483,22 @@ function rewriteFunction( for (const [id, flag] of lowerer.cycleInitFlags) { if (runsEarly(flag)) active.set(id, flag); } - // Classes, by name; their static field storage joins `active`. A class's - // own members run only once the class exists, or inside its own - // definition, where the inner binding is initialized. + // Classes, by name, and the static field storage each reaches (a base + // class's statics through a subclass too). A class's own members run only + // once the class exists, or inside its own definition, where the inner + // binding is initialized. const classes = new Map(); + const statics = new Map(); for (const [className, flag] of lowerer.classInitFlags) { if (!runsEarly(flag) || fn.name.startsWith(`%${className}.`)) continue; classes.set(className, flag); - if (flag.staticGlobals) for (const id of flag.staticGlobals) active.set(id, flag); + for (const id of flag.staticGlobals ?? []) statics.set(id, [...(statics.get(id) ?? []), flag]); } if (active.size === 0 && classes.size === 0) return; + const flagOf = (localId: string, loc: SrcLoc): CycleInitFlag | undefined => + active.get(localId) ?? statics.get(localId)?.find((flag) => coversReference(flag, loc)); + const namesStatic = (flag: CycleInitFlag, fnName: string): boolean => + (flag.staticOwners ?? []).some((owner) => fnName.startsWith(`%${owner}.static:`)); const classFlagOf = (e: IrExpr): CycleInitFlag | undefined => { if (classes.size === 0) return undefined; switch (e.kind) { @@ -495,8 +512,7 @@ function rewriteFunction( case "call": for (const [className, flag] of classes) { if ( - (e.callee.startsWith(`%${className}.static:`) || - e.callee === `%dyn.class.instanceof:${className}`) && + (namesStatic(flag, e.callee) || e.callee === `%dyn.class.instanceof:${className}`) && coversReference(flag, e.loc) ) return flag; @@ -504,10 +520,8 @@ function rewriteFunction( return undefined; case "closure": // A static method read as a value (`const f = K.m`) reads K. - for (const [className, flag] of classes) { - if (e.fnName.startsWith(`%${className}.static:`) && coversReference(flag, e.loc)) - return flag; - } + for (const flag of classes.values()) + if (namesStatic(flag, e.fnName) && coversReference(flag, e.loc)) return flag; return undefined; default: return undefined; @@ -580,9 +594,8 @@ function rewriteFunction( }; } if (e.kind !== "varRef" && e.kind !== "incDec" && e.kind !== "assignExpr") return e; - const flag = active.get(e.localId); + const flag = flagOf(e.localId, e.loc); if (flag === undefined) return e; - if (flag.className !== undefined && !coversReference(flag, e.loc)) return e; // `K.x = v` reads K before it evaluates v. if (e.kind === "assignExpr" && flag.className === undefined) { const t = temp(e.value.type); @@ -600,9 +613,8 @@ function rewriteFunction( }, stmt: (s: IrStmt): IrStmt => { if (s.kind !== "assign" || s.initializes) return s; - const flag = active.get(s.localId); + const flag = flagOf(s.localId, s.loc); if (flag === undefined) return s; - if (flag.className !== undefined && !coversReference(flag, s.loc)) return s; // `K.x = v` reads K before it evaluates v. if (flag.className !== undefined) return { kind: "block", body: [check(flag, s.loc), s], loc: s.loc }; @@ -648,7 +660,11 @@ function rewriteFunction( for (const [className, flag] of [...classes]) { if (flag.module !== initOf || !setsFlag(s, flag)) continue; classes.delete(className); - if (flag.staticGlobals) for (const id of flag.staticGlobals) active.delete(id); + for (const id of flag.staticGlobals ?? []) { + const rest = statics.get(id)!.filter((other) => other !== flag); + if (rest.length > 0) statics.set(id, rest); + else statics.delete(id); + } } const out = rewriteStmt(s); for (const [id, flag] of [...active]) { diff --git a/tests/corpus/6441-cycle-class-early-access/log.ts b/tests/corpus/6441-cycle-class-early-access/log.ts index 5c26b6259..072a8ccca 100644 --- a/tests/corpus/6441-cycle-class-early-access/log.ts +++ b/tests/corpus/6441-cycle-class-early-access/log.ts @@ -1,8 +1,16 @@ import { probe } from "./stats.ts"; -export class Sink { +class Base { + static count = 1; + static hello(): string { + return "hello"; + } +} + +export class Sink extends Base { readonly tag: string; constructor(tag: string) { + super(); this.tag = tag; } static describe(): string { diff --git a/tests/corpus/6441-cycle-class-early-access/main.ts b/tests/corpus/6441-cycle-class-early-access/main.ts index 05f2c7cc7..60555b264 100644 --- a/tests/corpus/6441-cycle-class-early-access/main.ts +++ b/tests/corpus/6441-cycle-class-early-access/main.ts @@ -2,11 +2,20 @@ // throws the temporal-dead-zone ReferenceError at the access, and the // same class works once its module finished. import { Sink, probed } from "./log.ts"; -import { early, earlyMethod, earlyRenamed, earlyCap } from "./stats.ts"; +import { + early, + earlyMethod, + earlyInherited, + earlyInheritedField, + earlyRenamed, + earlyCap, +} from "./stats.ts"; console.log("early:", early); console.log("early method:", earlyMethod); +console.log("early inherited:", earlyInherited); +console.log("early inherited field:", earlyInheritedField); console.log("early renamed:", earlyRenamed); console.log("early renamed constant:", earlyCap); console.log("probed:", probed); -console.log("late:", new Sink("late").tag, Sink.describe()); +console.log("late:", new Sink("late").tag, Sink.describe(), Sink.hello(), Sink.count); diff --git a/tests/corpus/6441-cycle-class-early-access/stats.ts b/tests/corpus/6441-cycle-class-early-access/stats.ts index 2bdaa22ba..f1b012855 100644 --- a/tests/corpus/6441-cycle-class-early-access/stats.ts +++ b/tests/corpus/6441-cycle-class-early-access/stats.ts @@ -9,16 +9,19 @@ 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, the class -// and a constant through renamed imports. +// 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. export const early = attempt(() => new Sink("early").tag); export const earlyMethod = attempt(() => { const describe = Sink.describe; return typeof describe; }); +export const earlyInherited = attempt(() => Sink.hello()); +export const earlyInheritedField = attempt(() => String(Renamed.count)); 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()} ${new Renamed("probe").tag} ${cap}`; + return `${new Sink("probe").tag} ${Sink.describe()} ${Sink.hello()} ${Sink.count} ${new Renamed("probe").tag} ${cap}`; }