mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Arrays whose elements are class-backed interface values are native arrays of the element capsules, so iteration, indexing and most array methods no longer go through the checked-dynamic array, and method calls receive a receiver they can dispatch. Methods without a native form for these elements run on the array's live dynamic view. A read of an interface property signature has the property's static type and reads the field of an implementing class directly. Accessors, object literals and JavaScript classes keep the checked property read.
131 lines
4.6 KiB
TypeScript
131 lines
4.6 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("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(");
|
|
});
|
|
});
|