mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Devirtualize calls by the classes that are constructed (#907)
This commit is contained in:
@@ -654,9 +654,16 @@ export function emitClassShapes(
|
||||
nw.push(` call void @scr_obj_lifetime_note(ptr %o)`);
|
||||
}
|
||||
if (meta.hierarchy) {
|
||||
// The vtable word is written once, here, and never changes while the
|
||||
// object lives: its store and the virtual-call loads carry
|
||||
// !invariant.group. A recycled block may have held an object of
|
||||
// another class, so the result is laundered first: every lifetime
|
||||
// gets a fresh pointer identity for the group.
|
||||
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
|
||||
nw.push(
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` store ptr @${mangleVtInstance(cls.name)}, ptr %vtp`,
|
||||
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o.vt, i64 0, i32 1`,
|
||||
` store ptr @${mangleVtInstance(cls.name)}, ptr %vtp, !invariant.group !{}`,
|
||||
);
|
||||
}
|
||||
if (isEmitterRooted) {
|
||||
@@ -672,7 +679,7 @@ export function emitClassShapes(
|
||||
);
|
||||
}
|
||||
nw.push(...undefFieldInits(host, meta, nullable));
|
||||
nw.push(` ret ptr %o`, `}`, ``);
|
||||
nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``);
|
||||
defs.push(...nw);
|
||||
|
||||
if (traced) {
|
||||
|
||||
@@ -999,7 +999,8 @@ export function emitCallExpr(
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}`);
|
||||
// Set once at allocation (classes.ts launders each new object).
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
|
||||
B.line(
|
||||
`${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr, IrFunction } from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
type VirtualCall = IrExpr & { kind: "virtualCall" };
|
||||
|
||||
export interface DevirtualizeFacts {
|
||||
/** Bodies the reachability worklist retained. */
|
||||
reachable: ReadonlySet<string>;
|
||||
/** Classes with a constructor edge other than a subclass's super(). */
|
||||
instantiated: ReadonlySet<string>;
|
||||
/** LIBRARY mode: exported classes are open to the host. */
|
||||
library: boolean;
|
||||
files: readonly ts.SourceFile[];
|
||||
}
|
||||
|
||||
/** Post-reachability devirtualization. A `virtualCall` on static class C
|
||||
* becomes a direct call when exactly one implementation can run:
|
||||
*
|
||||
* - no override of the nearest declaration is reachable below C (the
|
||||
* original rule), or
|
||||
* - every class in C's subtree that may be instantiated EXACTLY resolves
|
||||
* the method to the same declaration. Classes reached only through a
|
||||
* subclass's super() never carry their own vtable, so an abstract base
|
||||
* with one implementation, or a concrete base that is never itself
|
||||
* constructed, dispatches to its single constructed implementation.
|
||||
*
|
||||
* Instantiation is conservative: any constructor edge other than super()
|
||||
* counts (`new`, class values, dynamic and library construction), as does
|
||||
* every class some retained IR allocates (`new`) or exposes as a class
|
||||
* value (its whole subtree: a construct thunk exists for it). Those are
|
||||
* the only native allocation paths; runtime-provided classes always
|
||||
* count. Subtrees whose classes the compiler cannot see completely
|
||||
* (decorated classes, dynamic bases, exported library classes) stay
|
||||
* virtual. Methods replaceable at runtime never reach this pass: their
|
||||
* calls take the checked property path instead of a virtualCall. */
|
||||
export function devirtualizeVirtualCalls(
|
||||
lowerer: Lowerer,
|
||||
functions: IrFunction[],
|
||||
facts: DevirtualizeFacts,
|
||||
): void {
|
||||
const sites = new ConstructedClasses(lowerer, functions, facts);
|
||||
for (const fn of functions)
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (statement) => statement,
|
||||
expr: (expression) =>
|
||||
expression.kind === "virtualCall" ? sites.devirtualize(expression) : expression,
|
||||
});
|
||||
}
|
||||
|
||||
class ConstructedClasses {
|
||||
private readonly lowered = new Set<string>();
|
||||
/** Classes some retained IR allocates or exposes as a class value. */
|
||||
private readonly allocated = new Set<string>();
|
||||
private readonly exportedNames: ReadonlySet<string>;
|
||||
private readonly openMemo = new Map<ClassInfo, boolean>();
|
||||
private readonly subtreeMemo = new Map<ClassInfo, ClassInfo[]>();
|
||||
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
private readonly facts: DevirtualizeFacts,
|
||||
) {
|
||||
const subtree = (name: string): void => {
|
||||
this.allocated.add(name);
|
||||
const info = lowerer.classes.get(name);
|
||||
if (info) for (const c of this.descendants(info)) this.allocated.add(c.def.name);
|
||||
};
|
||||
for (const fn of functions) {
|
||||
this.lowered.add(fn.name);
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
if (e.kind === "new") this.allocated.add(e.className);
|
||||
else if (e.kind === "classRef") subtree(e.className);
|
||||
else if (e.kind === "newValue" && e.callee.type.kind === "classval")
|
||||
subtree(e.callee.type.className);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
this.exportedNames = facts.library ? exportedNames(facts.files) : new Set();
|
||||
}
|
||||
|
||||
devirtualize(e: VirtualCall): IrExpr {
|
||||
const lowerer = this.lowerer;
|
||||
const info = lowerer.classes.get(e.className);
|
||||
const nearest = info && lowerer.findMethodOn(info, e.method);
|
||||
if (!info || !nearest) return e;
|
||||
if (
|
||||
nearest.sig.abstract !== true &&
|
||||
!this.descendants(info).some(
|
||||
(candidate) =>
|
||||
candidate !== info &&
|
||||
candidate.methods.has(e.method) &&
|
||||
this.facts.reachable.has(`%${candidate.def.name}.${e.method}`),
|
||||
)
|
||||
)
|
||||
return this.direct(e, info, nearest.declarer);
|
||||
if (this.open(info)) return e;
|
||||
let target: ClassInfo | null = null;
|
||||
for (const candidate of this.descendants(info)) {
|
||||
if (!this.mayInstantiate(candidate)) continue;
|
||||
const found = lowerer.findMethodOn(candidate, e.method);
|
||||
if (!found || found.sig.abstract === true) return e;
|
||||
if (target !== null && target !== found.declarer) return e;
|
||||
target = found.declarer;
|
||||
}
|
||||
if (target === null || runtimeProvided(target)) return e;
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e;
|
||||
return this.direct(e, info, target);
|
||||
}
|
||||
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr {
|
||||
const receiver = e.args[0]!;
|
||||
const typed: IrExpr =
|
||||
target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name)
|
||||
? this.lowerer.upcastTo(receiver, target.def.name)
|
||||
: // Every instance that can reach this site is a `target`: the
|
||||
// reinterpret is proven by the instantiation set, like a
|
||||
// checker-narrowed downcast.
|
||||
{
|
||||
kind: "downcast",
|
||||
value: receiver,
|
||||
type: { kind: "object", className: target.def.name },
|
||||
loc: receiver.loc,
|
||||
};
|
||||
return {
|
||||
kind: "call",
|
||||
callee: `%${target.def.name}.${e.method}`,
|
||||
args: [typed, ...e.args.slice(1)],
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The class and every class below it, in the isSubclassOf sense
|
||||
* (generic instances included). */
|
||||
private descendants(info: ClassInfo): ClassInfo[] {
|
||||
let out = this.subtreeMemo.get(info);
|
||||
if (out === undefined) {
|
||||
out = [info];
|
||||
for (const candidate of this.lowerer.classes.values())
|
||||
if (this.lowerer.isSubclassOf(candidate.def.name, info.def.name)) out.push(candidate);
|
||||
this.subtreeMemo.set(info, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private mayInstantiate(info: ClassInfo): boolean {
|
||||
return (
|
||||
this.facts.instantiated.has(info.def.name) ||
|
||||
this.allocated.has(info.def.name) ||
|
||||
runtimeProvided(info)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when C's subtree may hold instances the compiler cannot
|
||||
* enumerate: a decorator can replace a class, a dynamic base can change
|
||||
* the chain, and a library host can construct exported classes. */
|
||||
private open(info: ClassInfo): boolean {
|
||||
const memo = this.openMemo.get(info);
|
||||
if (memo !== undefined) return memo;
|
||||
const open = this.descendants(info).some(
|
||||
(c) =>
|
||||
c.classDecorators !== undefined ||
|
||||
c.callableBase !== undefined ||
|
||||
c.factoryBaseExpression !== undefined ||
|
||||
c.localClass?.baseExpression !== undefined ||
|
||||
(this.facts.library && this.exported(c)),
|
||||
);
|
||||
this.openMemo.set(info, open);
|
||||
return open;
|
||||
}
|
||||
|
||||
private exported(info: ClassInfo): boolean {
|
||||
const decl = info.decl;
|
||||
if (!decl) return false;
|
||||
if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return true;
|
||||
const name =
|
||||
decl.name?.text ??
|
||||
(ts.isVariableDeclaration(decl.parent) && ts.isIdentifier(decl.parent.name)
|
||||
? decl.parent.name.text
|
||||
: undefined);
|
||||
if (name === undefined) return false;
|
||||
if (ts.isVariableDeclaration(decl.parent)) {
|
||||
const statement = decl.parent.parent?.parent;
|
||||
if (
|
||||
statement !== undefined &&
|
||||
ts.isVariableStatement(statement) &&
|
||||
(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export) !== 0
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return this.exportedNames.has(name);
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeProvided(info: ClassInfo): boolean {
|
||||
return (
|
||||
info.decl === null ||
|
||||
info.builtinError === true ||
|
||||
info.builtinEmitter === true ||
|
||||
info.builtinStream !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/** Local names a module exports through `export { ... }` or
|
||||
* `export default name` (declarations carry their own modifier). */
|
||||
function exportedNames(files: readonly ts.SourceFile[]): ReadonlySet<string> {
|
||||
const names = new Set<string>();
|
||||
for (const sf of files)
|
||||
for (const statement of sf.statements) {
|
||||
if (
|
||||
ts.isExportDeclaration(statement) &&
|
||||
statement.moduleSpecifier === undefined &&
|
||||
statement.exportClause &&
|
||||
ts.isNamedExports(statement.exportClause)
|
||||
) {
|
||||
for (const element of statement.exportClause.elements)
|
||||
names.add((element.propertyName ?? element.name).text);
|
||||
} else if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)) {
|
||||
names.add(statement.expression.text);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
@@ -7594,7 +7594,7 @@ export function superCallStmt(
|
||||
info.decl ?? info.ctor!,
|
||||
)[0]!;
|
||||
}
|
||||
lowerer.noteEdge(`%${base.def.name}.constructor`);
|
||||
lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`);
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
|
||||
@@ -60,7 +60,7 @@ import {
|
||||
pathModuleValue,
|
||||
utilTypesModuleValue,
|
||||
} from "./lower-builtin-values.js";
|
||||
import { transformStmtList } from "../../ir/traverse.js";
|
||||
import { devirtualizeVirtualCalls } from "./devirtualize.js";
|
||||
import { RuntimeOptionalProvenance } from "./runtime-optional-provenance.js";
|
||||
import { presentAtUse, reassignedBindings } from "./runtime-optional-guards.js";
|
||||
import { NarrowingStability } from "./narrowing-stability.js";
|
||||
@@ -2465,6 +2465,9 @@ export class Lowerer {
|
||||
* can contain any retained descendant, but importing its declaration
|
||||
* alone does not make every descendant constructible. */
|
||||
private constructedClasses: Set<string> | null = null;
|
||||
/** Set while a subclass constructor's super() call notes its base
|
||||
* constructor edge: that edge does not instantiate the base exactly. */
|
||||
private superConstructorEdge = false;
|
||||
private readonly virtualEdges = new Map<ClassInfo, Set<string>>();
|
||||
|
||||
classCanBeConstructed(info: ClassInfo): boolean {
|
||||
@@ -6050,11 +6053,17 @@ export class Lowerer {
|
||||
};
|
||||
const reachable = new Set<string>();
|
||||
this.constructedClasses = new Set();
|
||||
// Every constructor edge except super() may create an instance of
|
||||
// exactly that class (`new C`, class values, library roots, dynamic
|
||||
// construction, ...).
|
||||
const instantiated = new Set<string>();
|
||||
const queue: string[] = [];
|
||||
const loweredUnits = new Map<string, IrFunction>();
|
||||
const initFunctions: IrFunction[] = [];
|
||||
const instanceFunctions: IrFunction[] = [];
|
||||
this.onEdge = (name: string): void => {
|
||||
if (!this.superConstructorEdge && name.endsWith(".constructor"))
|
||||
instantiated.add(name.slice(1, -".constructor".length));
|
||||
if (reachable.has(name)) return;
|
||||
reachable.add(name);
|
||||
if (units.has(name)) queue.push(name);
|
||||
@@ -6284,39 +6293,14 @@ export class Lowerer {
|
||||
...this.liftedFns,
|
||||
...this.implicitFns,
|
||||
];
|
||||
// The worklist can leave a virtual site with only its base method.
|
||||
// The worklist can leave a virtual site with a single implementation.
|
||||
// Devirtualize after construction/override reachability has settled.
|
||||
for (const fn of functions)
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (statement) => statement,
|
||||
expr: (expression) => {
|
||||
if (expression.kind !== "virtualCall") return expression;
|
||||
const info = this.classes.get(expression.className);
|
||||
const method = info && this.findMethodOn(info, expression.method);
|
||||
if (
|
||||
!method ||
|
||||
method.sig.abstract ||
|
||||
[...this.classes.values()].some(
|
||||
(candidate) =>
|
||||
this.isSubclassOf(candidate.def.name, expression.className) &&
|
||||
candidate.def.name !== expression.className &&
|
||||
candidate.methods.has(expression.method) &&
|
||||
reachable.has(`%${candidate.def.name}.${expression.method}`),
|
||||
)
|
||||
)
|
||||
return expression;
|
||||
return {
|
||||
kind: "call",
|
||||
callee: `%${method.declarer.def.name}.${expression.method}`,
|
||||
args: [
|
||||
this.upcastTo(expression.args[0]!, method.declarer.def.name),
|
||||
...expression.args.slice(1),
|
||||
],
|
||||
type: expression.type,
|
||||
loc: expression.loc,
|
||||
};
|
||||
},
|
||||
});
|
||||
devirtualizeVirtualCalls(this, functions, {
|
||||
reachable,
|
||||
instantiated,
|
||||
library: extraRoots !== undefined,
|
||||
files: parts.map((fp) => fp.sf),
|
||||
});
|
||||
this.reachableForArtifacts = reachable;
|
||||
return { reachable, result: this.finishModule(functions) };
|
||||
}
|
||||
@@ -6327,6 +6311,17 @@ export class Lowerer {
|
||||
if (this.onEdge) this.onEdge(name);
|
||||
}
|
||||
|
||||
/** A subclass constructor's super() edge: the base constructor runs, but
|
||||
* no instance of exactly the base class is created. */
|
||||
noteSuperConstructorEdge(name: string): void {
|
||||
this.superConstructorEdge = true;
|
||||
try {
|
||||
this.noteEdge(name);
|
||||
} finally {
|
||||
this.superConstructorEdge = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Discovery hook for virtual dispatch: a virtualCall on `info`'s static
|
||||
* class reaches the nearest declaration at/above it plus every override
|
||||
* on a STRICT descendant (receivers of sibling branches can't flow into
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, compileLibrary } from "../src/index.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-constructed-dispatch-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The vtable slot load a virtual call of `method` emits. */
|
||||
const slotLoad = (method: string): RegExp => new RegExp(`= load ptr, ptr %t\\d+ ; ${method}\\n`);
|
||||
const directCall = (cls: string, method: string): RegExp =>
|
||||
new RegExp(`call double @sc_bf__x25_${cls}_${method}\\(`);
|
||||
|
||||
const loop = `
|
||||
function run(s: Shape, n: number): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < n; i++) t += s.area(i & 1023);
|
||||
return t;
|
||||
}
|
||||
`;
|
||||
|
||||
test("an abstract base with one constructed implementation calls it directly", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
abstract class Shape { abstract area(x: number): number; }
|
||||
class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
class Square extends Shape { s = 3; area(x: number): number { return x * this.s; } }
|
||||
function make(): Shape { return new Circle(); }
|
||||
${loop}
|
||||
console.log(run(make(), 1000));
|
||||
`);
|
||||
expect(llvm).toMatch(directCall("Circle", "area"));
|
||||
expect(llvm).not.toMatch(slotLoad("area"));
|
||||
});
|
||||
|
||||
test("a concrete base reached only through super() calls the subclass directly", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
class Shape { f = 1; area(x: number): number { return x * this.f; } }
|
||||
class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
function make(): Shape { return new Circle(); }
|
||||
${loop}
|
||||
console.log(run(make(), 1000));
|
||||
`);
|
||||
expect(llvm).toMatch(directCall("Circle", "area"));
|
||||
expect(llvm).not.toMatch(slotLoad("area"));
|
||||
});
|
||||
|
||||
test("two constructed implementations keep the vtable call", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
abstract class Shape { abstract area(x: number): number; }
|
||||
class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
class Square extends Shape { s = 3; area(x: number): number { return x * this.s; } }
|
||||
${loop}
|
||||
console.log(run(new Circle(), 1000), run(new Square(), 1000));
|
||||
`);
|
||||
expect(llvm).toMatch(slotLoad("area"));
|
||||
});
|
||||
|
||||
test("a constructed concrete base keeps the vtable call", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
class Shape { f = 1; area(x: number): number { return x * this.f; } }
|
||||
class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
${loop}
|
||||
console.log(run(new Circle(), 1000), run(new Shape(), 1000));
|
||||
`);
|
||||
expect(llvm).toMatch(slotLoad("area"));
|
||||
});
|
||||
|
||||
test("a class value of a subclass keeps the vtable call", async () => {
|
||||
const llvm = await emitLlvm(`
|
||||
abstract class Shape { abstract area(x: number): number; }
|
||||
class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
class Square extends Shape { s = 3; area(x: number): number { return x * this.s; } }
|
||||
const kinds: unknown[] = [Square];
|
||||
${loop}
|
||||
console.log(run(new Circle(), 1000), kinds.length);
|
||||
`);
|
||||
expect(llvm).toMatch(slotLoad("area"));
|
||||
});
|
||||
|
||||
test("library mode keeps the vtable call for an exported class", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-constructed-dispatch-lib-"));
|
||||
try {
|
||||
await writeFile(
|
||||
join(dir, "entry.ts"),
|
||||
`
|
||||
export abstract class Shape { abstract area(x: number): number; }
|
||||
export class Circle extends Shape { r = 2; area(x: number): number { return x * this.r + 1; } }
|
||||
abstract class Inner { abstract size(x: number): number; }
|
||||
class Leaf extends Inner { size(x: number): number { return x + 1; } }
|
||||
function measure(s: Inner, x: number): number { return s.size(x); }
|
||||
${loop}
|
||||
export function result(): number { return run(new Circle(), 10) + measure(new Leaf(), 1); }
|
||||
`,
|
||||
);
|
||||
const profilePath = join(dir, "profile.json");
|
||||
await writeFile(
|
||||
profilePath,
|
||||
JSON.stringify({
|
||||
profile_format: 1,
|
||||
name: "constructed-dispatch",
|
||||
entry: "entry.ts",
|
||||
emission: "llvm",
|
||||
abi: {
|
||||
prefix: "cd_",
|
||||
init_symbol: "cd_init",
|
||||
sink_register_symbol: "cd_sink",
|
||||
collect_symbol: "cd_collect",
|
||||
},
|
||||
exports: [{ export: "result", symbol: "cd_result", params: [], returns: "f64" }],
|
||||
}),
|
||||
);
|
||||
const result = await compileLibrary({ profilePath, outDir: join(dir, "out") });
|
||||
expect(result.ok, JSON.stringify(result.ok ? [] : result.diagnostics)).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const llvm = await readFile(result.llvmPath, "utf8");
|
||||
expect(llvm).toMatch(slotLoad("area"));
|
||||
expect(llvm).not.toMatch(slotLoad("size"));
|
||||
expect(llvm).toMatch(directCall("Leaf", "size"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/constructed-class-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Method calls through a base-typed receiver when only some classes in the
|
||||
// hierarchy are ever constructed: an abstract base with one constructed
|
||||
// implementation, a concrete base reached only through super(), classes
|
||||
// constructed through class values, generic subclasses, Error subclasses,
|
||||
// accessors, template methods, and super calls. Classes declared but never
|
||||
// constructed or referenced stay out of the program.
|
||||
|
||||
// Abstract base: Square is declared but never constructed.
|
||||
abstract class Shape {
|
||||
abstract area(x: number): number;
|
||||
describe(): string {
|
||||
return `area(2)=${this.area(2)}`;
|
||||
}
|
||||
}
|
||||
class Circle extends Shape {
|
||||
r = 2;
|
||||
area(x: number): number {
|
||||
return x * this.r + 1;
|
||||
}
|
||||
}
|
||||
class Square extends Shape {
|
||||
s = 3;
|
||||
area(x: number): number {
|
||||
return x * this.s;
|
||||
}
|
||||
}
|
||||
function makeShape(): Shape {
|
||||
return new Circle();
|
||||
}
|
||||
function sumAreas(s: Shape, n: number): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < n; i++) t += s.area(i & 7);
|
||||
return t;
|
||||
}
|
||||
console.log(sumAreas(makeShape(), 100), makeShape().describe());
|
||||
|
||||
// Concrete base never constructed itself: its constructor runs only
|
||||
// through super().
|
||||
class Counter {
|
||||
base = 10;
|
||||
label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
step(x: number): number {
|
||||
return x + this.base;
|
||||
}
|
||||
get kind(): string {
|
||||
return "counter";
|
||||
}
|
||||
}
|
||||
class Doubler extends Counter {
|
||||
constructor() {
|
||||
super("doubler");
|
||||
}
|
||||
step(x: number): number {
|
||||
return 2 * x + super.step(0);
|
||||
}
|
||||
get kind(): string {
|
||||
return `doubling ${super.kind}`;
|
||||
}
|
||||
}
|
||||
function runCounter(c: Counter): string {
|
||||
let t = 0;
|
||||
for (let i = 0; i < 5; i++) t += c.step(i);
|
||||
return `${c.label} ${c.kind} ${t}`;
|
||||
}
|
||||
console.log(runCounter(new Doubler()));
|
||||
|
||||
// Middle class inherits the only implementation from the root.
|
||||
class Animal {
|
||||
sound(): string {
|
||||
return "...";
|
||||
}
|
||||
}
|
||||
class Dog extends Animal {
|
||||
sound(): string {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
class Puppy extends Dog {}
|
||||
class Cat extends Animal {
|
||||
sound(): string {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
const animals: Animal[] = [new Puppy(), new Puppy()];
|
||||
console.log(animals.map((a) => a.sound()).join(" "));
|
||||
|
||||
// Two constructed implementations keep dynamic dispatch.
|
||||
abstract class Op {
|
||||
abstract apply(x: number): number;
|
||||
}
|
||||
class Inc extends Op {
|
||||
apply(x: number): number {
|
||||
return x + 1;
|
||||
}
|
||||
}
|
||||
class Neg extends Op {
|
||||
apply(x: number): number {
|
||||
return -x;
|
||||
}
|
||||
}
|
||||
const ops: Op[] = [new Inc(), new Neg(), new Inc()];
|
||||
console.log(ops.reduce((acc, op) => op.apply(acc), 5));
|
||||
|
||||
// Construction through a class value counts as construction.
|
||||
abstract class Greeter {
|
||||
abstract greet(): string;
|
||||
}
|
||||
class Hello extends Greeter {
|
||||
greet(): string {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
class Hi extends Greeter {
|
||||
greet(): string {
|
||||
return "hi";
|
||||
}
|
||||
}
|
||||
function construct(k: typeof Greeter2): Greeter {
|
||||
return new k();
|
||||
}
|
||||
class Greeter2 extends Greeter {
|
||||
greet(): string {
|
||||
return "greeter2";
|
||||
}
|
||||
}
|
||||
class Hey extends Greeter2 {
|
||||
greet(): string {
|
||||
return "hey";
|
||||
}
|
||||
}
|
||||
function greetWith(g: Greeter): string {
|
||||
return g.greet();
|
||||
}
|
||||
console.log(greetWith(construct(Hey)), greetWith(new Hello()));
|
||||
|
||||
// Generic subclasses.
|
||||
abstract class Box<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
abstract show(): string;
|
||||
}
|
||||
class NumBox extends Box<number> {
|
||||
show(): string {
|
||||
return `n:${this.value}`;
|
||||
}
|
||||
}
|
||||
class StrBox extends Box<string> {
|
||||
show(): string {
|
||||
return `s:${this.value}`;
|
||||
}
|
||||
}
|
||||
const boxes: Box<number>[] = [new NumBox(4)];
|
||||
console.log(boxes.map((b) => b.show()).join());
|
||||
|
||||
// Template method: the base loop calls this.step() on the only
|
||||
// constructed subclass.
|
||||
abstract class Task {
|
||||
run(n: number): number {
|
||||
let t = 0;
|
||||
for (let i = 0; i < n; i++) t += this.step(i);
|
||||
return t;
|
||||
}
|
||||
abstract step(i: number): number;
|
||||
}
|
||||
class SquareTask extends Task {
|
||||
step(i: number): number {
|
||||
return i * i;
|
||||
}
|
||||
}
|
||||
class CubeTask extends Task {
|
||||
step(i: number): number {
|
||||
return i * i * i;
|
||||
}
|
||||
}
|
||||
console.log(new SquareTask().run(10));
|
||||
|
||||
// Error subclasses with an overridden method.
|
||||
class AppError extends Error {
|
||||
code(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
class NotFound extends AppError {
|
||||
code(): number {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
function codeOf(e: AppError): number {
|
||||
return e.code();
|
||||
}
|
||||
try {
|
||||
throw new NotFound("missing");
|
||||
} catch (e) {
|
||||
if (e instanceof AppError) console.log(codeOf(e), e.message);
|
||||
}
|
||||
|
||||
// Abstract accessor with one constructed implementation.
|
||||
abstract class Named {
|
||||
abstract get name(): string;
|
||||
abstract set name(v: string);
|
||||
}
|
||||
class Person extends Named {
|
||||
private n = "ann";
|
||||
get name(): string {
|
||||
return this.n;
|
||||
}
|
||||
set name(v: string) {
|
||||
this.n = v.toUpperCase();
|
||||
}
|
||||
}
|
||||
class Robot extends Named {
|
||||
get name(): string {
|
||||
return "robot";
|
||||
}
|
||||
set name(_v: string) {}
|
||||
}
|
||||
const who: Named = new Person();
|
||||
who.name = "bob";
|
||||
console.log(who.name);
|
||||
|
||||
// A second implementation constructed only on a path that runs later.
|
||||
abstract class Reader {
|
||||
abstract read(): string;
|
||||
}
|
||||
class FileReader extends Reader {
|
||||
read(): string {
|
||||
return "file";
|
||||
}
|
||||
}
|
||||
class NetReader extends Reader {
|
||||
read(): string {
|
||||
return "net";
|
||||
}
|
||||
}
|
||||
function pick(flag: boolean): Reader {
|
||||
return flag ? new NetReader() : new FileReader();
|
||||
}
|
||||
console.log(pick(false).read(), pick(true).read());
|
||||
@@ -90,7 +90,8 @@ class Dog extends Animal {
|
||||
}
|
||||
}
|
||||
const a: Animal = new Dog("rex");
|
||||
console.log(a.id(), a.speak());
|
||||
const b: Animal = new Animal("cat");
|
||||
console.log(a.id(), a.speak(), b.speak());
|
||||
`,
|
||||
);
|
||||
// The devirtualized call is a direct sc_f_ call of Animal's id...
|
||||
@@ -157,8 +158,9 @@ class LoudCell extends Cell {
|
||||
}
|
||||
}
|
||||
const c: Cell = new LoudCell();
|
||||
const plain: Cell = new Cell();
|
||||
c.v = 3;
|
||||
console.log(c.v, c.label);
|
||||
console.log(c.v, c.label, plain.label);
|
||||
`,
|
||||
);
|
||||
// The overridden getter dispatches through its slot...
|
||||
@@ -248,12 +250,14 @@ class SealedBox extends Box {
|
||||
}
|
||||
}
|
||||
const b: Box = new SealedBox();
|
||||
const plain: Box = new Box();
|
||||
try {
|
||||
b.v = 9;
|
||||
} catch {
|
||||
console.log("sealed");
|
||||
}
|
||||
console.log(b.v);
|
||||
plain.v = 2;
|
||||
console.log(b.v, plain.v);
|
||||
`,
|
||||
);
|
||||
// JS shadowing: the derived class's synthesized setter occupies the
|
||||
|
||||
Reference in New Issue
Block a user