diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 05f873112..d99a0fef4 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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) { diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index afd2ae7b0..00b766795 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -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}`, ); diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts new file mode 100644 index 000000000..25b052af3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -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; + /** Classes with a constructor edge other than a subclass's super(). */ + instantiated: ReadonlySet; + /** 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(); + /** Classes some retained IR allocates or exposes as a class value. */ + private readonly allocated = new Set(); + private readonly exportedNames: ReadonlySet; + private readonly openMemo = new Map(); + private readonly subtreeMemo = new Map(); + + 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 { + const names = new Set(); + 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; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8451f0abc..3e3cd9d3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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: { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a53784784..c9fa46e40 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 | 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>(); classCanBeConstructed(info: ClassInfo): boolean { @@ -6050,11 +6053,17 @@ export class Lowerer { }; const reachable = new Set(); 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(); const queue: string[] = []; const loweredUnits = new Map(); 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 diff --git a/packages/compiler/test/constructed-class-dispatch.test.ts b/packages/compiler/test/constructed-class-dispatch.test.ts new file mode 100644 index 000000000..0bd43e3f4 --- /dev/null +++ b/packages/compiler/test/constructed-class-dispatch.test.ts @@ -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 { + 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 }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructed-class-dispatch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructed-class-dispatch.ts.json new file mode 100644 index 000000000..f52350278 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructed-class-dispatch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructed-class-dispatch.ts" + ], + "diags": [] +} diff --git a/tests/corpus/constructed-class-dispatch.ts b/tests/corpus/constructed-class-dispatch.ts new file mode 100644 index 000000000..16325178d --- /dev/null +++ b/tests/corpus/constructed-class-dispatch.ts @@ -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 { + value: T; + constructor(value: T) { + this.value = value; + } + abstract show(): string; +} +class NumBox extends Box { + show(): string { + return `n:${this.value}`; + } +} +class StrBox extends Box { + show(): string { + return `s:${this.value}`; + } +} +const boxes: Box[] = [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()); diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index 0378ade7c..027b3e406 100644 --- a/tests/harness/inheritance.test.ts +++ b/tests/harness/inheritance.test.ts @@ -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