Files
scriptc/packages/compiler/src/ir/callback-specialization.test.ts
T
Malte UblandClaude Opus 5.5 89bca68cbe Compute with NaN-boxed jsval values in the dynamic tier
Stage S3 of the two-tier design: dynamic-tier functions compute with
jsval, a new IR type holding a 64-bit NaN-boxed word whose numbers,
booleans, undefined and null are immediates, so a number between two
dynamic operators is no longer a heap node.

- IR: the jsval type and js.* builtins (operators and the boundary
  conversions fromDyn/toDyn/fromF64/fromBool/fromStr/fromBigInt/toF64).
  The jsval pass rewrites dynamic-tier functions after tier assignment:
  operator chains, comparisons, number checks and truthiness of operator
  results run on jsvals; dyn locals become jsval locals when that saves
  boxing; dynamic functions called only from dynamic functions return a
  jsval. IR version 17 serializes each function's tier.
- Runtime: scr_js.h and scr_js_value.c behind the dynTier feature
  (invariant I3); static runtime units never include scr_js.h.
- Backend: number cases inline (immediate doubles and NUM cells), always
  inlined jsval retain/release; 32-bit targets keep boxed cells.
- Interprocedural analyses see jsval conversions as the dyn forms they
  replace, so no fact about a static function changes.

Static functions are untouched: typed programs and the self-hosted
compiler's static functions compile to byte-identical LLVM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:11:32 -07:00

215 lines
7.4 KiB
TypeScript

