Files
scriptc/packages/compiler/test/ir.test.ts
Chris Tate c08ab955ae Enforce linting and consistent source formatting
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
2026-10-05 17:51:24 -05:00

169 lines
5.8 KiB
TypeScript

import { expect, test } from "vitest";
import { validateModule } from "../src/ir/validate.js";
import { deserializeModule, serializeModule } from "../src/ir/serialize.js";
import { fibModule } from "./fixtures/fib-ir.js";
import { BOOL, F64, type IrModule } from "../src/ir/ir.js";
test("hand-built fib module validates", () => {
expect(validateModule(fibModule)).toEqual([]);
});
test("validator rejects Date union arms unsupported by the backends", () => {
const bad = structuredClone(fibModule);
bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }];
expect(validateModule(bad).map((e) => e.message)).toContain("union u-date: arm 0 is date");
});
test("fib module JSON round-trips", () => {
const json = serializeModule(fibModule);
expect(deserializeModule(json)).toEqual(fibModule);
});
test("validator rejects type mismatches and bad references", () => {
const loc = { file: "t.ts", start: 0, end: 0 };
const bad: IrModule = {
irVersion: 13,
sourceFile: "t.ts",
entry: "__main",
functions: [
{
name: "__main",
params: [],
returnType: { kind: "void" },
locals: [{ id: "x.0", name: "x", type: F64, mutable: false }],
body: [
// init type mismatch: bool into f64 local
{
kind: "varDecl",
localId: "x.0",
init: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
},
// undeclared local
{
kind: "assign",
localId: "y.0",
value: { kind: "numLit", value: 1, type: F64, loc },
loc,
},
// assign to immutable
{
kind: "assign",
localId: "x.0",
value: { kind: "numLit", value: 1, type: F64, loc },
loc,
},
// call to unknown function
{
kind: "exprStmt",
expr: { kind: "call", callee: "nope", args: [], type: F64, loc },
loc,
},
],
loc,
},
],
};
const errors = validateModule(bad).map((e) => e.message);
expect(errors).toEqual([
expect.stringContaining("init: expected f64, got bool"),
expect.stringContaining('undeclared local/global "y.0"'),
expect.stringContaining('immutable local "x"'),
expect.stringContaining('undeclared function "nope"'),
]);
});
test("serializer round-trips ±Infinity and NaN", () => {
const mod = structuredClone(fibModule);
const fn = mod.functions[0]!;
const stmt = fn.body[0]!;
if (stmt.kind === "if" && stmt.cond.kind === "bin" && stmt.cond.right.kind === "numLit") {
stmt.cond.right.value = Infinity;
}
const back = deserializeModule(serializeModule(mod));
const stmt2 = back.functions[0]!.body[0]!;
if (stmt2.kind === "if" && stmt2.cond.kind === "bin" && stmt2.cond.right.kind === "numLit") {
expect(stmt2.cond.right.value).toBe(Infinity);
} else {
throw new Error("round-trip lost the statement shape");
}
if (stmt.kind === "if" && stmt.cond.kind === "bin" && stmt.cond.right.kind === "numLit") {
stmt.cond.right.value = -Infinity;
}
const back2 = deserializeModule(serializeModule(mod));
const stmt3 = back2.functions[0]!.body[0]!;
if (stmt3.kind === "if" && stmt3.cond.kind === "bin" && stmt3.cond.right.kind === "numLit") {
expect(stmt3.cond.right.value).toBe(-Infinity);
}
if (stmt.kind === "if" && stmt.cond.kind === "bin" && stmt.cond.right.kind === "numLit") {
stmt.cond.right.value = NaN;
}
const back3 = deserializeModule(serializeModule(mod));
const stmt4 = back3.functions[0]!.body[0]!;
if (stmt4.kind === "if" && stmt4.cond.kind === "bin" && stmt4.cond.right.kind === "numLit") {
expect(stmt4.cond.right.value).toBeNaN();
} else {
throw new Error("round-trip lost the statement shape");
}
});
test("deserializer rejects the previous IR version", () => {
const json = serializeModule(fibModule).replace('"irVersion": 13', '"irVersion": 12');
expect(() => deserializeModule(json)).toThrow(/version mismatch/);
});
test("recordClone survives the IR JSON round trip", () => {
const loc = { file: "clone.ts", start: 0, end: 1 };
const type = { kind: "record", shapeId: "r0" } as const;
const mod = structuredClone(fibModule);
mod.records = [
{
id: "r0",
fields: [{ name: "name", type: { kind: "string" } }],
},
];
mod.functions[0]!.locals.push({ id: "source.0", name: "source", type, mutable: false });
mod.functions[0]!.body.unshift({
kind: "exprStmt",
expr: {
kind: "recordClone",
source: { kind: "varRef", localId: "source.0", type, loc },
overrides: [
{ name: "name", value: { kind: "strLit", value: "next", type: { kind: "string" }, loc } },
],
type,
loc,
},
loc,
});
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
});
test("validator fences malformed recordClone nodes", () => {
const loc = { file: "clone.ts", start: 0, end: 1 };
const type = { kind: "record", shapeId: "r0" } as const;
const mod = structuredClone(fibModule);
mod.records = [{ id: "r0", fields: [{ name: "count", type: F64 }] }];
mod.functions[0]!.locals.push({ id: "source.0", name: "source", type, mutable: false });
mod.functions[0]!.body.unshift({
kind: "exprStmt",
expr: {
kind: "recordClone",
source: { kind: "varRef", localId: "source.0", type, loc },
overrides: [
{ name: "missing", value: { kind: "numLit", value: 1, type: F64, loc } },
{ name: "missing", value: { kind: "numLit", value: 2, type: F64, loc } },
],
type,
loc,
},
loc,
});
expect(validateModule(mod).map((e) => e.message)).toEqual(
expect.arrayContaining([
expect.stringContaining('has no field "missing"'),
expect.stringContaining('overrides field "missing" twice'),
]),
);
});