Files
scriptc/tests/harness/interface-dispatch.test.ts
T
Chris Tate 9843f8efd2 Inline interface dispatch and lend array elements to it
Interface dispatch now unboxes a receiver it has matched by key with one
load and no reference count change, and keeps its checked fallback out of
line so the key tests inline at each call site. A loop over an array of
interface values lends each element to a dispatch whose implementing
methods cannot release it; the fallback, which can run any code, takes
its own reference to the receiver.
2026-10-14 21:06:15 -05:00

175 lines
6.1 KiB
TypeScript

/* Interface method dispatch cost model (behavior is in the differential
* corpus, 5165-5169): a call through an interface implemented by program
* classes keeps its static argument and result types and reaches the
* implementing methods directly, while a method that an instance can
* replace keeps the checked dynamic call. */
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
async function emittedLlvm(name: string, source: string): Promise<string> {
const key = createHash("sha256").update(source).digest("hex").slice(0, 16);
const outDir = join(cacheDir, `iface-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.ts`);
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, name),
outDir,
backend: "llvm",
outputKind: "llvm",
});
if (!result.ok) {
throw new Error(
"interface program failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
return readFileSync(result.artifact.path, "utf8");
}
/** The bodies of a function and its numeric specializations. */
function bodies(llvm: string, name: string): string {
return llvm
.split("\ndefine ")
.filter((definition) => new RegExp(`^internal \\S+ @${name}[.(]`).test(definition))
.map((definition) => definition.slice(0, definition.indexOf("\n}\n")))
.join("\n");
}
const shapes = (extra: string): string => `interface Shape {
scale(k: number): number;
}
class Circle implements Shape {
constructor(public r: number) {}
scale(k: number): number {
return this.r * k;
}
}
class Rect implements Shape {
constructor(public w: number) {}
scale(k: number): number {
return this.w * k;
}
}
function run(s: Shape, n: number): number {
let t = 0;
for (let i = 0; i < n; i++) t += s.scale(i);
return t;
}
const circle = new Circle(2);
${extra}
console.log(run(circle, 10), run(new Rect(3), 10));
`;
describe("interface method dispatch", () => {
test("calls through an interface reach the implementing methods with typed values", async () => {
const llvm = await emittedLlvm("direct", shapes(""));
const run = bodies(llvm, "sc_bf_run");
expect(run).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/);
expect(run).not.toContain("@scr_dyn_call");
expect(run).not.toContain("@sc_bf__x25_dyn_class_property_");
const dispatch = llvm.slice(llvm.indexOf("; %dyn.class.protocol.0"));
expect(dispatch).toMatch(/call double @sc_bf__x25_Circle_scale\(ptr [^,]+, double/);
expect(dispatch).toMatch(/call double @sc_bf__x25_Rect_scale\(ptr [^,]+, double/);
});
test("arrays of interface values iterate natively and pass typed receivers", async () => {
const llvm = await emittedLlvm(
"array",
shapes(`function sum(list: Shape[]): number {
let t = 0;
for (const s of list) t += s.scale(2);
return t;
}
console.log(sum([circle, new Rect(1)]));`),
);
const sum = bodies(llvm, "sc_bf_sum");
expect(sum).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/);
expect(sum).not.toContain("@scr_dyn_iterator");
expect(sum).not.toContain("@sc_bf__x25_dyn_class_computed_");
});
test("property reads through an interface read the implementing field", async () => {
const llvm = await emittedLlvm(
"field",
`interface Sized {
size: number;
}
class Box implements Sized {
size = 3;
}
function size(s: Sized): number {
return s.size + 1;
}
console.log(size(new Box()));
`,
);
expect(bodies(llvm, "sc_bf_size")).not.toContain("@sc_bf__x25_dyn_class_property_");
const helper = llvm.indexOf("; %dyn.class.protocol.0");
expect(llvm.slice(helper, llvm.indexOf("\n}\n", helper))).toMatch(
/getelementptr inbounds %sc_o_Box, ptr [^,]+, i64 0, i32 \d+/,
);
});
test("matched receivers unbox without reference counting, ahead of an out-of-line fallback", async () => {
const llvm = await emittedLlvm("inline", shapes(""));
const helper = llvm.indexOf("; %dyn.class.protocol.0");
const dispatch = llvm.slice(llvm.lastIndexOf("define", helper), llvm.indexOf("\n}\n", helper));
expect(dispatch).toContain("; ->v.typed_ref.ptr");
expect(dispatch).not.toMatch(/call ptr @sc_retain_(?:Circle|Rect)\(/);
expect(dispatch).toMatch(/\binlinehint\b/);
expect(llvm).toMatch(
/define internal [^\n]*@sc_bf__x25_dyn_class_protocol_fallback_0[^\n]*\bnoinline\b/,
);
});
test("loops lend interface array elements to dispatch that cannot release them", async () => {
const lent = await emittedLlvm(
"lent",
shapes(`function sum(list: Shape[]): number {
let t = 0;
for (const s of list) t += s.scale(2);
return t;
}
console.log(sum([circle, new Rect(1)]));`),
);
expect(bodies(lent, "sc_bf_sum")).not.toContain("@scr_dyn_retain_v");
// A method that can empty the array keeps the owned element read.
const owned = await emittedLlvm(
"owned",
shapes(`const kept: Shape[] = [];
class Emptying implements Shape {
scale(k: number): number {
kept.length = 0;
return k;
}
}
kept.push(new Emptying(), circle);
function sum(list: Shape[]): number {
let t = 0;
for (const s of list) t += s.scale(2);
return t;
}
console.log(sum(kept));`),
);
expect(bodies(owned, "sc_bf_sum")).toContain("@scr_dyn_retain_v");
});
test("a method an instance can replace keeps the checked dynamic call", async () => {
const llvm = await emittedLlvm(
"replaced",
shapes(`(circle as unknown as { scale: (k: number) => number }).scale = (k) => -k;`),
);
const helper = llvm.indexOf("; %dyn.class.protocol.0");
const dispatch = llvm.slice(helper, llvm.indexOf("\n}\n", helper));
expect(dispatch).not.toContain("@sc_bf__x25_Circle_scale(");
expect(dispatch).not.toContain("@sc_bf__x25_Rect_scale(");
});
});