import { expect, test } from "vitest";
import { specializeCallbackCalls } from "./callback-specialization.js";
import { BOOL, F64, VOID, funcOf, type IrExpr, type IrFunction, type IrModule } from "./ir.js";
import { everyStmtList } from "./traverse.js";
const loc = { file: "callbacks.ts", start: 0, end: 0 };
const callback = funcOf([F64], BOOL);
const ref = (localId: string, type = callback): IrExpr => ({ kind: "varRef", localId, type, loc });
const closure = (fnName: string, captures: string[] = []): IrExpr => ({
kind: "closure",
fnName,
captures,
type: callback,
loc,
});
const call = (callee: string, args: IrExpr[]): IrExpr => ({
kind: "call",
callee,
args,
type: BOOL,
loc,
});
const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc };
function predicate(name: string): IrFunction {
return {
name,
params: [{ localId: "n", name: "n", type: F64 }],
locals: [{ id: "n", name: "n", type: F64, mutable: true }],
returnType: BOOL,
body: [{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }],
loc,
};
}
/** `consume(test)` calls `test(1)`; `main` calls it once per target. */
function fixture(targets: string[]): IrModule {
const consumer: IrFunction = {
name: "consume",
sourceName: "consume",
ownsPrototype: true,
params: [{ localId: "test", name: "test", type: callback }],
locals: [{ id: "test", name: "test", type: callback, mutable: true }],
returnType: BOOL,
body: [
{
kind: "return",
value: { kind: "callValue", callee: ref("test"), args: [one], type: BOOL, loc },
loc,
},
],
loc,
};
const entry: IrFunction = {
name: "main",
params: [],
locals: [],
returnType: VOID,
body: targets.map((target) => ({
kind: "exprStmt" as const,
expr: call("consume", [closure(target)]),
loc,
})),
loc,
};
return {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [...new Set(targets)].map(predicate).concat([consumer, entry]),
};
}
function calleesOf(fn: IrFunction | undefined): string[] {
const out: string[] = [];
everyStmtList(fn?.body ?? [], {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call") out.push(expr.callee);
if (expr.kind === "closure") out.push(`closure:${expr.fnName}`);
return true;
},
});
return out;
}
const named = (mod: IrModule, name: string) => mod.functions.find((fn) => fn.name === name);
test("each callback target gets its own copy and the unreachable original goes", () => {
const mod = specializeCallbackCalls(fixture(["a", "b", "a"]));
expect(calleesOf(named(mod, "main"))).toEqual([
"consume.cb0",
"closure:a",
"consume.cb1",
"closure:b",
"consume.cb0",
"closure:a",
]);
expect(named(mod, "consume")).toBeUndefined();
const copy = named(mod, "consume.cb0")!;
expect(copy).toMatchObject({ sourceName: "consume", ownsPrototype: true });
// Copies own their nodes: analyses key facts by node identity.
expect(copy.body[0]).not.toBe(named(mod, "consume.cb1")!.body[0]);
expect(copy.locals[0]).not.toBe(named(mod, "consume.cb1")!.locals[0]);
});
test("one target, rebound parameters and oversized bodies keep the module", () => {
const single = fixture(["a", "a"]);
expect(specializeCallbackCalls(single)).toBe(single);
const rebound = fixture(["a", "b"]);
named(rebound, "consume")!.body.unshift({
kind: "assign",
localId: "test",
value: closure("a"),
loc,
});
expect(specializeCallbackCalls(rebound)).toBe(rebound);
const large = fixture(["a", "b"]);
const body = named(large, "consume")!.body;
for (let i = 0; i < 200; i++) body.unshift({ kind: "exprStmt", expr: one, loc });
expect(specializeCallbackCalls(large)).toBe(large);
});
test("a function value or an unknown callback keeps the original next to the copies", () => {
const valued = fixture(["a", "b"]);
named(valued, "main")!.body.push({
kind: "exprStmt",
expr: { ...closure("consume"), type: funcOf([callback], BOOL) },
loc,
});
const specialized = specializeCallbackCalls(valued);
expect(named(specialized, "consume")).toBeDefined();
expect(named(specialized, "consume.cb1")).toBeDefined();
const unknown = fixture(["a"]);
const entry = named(unknown, "main")!;
entry.params = [{ localId: "p", name: "p", type: callback }];
entry.locals = [{ id: "p", name: "p", type: callback, mutable: true }];
entry.body.push({ kind: "exprStmt", expr: call("consume", [ref("p")]), loc });
const mixed = specializeCallbackCalls(unknown);
expect(calleesOf(named(mixed, "main"))).toEqual(["consume.cb0", "closure:a", "consume"]);
});
test("recursion forwarding the callback stays in its copy", () => {
const mod = fixture(["a", "b"]);
named(mod, "consume")!.body.unshift({
kind: "exprStmt",
expr: call("consume", [ref("test")]),
loc,
});
const out = specializeCallbackCalls(mod);
expect(calleesOf(named(out, "consume.cb0"))).toEqual(["consume.cb0"]);
expect(calleesOf(named(out, "consume.cb1"))).toEqual(["consume.cb1"]);
expect(named(out, "consume")).toBeUndefined();
});
test("closures over a specialized callback get copies; recursion never copies per level", () => {
const mod = fixture(["a", "b"]);
const consumer = named(mod, "consume")!;
consumer.locals[0]!.boxed = true;
const inner: IrFunction = {
name: "%inner",
params: [{ localId: "n", name: "n", type: F64 }],
captures: [{ localId: "c", name: "c", type: callback }],
locals: [
{ id: "n", name: "n", type: F64, mutable: true },
{ id: "c", name: "c", type: callback, mutable: true, boxed: true },
],
returnType: BOOL,
body: [
{
kind: "return",
value: { kind: "callValue", callee: ref("c"), args: [ref("n", F64)], type: BOOL, loc },
loc,
},
],
loc,
};
mod.functions.push(inner);
// consume(test) also recurses with `(n) => test(n)`.
consumer.body.unshift({
kind: "exprStmt",
expr: call("consume", [closure("%inner", ["test"])]),
loc,
});
const out = specializeCallbackCalls(mod);
const forA = calleesOf(named(out, "main"))[0]!;
const [recursion, made] = calleesOf(named(out, forA));
// The original handles the recursion's new closures.
expect(recursion).toBe("consume");
expect(named(out, "consume")).toBeDefined();
expect(made).toMatch(/^closure:%inner\.cb\d$/);
const innerCopy = named(out, made!.slice("closure:".length))!;
expect(innerCopy.captures).toEqual(inner.captures);
expect(innerCopy.captures).not.toBe(inner.captures);
});
test("copies stay within the per-function budget", () => {
const targets = Array.from({ length: 10 }, (_, i) => `t${i}`);
const out = specializeCallbackCalls(fixture(targets));
const callees = calleesOf(named(out, "main")).filter((name) => !name.startsWith("closure:"));
expect(callees.filter((name) => name.startsWith("consume.cb"))).toHaveLength(4);
expect(callees.slice(4)).toEqual(Array(6).fill("consume"));
});
test("function-keyed contracts extend to the copies", () => {
const mod = fixture(["a", "b"]);
mod.contracts = { noThrow: ["consume"], int32Locals: ["consume\0test", "main\0x"] };
const out = specializeCallbackCalls(mod);
expect(out.contracts?.noThrow).toEqual(["consume", "consume.cb0", "consume.cb1"]);
expect(out.contracts?.int32Locals).toEqual([
"consume\0test",
"main\0x",
"consume.cb0\0test",
"consume.cb1\0test",
]);
});