diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index c8f2d756d..f3284f195 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -523,6 +523,14 @@ export function runtimePackDiff(baseline, candidate) { }; } +/** The changed runtime pack files whose symbols executables link: runtime + * objects and vendor archives. */ +export function linkedRuntimeFiles(diff) { + return [...diff.changed, ...diff.added, ...diff.removed].filter( + (path) => path.endsWith(".o") || path.endsWith(".a"), + ); +} + /** Defined functions of textual LLVM IR (llvm-dis output) as name → hash of * the body. Attribute group and metadata numbers are dropped first, since * an unrelated change elsewhere in the unit renumbers them. */ @@ -638,7 +646,9 @@ export function programLines(entry) { * LLVM, is "runtime-only" when `runtimeOnly` says the changed runtime * objects explain it (the gate checks the runtime packs and the symbols). * A difference the baseline also shows when it builds the program again is - * "nondeterministic". Neither fails the gate unless `strict` is set. */ + * "nondeterministic": `nondeterministic` lists the artifact kinds whose + * baseline rebuild differs, and every differing artifact must be among + * them. Neither fails the gate unless `strict` is set. */ export function programVerdict({ baseline, candidate, @@ -646,7 +656,7 @@ export function programVerdict({ allowed, required = false, runtimeOnly = false, - nondeterministic = false, + nondeterministic = [], strict = false, }) { if (!baseline.ok || !candidate.ok) { @@ -659,7 +669,8 @@ export function programVerdict({ if (allowed) return { verdict: "allowed", fails: false }; if (runtimeOnly && differing.every(([kind]) => kind === "exe")) return { verdict: "runtime-only", fails: strict }; - if (nondeterministic) return { verdict: "nondeterministic", fails: strict }; + if (differing.every(([kind]) => nondeterministic.includes(kind))) + return { verdict: "nondeterministic", fails: strict }; return { verdict: "differs", fails: true }; } diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index 3931a8c69..2c79495b2 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -45,6 +45,7 @@ import { DYNAMIC_WORKLOADS, definedFunctionNames, dynamicTierSymbols, + linkedRuntimeFiles, llvmFunctionHashes, median, nmOutput, @@ -502,7 +503,7 @@ function runtimeChanges() { const objects = [...diff.changed, ...diff.added, ...diff.removed].filter((path) => path.endsWith(".o"), ); - for (const path of objects) + for (const path of linkedRuntimeFiles(diff)) for (const root of [trees.baseline, trees.candidate]) { const file = join(packDir(root), path); if (!existsSync(file)) continue; @@ -808,7 +809,7 @@ await pool( nondeterministic, strict: values.strict, }); - let { verdict, fails } = judge(false); + let { verdict, fails } = judge([]); // Build a difference the change cannot explain once more with the // baseline: when the baseline's two builds differ in the same // artifacts, the difference is the build's, not the change's. @@ -822,7 +823,7 @@ await pool( ); if (again.dir && !values.keep) rmSync(again.dir, { recursive: true, force: true }); } - if (recheck?.length) ({ verdict, fails } = judge(true)); + if (recheck?.length) ({ verdict, fails } = judge(recheck)); } const entry = { id: program.id, diff --git a/tests/harness/zero-cost-gate.test.ts b/tests/harness/zero-cost-gate.test.ts index c7fc56a8f..77da7e5b9 100644 --- a/tests/harness/zero-cost-gate.test.ts +++ b/tests/harness/zero-cost-gate.test.ts @@ -10,6 +10,7 @@ import { confinedToRuntime, definedFunctionNames, dynamicTierSymbols, + linkedRuntimeFiles, llvmFunctionHashes, mergeReports, nmOutput, @@ -222,14 +223,35 @@ test("the zero-cost gate classifies runtime-only and nondeterministic difference }); // A changed runtime cannot explain different LLVM. expect(verdict(llvmToo, { runtimeOnly: true })).toEqual({ verdict: "differs", fails: true }); - expect(verdict(llvmToo, { nondeterministic: true })).toEqual({ + expect(verdict(llvmToo, { nondeterministic: ["llvm", "exe"] })).toEqual({ verdict: "nondeterministic", fails: false, }); - expect(verdict(llvmToo, { nondeterministic: true, strict: true })).toEqual({ + expect(verdict(llvmToo, { nondeterministic: ["llvm", "exe"], strict: true })).toEqual({ verdict: "nondeterministic", fails: true, }); + // A nondeterministic executable does not excuse a deterministic LLVM difference. + expect(verdict(llvmToo, { nondeterministic: ["exe"] })).toEqual({ + verdict: "differs", + fails: true, + }); +}); + +test("the zero-cost gate reads the symbols of changed runtime objects and vendor archives", () => { + expect( + linkedRuntimeFiles({ + changed: [ + "artifacts/release/runtime/default/scr_a.o", + "artifacts/release/runtime/default/scr_a.bc", + ], + added: ["artifacts/vendor/libz/libscriptc-zlib.a"], + removed: [], + }), + ).toEqual([ + "artifacts/release/runtime/default/scr_a.o", + "artifacts/vendor/libz/libscriptc-zlib.a", + ]); }); test("the zero-cost gate names the runtime pack files and functions that changed", () => {