Check static members a cycle class inherits from its base classes

This commit is contained in:
Chris Tate
2026-10-20 02:26:01 -05:00
parent 2fbc08e36d
commit 95837b91c5
4 changed files with 60 additions and 24 deletions
@@ -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<string>;
/** 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<string>();
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<string, CycleInitFlag>();
const statics = new Map<string, CycleInitFlag[]>();
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]) {
@@ -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 {
@@ -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);
@@ -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}`;
}