Files
scriptc/tests/harness/library-int.test.ts
T

1410 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Ask 4 end to end — profile-declared i64/u64 integer boundary slots with
* compile-time prove-or-refuse inference, over the REAL pipeline
* (frontend lowering, both emissions, real archives, real probes):
*
* corpus the reference package's §3 conformance corpus, all 16
* cases as library fixtures: `send(x)`/`sendU64(x)` are
* profile-mapped exports whose parameter the profile
* declares i64/u64, so every internal call site is a
* declared-slot crossing. PROVE cases compile on both
* emissions and every crossed value equals the Node
* oracle's number exactly (this test process IS Node — the
* oracle runs the same case source); REFUSE cases name the
* listed obligation with the slot path and evidence, on
* both emissions (the verdict is emission-invariant).
* returns the outbound declared-integer edge: i64/u64 RETURNS
* cross as real int64_t/uint64_t through the C ABI, pinned
* against the oracle's singletons (9007199254740991, 0 for
* -0, -1 for -7 % 3, 4294967295).
* inbound trap the host-contract edge: an inbound i64/u64 past
* ±(2^53 − 1) cannot ride f64 exactly, so the wrapper
* delivers the structured SC4012 trap (profile teaching +
* remediation riding it) instead of silently rounding;
* in-range extremes convert exactly.
* sidecar sidecar-declared slots (msg arms, record fields, helper
* params/returns): integer_slots emitted as the resolved
* attestation in declaration order, the declared slots
* spelled i64 in the document (V10 bijection), program-side
* writes proven or refused by slot path, and unresolvable
* or non-number declarations refused at projection.
* layer 1 the unobservability differential: scriptc implements NO
* Layer-1 integer representation, so the reference
* package's on/off differential is trivially satisfied;
* what CAN be pinned is the observable consequence — the
* same program compiled with the slot declared f64 vs i64
* crosses byte-identical values (declaring a slot adds
* proofs and wrapper conversions, never new semantics).
*/
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { compileLibrary, renderAll, validateSidecar } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const fixtureRoot = join(repoRoot, "tests/library-mode");
const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain";
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-int", flavor);
type Emission = "llvm" | "c";
const EMISSIONS: Emission[] = ["llvm", "c"];
/* ── the corpus (reference package §3, sources verbatim) ─────────────────
* `body` is the case's program text: top-level statements, or the body of
* `export function f(a: number): void` when `param` is set. The oracle
* executes the same text in this Node process. */
interface CorpusCase {
name: string;
body: string;
param?: boolean;
/** Host-side arguments fed to f through the wrapper (PROVE cases). */
args?: string[];
expected: "prove" | "refuse";
obligation?: "representability" | "wholeness" | "range";
code?: string;
slot?: string;
evidence?: string[];
}
const CORPUS: CorpusCase[] = [
{
name: "max-safe-integer-exact",
body: `send(2 ** 53 - 1);`,
expected: "prove",
},
{
name: "literal-not-representable",
body: `send(9007199254740993);`,
expected: "refuse",
obligation: "representability",
code: "SC4021",
slot: "exports.send.params[0]",
// The refusal fires on the SPELLING, and the evidence names both the
// author's text and its f64 read-back.
evidence: ["9007199254740993", "reads back as 9007199254740992"],
},
{
name: "proven-range-overflow",
body: `if (a >= 0 && a <= 2 ** 30) {\n const t = Math.trunc(a);\n send(t * t);\n}`,
param: true,
expected: "refuse",
obligation: "range",
code: "SC4023",
slot: "exports.send.params[0]",
evidence: ["±(2^53 − 1)"],
},
{
name: "negative-zero-crosses-as-zero",
body: `send(-0);`,
expected: "prove",
},
{
name: "times-half-unprovable",
body: `if (a >= 0 && a <= 1000) {\n const t = a * 0.5;\n send(t);\n}`,
param: true,
expected: "refuse",
obligation: "wholeness",
code: "SC4022",
slot: "exports.send.params[0]",
// The guard already proved the range and excluded NaN: the ONLY
// failed obligation is wholeness, and the evidence says so.
evidence: ["[0, 500]", "non-integers"],
},
{
name: "times-half-with-trunc",
body: `if (a >= 0 && a <= 1000) {\n const t = a * 0.5;\n send(Math.trunc(t));\n}`,
param: true,
args: ["0", "999", "1000", "500.5", "-3"],
expected: "prove",
},
{
name: "bounded-counter-loop",
body: `for (let n = 0; n < 10; n = n + 1) {\n send(n);\n}`,
expected: "prove", // the precision gate: 0..9 in order, pinned below
},
{
name: "division-non-integer",
body: `send(7 / 2);`,
expected: "refuse",
obligation: "wholeness",
code: "SC4022",
slot: "exports.send.params[0]",
},
{
name: "remainder-negative-dividend",
body: `send(-7 % 3);`,
expected: "prove",
},
{
name: "bitwise-or-int32",
body: `send(a | 0);`,
param: true,
args: ["3.7", "-2.5", "nan", "1e300", "4294967297"],
expected: "prove",
},
{
name: "unsigned-shift-u64",
body: `sendU64(a >>> 0);`,
param: true,
args: ["5.9", "-1", "nan", "4294967295"],
expected: "prove",
},
{
name: "u64-negative-proven-range",
body: `if (a >= -100 && a <= 100) {\n sendU64(Math.trunc(a));\n}`,
param: true,
expected: "refuse",
obligation: "range",
code: "SC4023",
slot: "exports.sendU64.params[0]",
evidence: ["[-100, 100]", "non-negative"],
},
{
name: "conditional-range-refinement",
body: `if (a >= 2 && a <= 6) {\n send(Math.round(a));\n}`,
param: true,
args: ["2", "5.4", "6", "1", "nan"],
expected: "prove",
},
{
name: "data-dependent-loop-bound",
body: `for (let n = 0; n < a; n = n + 1) {\n send(n);\n}`,
param: true,
expected: "refuse",
obligation: "range",
code: "SC4023",
slot: "exports.send.params[0]",
},
{
name: "nan-reaches-slot",
body: `send(0 / 0);`,
expected: "refuse",
obligation: "wholeness",
code: "SC4022",
slot: "exports.send.params[0]",
evidence: ["NaN"],
},
{
name: "infinity-reaches-slot",
body: `send(1 / 0);`,
expected: "refuse",
obligation: "range",
code: "SC4023",
slot: "exports.send.params[0]",
evidence: ["Infinity"],
},
];
const PRELUDE = `let crossed: number[] = [];
export function send(x: number): void { crossed.push(x); }
export function sendU64(x: number): void { crossed.push(x); }
export function count(): number { return crossed.length; }
export function at(i: number): number { return crossed[i]; }
`;
function corpusSource(c: CorpusCase): string {
return c.param === true ? `${PRELUDE}export function f(a: number): void {\n${c.body}\n}\n` : `${PRELUDE}${c.body}\n`;
}
function corpusProfile(c: CorpusCase, emission: Emission, entry: string, sendClass = "i64"): object {
return {
profile_format: 1,
name: "int-corpus",
entry,
emission,
abi: {
prefix: "kc_",
init_symbol: "kc_init",
sink_register_symbol: "kc_set_panic_sink",
collect_symbol: null,
result_reset_symbol: null,
},
exports: [
{ export: "send", symbol: "kc_send", params: [sendClass], returns: "void" },
{ export: "sendU64", symbol: "kc_send_u64", params: ["u64"], returns: "void" },
{ export: "count", symbol: "kc_count", params: [], returns: "f64" },
{ export: "at", symbol: "kc_at", params: ["f64"], returns: "f64" },
...(c.param === true ? [{ export: "f", symbol: "kc_f", params: ["f64"], returns: "void" }] : []),
],
};
}
/** The Node oracle: this test process runs the SAME case source and
* records what crossed. Number(arg) mirrors the probe's strtod. */
function nodeOracle(c: CorpusCase): number[] {
const crossed: number[] = [];
const send = (x: number): void => {
crossed.push(x);
};
const fn = new Function("send", "sendU64", "a", c.body) as (s: unknown, u: unknown, a?: number) => void;
if (c.param === true) {
for (const a of c.args ?? []) fn(send, send, Number(a));
} else {
fn(send, send);
}
return crossed;
}
async function buildCase(tag: string, source: string, profile: object): Promise<
| { ok: true; archive: string; outDir: string; sidecarPath?: string }
| { ok: false; diagnostics: { code: string; message: string; hint?: string; note?: string }[] }
> {
const outDir = join(cacheDir, tag);
mkdirSync(outDir, { recursive: true });
const entry = join(outDir, "lib.ts");
writeFileSync(entry, source);
const profilePath = join(outDir, "profile.json");
writeFileSync(profilePath, JSON.stringify({ ...profile, entry }, null, 2));
const result = await compileLibrary({ profilePath, outDir });
if (!result.ok) {
return {
ok: false,
diagnostics: result.diagnostics.map((d) => ({
code: d.code,
message: d.message,
...(d.hint !== undefined ? { hint: d.hint } : {}),
...(d.note !== undefined ? { note: d.note } : {}),
})),
};
}
return {
ok: true,
archive: result.archivePath,
outDir,
...(result.sidecarPath !== undefined ? { sidecarPath: result.sidecarPath } : {}),
};
}
function buildProbe(probeSrc: string, archive: string, outDir: string, defines: string[] = []): string {
const bin = join(outDir, "probe");
execFileSync("clang", ["-std=c11", ...defines.map((d) => `-D${d}`), probeSrc, archive, "-lm", "-o", bin]);
return bin;
}
function runProbe(bin: string, args: string[] = []): { stdout: string; status: number | null; signal: string | null } {
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 60_000 });
return { stdout: r.stdout ?? "", status: r.status, signal: r.signal };
}
/* ── the corpus, both emissions ──────────────────────────────────────── */
describe.each(EMISSIONS)("ask-4 corpus, %s emission", (emission) => {
for (const c of CORPUS) {
test(`${c.name} — ${c.expected.toUpperCase()}${c.obligation !== undefined ? ` (${c.obligation})` : ""}`, async () => {
const r = await buildCase(`corpus-${c.name}-${emission}`, corpusSource(c), corpusProfile(c, emission, "lib.ts"));
if (c.expected === "refuse") {
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual([c.code]);
const d = r.diagnostics[0]!;
// The teaching triple: the slot path, the failed obligation, the
// evidence, and (as the hint) the author's fix.
expect(d.message).toContain(`'${c.slot}'`);
expect(d.message).toContain(`${c.obligation} failed`);
for (const ev of c.evidence ?? []) expect(d.message).toContain(ev);
expect(d.hint).toBeTruthy();
return;
}
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
// The crossing values, pinned against the Node oracle exactly —
// this process IS Node, running the same case source.
const probe = buildProbe(join(fixtureRoot, "int-corpus/probe.c"), r.archive, r.outDir, c.param === true ? ["HAS_F"] : []);
const run = runProbe(probe, c.param === true ? (c.args ?? []) : []);
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
const got = run.stdout.split("\n").filter((l) => l !== "").map(Number);
const want = nodeOracle(c);
expect(got.length).toBe(want.length);
got.forEach((v, i) => {
expect(Object.is(v, want[i]), `crossing ${i}: got ${v}, Node says ${want[i]}`).toBe(true);
});
if (c.name === "bounded-counter-loop") {
// The precision gate's runtime half: 0 through 9, in order.
expect(got).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
});
}
/* ── outbound integer returns: real int64_t/uint64_t crossings ─────── */
test("declared integer returns cross as exact int64_t/uint64_t", async () => {
const dir = join(fixtureRoot, "int-returns");
const outDir = join(cacheDir, `int-returns-${emission}`);
mkdirSync(outDir, { recursive: true });
const profile = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8")) as { entry: string; emission: string };
profile.emission = emission;
profile.entry = join(dir, profile.entry);
const profilePath = join(outDir, "profile.json");
writeFileSync(profilePath, JSON.stringify(profile));
const result = await compileLibrary({ profilePath, outDir });
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const probe = buildProbe(join(dir, "probe.c"), result.archivePath, outDir);
const run = runProbe(probe);
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
// The Node oracle's numbers, converted exactly: 2**53-1, -0 as the
// mathematically exact integer 0, JS remainder's -1, ToUint32's max.
expect(run.stdout).toBe(`max=9007199254740991\nnegzero=0\nrem=-1\nu32max=4294967295\n`);
});
/* ── the inbound host-contract trap ─────────────────────────────────── */
test("an inbound integer past 2^53−1 traps SC4012; in-range extremes convert exactly", async () => {
const dir = join(fixtureRoot, "int-trap");
const outDir = join(cacheDir, `int-trap-${emission}`);
mkdirSync(outDir, { recursive: true });
const profile = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8")) as { entry: string; emission: string };
profile.emission = emission;
profile.entry = join(dir, profile.entry);
const profilePath = join(outDir, "profile.json");
writeFileSync(profilePath, JSON.stringify(profile));
const result = await compileLibrary({ profilePath, outDir });
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const probe = buildProbe(join(dir, "probe.c"), result.archivePath, outDir);
const ok = runProbe(probe, ["ok"]);
expect(ok.signal).toBeNull();
expect(ok.status).toBe(0);
expect(ok.stdout).toBe(
`i64 max: 9007199254740991\ni64 min: -9007199254740991\nu64 max: 9007199254740991\nok, sink_calls=0\n`,
);
for (const mode of ["trap-i64", "trap-u64"] as const) {
const run = runProbe(probe, [mode]);
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
// The structured SC4012 message: the profile's teaching as the
// text, the code, the trapping export's symbol, the remediation —
// and the host survives (nothing was silently rounded).
expect(run.stdout).toBe(
`sink[1]:
text=[this core's integer channel carries at most 2^53 - 1]
code=[SC4012]
symbol=[${mode === "trap-i64" ? "kt_take" : "kt_take_u"}]
remediation=[keep host-side ids within the exact-integer range]
fields=4
survived, sink_calls=1
`,
);
}
});
/* ── Layer-1 unobservability ────────────────────────────────────────── */
test("declaring the slot integer changes no program output (Layer 1 differential)", async () => {
// scriptc implements NO Layer-1 integer representation (semantics
// stay f64, byte-exact to Node), so the reference's on/off flip is
// trivially satisfied; the observable consequence pinned here: the
// SAME program under an f64-declared and an i64-declared send slot
// crosses byte-identical values — the declaration adds proofs and
// wrapper conversions, never semantics.
const loop = CORPUS.find((c) => c.name === "bounded-counter-loop")!;
const outputs: string[] = [];
for (const cls of ["f64", "i64"]) {
const r = await buildCase(
`layer1-${cls}-${emission}`,
corpusSource(loop),
corpusProfile(loop, emission, "lib.ts", cls),
);
expect(r.ok).toBe(true);
if (!r.ok) return;
const probe = buildProbe(join(fixtureRoot, "int-corpus/probe.c"), r.archive, r.outDir);
const run = runProbe(probe);
expect(run.status).toBe(0);
outputs.push(run.stdout);
}
expect(outputs[1]).toBe(outputs[0]);
});
});
/* ── the rendered refusals, snapshotted per code ─────────────────────────
* One program refusing all three obligations at once: the full rendered
* output (codes, spans, evidence, fixes) is the quality artifact — the
* diagnostics-corpus discipline applied to the library-only codes the
* executable-lane corpus cannot reach. */
test("SC4021/SC4022/SC4023 render with the teaching triple (snapshot)", async () => {
const source = `${PRELUDE}send(9007199254740993);
export function half(a: number): void {
if (a >= 0 && a <= 1000) {
send(a * 0.5);
}
}
export function grow(a: number): void {
if (a >= 0 && a <= 2 ** 30) {
const t = Math.trunc(a);
send(t * t);
}
}
`;
const c: CorpusCase = { name: "render", body: "", param: false, expected: "refuse" };
const profile = corpusProfile(c, "c", "lib.ts") as { exports: object[] };
profile.exports.push(
{ export: "half", symbol: "kc_half", params: ["f64"], returns: "void" },
{ export: "grow", symbol: "kc_grow", params: ["f64"], returns: "void" },
);
const outDir = join(cacheDir, "render-refusals");
mkdirSync(outDir, { recursive: true });
const entry = join(outDir, "lib.ts");
writeFileSync(entry, source);
const profilePath = join(outDir, "profile.json");
writeFileSync(profilePath, JSON.stringify({ ...profile, entry }, null, 2));
const result = await compileLibrary({ profilePath, outDir });
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.diagnostics.map((d) => d.code).sort()).toEqual(["SC4021", "SC4022", "SC4023"]);
const rendered = renderAll(result.diagnostics, result.sourceTexts, { color: false }).replaceAll(outDir + "/", "");
await expect(rendered).toMatchFileSnapshot("__snapshots__/library-int-refusals.txt");
});
/* ── sidecar-declared slots (msg arms, record fields, helpers) ───────────
* The projection resolves the profile's sidecar.integer_slots, spells the
* declared slots i64, emits the attestation list, and the inference
* proves every program-side write. Refusals are pre-emission and pinned
* emission-invariant by the corpus above, so one lane suffices here. */
const SIDECAR_ENTRY = `export type Scalar = number;
export type Count = Scalar;
export interface Model { total: Count; label: string; }
export type Msg = { kind: "count"; n: Count } | { kind: "clear" };
export function init(): Model { return { total: 0, label: "" }; }
export function update(m: Model, msg: Msg): Model { return m; }
let last: Msg = { kind: "clear" };
export function poke(v: number): void {
if (v >= 0 && v <= 100) { last = { kind: "count", n: Math.trunc(v) }; }
}
export function lastN(): number { return last.kind === "count" ? last.n : -1; }
export function clampIdx(m: Model, i: Count): Count { return i | 0; }
`;
function sidecarProfile(integerSlots: object[], patch: Record<string, unknown> = {}): object {
return {
profile_format: 1,
name: "int-sidecar",
entry: "lib.ts",
emission: "c",
abi: {
prefix: "ks_",
init_symbol: "ks_init",
sink_register_symbol: "ks_set_panic_sink",
collect_symbol: null,
result_reset_symbol: null,
},
exports: [
{ export: "poke", symbol: "ks_poke", params: ["f64"], returns: "void" },
{ export: "lastN", symbol: "ks_last_n", params: [], returns: "f64" },
],
sidecar: {
wire_version: 3,
abi_version: 1,
snapshot_format: 1,
build_id_symbol: "ks_build_id",
abi_version_symbol: "ks_abi_version",
model: "Model",
msg: "Msg",
integer_slots: integerSlots,
},
...patch,
};
}
describe("ask-4 sidecar-declared slots", () => {
const DECLARED = [
{ slot: "Msg.count", class: "i64" },
{ slot: "Model.total", class: "i64" },
{ slot: "helpers.clampIdx.return", class: "i64" },
];
test("integer_slots attest the declared classes; declared slots spell i64", async () => {
const r = await buildCase("sidecar-ok", SIDECAR_ENTRY, sidecarProfile(DECLARED));
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as {
integer_slots: { slot: string; class: string }[];
msg: { arms: { name: string; payload: { kind: string; class?: string } }[] };
types: { structs: { name: string; fields: { name: string; type: { kind: string } }[] }[] };
model_helpers: { name: string; returns: { kind: string } }[];
};
// The resolved-decision list, profile declaration order, each entry
// recording its DECLARED class (all i64 here; the TypeRef spellings
// below are the frozen format-1 vocabulary).
expect(doc.integer_slots).toEqual([
{ slot: "Msg.count", class: "i64" },
{ slot: "Model.total", class: "i64" },
{ slot: "helpers.clampIdx.return", class: "i64" },
]);
expect(doc.msg.arms.find((a) => a.name === "count")!.payload).toEqual({ kind: "number", class: "i64" });
const model = doc.types.structs.find((s) => s.name === "Model")!;
expect(model.fields.find((f) => f.name === "total")!.type).toEqual({ kind: "i64" });
expect(model.fields.find((f) => f.name === "label")!.type).toEqual({ kind: "bytes" });
expect(doc.model_helpers.find((h) => h.name === "clampIdx")!.returns).toEqual({ kind: "i64" });
// The document conforms end to end (V10's bijection included).
expect(validateSidecar(doc)).toEqual([]);
});
test("a u64 declaration attests u64; the TypeRef still spells i64", async () => {
// The attestation-level class: integer_slots records the DECLARED
// class in its own {i64, u64} vocabulary, while the frozen format-1
// TypeRef keeps spelling i64 (unsigned-ness is a boundary-slot
// refinement, not a type-table concept). The regression pinned here:
// u64 declarations used to flatten to i64 in the attestation itself,
// misstating the discharged proof's domain.
const r = await buildCase("sidecar-u64-attest", SIDECAR_ENTRY, sidecarProfile([{ slot: "Model.total", class: "u64" }]));
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as {
integer_slots: { slot: string; class: string }[];
types: { structs: { name: string; fields: { name: string; type: { kind: string } }[] }[] };
};
expect(doc.integer_slots).toEqual([{ slot: "Model.total", class: "u64" }]);
expect(doc.types.structs.find((s) => s.name === "Model")!.fields.find((f) => f.name === "total")!.type).toEqual({ kind: "i64" });
expect(validateSidecar(doc)).toEqual([]);
});
test("optional numbers compose with integer classes across records, msg arms, and helpers", async () => {
const source = `export interface Model { maybeId: number | null; }
export type Msg = { kind: "set"; id: number | null } | { kind: "clear" };
export function init(): Model { return { maybeId: null }; }
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "clear") return { maybeId: null };
return { maybeId: msg.id };
}
export function normalize(m: Model, id: number | null): number | null { return id; }
`;
const slots = [
{ slot: "Model.maybeId", class: "u64" },
{ slot: "Msg.set", class: "u64" },
{ slot: "helpers.normalize.params[0]", class: "u64" },
{ slot: "helpers.normalize.return", class: "u64" },
];
const r = await buildCase("sidecar-optional-int", source, sidecarProfile(slots, { exports: [] }));
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as {
integer_slots: { slot: string; class: string }[];
types: { structs: { name: string; fields: { name: string; type: unknown }[] }[] };
msg: { arms: { name: string; payload: { kind: string; type?: unknown } }[] };
model_helpers: { name: string; params: unknown[]; returns: unknown }[];
};
const optionalI64 = { kind: "optional", inner: { kind: "i64" } };
expect(doc.integer_slots).toEqual(slots);
expect(doc.types.structs.find((s) => s.name === "Model")!.fields).toEqual([
{ name: "maybeId", type: optionalI64 },
]);
expect(doc.msg.arms.find((a) => a.name === "set")!.payload).toEqual({
kind: "scalar",
type: optionalI64,
});
const helper = doc.model_helpers.find((h) => h.name === "normalize")!;
expect(helper.params).toEqual([optionalI64]);
expect(helper.returns).toEqual(optionalI64);
expect(validateSidecar(doc)).toEqual([]);
});
test("an optional record integer slot checks its present numeric arm", async () => {
const source = `export interface Model { maybeId: number | null; }
export type Msg = { kind: "clear" } | { kind: "noop" };
export function init(): Model { return { maybeId: null }; }
export function update(m: Model, msg: Msg): Model { return m; }
export function replace(m: Model, id: number): Model {
if (id >= 0 && id <= 100) return { maybeId: id * 0.5 };
return { maybeId: null };
}
`;
const r = await buildCase(
"sidecar-optional-int-refuse",
source,
sidecarProfile([{ slot: "Model.maybeId", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(
r.diagnostics.map((d) => d.code),
r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.maybeId'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("a unit-tag branch makes an optional integer return vacuous", async () => {
const source = `export interface Model { value: number; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model { return m; }
export function normalize(m: Model, x: number | null): number | null {
if (x === null) return x;
return 1;
}
`;
const r = await buildCase(
"sidecar-optional-int-unit-branch",
source,
sidecarProfile(
[{ slot: "helpers.normalize.return", class: "u64" }],
{ exports: [] },
),
);
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as unknown;
expect(validateSidecar(doc)).toEqual([]);
});
test("same-named fields with different field types remain distinct integer shapes", async () => {
const source = `export interface A { value: number; extra: string; }
export interface B { value: number; extra: boolean; }
export interface Model { a: A; b: B; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model {
return {
a: { value: 1, extra: "" },
b: { value: 2, extra: false },
};
}
export function update(m: Model, msg: Msg): Model { return m; }
`;
const slots = [
{ slot: "A.value", class: "u64" },
{ slot: "B.value", class: "i64" },
];
const r = await buildCase(
"sidecar-int-distinct-shapes",
source,
sidecarProfile(slots, { exports: [] }),
);
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as {
integer_slots: { slot: string; class: string }[];
types: { structs: { name: string; fields: { name: string; type: { kind: string } }[] }[] };
};
expect(doc.integer_slots).toEqual(slots);
for (const name of ["A", "B"]) {
expect(doc.types.structs.find((s) => s.name === name)!.fields.find((f) => f.name === "value")!.type).toEqual({ kind: "i64" });
}
expect(validateSidecar(doc)).toEqual([]);
});
test("duplicate-name composed union arms keep a containing record integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; a: number }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; b: string }
| { kind: "secondOnly" };
export type Nested = First | Second;
export interface Model { value: number; nested: Nested; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model {
return { value: 0.5, nested: { kind: "dup", b: "" } };
}
export function update(m: Model, msg: Msg): Model { return m; }
`;
const r = await buildCase(
"sidecar-int-composed-duplicate-arm-shape",
source,
sidecarProfile([{ slot: "Model.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("same-named composed optional-number arms each keep the integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; a: number | null }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; b: number | null }
| { kind: "secondOnly" };
export type Nested = First | Second;
export interface Model { nested: Nested; }
export type Msg = { kind: "fractional" } | { kind: "noop" };
export function init(): Model {
return { nested: { kind: "dup", a: 1 } };
}
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "fractional") {
return { nested: { kind: "dup", b: 0.5 } };
}
return m;
}
`;
const r = await buildCase(
"sidecar-int-composed-duplicate-optional-arm",
source,
sidecarProfile([{ slot: "Nested.dup", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Nested.dup'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("same-named composed number_bytes arms each keep the integer obligation live", async () => {
const source = `export type Count = number;
export type Text = string;
export type Bytes = Uint8Array;
export type First =
| { kind: "dup"; n: Count; payload: Text }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; n: Count; payload: Bytes }
| { kind: "secondOnly" };
export type Msg = First | Second;
export interface Model { value: number; }
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model {
const later: Msg = { kind: "dup", n: 0.5, payload: new Uint8Array(0) };
return later.n > 0 ? m : m;
}
`;
const r = await buildCase(
"sidecar-int-composed-duplicate-number-bytes",
source,
sidecarProfile([{ slot: "Msg.dup.n", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Msg.dup.n'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("a composed number_bytes arm cannot change the selected wire field names", async () => {
const source = `export type First =
| { kind: "dup"; n: number; a: string }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; n: number; b: string }
| { kind: "secondOnly" };
export type Msg = First | Second;
export interface Model { value: number; }
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model { return m; }
`;
const r = await buildCase(
"sidecar-int-composed-number-bytes-fields",
source,
sidecarProfile([{ slot: "Msg.dup.n", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain("'Msg.dup.n'");
expect(r.diagnostics[0]!.message).toContain("same required number-then-bytes fields");
});
test("computed record writes check every optional integer field they can select", async () => {
const source = `export interface Model {
maybe: number | null;
other: number | null;
}
export type Msg = { kind: "fractional" } | { kind: "noop" };
export function init(): Model { return { maybe: null, other: null }; }
function write(m: Model, key: keyof Model, value: number | null): void {
m[key] = value;
}
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "fractional") write(m, "maybe", 0.5);
return m;
}
`;
const r = await buildCase(
"sidecar-optional-int-computed-write",
source,
sidecarProfile([{ slot: "Model.maybe", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.maybe'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("null- and undefined-optional integer fields remain distinct IR shapes", async () => {
const source = `export interface A { value: number | null; }
export interface B { value: number | undefined; }
export interface Model { a: A; b: B; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model {
return {
a: { value: null },
b: { value: undefined },
};
}
export function update(m: Model, msg: Msg): Model { return m; }
`;
const slots = [
{ slot: "A.value", class: "u64" },
{ slot: "B.value", class: "i64" },
];
const r = await buildCase(
"sidecar-int-distinct-optional-shapes",
source,
sidecarProfile(slots, { exports: [] }),
);
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as {
integer_slots: { slot: string; class: string }[];
};
expect(doc.integer_slots).toEqual(slots);
expect(validateSidecar(doc)).toEqual([]);
});
test("deep structural record matching keeps a live integer obligation", async () => {
const depth = 40;
const declarations = Array.from({ length: depth + 1 }, (_, i) =>
i === depth
? `export interface N${i} { leaf: string }`
: `export interface N${i} { next: N${i + 1} }`,
).join("\n");
let nested = `{ leaf: "" }`;
for (let i = depth - 1; i >= 0; i--) nested = `{ next: ${nested} }`;
const source = `${declarations}
export interface Model { value: number; nested: N0 }
export type Msg = { kind: "noop" } | { kind: "other" }
export function init(): Model { return { value: 0.5, nested: ${nested} } }
export function update(m: Model, msg: Msg): Model { return m }
`;
const r = await buildCase(
"sidecar-int-deep-shape",
source,
sidecarProfile([{ slot: "Model.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("declared empty-object siblings do not make a live integer obligation vacuous", async () => {
const source = `export interface Empty {}
export interface Model { value: number; inline: {}; named: Empty }
export type Msg = { kind: "noop" } | { kind: "other" }
export function init(): Model {
return { value: 0.5, inline: {}, named: {} }
}
export function update(m: Model, msg: Msg): Model { return m }
`;
const r = await buildCase(
"sidecar-int-empty-object-sibling",
source,
sidecarProfile([{ slot: "Model.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test.each([
{
name: "an unsupported later sibling",
evidence: "tuple",
source: `export interface Model { value: number; pair: [number, number] }
export type Msg = { kind: "noop" } | { kind: "other" }
export function init(): Model { return { value: 0, pair: [1, 2] } }
export function update(m: Model, msg: Msg): Model { return m }
`,
},
{
name: "a recursive later sibling",
evidence: "cyclic",
source: `export interface Model { value: number; next: Model | null }
export type Msg = { kind: "noop" } | { kind: "other" }
export function init(): Model { throw new Error("unreachable") }
export function update(m: Model, msg: Msg): Model { return m }
`,
},
])("integer pattern construction preserves the SC4009 refusal for $name", async ({ name, evidence, source }) => {
const r = await buildCase(
`sidecar-int-invalid-shape-${name.replaceAll(" ", "-")}`,
source,
sidecarProfile([{ slot: "Model.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain(evidence);
});
test("same-shaped tagged-union integer slots refuse instead of overwriting an obligation", async () => {
const source = `export interface Model { value: number; }
export type Msg = { kind: "first"; id: number } | { kind: "second"; id: number };
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model { return m; }
`;
const r = await buildCase(
"sidecar-int-shape-collision",
source,
sidecarProfile(
[
{ slot: "Msg.first", class: "u64" },
{ slot: "Msg.second", class: "i64" },
],
{ exports: [] },
),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain("'Msg.first' (u64)");
expect(r.diagnostics[0]!.message).toContain("'Msg.second' (i64)");
expect(r.diagnostics[0]!.message).toContain("same lowered record field");
expect(r.diagnostics[0]!.hint).toContain("structurally identical");
});
test("an unproven update write into a declared model field refuses by slot path", async () => {
// The model-slot prove-or-refuse gate: a declared Model field class
// obligates every write the contract surface performs — init and
// update are force-lowered so their record constructions enter the
// boundary check (the regression pinned here: dead-stripped
// init/update bodies let an unprovable write attest).
const broken = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
"export function update(m: Model, msg: Msg): Model { return { total: m.total * 0.5, label: m.label }; }",
);
const r = await buildCase("sidecar-refuse-model-update", broken, sidecarProfile([{ slot: "Model.total", class: "i64" }]));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Model.total'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
expect(r.diagnostics[0]!.hint).toBeTruthy();
});
test("an unbounded counter increment into a declared model field refuses range", async () => {
// The read side of the declared-slot contract: m.total reads back
// whole-in-class-range (every write was checked), so the unguarded
// increment fails RANGE — the counter may leave ±(2^53 − 1) — never a
// spurious NaN complaint. A clamped increment proves (next test).
const broken = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
"export function update(m: Model, msg: Msg): Model { return { total: m.total + 1, label: m.label }; }",
);
const r = await buildCase("sidecar-refuse-model-counter", broken, sidecarProfile([{ slot: "Model.total", class: "i64" }]));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4023"]);
expect(r.diagnostics[0]!.message).toContain("'Model.total'");
expect(r.diagnostics[0]!.message).toContain("range failed");
});
test("a direct field guard proves and remains byte-exact to Node in both emissions", async () => {
const guarded = `${SIDECAR_ENTRY}
export function guardedStep(v: number): number {
const m: Model = { total: v, label: "" };
if (m.total < 1000) { m.total = m.total + 1; }
return m.total;
}
`;
const inputs = [Number.MIN_SAFE_INTEGER, -1, 999, 1000, Number.MAX_SAFE_INTEGER];
const nodeOutput = inputs
.map((v) => String(v < 1000 ? v + 1 : v))
.join("\n") + "\n";
for (const emission of EMISSIONS) {
const r = await buildCase(
`sidecar-prove-model-field-guard-${emission}`,
guarded,
sidecarProfile(
[{ slot: "Model.total", class: "i64" }],
{
emission,
exports: [
{ export: "guardedStep", symbol: "ks_guarded_step", params: ["i64"], returns: "f64" },
],
},
),
);
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) continue;
const probeSrc = join(r.outDir, "guarded-field-probe.c");
writeFileSync(probeSrc, `#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
extern void ks_init(void);
extern double ks_guarded_step(int64_t v);
int main(void) {
const int64_t inputs[] = {
-INT64_C(9007199254740991), -INT64_C(1), INT64_C(999),
INT64_C(1000), INT64_C(9007199254740991)
};
ks_init();
for (unsigned i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) {
printf("%.17g\\n", ks_guarded_step(inputs[i]));
}
return 0;
}
`);
const run = runProbe(buildProbe(probeSrc, r.archive, r.outDir));
expect(run.signal).toBeNull();
expect(run.status).toBe(0);
expect(run.stdout).toBe(nodeOutput);
}
});
test("a ternary guard and literal bracket spelling share the static field path", async () => {
const guarded = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
`export function update(m: Model, msg: Msg): Model {
m.total = m["total"] < 1000 ? m.total + 1 : 0;
return m;
}`,
);
const r = await buildCase(
"sidecar-prove-model-field-ternary",
guarded,
sidecarProfile([{ slot: "Model.total", class: "i64" }]),
);
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
});
test("a call after a field guard kills the refinement with the same range refusal", async () => {
const guarded = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
`function touch(): void {}
export function update(m: Model, msg: Msg): Model {
if (m.total < 1000) {
touch();
m.total = m.total + 1;
}
return m;
}`,
);
const r = await buildCase(
"sidecar-refuse-model-field-call-kill",
guarded,
sidecarProfile([{ slot: "Model.total", class: "i64" }]),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4023"]);
expect(r.diagnostics[0]!.message).toContain("'Model.total'");
expect(r.diagnostics[0]!.message).toContain("range failed");
});
test("a possible aliasing write after a field guard kills the refinement", async () => {
const guarded = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
`export function update(m: Model, msg: Msg): Model {
const alias = m;
if (m.total < 1000) {
alias.label = "changed";
m.total = m.total + 1;
}
return m;
}`,
);
const r = await buildCase(
"sidecar-refuse-model-field-write-kill",
guarded,
sidecarProfile([{ slot: "Model.total", class: "i64" }]),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4023"]);
expect(r.diagnostics[0]!.message).toContain("'Model.total'");
expect(r.diagnostics[0]!.message).toContain("range failed");
});
test("guarded model-field writes prove and the build attests them", async () => {
const guarded = SIDECAR_ENTRY.replace(
"export function update(m: Model, msg: Msg): Model { return m; }",
"export function update(m: Model, msg: Msg): Model { return { total: (m.total + 1) % 1000000, label: m.label }; }",
);
const r = await buildCase("sidecar-prove-model", guarded, sidecarProfile([{ slot: "Model.total", class: "u64" }]));
expect(r.ok, r.ok ? "" : r.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
if (!r.ok) return;
const doc = JSON.parse(readFileSync(r.sidecarPath!, "utf8")) as { integer_slots: { slot: string; class: string }[] };
expect(doc.integer_slots).toEqual([{ slot: "Model.total", class: "u64" }]);
});
test("an unproven write into a declared msg arm refuses by slot path", async () => {
const broken = SIDECAR_ENTRY.replace("n: Math.trunc(v)", "n: v * 0.5");
const r = await buildCase("sidecar-refuse-arm", broken, sidecarProfile(DECLARED));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Msg.count'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("an unproven write into a synthesized named-union payload field refuses", async () => {
const source = `export type TextInputEvent =
| { kind: "set_composition"; text: string; cursor: number }
| { kind: "clear_composition" };
export interface Model { input: TextInputEvent; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model {
return { input: { kind: "set_composition", text: "", cursor: 0.5 } };
}
export function update(m: Model, msg: Msg): Model { return m; }
`;
const r = await buildCase(
"sidecar-refuse-synthesized-union-record-slot",
source,
sidecarProfile([{ slot: "TextInputEvent_set_composition.cursor", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'TextInputEvent_set_composition.cursor'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("same-named composed synthesized union records each keep the integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; value: number; first: string }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; value: number; second: boolean }
| { kind: "secondOnly" };
export type Nested = First | Second;
export interface Model { nested: Nested; }
export type Msg = { kind: "fractional" } | { kind: "noop" };
export function init(): Model {
return { nested: { kind: "dup", value: 1, first: "" } };
}
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "fractional") {
return { nested: { kind: "dup", value: 0.5, second: false } };
}
return m;
}
`;
const r = await buildCase(
"sidecar-refuse-composed-synthesized-union-record-slot",
source,
sidecarProfile([{ slot: "Nested_dup.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Nested_dup.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("nested synthesized union records keep every composed-arm integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; payload: { nested: { value: number; first: string }; marker: string } }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; payload: { nested: { value: number; second: boolean }; marker: string } }
| { kind: "secondOnly" };
export type Nested = First | Second;
export interface Model { nested: Nested; }
export type Msg = { kind: "fractional" } | { kind: "noop" };
export function init(): Model {
return {
nested: {
kind: "dup",
payload: { nested: { value: 1, first: "" }, marker: "" },
},
};
}
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "fractional") {
return {
nested: {
kind: "dup",
payload: { nested: { value: 0.5, second: false }, marker: "" },
},
};
}
return m;
}
`;
const r = await buildCase(
"sidecar-refuse-composed-nested-synthesized-union-record-slot",
source,
sidecarProfile([{ slot: "Nested_payload_nested.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Nested_payload_nested.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
const proved = await buildCase(
"sidecar-prove-composed-nested-synthesized-union-record-slot",
source.replace("value: 0.5", "value: 2"),
sidecarProfile([{ slot: "Nested_payload_nested.value", class: "u64" }], { exports: [] }),
);
expect(
proved.ok,
proved.ok ? "" : proved.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
).toBe(true);
});
test("colliding synthesized names refuse before a tagged integer obligation can escape", async () => {
const source = `export interface A {
B_C: { value: number; plain: string };
}
export type A_B =
| { kind: "C"; value: number; tagged: boolean }
| { kind: "noop" };
export interface Model { a: A; tagged: A_B; }
export type Msg = { kind: "noop" } | { kind: "other" };
export function init(): Model {
return {
a: { B_C: { value: 1, plain: "" } },
tagged: { kind: "C", value: 0.5, tagged: false },
};
}
export function update(m: Model, msg: Msg): Model { return m; }
`;
const r = await buildCase(
"sidecar-refuse-colliding-synthesized-integer-records",
source,
sidecarProfile([{ slot: "A_B_C.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain("'A_B.C'");
expect(r.diagnostics[0]!.message).toContain("'A.B_C'");
expect(r.diagnostics[0]!.message).toContain("'A_B_C'");
});
test("an unproven write into a synthesized msg payload field refuses", async () => {
const source = `export interface Model { value: number; }
export type Msg =
| { kind: "audio_event"; name: string; at: number }
| { kind: "noop" };
let last: Msg = { kind: "noop" };
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "noop") {
last = { kind: "audio_event", name: "", at: 0.5 };
}
return m;
}
`;
const r = await buildCase(
"sidecar-refuse-synthesized-msg-record-slot",
source,
sidecarProfile([{ slot: "Msg_audio_event.at", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Msg_audio_event.at'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("nested synthesized msg records keep every composed-arm integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; payload: { value: number; first: string } }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; payload: { value: number; second: boolean } }
| { kind: "secondOnly" };
export type Msg = First | Second;
export interface Model { value: number; }
let last: Msg = { kind: "dup", payload: { value: 1, first: "" } };
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "secondOnly") {
last = { kind: "dup", payload: { value: 0.5, second: false } };
}
return m;
}
`;
const r = await buildCase(
"sidecar-refuse-composed-nested-synthesized-msg-record-slot",
source,
sidecarProfile([{ slot: "Msg_payload.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Msg_payload.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("same-named composed synthesized msg records each keep the integer obligation live", async () => {
const source = `export type First =
| { kind: "dup"; first: string; value: number }
| { kind: "firstOnly" };
export type Second =
| { kind: "dup"; second: boolean; value: number }
| { kind: "secondOnly" };
export type Msg = First | Second;
export interface Model { value: number; }
let last: Msg = { kind: "dup", first: "", value: 1 };
export function init(): Model { return { value: 0 }; }
export function update(m: Model, msg: Msg): Model {
if (msg.kind === "secondOnly") {
last = { kind: "dup", second: false, value: 0.5 };
}
return m;
}
`;
const r = await buildCase(
"sidecar-refuse-composed-synthesized-msg-record-slot",
source,
sidecarProfile([{ slot: "Msg_dup.value", class: "u64" }], { exports: [] }),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'Msg_dup.value'");
expect(r.diagnostics[0]!.message).toContain("wholeness failed");
});
test("an unproven declared helper return refuses by slot path", async () => {
const broken = SIDECAR_ENTRY.replace("return i | 0;", "return i * 0.5;");
const r = await buildCase("sidecar-refuse-helper", broken, sidecarProfile(DECLARED));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4022"]);
expect(r.diagnostics[0]!.message).toContain("'helpers.clampIdx.return'");
});
test("a declared path resolving to no slot refuses at projection", async () => {
const r = await buildCase("sidecar-refuse-nopath", SIDECAR_ENTRY, sidecarProfile([{ slot: "Msg.nope", class: "i64" }]));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain("'Msg.nope'");
expect(r.diagnostics[0]!.message).toContain("no number slot");
});
test("a declared path naming a non-number slot refuses at projection", async () => {
const r = await buildCase("sidecar-refuse-nonnum", SIDECAR_ENTRY, sidecarProfile([{ slot: "Model.label", class: "u64" }]));
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics.map((d) => d.code)).toEqual(["SC4009"]);
expect(r.diagnostics[0]!.message).toContain("not a number or optional number slot");
});
test("a profile teaching rides an integer refusal as the attributed note", async () => {
const broken = SIDECAR_ENTRY.replace("n: Math.trunc(v)", "n: v * 0.5");
const r = await buildCase(
"sidecar-refuse-teach",
broken,
sidecarProfile(DECLARED, {
determinism: { teachings: { SC4022: "counters in this core are integral by contract; truncate before posting" } },
}),
);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.diagnostics[0]!.code).toBe("SC4022");
expect(r.diagnostics[0]!.note).toBe(
"from the 'int-sidecar' profile: counters in this core are integral by contract; truncate before posting",
);
});
});