mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
- Reuse decoded AST nodes, parent references, and recursive class type analysis during compilation. - Split expression and library validation into smaller functions while preserving diagnostics. - Shorten sanitizer bootstrap and reuse verified CI toolchains.
245 lines
14 KiB
TypeScript
245 lines
14 KiB
TypeScript
import { BOOL, F64, STRING, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrStmt, type IrType } from "../../packages/compiler/src/ir/ir.js";
|
|
import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/build.js";
|
|
|
|
const loc = { file: "validator-input.ts", start: 12, end: 34 };
|
|
const expression = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
|
|
const base = (): IrModule => ({
|
|
irVersion: 13, sourceFile: loc.file, entry: "main",
|
|
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
|
|
});
|
|
|
|
export interface ValidatorCase {
|
|
name: string;
|
|
module: IrModule;
|
|
/** A non-empty substring pins why malformed input is rejected. */
|
|
diagnostic?: string;
|
|
}
|
|
|
|
/** Structurally well-formed IR with valid and deliberately invalid semantic
|
|
* contracts. Each case can pass the checked JSON boundary; the production
|
|
* validator then owns the decision and diagnostic ordering. */
|
|
export function validatorCases(): ValidatorCase[] {
|
|
const cases: ValidatorCase[] = [];
|
|
const add = (name: string, change: (module: IrModule) => void, diagnostic?: string): void => {
|
|
const module = base();
|
|
change(module);
|
|
cases.push({ name, module, ...(diagnostic ? { diagnostic } : {}) });
|
|
};
|
|
add("empty module", () => {});
|
|
for (const direction of ["left", "right", "alternating"]) {
|
|
for (const invalid of [false, true]) {
|
|
add(`deep logical ${direction} ${invalid ? "invalid" : "valid"}`, (m) => {
|
|
let tree: IrExpr = invalid ? { kind: "boolLit", value: true, type: F64, loc } : boolLit(true, loc);
|
|
for (let depth = 0; depth < 128; depth++) {
|
|
const leaf = boolLit(depth % 2 === 0, loc);
|
|
const left = direction === "left" || (direction === "alternating" && depth % 2 === 0);
|
|
tree = { kind: "logical", op: depth % 2 === 0 ? "&&" : "||",
|
|
left: left ? tree : leaf, right: left ? leaf : tree, type: BOOL, loc };
|
|
}
|
|
m.functions[0]!.body = [expression(tree)];
|
|
}, invalid ? "boolLit must be bool" : undefined);
|
|
}
|
|
}
|
|
for (const arm of ["cond", "then", "else", "mixed"]) {
|
|
for (const invalid of [false, true]) {
|
|
add(`deep conditional ${arm} ${invalid ? "invalid" : "valid"}`, (m) => {
|
|
const nullable: IrType = { kind: "union", unionId: "optional-bool" };
|
|
m.unions = [{ id: "optional-bool", arms: [BOOL, { kind: "undefinedT" }] }];
|
|
let tree: IrExpr = invalid ? { kind: "boolLit", value: true, type: F64, loc } : boolLit(true, loc);
|
|
for (let depth = 0; depth < 128; depth++) {
|
|
const leaf = boolLit(depth % 2 === 0, loc);
|
|
if (arm === "mixed" && depth % 3 === 0) {
|
|
tree = { kind: "logical", op: "&&", left: leaf, right: tree, type: BOOL, loc };
|
|
} else if (arm === "mixed" && depth % 3 === 1) {
|
|
tree = { kind: "nullish", left: { kind: "unionWrap", unionId: "optional-bool", tag: 1,
|
|
value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc }, type: nullable, loc },
|
|
right: tree, type: BOOL, loc };
|
|
} else {
|
|
tree = { kind: "ternary", cond: arm === "cond" ? tree : leaf,
|
|
then: arm === "then" ? tree : leaf, else_: arm === "else" || arm === "mixed" ? tree : leaf, type: BOOL, loc };
|
|
}
|
|
}
|
|
m.functions[0]!.body = [expression(tree)];
|
|
}, invalid ? "boolLit must be bool" : undefined);
|
|
}
|
|
}
|
|
add("duplicate function", (m) => { m.functions.push(structuredClone(m.functions[0]!)); }, "duplicate function");
|
|
add("unknown local", (m) => { m.functions[0]!.body = [expression(varRef("missing", F64, loc))]; }, "missing");
|
|
add("parameter without local", (m) => {
|
|
m.functions[0]!.params = [{ name: "x", localId: "x", type: F64 }];
|
|
}, "has no local entry");
|
|
add("local read type", (m) => {
|
|
m.functions[0]!.locals = [{ id: "x", name: "x", type: F64, mutable: true }];
|
|
m.functions[0]!.body = [expression(varRef("x", STRING, loc))];
|
|
}, "varRef");
|
|
add("local initializer type", (m) => {
|
|
m.functions[0]!.locals = [{ id: "x", name: "x", type: F64, mutable: true }];
|
|
m.functions[0]!.body = [{ kind: "varDecl", localId: "x", init: strLit("wrong", loc), loc }];
|
|
}, "expected f64");
|
|
add("return type", (m) => {
|
|
m.functions[0]!.returnType = F64;
|
|
m.functions[0]!.body = [{ kind: "return", value: strLit("wrong", loc), loc }];
|
|
}, "return value");
|
|
add("missing return", (m) => { m.functions[0]!.returnType = F64; }, "return");
|
|
add("complete return", (m) => {
|
|
m.functions[0]!.returnType = F64;
|
|
m.functions[0]!.body = [{ kind: "if", cond: boolLit(true, loc),
|
|
then: [{ kind: "return", value: numLit(1, loc), loc }],
|
|
else_: [{ kind: "return", value: numLit(2, loc), loc }], loc }];
|
|
});
|
|
add("condition type", (m) => {
|
|
m.functions[0]!.body = [{ kind: "if", cond: numLit(1, loc), then: [], else_: null, loc }];
|
|
}, "if condition");
|
|
add("break outside loop", (m) => { m.functions[0]!.body = [{ kind: "break", loc }]; }, "break");
|
|
add("continue outside loop", (m) => { m.functions[0]!.body = [{ kind: "continue", loc }]; }, "continue");
|
|
add("label resolution", (m) => {
|
|
m.functions[0]!.body = [{ kind: "while", cond: boolLit(true, loc),
|
|
body: [{ kind: "break", label: "missing", loc }], loc }];
|
|
}, "missing");
|
|
add("loop control flow", (m) => {
|
|
m.functions[0]!.body = [{ kind: "for", init: null, cond: null, update: null, labels: ["outer"],
|
|
body: [{ kind: "while", cond: boolLit(true, loc), body: [{ kind: "break", label: "outer", loc }], loc }], loc }];
|
|
});
|
|
add("duplicate switch default", (m) => {
|
|
m.functions[0]!.body = [{ kind: "switch", disc: numLit(1, loc), cases: [
|
|
{ test: null, body: [] }, { test: null, body: [] },
|
|
], loc }];
|
|
}, "default clauses");
|
|
add("unknown function call", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "call", callee: "missing", args: [], type: VOID, loc })];
|
|
}, "missing");
|
|
add("call argument count", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "call", callee: "main", args: [numLit(1, loc)], type: VOID, loc })];
|
|
}, "args");
|
|
add("array index type", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "arrayGet", arr: { kind: "arrayLit", elems: [], type: arrayOf(F64), loc },
|
|
index: strLit("wrong", loc), type: F64, loc })];
|
|
}, "index");
|
|
add("array element type", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "arrayLit", elems: [strLit("wrong", loc)], type: arrayOf(F64), loc })];
|
|
}, "expected f64");
|
|
for (const collection of ["map", "set"] as const) {
|
|
for (const mixed of [false, true]) {
|
|
add(`${collection} ${mixed ? "mixed" : "identity"} key union`, (m) => {
|
|
const reference: IrType = { kind: "record", shapeId: "key" };
|
|
const key: IrType = { kind: "union", unionId: "keys" };
|
|
m.records = [{ id: "key", fields: [{ name: "id", type: F64 }] }];
|
|
m.unions = [{ id: "keys", arms: [reference, mixed ? STRING : arrayOf(F64)] }];
|
|
const expr: IrExpr = collection === "map"
|
|
? { kind: "mapNew", type: mapOf(key, F64), loc }
|
|
: { kind: "setNew", type: setOf(key), loc };
|
|
m.functions[0]!.body = [expression(expr)];
|
|
}, mixed ? (collection === "map" ? "mapNew key kind union" : "setNew element kind union") : undefined);
|
|
}
|
|
}
|
|
add("library signature", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "libCall", fn: "number.isFinite", args: [strLit("wrong", loc)], type: BOOL, loc })];
|
|
}, "expected f64");
|
|
for (const invalid of [false, true]) {
|
|
add(`library custom union result ${invalid ? "invalid" : "valid"}`, (m) => {
|
|
m.unions = [{ id: "env", arms: [STRING, { kind: "undefinedT" }] }];
|
|
m.functions[0]!.body = [expression({ kind: "libCall", fn: "process.envGet",
|
|
args: [strLit("PATH", loc)], type: invalid ? F64 : { kind: "union", unionId: "env" }, loc })];
|
|
}, invalid ? "must return the 'string | undefined' union" : undefined);
|
|
add(`library custom record result ${invalid ? "invalid" : "valid"}`, (m) => {
|
|
m.records = [{ id: "dirent", fields: [
|
|
{ name: "%dtype", type: invalid ? STRING : F64 },
|
|
{ name: "name", type: STRING }, { name: "parentPath", type: STRING },
|
|
] }];
|
|
m.functions[0]!.body = [expression({ kind: "libCall", fn: "fs.readdirTypesSync",
|
|
args: [strLit(".", loc)], type: arrayOf({ kind: "record", shapeId: "dirent" }), loc })];
|
|
}, invalid ? "must return the Dirent record array" : undefined);
|
|
}
|
|
for (const [fn, diagnostic] of [
|
|
["net.sockRead", "must return the 'Buffer | null' union"],
|
|
["spawnRes.status", "must return the 'number | null' union"],
|
|
["error.new", "must return a builtin error class"],
|
|
["stream.prop", "receiver must be a stream-hierarchy object"],
|
|
["emitter.new", "must return '%EventEmitter'"],
|
|
] as const) {
|
|
add(`library specialized validation ${fn}`, (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "libCall", fn, args: [], type: F64, loc })];
|
|
}, diagnostic);
|
|
}
|
|
add("library callback validation retains generic result check", (m) => {
|
|
m.functions[0]!.body = [expression({ kind: "libCall", fn: "cp.execFile", args: [
|
|
strLit("tool", loc), { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }, numLit(0, loc),
|
|
], type: F64, loc })];
|
|
}, "must be child");
|
|
add("duplicate records", (m) => { m.records = [{ id: "r", fields: [] }, { id: "r", fields: [] }]; }, "duplicate record");
|
|
add("record ordering", (m) => {
|
|
m.records = [{ id: "r", fields: [{ name: "z", type: F64 }, { name: "a", type: STRING }] }];
|
|
}, "canonical");
|
|
add("tuple positions", (m) => { m.records = [{ id: "r", tuple: true, fields: [{ name: "1", type: F64 }] }]; }, "tuple fields");
|
|
add("tuple with ten positions", (m) => {
|
|
m.records = [{ id: "tuple", tuple: true, fields: Array.from({ length: 12 }, (_, i) => ({ name: String(i), type: F64 }))
|
|
.sort((a, b) => a.name < b.name ? -1 : 1) }];
|
|
});
|
|
add("tuple duplicate positions", (m) => {
|
|
m.records = [{ id: "tuple", tuple: true, fields: [{ name: "0", type: F64 }, { name: "0", type: F64 }] }];
|
|
}, "tuple fields");
|
|
add("empty tuple", (m) => { m.records = [{ id: "tuple", tuple: true, fields: [] }]; }, "tuple fields");
|
|
add("recursive records", (m) => {
|
|
m.records = [{ id: "node", fields: [{ name: "children", type: arrayOf({ kind: "record", shapeId: "node" }) }] }];
|
|
});
|
|
add("undeclared record", (m) => {
|
|
m.records = [{ id: "node", fields: [{ name: "child", type: { kind: "record", shapeId: "missing" } }] }];
|
|
}, "undeclared shape");
|
|
add("duplicate union arms", (m) => { m.unions = [{ id: "u", arms: [F64, F64] }]; }, "identical");
|
|
add("short union", (m) => { m.unions = [{ id: "u", arms: [F64] }]; }, "fewer than 2");
|
|
add("union payload", (m) => {
|
|
m.unions = [{ id: "u", arms: [F64, STRING] }];
|
|
m.functions[0]!.body = [expression({ kind: "unionWrap", unionId: "u", tag: 0,
|
|
value: strLit("wrong", loc), type: { kind: "union", unionId: "u" }, loc })];
|
|
}, "unionWrap value");
|
|
add("discriminator valid", (m) => {
|
|
m.records = [
|
|
{ id: "empty", fields: [{ name: "kind", type: STRING }] },
|
|
{ id: "value", fields: [{ name: "kind", type: STRING }, { name: "value", type: F64 }] },
|
|
];
|
|
m.unions = [{ id: "u", arms: [{ kind: "record", shapeId: "empty" }, { kind: "record", shapeId: "value" }],
|
|
discriminant: { field: "kind", cases: [{ tag: 0, values: ["empty"] }, { tag: 1, values: ["number", "value"] }] } }];
|
|
});
|
|
const discriminated = structuredClone(cases[cases.length - 1]!.module);
|
|
for (const variant of ["missing tag", "duplicate value", "wrong literal type", "empty values"]) {
|
|
const m = structuredClone(discriminated);
|
|
const guard = m.unions![0]!.discriminant!;
|
|
if (variant === "missing tag") guard.cases.pop();
|
|
if (variant === "duplicate value") guard.cases[1]!.values = ["empty"];
|
|
if (variant === "wrong literal type") guard.cases[0]!.values = [false];
|
|
if (variant === "empty values") guard.cases[0]!.values = [];
|
|
cases.push({ name: "discriminator " + variant, module: m, diagnostic: "discriminant" });
|
|
}
|
|
add("FFI scalar declaration", (m) => {
|
|
m.ffiImports = [{ name: "sum", symbol: "native_sum", params: ["f64", "i32"], returns: "f64" }];
|
|
});
|
|
add("FFI callback and context", (m) => {
|
|
m.ffiImports = [{ name: "visit", symbol: "native_visit", params: [
|
|
{ callback: { id: "cb", params: ["f64", { context: "cb" }], returns: "void", lifetime: "call", invoke: "script-thread" } },
|
|
{ context: "cb" },
|
|
], returns: "void" }];
|
|
});
|
|
add("FFI mismatched contexts", (m) => {
|
|
m.ffiImports = [{ name: "visit", symbol: "native_visit", params: [
|
|
{ callback: { id: "cb", params: ["f64", { context: "cb" }], returns: "void", lifetime: "call", invoke: "script-thread" } },
|
|
], returns: "void" }];
|
|
}, "inconsistent context");
|
|
add("FFI foreign callback contract", (m) => {
|
|
m.ffiImports = [{ name: "visit", symbol: "native_visit", params: [
|
|
{ callback: { id: "cb", params: ["f64"], returns: "f64", lifetime: "call", invoke: "foreign" } },
|
|
], returns: "void" }];
|
|
}, "not retained");
|
|
add("FFI release without registration", (m) => {
|
|
m.ffiImports = [{ name: "release", symbol: "native_release", params: [
|
|
{ callback: { release: "register:cb", params: ["f64"], returns: "void" } },
|
|
], returns: "void" }];
|
|
}, "no retained target");
|
|
add("several independent diagnostics", (m) => {
|
|
m.entry = "missing";
|
|
m.records = [{ id: "tuple", tuple: true, fields: [] }];
|
|
m.unions = [{ id: "u", arms: [F64, F64] }];
|
|
m.functions[0]!.body = [{ kind: "break", loc }, expression(varRef("missing", F64, loc))];
|
|
}, "missing");
|
|
return cases;
|
|
}
|