mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Contract checks compile in the self-hosted compiler
Narrow IR nodes by kind instead of structural casts, and pass concrete arrays instead of Iterable, so the self-hosting build of the compiler accepts the contract checker and the may-throw explanations.
This commit is contained in:
@@ -546,7 +546,11 @@ test("explain names the first throwing operation and each call edge", () => {
|
||||
const callLoc = { file: "tdz.ts", start: 5, end: 9 };
|
||||
const mod = moduleWith(
|
||||
fn("thrower", [exprStmt(nodeThrow)], []),
|
||||
fn("caller", [exprStmt({ kind: "call", callee: "thrower", args: [], type: VOID, loc: callLoc })], []),
|
||||
fn(
|
||||
"caller",
|
||||
[exprStmt({ kind: "call", callee: "thrower", args: [], type: VOID, loc: callLoc })],
|
||||
[],
|
||||
),
|
||||
fn("quiet", [exprStmt(value)], []),
|
||||
);
|
||||
const result = computeMayThrow(mod, { explain: true });
|
||||
|
||||
@@ -448,7 +448,6 @@ const ERROR_KIND_NAMES: readonly string[] = [
|
||||
|
||||
/** A short description of a node that seeds the may-throw fixpoint. */
|
||||
function describeThrowingNode(rec: IrExpr | IrStmt): string {
|
||||
const detail = rec as { method?: string; fn?: string; name?: string };
|
||||
switch (rec.kind) {
|
||||
case "throw":
|
||||
return "throw statement";
|
||||
@@ -494,10 +493,17 @@ function describeThrowingNode(rec: IrExpr | IrStmt): string {
|
||||
: "Error";
|
||||
return message?.kind === "strLit" ? `throws ${name}: ${message.value}` : `throws ${name}`;
|
||||
}
|
||||
return `library call ${detail.fn ?? ""}`.trimEnd();
|
||||
return `library call ${rec.fn}`;
|
||||
}
|
||||
case "intrinsic":
|
||||
return `intrinsic ${detail.name ?? ""}`.trimEnd();
|
||||
return `intrinsic ${rec.name}`;
|
||||
case "strIntrinsic":
|
||||
case "regexIntrinsic":
|
||||
case "arrIntrinsic":
|
||||
case "bytesIntrinsic":
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
return `${rec.kind} ${rec.method}`;
|
||||
case "arraySetLength":
|
||||
return "array length assignment (RangeError)";
|
||||
case "jsonStringify":
|
||||
@@ -508,7 +514,7 @@ function describeThrowingNode(rec: IrExpr | IrStmt): string {
|
||||
case "recordSet":
|
||||
return "write into a possibly published object (TypeError)";
|
||||
default:
|
||||
return detail.method !== undefined ? `${rec.kind} ${detail.method}` : rec.kind;
|
||||
return rec.kind;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ class ContractChecker {
|
||||
private readonly globalsByLoc = new Map<string, string[]>();
|
||||
private readonly classesByLoc = new Map<string, IrClassDef[]>();
|
||||
private readonly contracts: IrContracts = {};
|
||||
/** Whether any verified fact was recorded. */
|
||||
private recorded = false;
|
||||
|
||||
constructor(
|
||||
readonly mod: IrModule,
|
||||
@@ -233,13 +235,14 @@ class ContractChecker {
|
||||
immutable.run();
|
||||
if (this.diagnostics.length > 0) return { diagnostics: this.diagnostics, contracts: null };
|
||||
const c = this.contracts;
|
||||
const empty = Object.values(c).every((v) => v === undefined || (v as unknown[]).length === 0);
|
||||
return { diagnostics: [], contracts: empty ? null : c };
|
||||
return { diagnostics: [], contracts: this.recorded ? c : null };
|
||||
}
|
||||
|
||||
record(field: keyof IrContracts, values: Iterable<string>): void {
|
||||
record(field: keyof IrContracts, values: readonly string[]): void {
|
||||
const list = [...new Set(values)].sort();
|
||||
if (list.length > 0) this.contracts[field] = list;
|
||||
if (list.length === 0) return;
|
||||
this.contracts[field] = list;
|
||||
this.recorded = true;
|
||||
}
|
||||
|
||||
/* ── NoThrow ── */
|
||||
@@ -349,10 +352,10 @@ class Int32Contracts {
|
||||
if (this.fields.size + this.params.size + this.returns.size + this.locals.size === 0) return;
|
||||
this.structural();
|
||||
this.writes();
|
||||
this.c.record("int32Fields", this.fields.keys());
|
||||
this.c.record("int32Params", this.params.keys());
|
||||
this.c.record("int32Returns", this.returns.keys());
|
||||
this.c.record("int32Locals", this.locals.keys());
|
||||
this.c.record("int32Fields", [...this.fields.keys()]);
|
||||
this.c.record("int32Params", [...this.params.keys()]);
|
||||
this.c.record("int32Returns", [...this.returns.keys()]);
|
||||
this.c.record("int32Locals", [...this.locals.keys()]);
|
||||
}
|
||||
|
||||
private refuse(note: ContractNote, why: string, hint: string): void {
|
||||
@@ -595,11 +598,11 @@ class Int32Contracts {
|
||||
else if (!int32(arg)) fail(note, arg, arg.loc, "the argument for");
|
||||
});
|
||||
};
|
||||
const incDecStored = (e: IrExpr & { prefix: boolean; op: "+" | "-" }): boolean => {
|
||||
const incDecStored = (e: IrExpr, prefix: boolean, op: "+" | "-"): boolean => {
|
||||
const result = ranges.get(e);
|
||||
if (!result) return false;
|
||||
if (e.prefix) return withinInt32(result);
|
||||
return withinInt32(integerArithmeticRange(e.op, result, { min: 1, max: 1 }));
|
||||
if (prefix) return withinInt32(result);
|
||||
return withinInt32(integerArithmeticRange(op, result, { min: 1, max: 1 }));
|
||||
};
|
||||
walk(fn.body, {
|
||||
stmt: (s) => {
|
||||
@@ -639,7 +642,7 @@ class Int32Contracts {
|
||||
if (note && !int32(e.value)) fail(note, e.value, e.value.loc, "the value stored into");
|
||||
} else if (e.kind === "incDec") {
|
||||
const note = localNote(e.localId);
|
||||
if (note && !incDecStored(e))
|
||||
if (note && !incDecStored(e, e.prefix, e.op))
|
||||
c.report(
|
||||
"SC7002",
|
||||
e.loc,
|
||||
@@ -921,12 +924,9 @@ class ImmutableContracts {
|
||||
walk(fn.body, {
|
||||
stmt: (s) => {
|
||||
const value = s.kind === "varDecl" ? s.init : s.kind === "assign" ? s.value : undefined;
|
||||
if (
|
||||
value &&
|
||||
candidates.has((s as { localId: string }).localId) &&
|
||||
!this.fresh(value, candidates)
|
||||
) {
|
||||
candidates.delete((s as { localId: string }).localId);
|
||||
const target = s.kind === "varDecl" || s.kind === "assign" ? s.localId : "";
|
||||
if (value && candidates.has(target) && !this.fresh(value, candidates)) {
|
||||
candidates.delete(target);
|
||||
changed = true;
|
||||
}
|
||||
if (s.kind === "forOf" && candidates.delete(s.localId)) changed = true;
|
||||
@@ -1131,9 +1131,11 @@ class ImmutableContracts {
|
||||
store(`s:${s.shapeId}\0${s.field}`, s.value);
|
||||
break;
|
||||
case "recordKeySet":
|
||||
mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
|
||||
escape(s.value, s.loc, "a dynamically keyed property");
|
||||
break;
|
||||
case "recordKeyDelete":
|
||||
mutate((s as { obj: IrExpr }).obj, s.loc, `'${c.text(s.loc)}'`);
|
||||
if (s.kind === "recordKeySet") escape(s.value, s.loc, "a dynamically keyed property");
|
||||
mutate(s.obj, s.loc, `'${c.text(s.loc)}'`);
|
||||
break;
|
||||
case "arraySet":
|
||||
mutate(s.arr, s.loc, `'${c.text(s.loc)}'`);
|
||||
@@ -1143,7 +1145,7 @@ class ImmutableContracts {
|
||||
case "arraySetUndefined":
|
||||
case "arrayDelete":
|
||||
case "bytesSet":
|
||||
mutate((s as { arr: IrExpr }).arr, s.loc, `'${c.text(s.loc)}'`);
|
||||
mutate(s.arr, s.loc, `'${c.text(s.loc)}'`);
|
||||
break;
|
||||
case "forOf": {
|
||||
const slot = this.localSlot(fn, s.localId);
|
||||
@@ -1279,8 +1281,14 @@ class ImmutableContracts {
|
||||
for (let steps = 0; steps < 16; steps++) {
|
||||
if (root.kind === "fieldGet" || root.kind === "recordGet") root = root.obj;
|
||||
else if (root.kind === "arrayGet") root = root.arr;
|
||||
else if (PASS_THROUGH.has(root.kind) && "value" in root)
|
||||
root = (root as { value: IrExpr }).value;
|
||||
else if (
|
||||
root.kind === "upcast" ||
|
||||
root.kind === "downcast" ||
|
||||
root.kind === "unionWrap" ||
|
||||
root.kind === "unionNarrow" ||
|
||||
root.kind === "assignExpr"
|
||||
)
|
||||
root = root.value;
|
||||
else if (root.kind === "varRef") {
|
||||
// Compiler temporaries (no source binding): follow their initializer.
|
||||
const id = root.localId;
|
||||
@@ -1320,8 +1328,9 @@ class ImmutableContracts {
|
||||
}
|
||||
case "recordGet":
|
||||
return (
|
||||
this.mayHold.has(`s:${(e.obj.type as { shapeId?: string }).shapeId ?? ""}\0${e.field}`) ||
|
||||
derived(e.obj)
|
||||
this.mayHold.has(
|
||||
`s:${e.obj.type.kind === "record" ? e.obj.type.shapeId : ""}\0${e.field}`,
|
||||
) || derived(e.obj)
|
||||
);
|
||||
case "arrayGet":
|
||||
return this.mayHold.has(`e:${typeKey(e.arr.type)}`) || derived(e.arr);
|
||||
@@ -1481,7 +1490,7 @@ function describeSink(e: IrExpr): string {
|
||||
case "jsExit":
|
||||
return "the JavaScript island";
|
||||
case "intrinsic":
|
||||
return `the runtime (${(e as { name: string }).name})`;
|
||||
return `the runtime (${e.name})`;
|
||||
default:
|
||||
return `an operation scriptc does not follow (${e.kind})`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user