sots-re/verify/harness/compare/tracecmp.py

1003 lines
42 KiB
Python

#!/usr/bin/env python3
"""tracecmp.py -- validate shim trace/compare logs (TRACE_FORMAT.md) and report
divergences; or replay a golden trace against an implementation's output.
usage:
tracecmp.py LOG.jsonl [LOG2.jsonl ...] report per-hook divergences
tracecmp.py --replay IMPL.jsonl GOLDEN.jsonl [...] diff GOLDEN (ret/side) vs IMPL
records matched by call_id
flags:
--hook NAME only this hook (repeatable)
--tolerance SPEC float policy; SPEC = [HOOK=][abs:|rel:|ulp:]NUMBER
e.g. 1e-6 | rel:1e-5 | Mars::ParseBlock=ulp:2 (repeatable;
a bare NUMBER without HOOK sets the default for every hook)
--unordered SPEC treat a list path as a set; SPEC = [HOOK=]PATH (repeatable)
--ptr exact|ignore pointer policy for every hook (default: ignore)
--numeric inside `json` values, compare int vs float as numbers
--first N diffs shown per hook (default 5)
--json-out PATH write the full report as JSON (convention:
verify/results/compare/<run>.json)
--strict-coverage also fail on any undeclared write or unstated hook
--skip-invalid drop unparsable/invalid records with a warning instead of
failing the run
Every report ends with a `coverage` section stating, per hook, which regions were
actually compared, which coarse spans were guarded, what the guards saw the original
write outside those regions, and what the hook's descriptor admits it does not model.
A clean compare therefore always says what it did not check.
exit: 0 = no divergences; 1 = divergences; 2 = invalid input (or usage).
A hook whose descriptor claims complete coverage while a guard region caught an
undeclared write is counted as a divergence (it is one: the model is wrong).
Stdlib only.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import re
import struct
import sys
from dataclasses import dataclass, field, replace as dc_replace
from typing import Any
FORMAT_VERSION = 1
MODES = ("trace", "compare", "replace")
INT_TYPES = {"i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64"}
FLOAT_TYPES = {"f32", "f64"}
TV_TYPES = INT_TYPES | FLOAT_TYPES | {"bool", "str", "wstr", "ptr", "enum", "null",
"bytes", "list", "set", "struct", "json"}
RECORD_KEYS = {"ts", "hook", "mode", "call_id", "thread", "depth", "args", "ret", "side",
"ours", "diverged", "diff", "coverage", "err", "note"}
COVERAGE_STATES = ("complete", "partial", "unstated")
INT_RANGE = {"i8": (-2**7, 2**7 - 1), "i16": (-2**15, 2**15 - 1), "i32": (-2**31, 2**31 - 1),
"i64": (-2**63, 2**63 - 1), "u8": (0, 2**8 - 1), "u16": (0, 2**16 - 1),
"u32": (0, 2**32 - 1), "u64": (0, 2**64 - 1)}
_HEX_RE = re.compile(r"^[0-9a-f]*$")
_SHA_RE = re.compile(r"^[0-9a-f]{64}$")
_PTR_RE = re.compile(r"^0x[0-9a-fA-F]{1,16}$")
NONFINITE = {"nan": math.nan, "inf": math.inf, "-inf": -math.inf}
# --- policy ------------------------------------------------------------------
@dataclass
class Policy:
ftol: float = 0.0
ftol_kind: str = "abs" # abs | rel | ulp
ptr: str = "ignore" # ignore | exact
numeric: bool = False # json: int vs float compare as numbers
unordered: set = field(default_factory=set)
def merged(self, d: dict) -> "Policy":
p = dc_replace(self, unordered=set(self.unordered))
if "ftol" in d:
p.ftol = float(d["ftol"])
if "ftol_kind" in d:
p.ftol_kind = d["ftol_kind"]
if "ptr" in d:
p.ptr = d["ptr"]
if "numeric" in d:
p.numeric = bool(d["numeric"])
if "unordered" in d:
p.unordered |= set(d["unordered"])
return p
class PolicyTable:
"""defaults < log meta per-hook < CLI default < CLI per-hook."""
def __init__(self):
self.default: dict = {}
self.meta: dict[str, dict] = {}
self.cli: dict[str, dict] = {}
def add_meta(self, hooks: dict) -> None:
for h, d in (hooks or {}).items():
self.meta.setdefault(h, {}).update(d)
def add_cli(self, hook: str | None, d: dict) -> None:
if hook is None:
self.default.update(d)
else:
self.cli.setdefault(hook, {}).update(d)
def for_hook(self, hook: str) -> Policy:
p = Policy().merged(self.meta.get(hook, {}))
p = p.merged(self.default)
return p.merged(self.cli.get(hook, {}))
def parse_tolerance(spec: str) -> tuple[str | None, dict]:
hook = None
if "=" in spec:
hook, spec = spec.split("=", 1)
kind = "abs"
if ":" in spec:
kind, spec = spec.split(":", 1)
if kind not in ("abs", "rel", "ulp"):
raise ValueError(f"bad tolerance kind {kind!r}")
return hook, {"ftol": float(spec), "ftol_kind": kind}
def parse_unordered(spec: str) -> tuple[str | None, dict]:
hook = None
if "=" in spec:
hook, spec = spec.split("=", 1)
return hook, {"unordered": [spec]}
# --- validation ----------------------------------------------------------------
class Invalid(Exception):
pass
def _is_int(x) -> bool:
return isinstance(x, int) and not isinstance(x, bool)
def validate_tv(tv: Any, path: str, errs: list[str]) -> None:
if not isinstance(tv, dict):
errs.append(f"{path}: typed value must be an object, got {type(tv).__name__}")
return
t = tv.get("t")
if t not in TV_TYPES:
errs.append(f"{path}: unknown type {t!r}")
return
if "n" in tv and not isinstance(tv["n"], (str, int)):
errs.append(f"{path}: 'n' must be a string (name) or int (bytes length)")
if t == "bytes":
n, sha = tv.get("n"), tv.get("sha256")
if not _is_int(n) or n < 0:
errs.append(f"{path}: bytes.n must be int >= 0")
if not isinstance(sha, str) or not _SHA_RE.match(sha):
errs.append(f"{path}: bytes.sha256 must be 64 lowercase hex chars")
if "hex" in tv:
h = tv["hex"]
if not isinstance(h, str) or not _HEX_RE.match(h) or (_is_int(n) and len(h) != 2 * n):
errs.append(f"{path}: bytes.hex must be 2*n lowercase hex chars")
if "head" in tv and (not isinstance(tv["head"], str) or not _HEX_RE.match(tv["head"])):
errs.append(f"{path}: bytes.head must be lowercase hex")
return
if "v" not in tv:
errs.append(f"{path}: missing 'v'")
return
v = tv["v"]
if t == "bool":
if not isinstance(v, bool):
errs.append(f"{path}: bool.v must be true/false")
elif t in INT_TYPES:
iv = None
if _is_int(v):
iv = v
elif isinstance(v, str) and re.match(r"^-?\d+$", v):
iv = int(v)
else:
errs.append(f"{path}: {t}.v must be an integer (or decimal string)")
if iv is not None:
lo, hi = INT_RANGE[t]
if not lo <= iv <= hi:
errs.append(f"{path}: {t}.v={iv} out of range")
elif t in FLOAT_TYPES:
if isinstance(v, bool) or not (isinstance(v, (int, float)) or v in NONFINITE):
errs.append(f"{path}: {t}.v must be a number or 'nan'/'inf'/'-inf'")
elif t in ("str", "wstr"):
if not isinstance(v, str):
errs.append(f"{path}: {t}.v must be a string")
elif t == "str" and any(ord(c) > 0xFF for c in v):
errs.append(f"{path}: str.v carries a code point > U+00FF (bytes must be \\u00XX)")
elif t == "ptr":
if not isinstance(v, str) or not _PTR_RE.match(v):
errs.append(f"{path}: ptr.v must be a hex string like 0x00a36fd0")
elif t == "enum":
if not _is_int(v):
errs.append(f"{path}: enum.v must be an integer")
elif t == "null":
if v is not None:
errs.append(f"{path}: null.v must be null")
elif t in ("list", "set"):
if not isinstance(v, list):
errs.append(f"{path}: {t}.v must be an array")
else:
for i, e in enumerate(v):
validate_tv(e, f"{path}.v[{i}]", errs)
elif t == "struct":
if not isinstance(v, dict):
errs.append(f"{path}: struct.v must be an object")
else:
for k, e in v.items():
validate_tv(e, f"{path}.v.{k}", errs)
elif t == "json":
pass # any JSON
def validate_side(side: Any, path: str, errs: list[str], need_before: bool = False) -> None:
if not isinstance(side, dict):
errs.append(f"{path}: must be an object")
return
for name, ent in side.items():
if not isinstance(ent, dict) or "after" not in ent:
errs.append(f"{path}.{name}: must be {{\"before\"?, \"after\"}}")
continue
if ent.get("before") is not None:
validate_tv(ent["before"], f"{path}.{name}.before", errs)
validate_tv(ent["after"], f"{path}.{name}.after", errs)
def validate_coverage(cov: Any, errs: list[str]) -> None:
"""Record-level `coverage`: {guards: [name], undeclared: [{region, off, len}], n: int}.
Present iff the call declared at least one Guard region, so an empty `undeclared` means
"watched and nothing moved" -- which is different from, and much stronger than, absent.
"""
if not isinstance(cov, dict):
errs.append("coverage must be an object")
return
guards = cov.get("guards")
if not isinstance(guards, list) or not all(isinstance(g, str) for g in guards):
errs.append("coverage.guards must be an array of strings")
und = cov.get("undeclared")
if not isinstance(und, list):
errs.append("coverage.undeclared must be an array")
else:
for i, w in enumerate(und):
if (not isinstance(w, dict) or not isinstance(w.get("region"), str)
or not _is_int(w.get("off")) or not _is_int(w.get("len"))):
errs.append(f"coverage.undeclared[{i}]: must be {{region, off, len}}")
if "n" in cov and (not _is_int(cov["n"]) or cov["n"] < 0):
errs.append("coverage.n must be int >= 0")
if isinstance(und, list) and _is_int(cov.get("n")) and cov["n"] < len(und):
errs.append("coverage.n is smaller than the spans it reports")
def validate_meta_coverage(hook: str, cov: Any, warns: list[str]) -> None:
"""meta.hooks[H].coverage: the descriptor's own admission of what it does not check."""
if not isinstance(cov, dict):
warns.append(f"meta.hooks.{hook}.coverage must be an object")
return
if cov.get("state") not in COVERAGE_STATES:
warns.append(f"meta.hooks.{hook}.coverage.state must be one of {COVERAGE_STATES}")
for i, n in enumerate(cov.get("unmodelled") or []):
if not isinstance(n, dict) or not isinstance(n.get("what"), str) or not n.get("what"):
warns.append(f"meta.hooks.{hook}.coverage.unmodelled[{i}]: needs a non-empty 'what'")
def validate_record(rec: Any) -> tuple[list[str], list[str]]:
"""-> (errors, warnings). Errors make the record invalid."""
errs: list[str] = []
warns: list[str] = []
if not isinstance(rec, dict):
return ["record is not an object"], warns
for k in ("ts", "hook", "mode", "call_id", "thread", "args", "ret", "side"):
if k not in rec:
errs.append(f"missing required field {k!r}")
if errs:
return errs, warns
if not _is_int(rec["ts"]) and not isinstance(rec["ts"], float):
errs.append("ts must be a number")
if not isinstance(rec["hook"], str) or not rec["hook"]:
errs.append("hook must be a non-empty string")
if rec["mode"] not in MODES:
errs.append(f"mode must be one of {MODES}")
if not _is_int(rec["call_id"]) or rec["call_id"] < 0:
errs.append("call_id must be int >= 0")
if not _is_int(rec["thread"]):
errs.append("thread must be int")
if "depth" in rec and (not _is_int(rec["depth"]) or rec["depth"] < 0):
errs.append("depth must be int >= 0")
if not isinstance(rec["args"], list):
errs.append("args must be an array")
else:
for i, a in enumerate(rec["args"]):
validate_tv(a, f"args[{i}]", errs)
if rec["ret"] is not None:
validate_tv(rec["ret"], "ret", errs)
validate_side(rec["side"], "side", errs)
if "coverage" in rec:
validate_coverage(rec["coverage"], errs)
if "err" in rec and not isinstance(rec["err"], str):
errs.append("err must be a string")
if "note" in rec and not isinstance(rec["note"], str):
errs.append("note must be a string")
mode = rec["mode"]
if mode == "compare":
ours = rec.get("ours")
if not isinstance(ours, dict) or "ret" not in ours or "side" not in ours:
if "err" not in rec:
errs.append("compare record needs ours={ret, side} (or err)")
else:
if ours["ret"] is not None:
validate_tv(ours["ret"], "ours.ret", errs)
validate_side(ours["side"], "ours.side", errs)
if "diverged" not in rec:
warns.append("compare record without 'diverged'")
elif not isinstance(rec["diverged"], bool):
errs.append("diverged must be a bool")
if "diff" in rec and not isinstance(rec["diff"], list):
errs.append("diff must be an array")
else:
if "ours" in rec:
warns.append(f"{mode} record carries 'ours' (ignored)")
for k in rec:
if k not in RECORD_KEYS:
warns.append(f"unknown field {k!r}")
return errs, warns
# --- comparison ------------------------------------------------------------------
def _diff(path: str, why: str, a: Any, b: Any, **extra) -> dict:
d = {"path": path, "why": why, "orig": a, "ours": b}
d.update(extra)
return d
def _num(v, width: str = "f64") -> float:
"""tv float payload -> Python float. f32 values are rounded to float32:
the emitter's %.9g round-trips the float32, not the exact double."""
if isinstance(v, str):
return NONFINITE[v]
x = float(v)
if width == "f32" and math.isfinite(x):
x = struct.unpack("<f", struct.pack("<f", x))[0]
return x
def _ulp_diff(a: float, b: float, width: str) -> int:
fmt, ifmt = ("f", "i") if width == "f32" else ("d", "q")
def to_int(x):
i = struct.unpack("<" + ifmt, struct.pack("<" + fmt, x))[0]
return i if i >= 0 else -(i & (2**(31 if fmt == "f" else 63) - 1))
return abs(to_int(a) - to_int(b))
def floats_equal(a: float, b: float, pol: Policy, width: str = "f64") -> bool:
if math.isnan(a) or math.isnan(b):
return math.isnan(a) and math.isnan(b)
if math.isinf(a) or math.isinf(b):
return a == b
if a == b:
return True
if pol.ftol <= 0:
return False
if pol.ftol_kind == "abs":
return abs(a - b) <= pol.ftol
if pol.ftol_kind == "rel":
return abs(a - b) <= pol.ftol * max(abs(a), abs(b))
if pol.ftol_kind == "ulp":
return _ulp_diff(a, b, width) <= pol.ftol
raise ValueError(pol.ftol_kind)
def _canon(x) -> str:
return json.dumps(x, sort_keys=True, separators=(",", ":"))
def compare_json(a: Any, b: Any, pol: Policy, path: str, out: list) -> None:
if isinstance(a, bool) or isinstance(b, bool) or a is None or b is None or \
isinstance(a, str) or isinstance(b, str):
if type(a) is not type(b) or a != b:
out.append(_diff(path, "exact", a, b))
return
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
both_float = isinstance(a, float) and isinstance(b, float)
if type(a) is not type(b) and not pol.numeric:
out.append(_diff(path, "type", a, b))
return
if both_float or pol.numeric:
if not floats_equal(float(a), float(b), pol):
out.append(_diff(path, "ftol" if pol.ftol > 0 else "exact", a, b))
elif a != b:
out.append(_diff(path, "exact", a, b))
return
if isinstance(a, list) and isinstance(b, list):
if path in pol.unordered:
a = sorted(a, key=_canon)
b = sorted(b, key=_canon)
if len(a) != len(b):
out.append(_diff(path, "len", len(a), len(b)))
return
for i, (x, y) in enumerate(zip(a, b)):
compare_json(x, y, pol, f"{path}[{i}]", out)
return
if isinstance(a, dict) and isinstance(b, dict):
ka, kb = set(a), set(b)
for k in sorted(ka - kb):
out.append(_diff(f"{path}.{k}", "missing", a[k], None))
for k in sorted(kb - ka):
out.append(_diff(f"{path}.{k}", "extra", None, b[k]))
for k in sorted(ka & kb):
compare_json(a[k], b[k], pol, f"{path}.{k}", out)
return
out.append(_diff(path, "type", a, b))
def compare_tv(a: Any, b: Any, pol: Policy, path: str, out: list) -> None:
"""Append diff entries for typed values a (orig) vs b (ours)."""
if a is None or b is None:
if a is not b:
out.append(_diff(path, "missing" if b is None else "extra", a, b))
return
ta, tb = a.get("t"), b.get("t")
if ta != tb:
out.append(_diff(path, "type", a, b))
return
t = ta
if t == "ptr":
va, vb = a["v"], b["v"]
za, zb = int(va, 16) == 0, int(vb, 16) == 0
if za != zb:
out.append(_diff(path, "exact", a, b))
elif pol.ptr == "exact" and int(va, 16) != int(vb, 16):
out.append(_diff(path, "exact", a, b))
return
if t == "null":
return
if t == "bool" or t == "enum" or t in ("str", "wstr"):
if a["v"] != b["v"]:
out.append(_diff(path, "exact", a, b))
return
if t in INT_TYPES:
if int(a["v"]) != int(b["v"]):
out.append(_diff(path, "exact", a, b))
return
if t in FLOAT_TYPES:
if not floats_equal(_num(a["v"], t), _num(b["v"], t), pol, t):
out.append(_diff(path, "ftol" if pol.ftol > 0 else "exact", a, b))
return
if t == "bytes":
if a.get("n") != b.get("n"):
out.append(_diff(path, "len", a, b))
return
if a.get("sha256") != b.get("sha256"):
extra = {}
if "hex" in a and "hex" in b:
ha, hb = a["hex"], b["hex"]
off = next((i for i in range(0, min(len(ha), len(hb)), 2) if ha[i:i+2] != hb[i:i+2]), None)
if off is not None:
extra["first_diff_offset"] = off // 2
out.append(_diff(path, "hash", a, b, **extra))
elif "hex" in a and "hex" in b and a["hex"] != b["hex"]:
out.append(_diff(path, "hash", a, b, note="same sha256, different hex: corrupt log"))
return
if t in ("list", "set"):
va, vb = a["v"], b["v"]
if t == "set" or path in pol.unordered:
va = sorted(va, key=_canon)
vb = sorted(vb, key=_canon)
if len(va) != len(vb):
out.append(_diff(path, "len", len(va), len(vb)))
return
for i, (x, y) in enumerate(zip(va, vb)):
compare_tv(x, y, pol, f"{path}.v[{i}]", out)
return
if t == "struct":
va, vb = a["v"], b["v"]
for k in sorted(set(va) - set(vb)):
out.append(_diff(f"{path}.v.{k}", "missing", va[k], None))
for k in sorted(set(vb) - set(va)):
out.append(_diff(f"{path}.v.{k}", "extra", None, vb[k]))
for k in sorted(set(va) & set(vb)):
compare_tv(va[k], vb[k], pol, f"{path}.v.{k}", out)
return
if t == "json":
compare_json(a["v"], b["v"], pol, path, out)
return
raise AssertionError(t)
def _side_after(side: dict, name: str) -> Any:
ent = side[name]
if isinstance(ent, dict) and ent.get("t") is None and "after" in ent:
return ent["after"]
return ent # replay input may give the tv directly
def compare_outputs(orig_ret, orig_side: dict, ours_ret, ours_side: dict, pol: Policy) -> list[dict]:
diffs: list[dict] = []
compare_tv(orig_ret, ours_ret, pol, "ret", diffs)
names_a, names_b = set(orig_side), set(ours_side)
for n in sorted(names_a - names_b):
diffs.append(_diff(f"side.{n}.after", "missing", _side_after(orig_side, n), None))
for n in sorted(names_b - names_a):
diffs.append(_diff(f"side.{n}.after", "extra", None, _side_after(ours_side, n)))
for n in sorted(names_a & names_b):
compare_tv(_side_after(orig_side, n), _side_after(ours_side, n), pol, f"side.{n}.after", diffs)
return diffs
def compare_record(rec: dict, pol: Policy) -> tuple[list[dict], list[str]]:
"""Recompute a compare record's diff. -> (diffs, warnings)"""
warns: list[str] = []
if "err" in rec:
return [_diff("call", "err", None, rec["err"])], warns
ours = rec["ours"]
diffs = compare_outputs(rec["ret"], rec["side"], ours["ret"], ours["side"], pol)
# snapshot sanity: 'before' on both sides should be identical
for n in set(rec["side"]) & set(ours["side"]):
ba, bb = rec["side"][n].get("before"), ours["side"][n].get("before")
if ba is not None and bb is not None:
tmp: list = []
compare_tv(ba, bb, pol, f"side.{n}.before", tmp)
if tmp:
warns.append(f"call {rec['call_id']}: side.{n}.before differs between sides (snapshot bug?)")
if "diverged" in rec and rec["diverged"] != bool(diffs):
warns.append(f"call {rec['call_id']}: shim said diverged={rec['diverged']}, harness found {len(diffs)} diff(s)")
return diffs, warns
# --- loading -----------------------------------------------------------------------
@dataclass
class Log:
path: str
meta: dict = field(default_factory=dict)
records: list = field(default_factory=list)
invalid: list = field(default_factory=list) # (lineno, message)
warnings: list = field(default_factory=list) # (lineno, message)
def load_log(path: str, policies: PolicyTable | None = None) -> Log:
log = Log(path)
seen: dict[int, int] = {}
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.rstrip("\n")
if line.endswith("\r"):
log.warnings.append((lineno, "CRLF line ending"))
line = line[:-1]
if not line.strip():
log.invalid.append((lineno, "blank line"))
continue
try:
rec = json.loads(line)
except ValueError as e:
log.invalid.append((lineno, f"not JSON: {e}"))
continue
if isinstance(rec, dict) and "meta" in rec and len(rec) == 1:
m = rec["meta"]
if not isinstance(m, dict):
log.invalid.append((lineno, "meta must be an object"))
continue
if m.get("format") != FORMAT_VERSION:
log.invalid.append((lineno, f"unsupported format {m.get('format')!r} (want {FORMAT_VERSION})"))
continue
if lineno != 1:
log.warnings.append((lineno, "meta record not on line 1"))
log.meta = m
if policies is not None:
policies.add_meta(m.get("hooks", {}))
continue
errs, warns = validate_record(rec)
for w in warns:
log.warnings.append((lineno, w))
if errs:
log.invalid.append((lineno, "; ".join(errs)))
continue
cid = rec["call_id"]
if cid in seen:
log.invalid.append((lineno, f"duplicate call_id {cid} (first at line {seen[cid]})"))
continue
seen[cid] = lineno
rec["_line"] = lineno
log.records.append(rec)
return log
def load_impl(path: str) -> tuple[dict[int, dict], list[tuple[int, str]]]:
"""Implementation output for --replay: records keyed by call_id."""
out: dict[int, dict] = {}
invalid: list[tuple[int, str]] = []
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except ValueError as e:
invalid.append((lineno, f"not JSON: {e}"))
continue
if isinstance(rec, dict) and "meta" in rec and len(rec) == 1:
continue
if not isinstance(rec, dict) or not _is_int(rec.get("call_id")):
invalid.append((lineno, "impl record needs an integer call_id"))
continue
errs: list[str] = []
if rec.get("ret") is not None:
validate_tv(rec["ret"], "ret", errs)
side = rec.get("side", {})
if not isinstance(side, dict):
errs.append("side must be an object")
else:
for n in side:
validate_tv(_side_after(side, n), f"side.{n}", errs)
if errs:
invalid.append((lineno, "; ".join(errs)))
continue
if rec["call_id"] in out:
invalid.append((lineno, f"duplicate call_id {rec['call_id']}"))
continue
rec.setdefault("side", {})
rec.setdefault("ret", None)
rec["_line"] = lineno
out[rec["call_id"]] = rec
return out, invalid
# --- report ------------------------------------------------------------------------
MAX_SPANS_REPORTED = 12
def new_hook_stats() -> dict:
return {"calls": 0, "modes": {}, "compared": 0, "diverged": 0, "errors": 0,
"diverged_call_ids": [], "diffs": [],
# --- coverage: what this run actually checked, and what it admits it did not ---
"coverage": {
"state": "unstated", # from meta.hooks[H].coverage (absent -> unstated)
"why": "",
"unmodelled": [], # [{what, risk, why, mitigation}]
"checked_regions": [], # side names actually diffed (union over records)
"guards": [], # guard names the shim watched
"guarded_calls": 0, # calls that carried a coverage block
"undeclared_calls": 0, # calls where a guard saw an undeclared write
"undeclared_writes": 0, # total spans, honest even when the list truncates
"spans": {}, # mode -> ["region+off:len", ...] (capped)
}}
def _hook_coverage_from_meta(logs: list) -> dict:
"""meta.hooks[H].coverage, merged across the input logs (last log wins on conflict)."""
out: dict[str, dict] = {}
for log in logs:
for h, d in (log.meta.get("hooks") or {}).items():
if isinstance(d, dict) and isinstance(d.get("coverage"), dict):
out[h] = d["coverage"]
return out
def _account_coverage(st: dict, rec: dict) -> None:
"""Fold one record's declared regions and guard findings into the hook's coverage stats."""
cov = st["coverage"]
for name in rec.get("side") or {}:
if name not in cov["checked_regions"]:
cov["checked_regions"].append(name)
block = rec.get("coverage")
if not isinstance(block, dict):
return
cov["guarded_calls"] += 1
for g in block.get("guards") or []:
if g not in cov["guards"]:
cov["guards"].append(g)
spans = block.get("undeclared") or []
total = block.get("n", len(spans))
if total:
cov["undeclared_calls"] += 1
cov["undeclared_writes"] += total
bucket = cov["spans"].setdefault(rec["mode"], [])
for w in spans:
key = "%s+0x%x:%d" % (w["region"], w["off"], w["len"])
if key not in bucket and len(bucket) < MAX_SPANS_REPORTED:
bucket.append(key)
def run_report(logs: list[Log], policies: PolicyTable, hooks: set[str] | None, first: int) -> dict:
per_hook: dict[str, dict] = {}
warnings: list[str] = []
for log in logs:
for ln, w in log.warnings:
warnings.append(f"{os.path.basename(log.path)}:{ln}: {w}")
for rec in log.records:
h = rec["hook"]
if hooks and h not in hooks:
continue
st = per_hook.setdefault(h, new_hook_stats())
st["calls"] += 1
st["modes"][rec["mode"]] = st["modes"].get(rec["mode"], 0) + 1
_account_coverage(st, rec)
if rec["mode"] != "compare":
if "err" in rec:
st["errors"] += 1
continue
st["compared"] += 1
diffs, w = compare_record(rec, policies.for_hook(h))
warnings.extend(f"{os.path.basename(log.path)}:{rec['_line']}: {x}" for x in w)
if diffs:
st["diverged"] += 1
if "err" in rec:
st["errors"] += 1
st["diverged_call_ids"].append(rec["call_id"])
if len(st["diffs"]) < first:
st["diffs"].append({"call_id": rec["call_id"], "line": rec["_line"],
"file": os.path.basename(log.path), "diff": diffs})
return _finish(per_hook, logs, warnings, "report")
def run_replay(golden: list[Log], impl: dict[int, dict], policies: PolicyTable,
hooks: set[str] | None, first: int) -> dict:
per_hook: dict[str, dict] = {}
warnings: list[str] = []
seen: set[int] = set()
for log in golden:
for ln, w in log.warnings:
warnings.append(f"{os.path.basename(log.path)}:{ln}: {w}")
for rec in log.records:
h = rec["hook"]
if hooks and h not in hooks:
continue
st = per_hook.setdefault(h, new_hook_stats())
st["calls"] += 1
st["modes"][rec["mode"]] = st["modes"].get(rec["mode"], 0) + 1
_account_coverage(st, rec)
st["compared"] += 1
cid = rec["call_id"]
seen.add(cid)
ours = impl.get(cid)
if ours is None:
diffs = [_diff("call", "missing", None, None, note=f"call_id {cid} absent from impl")]
else:
if "hook" in ours and ours["hook"] != h:
warnings.append(f"impl call_id {cid}: hook {ours['hook']!r} != golden {h!r}")
diffs = compare_outputs(rec["ret"], rec["side"], ours["ret"], ours["side"], policies.for_hook(h))
if diffs:
st["diverged"] += 1
st["diverged_call_ids"].append(cid)
if len(st["diffs"]) < first:
st["diffs"].append({"call_id": cid, "line": rec["_line"],
"file": os.path.basename(log.path), "diff": diffs})
extra = sorted(set(impl) - seen)
if extra:
warnings.append(f"impl has {len(extra)} call_id(s) not in golden: {extra[:10]}{'…' if len(extra) > 10 else ''}")
return _finish(per_hook, golden, warnings, "replay")
def _finish(per_hook: dict, logs: list[Log], warnings: list[str], kind: str) -> dict:
total_div = sum(s["diverged"] for s in per_hook.values())
invalid = [(os.path.basename(l.path), ln, m) for l in logs for ln, m in l.invalid]
# Fold the shim's own coverage declaration in, and decide the coverage verdict per hook.
#
# unstated the descriptor never said what it does not check -> the compare's
# completeness is unknown, and a clean run means nothing on its own.
# contradicted the descriptor claims complete coverage, but a Guard region caught the
# original writing outside every declared region. The claim is false, and
# that counts as a divergence (exit 1) -- it is the exact failure B3 hit.
# partial the honest normal case: admissions on record, guards agreeing with them.
# complete claims completeness, and no guard contradicted it.
meta_cov = _hook_coverage_from_meta(logs)
unstated, contradicted = [], []
for h, s in per_hook.items():
cov = s["coverage"]
m = meta_cov.get(h)
if isinstance(m, dict):
cov["state"] = m.get("state", "unstated")
cov["why"] = m.get("why", "")
cov["unmodelled"] = list(m.get("unmodelled") or [])
for k in ("checked_regions", "guards"):
cov[k] = sorted(cov[k])
if cov["state"] == "unstated":
cov["verdict"] = "unstated"
unstated.append(h)
elif cov["state"] == "complete" and cov["undeclared_writes"]:
cov["verdict"] = "contradicted"
contradicted.append(h)
else:
cov["verdict"] = cov["state"]
if cov["verdict"] == "unstated":
warnings.append(f"{h}: no coverage statement in meta -- a clean compare for this hook "
f"does not say what it did not check")
if cov["verdict"] == "contradicted":
warnings.append(f"{h}: claims complete coverage but a guard saw "
f"{cov['undeclared_writes']} undeclared write(s)")
return {
"kind": kind,
"format": FORMAT_VERSION,
"inputs": [l.path for l in logs],
"meta": [l.meta for l in logs if l.meta],
"hooks": dict(sorted(per_hook.items())),
"totals": {"calls": sum(s["calls"] for s in per_hook.values()),
"compared": sum(s["compared"] for s in per_hook.values()),
"diverged": total_div,
"invalid_records": len(invalid),
"guarded_calls": sum(s["coverage"]["guarded_calls"] for s in per_hook.values()),
"undeclared_calls": sum(s["coverage"]["undeclared_calls"] for s in per_hook.values()),
"undeclared_writes": sum(s["coverage"]["undeclared_writes"] for s in per_hook.values()),
"coverage_unstated": len(unstated),
"coverage_contradicted": len(contradicted)},
"coverage_unstated": sorted(unstated),
"coverage_contradicted": sorted(contradicted),
"invalid": [{"file": f, "line": ln, "error": m} for f, ln, m in invalid],
"warnings": warnings,
}
def _short(x: Any, n: int = 60) -> str:
s = _canon(x)
return s if len(s) <= n else s[:n - 1] + "…"
def print_coverage(rep: dict, out=None) -> None:
"""What this run checked, and what it says it did not.
Printed on every run, clean or not. A compare that reports zero divergences bounds only the
regions it declared; without this section that fact is invisible, which is exactly how B3's
unposted event survived a clean pass.
"""
out = out or sys.stdout
P = lambda s="": print(s, file=out) # noqa: E731
if not rep["hooks"]:
return
P()
P("### coverage")
P()
P("| hook | verdict | compared regions | guards | undeclared writes | unmodelled |")
P("|---|---|---|---|---|---|")
for h, s in rep["hooks"].items():
c = s["coverage"]
regions = ", ".join(c["checked_regions"][:6]) or "-"
if len(c["checked_regions"]) > 6:
regions += f", +{len(c['checked_regions']) - 6}"
guards = ", ".join(c["guards"]) or "-"
und = (f"{c['undeclared_writes']} in {c['undeclared_calls']} call(s)"
if c["undeclared_writes"] else ("0" if c["guarded_calls"] else "not watched"))
P(f"| {h} | {c.get('verdict', c['state'])} | {regions} | {guards} | {und} | "
f"{len(c['unmodelled'])} |")
for h, s in rep["hooks"].items():
c = s["coverage"]
if not c["unmodelled"] and not c["spans"]:
continue
P()
P(f"#### {h} — not checked by this run")
if c.get("why"):
P(f"- claims complete coverage: {c['why']}")
for n in c["unmodelled"]:
mit = f" [{n['mitigation']}]" if n.get("mitigation") else ""
P(f"- ({n.get('risk', '?')}) {n.get('what', '?')} — {n.get('why', '')}{mit}")
for mode, spans in sorted(c["spans"].items()):
if spans:
P(f"- guard hits in {mode} mode: {', '.join(spans)}")
# A write the original makes in compare mode but ours never makes in replace mode (or
# the reverse) is the asymmetry that a save-hash oracle would eventually catch the hard
# way. Say it here instead.
if "compare" in c["spans"] and "replace" in c["spans"]:
cmp_s, rep_s = set(c["spans"]["compare"]), set(c["spans"]["replace"])
only_c = sorted(cmp_s - rep_s)
only_r = sorted(rep_s - cmp_s)
if only_c:
P(f"- ONLY the original writes these: {', '.join(only_c)}")
if only_r:
P(f"- ONLY ours writes these: {', '.join(only_r)}")
def print_report(rep: dict, out=None) -> None:
out = out or sys.stdout
P = lambda s="": print(s, file=out) # noqa: E731
P(f"## tracecmp {rep['kind']}: {', '.join(os.path.basename(p) for p in rep['inputs'])}")
P()
for m in rep["meta"]:
P(f"- build: {m.get('build', '?')} started: {m.get('started', '?')} inline_max: {m.get('inline_max', '?')}")
t = rep["totals"]
P(f"- calls: {t['calls']} compared: {t['compared']} diverged: {t['diverged']} "
f"invalid records: {t['invalid_records']} warnings: {len(rep['warnings'])}")
P(f"- coverage: {t.get('guarded_calls', 0)} guarded call(s), "
f"{t.get('undeclared_writes', 0)} undeclared write(s) in {t.get('undeclared_calls', 0)} call(s); "
f"{t.get('coverage_unstated', 0)} hook(s) unstated, "
f"{t.get('coverage_contradicted', 0)} contradicted")
P()
P("| hook | calls | modes | compared | diverged | errors |")
P("|---|---|---|---|---|---|")
for h, s in rep["hooks"].items():
modes = " ".join(f"{k}:{v}" for k, v in sorted(s["modes"].items()))
P(f"| {h} | {s['calls']} | {modes} | {s['compared']} | {s['diverged']} | {s['errors']} |")
print_coverage(rep, out)
for h, s in rep["hooks"].items():
if not s["diffs"]:
continue
P()
P(f"### {h}: first {len(s['diffs'])} of {s['diverged']} divergent call(s)")
for d in s["diffs"]:
P(f"- call_id {d['call_id']} ({d['file']}:{d['line']})")
for e in d["diff"][:8]:
extra = "".join(f" {k}={v}" for k, v in e.items() if k not in ("path", "why", "orig", "ours"))
P(f" {e['path']} [{e['why']}] orig={_short(e['orig'])} ours={_short(e['ours'])}{extra}")
if len(d["diff"]) > 8:
P(f" … {len(d['diff']) - 8} more")
more = s["diverged_call_ids"][len(s["diffs"]):]
if more:
P(f" other divergent call_ids: {more[:20]}{' …' if len(more) > 20 else ''}")
if rep["invalid"]:
P()
P(f"### invalid records ({len(rep['invalid'])})")
for e in rep["invalid"][:20]:
P(f"- {e['file']}:{e['line']}: {e['error']}")
if rep["warnings"]:
P()
P(f"### warnings ({len(rep['warnings'])})")
for w in rep["warnings"][:20]:
P(f"- {w}")
# --- main ----------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("logs", nargs="+", metavar="LOG", help="trace/compare JSONL (golden logs in --replay)")
ap.add_argument("--replay", metavar="IMPL", help="implementation output JSONL to diff against LOG")
ap.add_argument("--hook", action="append", default=[], help="only this hook (repeatable)")
ap.add_argument("--tolerance", action="append", default=[], metavar="SPEC", help="[HOOK=][abs:|rel:|ulp:]NUMBER")
ap.add_argument("--unordered", action="append", default=[], metavar="SPEC", help="[HOOK=]PATH")
ap.add_argument("--ptr", choices=("ignore", "exact"), default=None)
ap.add_argument("--numeric", action="store_true")
ap.add_argument("--first", type=int, default=5, metavar="N")
ap.add_argument("--json-out", metavar="PATH")
ap.add_argument("--strict-coverage", action="store_true",
help="also fail (exit 1) on any undeclared write or unstated hook, not only "
"on a hook whose 'complete' claim a guard contradicted")
ap.add_argument("--skip-invalid", action="store_true")
ap.add_argument("--quiet", action="store_true", help="no markdown report on stdout")
return ap
def main(argv: list[str] | None = None) -> int:
ap = build_parser()
a = ap.parse_args(argv)
policies = PolicyTable()
try:
for s in a.tolerance:
policies.add_cli(*parse_tolerance(s))
for s in a.unordered:
policies.add_cli(*parse_unordered(s))
except ValueError as e:
ap.error(str(e))
if a.ptr:
policies.add_cli(None, {"ptr": a.ptr})
if a.numeric:
policies.add_cli(None, {"numeric": True})
hooks = set(a.hook) or None
logs = []
for p in a.logs:
if not os.path.exists(p):
print(f"error: no such file {p}", file=sys.stderr)
return 2
logs.append(load_log(p, policies))
impl_invalid: list = []
if a.replay:
if not os.path.exists(a.replay):
print(f"error: no such file {a.replay}", file=sys.stderr)
return 2
impl, impl_invalid = load_impl(a.replay)
rep = run_replay(logs, impl, policies, hooks, a.first)
rep["impl"] = a.replay
rep["invalid"].extend({"file": os.path.basename(a.replay), "line": ln, "error": m} for ln, m in impl_invalid)
rep["totals"]["invalid_records"] += len(impl_invalid)
else:
rep = run_report(logs, policies, hooks, a.first)
if a.json_out:
os.makedirs(os.path.dirname(os.path.abspath(a.json_out)), exist_ok=True)
with open(a.json_out, "w", encoding="utf-8") as f:
json.dump(rep, f, indent=1, sort_keys=True)
f.write("\n")
if not a.quiet:
print_report(rep)
if rep["totals"]["invalid_records"] and not a.skip_invalid:
return 2
# A descriptor that claims complete coverage while a guard watched the original write
# outside every declared region has been proved wrong: that is a divergence between the
# harness's model and the game, and it fails the run like any other.
failed = rep["totals"]["diverged"] or rep["totals"]["coverage_contradicted"]
if a.strict_coverage:
failed = failed or rep["totals"]["undeclared_writes"] or rep["totals"]["coverage_unstated"]
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())