mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
- 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.
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
import {
|
|
BOOL,
|
|
F64,
|
|
VOID,
|
|
arrayOf,
|
|
type IrExpr,
|
|
type IrFunction,
|
|
type IrModule,
|
|
type IrStmt,
|
|
} from "../../packages/compiler/src/ir/ir.js";
|
|
import { IR_VERSION } from "../../packages/compiler/src/ir/serialize.js";
|
|
|
|
export interface RangeCase {
|
|
name: string;
|
|
module: IrModule;
|
|
expected: { start: number; min: number | null; max: number | null; present?: boolean }[];
|
|
}
|
|
|
|
/** Source positions identify expression occurrences across serialization;
|
|
* object identity is deliberately exercised inside the native fixture too. */
|
|
export function integerRangeCases(): RangeCase[] {
|
|
let position = 0;
|
|
const loc = () => ({ file: "ranges.ts", start: position++, end: position });
|
|
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc: loc() });
|
|
const reference = (localId = "x"): IrExpr => ({ kind: "varRef", localId, type: F64, loc: loc() });
|
|
const binary = (
|
|
op: Extract<IrExpr, { kind: "bin" }>["op"],
|
|
left: IrExpr,
|
|
right: IrExpr,
|
|
): IrExpr => ({ kind: "bin", op, left, right, type: F64, loc: loc() });
|
|
const statement = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc: loc() });
|
|
const assign = (value: IrExpr, localId = "x"): IrStmt => ({
|
|
kind: "assign",
|
|
localId,
|
|
value,
|
|
loc: loc(),
|
|
});
|
|
const bool = (): IrExpr => ({ kind: "boolLit", value: true, type: BOOL, loc: loc() });
|
|
const fn = (body: IrStmt[]): IrFunction => ({
|
|
name: "main",
|
|
params: [],
|
|
locals: [{ id: "x", name: "x", type: F64, mutable: true }],
|
|
returnType: VOID,
|
|
body,
|
|
loc: loc(),
|
|
});
|
|
const cases: RangeCase[] = [];
|
|
const add = (
|
|
name: string,
|
|
body: IrStmt[],
|
|
expected: RangeCase["expected"],
|
|
transform?: (fn: IrFunction) => void,
|
|
): void => {
|
|
const func = fn(body);
|
|
transform?.(func);
|
|
cases.push({
|
|
name,
|
|
module: { irVersion: IR_VERSION, sourceFile: "ranges.ts", entry: "main", functions: [func] },
|
|
expected,
|
|
});
|
|
};
|
|
const proof = (expr: IrExpr, min: number | null, max = min, present = true) => ({
|
|
start: expr.loc.start,
|
|
min,
|
|
max,
|
|
present,
|
|
});
|
|
|
|
for (const value of [
|
|
0,
|
|
-0,
|
|
1,
|
|
-1,
|
|
0.5,
|
|
Infinity,
|
|
-Infinity,
|
|
Number.MAX_SAFE_INTEGER,
|
|
Number.MIN_SAFE_INTEGER,
|
|
Number.MAX_SAFE_INTEGER + 1,
|
|
]) {
|
|
const input = number(value);
|
|
const exact = Number.isSafeInteger(value) && !Object.is(value, -0) ? value : null;
|
|
add(
|
|
`literal ${Object.is(value, -0) ? "-0" : String(value)}`,
|
|
[statement(input)],
|
|
[proof(input, exact)],
|
|
);
|
|
}
|
|
for (const op of ["|", "&", "^", "<<", ">>", ">>>"] as const) {
|
|
const value = binary(op, reference(), number(0));
|
|
add(
|
|
`bitwise ${op}`,
|
|
[statement(value)],
|
|
[
|
|
proof(
|
|
value,
|
|
op === "&" || op === ">>>" ? 0 : -2147483648,
|
|
op === "&" ? 0 : op === ">>>" ? 4294967295 : 2147483647,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
for (const op of ["+", "-", "*", "/", "%"] as const) {
|
|
const value = binary(op, reference(), number(3));
|
|
const min = op === "+" ? 12 : op === "-" ? 6 : op === "*" ? 27 : op === "%" ? 0 : null;
|
|
add(
|
|
`arithmetic ${op}`,
|
|
[assign(number(9)), statement(value)],
|
|
[proof(value, min, op === "%" ? 2 : min)],
|
|
);
|
|
}
|
|
const overflow = binary("+", number(Number.MAX_SAFE_INTEGER), number(1));
|
|
add("inexact sum", [statement(overflow)], [proof(overflow, null)]);
|
|
const bounded = binary(
|
|
"+",
|
|
binary("|", reference(), number(0)),
|
|
binary(">>>", reference(), number(0)),
|
|
);
|
|
add(
|
|
"combined signed and unsigned bounds",
|
|
[statement(bounded)],
|
|
[proof(bounded, -2147483648, 6442450942)],
|
|
);
|
|
const unary: IrExpr = { kind: "unary", op: "~", operand: reference(), type: F64, loc: loc() };
|
|
add("bitwise complement", [statement(unary)], [proof(unary, -2147483648, 2147483647)]);
|
|
|
|
for (const storage of ["boxed", "tdz", "captured", "parameter", "global"] as const) {
|
|
const read = reference(storage === "global" ? "global" : "x");
|
|
const body = storage === "parameter" ? [statement(read)] : [assign(number(7)), statement(read)];
|
|
add(`${storage} storage is conservative`, body, [proof(read, null)], (func) => {
|
|
if (storage === "boxed") func.locals[0]!.boxed = true;
|
|
if (storage === "tdz") func.locals[0]!.tdz = true;
|
|
if (storage === "captured") func.captures = [{ localId: "x", name: "x", type: F64 }];
|
|
if (storage === "parameter") func.params = [{ localId: "x", name: "x", type: F64 }];
|
|
});
|
|
}
|
|
for (const suspension of ["async", "generator"] as const) {
|
|
const value = number(9);
|
|
add(
|
|
`${suspension} suspends analysis`,
|
|
[statement(value)],
|
|
[proof(value, null, null, false)],
|
|
(func) => {
|
|
if (suspension === "async") func.async = true;
|
|
else
|
|
func.generator = {
|
|
yieldT: F64,
|
|
nextT: F64,
|
|
resultType: { kind: "record", shapeId: "result" },
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
const regions = [
|
|
"block",
|
|
"if",
|
|
"while",
|
|
"doWhile",
|
|
"for",
|
|
"forOf",
|
|
"switch",
|
|
"tryCatch",
|
|
] as const;
|
|
for (const region of regions) {
|
|
const before = reference();
|
|
const inside = reference();
|
|
const after = reference();
|
|
const body = [assign(number(11)), statement(inside)];
|
|
let control: IrStmt;
|
|
switch (region) {
|
|
case "block":
|
|
control = { kind: "block", body, loc: loc() };
|
|
break;
|
|
case "if":
|
|
control = { kind: "if", cond: before, then: body, else_: [], loc: loc() };
|
|
break;
|
|
case "while":
|
|
control = { kind: "while", cond: before, body, loc: loc() };
|
|
break;
|
|
case "doWhile":
|
|
control = { kind: "doWhile", cond: before, body, loc: loc() };
|
|
break;
|
|
case "for":
|
|
control = {
|
|
kind: "for",
|
|
init: assign(before),
|
|
cond: bool(),
|
|
update: assign(number(100)),
|
|
body,
|
|
loc: loc(),
|
|
};
|
|
break;
|
|
case "forOf":
|
|
control = {
|
|
kind: "forOf",
|
|
localId: "x",
|
|
iterable: { kind: "arrayLit", elems: [before], type: arrayOf(F64), loc: loc() },
|
|
body,
|
|
loc: loc(),
|
|
};
|
|
break;
|
|
case "switch":
|
|
control = {
|
|
kind: "switch",
|
|
disc: before,
|
|
cases: [
|
|
{ test: number(1), body },
|
|
{ test: null, body: [] },
|
|
],
|
|
loc: loc(),
|
|
};
|
|
break;
|
|
case "tryCatch":
|
|
control = {
|
|
kind: "tryCatch",
|
|
tryBody: body,
|
|
catchBody: [statement(before)],
|
|
catchLocalId: null,
|
|
finallyBody: [],
|
|
loc: loc(),
|
|
};
|
|
break;
|
|
}
|
|
const expected = [
|
|
proof(inside, 11),
|
|
proof(
|
|
after,
|
|
region === "block" ? 11 : region === "if" ? 3 : null,
|
|
region === "block" || region === "if" ? 11 : null,
|
|
),
|
|
];
|
|
if (region !== "block")
|
|
expected.push(
|
|
proof(before, region === "if" || region === "for" || region === "forOf" ? 3 : null),
|
|
);
|
|
add(
|
|
`${region} regions retain only valid entry and exit facts`,
|
|
[assign(number(3)), control, statement(after)],
|
|
expected,
|
|
);
|
|
}
|
|
const thenRead = reference();
|
|
const elseRead = reference();
|
|
add(
|
|
"branches never pass facts to their siblings",
|
|
[
|
|
{
|
|
kind: "if",
|
|
cond: bool(),
|
|
then: [assign(number(4)), statement(thenRead)],
|
|
else_: [statement(elseRead)],
|
|
loc: loc(),
|
|
},
|
|
],
|
|
[proof(thenRead, 4), proof(elseRead, null)],
|
|
);
|
|
const caught = reference();
|
|
const final = reference();
|
|
add(
|
|
"catch and finally have independent facts",
|
|
[
|
|
{
|
|
kind: "tryCatch",
|
|
tryBody: [assign(number(4))],
|
|
catchBody: [statement(caught), assign(number(5))],
|
|
catchLocalId: null,
|
|
finallyBody: [statement(final)],
|
|
loc: loc(),
|
|
},
|
|
],
|
|
[proof(caught, null), proof(final, null)],
|
|
);
|
|
|
|
const children: IrExpr[] = [number(2), reference(), number(6)];
|
|
const opaque: IrExpr = {
|
|
kind: "seqExpr",
|
|
stmts: [assign(children[0]!), statement(children[1]!)],
|
|
result: children[2]!,
|
|
type: F64,
|
|
loc: loc(),
|
|
};
|
|
const afterOpaque = reference();
|
|
add(
|
|
"sequence expressions preserve evaluated writes and result facts",
|
|
[assign(number(1)), statement(opaque), statement(afterOpaque)],
|
|
[
|
|
proof(children[0]!, 2),
|
|
proof(children[1]!, 2),
|
|
proof(children[2]!, 6),
|
|
proof(opaque, 6),
|
|
proof(afterOpaque, 2),
|
|
],
|
|
);
|
|
const left = reference();
|
|
const hidden = reference();
|
|
const right: IrExpr = { kind: "call", callee: "unknown", args: [hidden], type: F64, loc: loc() };
|
|
const afterward = reference();
|
|
add(
|
|
"calls preserve uncaptured slots and already read operands",
|
|
[assign(number(5)), statement(binary("|", left, right)), statement(afterward)],
|
|
[proof(left, 5), proof(hidden, 5), proof(afterward, 5)],
|
|
);
|
|
const stored = reference();
|
|
const arr: IrExpr = { kind: "arrayLit", elems: [], type: arrayOf(F64), loc: loc() };
|
|
const afterStore = reference();
|
|
add(
|
|
"stores preserve unwritten slots and visit all operands",
|
|
[
|
|
assign(number(3)),
|
|
{ kind: "arraySet", arr, index: number(0), value: stored, loc: loc() },
|
|
statement(afterStore),
|
|
],
|
|
[proof(stored, 3), proof(afterStore, 3)],
|
|
);
|
|
const declared = reference();
|
|
const uninitialized = reference();
|
|
add(
|
|
"declaration reset",
|
|
[
|
|
{ kind: "varDecl", localId: "x", init: number(6), loc: loc() },
|
|
statement(declared),
|
|
{ kind: "varDecl", localId: "x", init: null, loc: loc() },
|
|
statement(uninitialized),
|
|
],
|
|
[proof(declared, 6), proof(uninitialized, null)],
|
|
);
|
|
const returned = reference();
|
|
const unreachable = reference();
|
|
add(
|
|
"return ends straight-line facts",
|
|
[assign(number(4)), { kind: "return", value: returned, loc: loc() }, statement(unreachable)],
|
|
[proof(returned, 4), proof(unreachable, null)],
|
|
);
|
|
const cursor = reference("cursor"),
|
|
decoded = reference("decoded"),
|
|
head = reference();
|
|
add(
|
|
"filtered strides bound output cursors and preserve decoded integers",
|
|
[
|
|
assign(number(0), "cursor"),
|
|
{
|
|
kind: "for",
|
|
init: { kind: "varDecl", localId: "x", init: number(0), loc: loc() },
|
|
cond: { kind: "bin", op: "<", left: head, right: number(100), type: BOOL, loc: loc() },
|
|
update: assign(binary("+", reference(), number(24))),
|
|
loc: loc(),
|
|
body: [
|
|
{
|
|
kind: "varDecl",
|
|
localId: "decoded",
|
|
init: binary(">>>", reference("input"), number(0)),
|
|
loc: loc(),
|
|
},
|
|
{
|
|
kind: "if",
|
|
cond: bool(),
|
|
then: [{ kind: "continue", loc: loc() }],
|
|
else_: null,
|
|
loc: loc(),
|
|
},
|
|
statement(cursor),
|
|
statement(decoded),
|
|
assign(binary("+", reference("cursor"), number(16)), "cursor"),
|
|
],
|
|
},
|
|
],
|
|
[proof(cursor, 0, 80), proof(decoded, 0, 4294967295), proof(head, 0, 124)],
|
|
(func) => {
|
|
for (const id of ["cursor", "decoded"])
|
|
func.locals.push({ id, name: id, type: F64, mutable: true });
|
|
},
|
|
);
|
|
return cases;
|
|
}
|