from __future__ import annotations
import argparse
import re
import sys
from bisect import bisect_right
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from wiring_guard import USE_RE, remove_cfg_test, strip_rust
ROOT_DIRS = ("examples/", "benches/", "fuzz/")
BINDING_FILES = ("src/ffi.rs", "src/python.rs", "src/wasm.rs")
PUB_DEF = r'\bpub(?:\([^)]*\))?\s+(?:(?:const|unsafe|async|extern(?:\s+"[^"]*")?|default)\s+)*(?:fn|struct|enum|const|static|trait|type|union)\s+'
def _varint(b: bytes, i: int) -> tuple[int, int]:
r = s = 0
while True:
c = b[i]
i += 1
r |= (c & 0x7F) << s
if c < 0x80:
return r, i
s += 7
def _fields(b: bytes):
i, n = 0, len(b)
while i < n:
key, i = _varint(b, i)
f, wt = key >> 3, key & 7
if wt == 0:
v, i = _varint(b, i)
elif wt == 2:
ln, i = _varint(b, i)
v = b[i:i + ln]
i += ln
elif wt == 1:
v, i = b[i:i + 8], i + 8
elif wt == 5:
v, i = b[i:i + 4], i + 4
else:
raise ValueError(f"unsupported protobuf wire type {wt}")
yield f, wt, v
def _packed(v, wt) -> list[int]:
if wt != 2:
return [v]
out, i = [], 0
while i < len(v):
x, i = _varint(v, i)
out.append(x)
return out
def _span(r: list[int]) -> tuple[tuple[int, int], tuple[int, int]] | None:
if len(r) == 4:
return (r[0], r[1]), (r[2], r[3])
if len(r) == 3:
return (r[0], r[1]), (r[0], r[2])
return None
def load_scip(path: Path) -> list[dict]:
docs = []
for f, _wt, v in _fields(path.read_bytes()):
if f != 2:
continue
doc = {"path": "", "occ": [], "impl": []}
for f2, _w2, v2 in _fields(v):
if f2 == 1:
doc["path"] = v2.decode()
elif f2 == 2:
occ = {"range": [], "symbol": "", "roles": 0, "enc": []}
for f3, w3, v3 in _fields(v2):
if f3 == 1:
occ["range"] = _packed(v3, w3)
elif f3 == 2:
occ["symbol"] = v3.decode()
elif f3 == 3:
occ["roles"] = v3
elif f3 == 7:
occ["enc"] = _packed(v3, w3)
doc["occ"].append(occ)
elif f2 == 3:
sym, targets = "", []
for f3, _w3, v3 in _fields(v2):
if f3 == 1:
sym = v3.decode()
elif f3 == 4:
rel_sym, is_impl = "", False
for f4, _w4, v4 in _fields(v3):
if f4 == 1:
rel_sym = v4.decode()
elif f4 == 3:
is_impl = bool(v4)
if is_impl and rel_sym:
targets.append(rel_sym)
for t in targets:
doc["impl"].append((sym, t))
docs.append(doc)
return docs
class Analysis:
def __init__(self) -> None:
self.items: dict[str, set[str]] = {} self.level: dict[str, str] = {} self.example_refs = 0
self.binding_refs = 0
self.unindexed: list[str] = [] self.fuzz_refs = 0 self.fuzz_docs = 0
self.impl_links = 0 self.external_impls = 0 self.generated_items = 0
IMPL_HEADER_RE = re.compile(r"\bimpl\b[^{;]*\{")
PUB_NAME_RE = re.compile(PUB_DEF + r"([A-Za-z_][A-Za-z0-9_]*)")
FUZZ_INDEX = "fuzz.scip"
FUZZ_PREFIX = "fuzz/"
IMPL_METHOD_RE = re.compile(r"impl#\[(.*)\]\[([^\[\]]+)\]([A-Za-z_][A-Za-z0-9_]*)\(\)\.$")
TYPE_DEF_RE = re.compile(r"(?:^|[/ ])([A-Za-z_][A-Za-z0-9_]*)#$")
TYPE_OR_MEMBER_RE = re.compile(r"(?:^|[/ ])([A-Za-z_][A-Za-z0-9_]*)#(?:[A-Za-z_][A-Za-z0-9_]*\.)?$")
INHERENT_OR_IMPL_RE = re.compile(r"impl#\[([^\]]*(?:\[[^\]]*\][^\]]*)*)\]")
FIELD_RE = re.compile(r"#[A-Za-z_][A-Za-z0-9_]*\.$")
MEMBER_METHOD_RE = re.compile(r"(?:^|[/ ])([A-Za-z_][A-Za-z0-9_]*)#([A-Za-z_][A-Za-z0-9_]*)\(\)\.$")
def _trait_name(raw: str) -> str:
name = raw.strip("`").split("<", 1)[0]
return name.rsplit("::", 1)[-1]
def link_trait_impls(defined: set[str]) -> tuple[dict[str, set[str]], set[str]]:
members: dict[tuple[str, str], set[str]] = {}
impls: list[tuple[str, str, str]] = []
for s in defined:
m = IMPL_METHOD_RE.search(s)
if m:
impls.append((s, _trait_name(m.group(2)), m.group(3)))
continue
m = MEMBER_METHOD_RE.search(s)
if m and not m.group(1).startswith("impl"):
members.setdefault((m.group(1), m.group(2)), set()).add(s)
in_crate = {name for name, _ in members}
linked: dict[str, set[str]] = {}
external: set[str] = set()
for sym, trait, method in impls:
targets = members.get((trait, method))
if targets:
for t in targets:
linked.setdefault(t, set()).add(sym)
elif trait not in in_crate:
external.add(sym)
return linked, external
def _keep_mask(text: str) -> tuple[list[str], list[str]]:
code = remove_cfg_test(strip_rust(text))
blank = lambda m: re.sub(r"[^\n]", " ", m.group(0)) code = USE_RE.sub(blank, code)
code = IMPL_HEADER_RE.sub(lambda m: blank(m)[:-1] + "{", code)
return text.split("\n"), code.split("\n")
def unindexed_items(root: Path, indexed: set[str]) -> list[str]:
out = set()
for p in sorted((Path(root) / "src").rglob("*.rs")):
rel = p.relative_to(root).as_posix()
if rel in BINDING_FILES:
continue
_raw, code = _keep_mask(p.read_text(encoding="utf-8", errors="replace"))
for line in code:
for m in PUB_NAME_RE.finditer(line):
key = f"{rel}::{m.group(1)}"
if key not in indexed:
out.add(key)
return sorted(out)
def item_key(rel: str, name: str, sym: str) -> str:
m = INHERENT_OR_IMPL_RE.search(sym)
return f"{rel}::{_trait_name(m.group(1))}::{name}" if m else f"{rel}::{name}"
def legacy_key(key: str) -> str:
rel, _, rest = key.partition("::")
return f"{rel}::{rest.rsplit('::', 1)[-1]}"
def _visible(code_lines: list[str], pos: tuple[int, int]) -> bool:
line, ch = pos
return line < len(code_lines) and ch < len(code_lines[line]) and not code_lines[line][ch].isspace()
MACRO_RULES_RE = re.compile(r"\bmacro_rules!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{")
MACRO_CALL_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)!\s*[\(\[\{]")
IDENT_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b")
MEMBER_DEF_RE = re.compile(r"\b(?:fn|const)\s+([A-Za-z_][A-Za-z0-9_]*)")
META_PATH_RE = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\s*::\s*([A-Za-z_][A-Za-z0-9_]*)")
META_PARAM_RE = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\s*:\s*[a-z]+")
def macro_params(body: str) -> list[str]:
head = body.split("=>", 1)[0]
return META_PARAM_RE.findall(head)
def split_members(body: str) -> tuple[dict[str, str], str]:
parts: dict[str, str] = {}
rest, last = [], 0
for m in MEMBER_DEF_RE.finditer(body):
if m.start() < last:
continue i, depth, opened = m.end(), 0, False
while i < len(body):
c = body[i]
if c == "{":
depth, opened = depth + 1, True
elif c == "}":
depth -= 1
if opened and depth == 0:
i += 1
break
elif c == ";" and not opened:
i += 1
break
i += 1
rest.append(body[last:m.start()])
parts[m.group(1)] = parts.get(m.group(1), "") + body[m.start():i]
last = i
rest.append(body[last:])
return parts, "".join(rest)
def macro_bodies(text: str) -> dict[str, str]:
out: dict[str, str] = {}
for m in MACRO_RULES_RE.finditer(text):
depth, i = 1, m.end()
while i < len(text) and depth:
depth += {"{": 1, "}": -1}.get(text[i], 0)
i += 1
out[m.group(1)] = text[m.end():i - 1]
return out
def analyze(root: Path, scip_paths: list[Path], keep_graph: bool = False) -> Analysis:
root = Path(root)
a = Analysis()
edges: dict[object, set[str]] = {}
roots_core: set[str] = set()
roots_binding: set[str] = set() roots_by_binding: dict[str, set[str]] = {}
roots_example: set[str] = set()
implementers: dict[str, set[str]] = {}
defined: set[str] = set()
impl_targets: dict[str, set[str]] = {}
src_defined: set[str] = set()
texts: dict[str, tuple[list[str], list[str]]] = {}
seen: set[tuple] = set()
generated: dict[tuple[str, str], dict[str, str]] = {}
gen_args: dict[tuple[str, str, str], list[str]] = {}
def lines_of(rel: str) -> tuple[list[str], list[str]]:
if rel not in texts:
p = root / rel
texts[rel] = _keep_mask(p.read_text(encoding="utf-8", errors="replace")) if p.exists() else ([], [])
return texts[rel]
scip_paths = sorted(scip_paths, key=lambda sp: Path(sp).name == FUZZ_INDEX)
for sp in scip_paths:
prefix = FUZZ_PREFIX if Path(sp).name == FUZZ_INDEX else ""
for doc in load_scip(sp):
doc["path"] = prefix + doc["path"]
rel = doc["path"]
if prefix:
a.fuzz_docs += 1
for sym, target in doc["impl"]:
implementers.setdefault(target, set()).add(sym)
impl_targets.setdefault(sym, set()).add(target)
is_src = rel.startswith("src/")
is_root_file = rel.startswith(ROOT_DIRS)
if not (is_src or is_root_file):
continue raw, code = lines_of(rel)
defs = []
refs = []
for o in doc["occ"]:
s = o["symbol"]
sp_ = _span(o["range"])
if not s or s.startswith("local ") or sp_ is None:
continue
if o["roles"] & 1:
defined.add(s)
if is_src:
src_defined.add(s)
enc = _span(o["enc"]) if o["enc"] else None
if enc is not None:
defs.append((enc[0], enc[1], s))
if is_src and rel not in BINDING_FILES and _visible(code, sp_[0]):
line = raw[sp_[0][0]] if sp_[0][0] < len(raw) else ""
name = line[sp_[0][1]:sp_[1][1]]
if name and re.search(PUB_DEF + re.escape(name) + r"\b", line):
a.items.setdefault(item_key(rel, name, s), set()).add(s)
else:
call = MACRO_CALL_RE.search(code[sp_[0][0]] if sp_[0][0] < len(code) else "")
tm = TYPE_DEF_RE.search(s)
if call and tm and tm.group(1) == name:
generated.setdefault((rel, call.group(1)), {})[name] = s
line = code[sp_[0][0]]
inner = line[call.end():].split(")")[0].split("]")[0].split("}")[0]
gen_args[(rel, call.group(1), name)] = [x.strip() for x in inner.split(",")]
else:
if is_src and not _visible(code, sp_[0]):
continue
refs.append((sp_[0], s))
defs.sort(key=lambda d: (d[0], (-d[1][0], -d[1][1])))
starts = [d[0] for d in defs]
open_defs: list[tuple] = []
for st, en, ds in defs:
while open_defs and open_defs[-1][1] < st:
open_defs.pop()
if open_defs and open_defs[-1][2].endswith("#") and not ds.endswith(")."):
edges.setdefault(open_defs[-1][2], set()).add(ds)
open_defs.append((st, en, ds))
for pos, s in refs:
key = (rel, pos, s)
if key in seen:
continue
seen.add(key)
ctx = None
j = bisect_right(starts, pos) - 1
while j >= 0:
st, en, ds = defs[j]
if st <= pos <= en:
ctx = ds
break
j -= 1
if is_root_file:
roots_example.add(s)
if not prefix:
a.example_refs += 1 elif s in src_defined:
a.fuzz_refs += 1 elif rel in BINDING_FILES:
roots_core.add(s)
roots_binding.add(s)
roots_by_binding.setdefault(rel, set()).add(s)
a.binding_refs += 1
elif ctx is None:
roots_core.add(s) else:
edges.setdefault(ctx, set()).add(s)
defined_by_name: dict[tuple[str, str], set[str]] = {}
for d in defined:
head, _, tail = d.rpartition("/")
for suffix in ("#", "().", "."):
if tail.endswith(suffix) and not tail[:-len(suffix)].count("#"):
defined_by_name.setdefault((head, tail[:-len(suffix)]), set()).add(d)
for (rel, macro), types in sorted(generated.items()):
body = macro_bodies("\n".join(lines_of(rel)[1])).get(macro)
if body is None:
continue
members = sorted(set(PUB_NAME_RE.findall(body)))
member_text, outside = split_members(body)
params = macro_params(body)
for tname, tsym in sorted(types.items()):
base = tsym[:-len(tname) - 1] gen_syms = {tsym}
a.items.setdefault(f"{rel}::{tname}", set()).add(tsym)
for n in members:
syms = {f"{base}impl#[{tname}]{n}().", f"{base}impl#[{tname}]{n}."}
a.items.setdefault(f"{rel}::{tname}::{n}", set()).update(syms)
gen_syms |= syms
mod = base.rstrip("/")
bound = dict(zip(params, gen_args.get((rel, macro, tname), [])))
def reach(text: str, own: str | None) -> set[str]:
out: set[str] = set()
for ident in set(IDENT_RE.findall(text)) - ({own} if own else set()):
out |= defined_by_name.get((mod, ident), set())
if ident in member_text: out |= {f"{base}impl#[{tname}]{ident}().", f"{base}impl#[{tname}]{ident}."}
for param, name in META_PATH_RE.findall(text):
other = bound.get(param)
if other:
out |= {f"{base}impl#[{other}]{name}().", f"{base}impl#[{other}]{name}."}
return out
edges.setdefault(tsym, set()).update(reach(outside, None) - {tsym})
for n, text in member_text.items():
own = {f"{base}impl#[{tname}]{n}().", f"{base}impl#[{tname}]{n}."}
for g in own: edges.setdefault(g, set()).update(reach(text, n) - own)
a.generated_items += len(members) + 1
a.unindexed = unindexed_items(root, {legacy_key(k) for k in a.items})
external: set[str] = {s for s, ts in impl_targets.items() if any(t not in defined for t in ts)}
a.impl_links = sum(len(v) for v in implementers.values())
linked, ext_by_name = link_trait_impls(defined)
for trait_method, impl_methods in linked.items():
implementers.setdefault(trait_method, set()).update(impl_methods)
a.impl_links += len(impl_methods)
external |= ext_by_name
a.external_impls = len(external)
type_defs: dict[str, list[str]] = {}
for d in defined:
m = TYPE_DEF_RE.search(d)
if m:
type_defs.setdefault(m.group(1), []).append(d)
def self_type(impl_sym: str) -> str | None:
m = IMPL_METHOD_RE.search(impl_sym)
name = _trait_name(m.group(1)) if m else ""
return name if name in type_defs else None
def type_of(sym: str) -> str | None:
m = INHERENT_OR_IMPL_RE.search(sym)
if m:
return _trait_name(m.group(1))
m = TYPE_OR_MEMBER_RE.search(sym)
return m.group(1) if m else None
def reach(start: set[str]) -> set[str]:
live = set(start)
stack = list(start)
pending = set(external) while True:
while stack:
s = stack.pop()
for t in edges.get(s, ()):
if t not in live:
live.add(t)
stack.append(t)
m = FIELD_RE.search(s)
if m:
owner = s[:m.start() + 1]
if owner not in live and owner in defined:
live.add(owner)
stack.append(owner)
for t in implementers.get(s, ()):
if t not in live:
pending.add(t)
reached_types = {ty for ty in map(type_of, live) if ty}
ready = set()
for t in pending:
if t in live:
ready.add(t)
continue
ty = self_type(t)
if ty is None or ty in reached_types:
ready.add(t)
live.add(t)
stack.append(t)
pending -= ready
if not stack:
return live
live_core = reach(roots_core)
live_all = reach(roots_core | roots_example)
for key, syms in a.items.items():
if syms & live_core:
a.level[key] = "live"
elif syms & live_all:
a.level[key] = "L1"
else:
a.level[key] = "L0"
if keep_graph:
a.reach = reach
a.roots_core = set(roots_core)
a.roots_binding = set(roots_binding)
a.roots_by_binding = {k: set(v) for k, v in roots_by_binding.items()}
a.roots_example = set(roots_example)
return a
def baseline_unwired(root: Path) -> set[str]:
p = root / "scripts" / "wiring-baseline.txt"
if not p.exists():
return set()
out = set()
for line in p.read_text(encoding="utf-8").splitlines():
parts = line.split()
if len(parts) == 2 and parts[0] == "unwired":
out.add(parts[1])
return out
def report(a: Analysis, baseline: set[str]) -> str:
l0 = sorted(k for k, v in a.level.items() if v == "L0")
l1 = sorted(k for k, v in a.level.items() if v == "L1")
live = sum(1 for v in a.level.values() if v == "live")
by_legacy: dict[str, list[str]] = {}
for k in a.level:
by_legacy.setdefault(legacy_key(k), []).append(k)
missed = [k for k in l0 if legacy_key(k) not in baseline]
resolved = sorted(b for b in baseline
if by_legacy.get(b) and all(a.level[k] in ("live", "L1") for k in by_legacy[b]))
out = [
"# ALICE-Physics Integration Status",
"",
"_Generated by `scripts/scip_reach.py` from rust-analyzer SCIP indexes (`scripts/scip_index.sh`); no timestamp, the file changes only when its content does._",
"",
"Every `pub` / `pub(crate)` item defined in `src/` (binding files excluded), classified by what reaches it.",
"References are resolved to one definition each, so items that share a name are told apart.",
"",
"| Level | Meaning | Count |",
"|-------|---------|------:|",
f"| L0 | not reached by any non-test code, examples included | {len(l0)} |",
f"| L1 | reached only from `examples/` / `benches/` / `fuzz/` | {len(l1)} |",
f"| live | reached without examples (crate-internal roots or a binding) | {live} |",
f"| | **total** | **{len(a.level)}** |",
"",
"L1 is a label, not a defect: a module users call directly is example-only inside this crate.",
"It does mean the item is not reached from `PhysicsWorld`, another module, or a binding.",
"",
"## Compared with the wiring guard",
"",
f"`scripts/wiring-baseline.txt` lists {len(baseline)} unwired items.",
"",
f"### L0 here but not in the baseline ({len(missed)})",
"",
"The name-based guard counts these as wired; resolved references find no caller.",
"",
]
out += [f"- `{k}`" for k in missed] or ["- (none)"]
out += [
"",
f"### In the baseline but reached here ({len(resolved)})",
"",
"The guard lists these as unwired; a resolved reference reaches them (level in brackets).",
"",
]
out += [f"- `{k}` ({'/'.join(sorted({a.level[x] for x in by_legacy[k]}))})" for k in resolved] or ["- (none)"]
out += ["", f"## L0 — unreached ({len(l0)})", ""]
out += [f"- `{k}`" for k in l0] or ["- (none)"]
out += ["", f"## Not indexed ({len(a.unindexed)})", "",
"`pub` items in the source that the SCIP index has no definition for (items inside a "
"`macro_rules` body). Their reach is not checked; the baseline lists them so the set cannot grow unnoticed.", ""]
out += [f"- `{k}`" for k in a.unindexed] or ["- (none)"]
out += [
"",
"## Limits",
"",
"- A pattern in a `match` arm counts as a reference: a type that is only matched on, never constructed, is reached.",
"- Code inside macro expansions is resolved as far as rust-analyzer resolves it.",
"- Calls through a trait (`dyn Tr`, `T: Tr`) reach the impls of that method whose self type is reached (the type, a field, or one of its methods is live); an impl of a type nothing reaches stays unreached.",
"- Trait-impl links come from the impl symbol names; rust-analyzer's SCIP output has no implementation relationships.",
"- Methods are listed as `file::Type::method`, so same-named methods of different types in one file are told apart.",
"- Items in `src/ffi.rs`, `src/python.rs` and `src/wasm.rs` are roots and are not listed.",
]
out += ["", f"## L1 — example-only ({len(l1)})", ""]
by_file: dict[str, list[str]] = {}
for k in l1:
f, n = k.split("::", 1)
by_file.setdefault(f, []).append(n)
if not by_file:
out.append("- (none)")
for f in sorted(by_file):
out.append(f"- `{f}`: " + ", ".join(f"`{n}`" for n in by_file[f]))
out.append("")
return "\n".join(out)
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--root", default=str(Path(__file__).resolve().parent.parent))
ap.add_argument("--scip", default="target/scip")
ap.add_argument("--write", help="write the markdown ledger to this path")
ap.add_argument("--baseline", default="scripts/integration-baseline.txt")
ap.add_argument("--check-baseline", action="store_true", help="fail on new L0 items or stale baseline entries")
ap.add_argument("--write-baseline", action="store_true", help="record the current L0 items as the baseline")
args = ap.parse_args(argv)
root = Path(args.root)
sdir = Path(args.scip) if Path(args.scip).is_absolute() else root / args.scip
paths = [sdir / "native.scip", sdir / "wasm.scip", sdir / FUZZ_INDEX]
missing = [str(p) for p in paths if not p.exists()]
if missing:
print(f"error: SCIP index missing: {missing} (run scripts/scip_index.sh)", file=sys.stderr)
return 1
a = analyze(root, paths)
errors = []
if not a.items:
errors.append("0 pub items found in src/ (the analysis looked at nothing)")
if a.example_refs == 0:
errors.append("0 references from examples/benches (index or path filter is wrong)")
if a.binding_refs == 0:
errors.append("0 references from binding files (feature-gated modules were not indexed)")
if a.fuzz_docs == 0 or a.fuzz_refs == 0:
errors.append(f"fuzz index: {a.fuzz_docs} documents, {a.fuzz_refs} references to the crate "
"(fuzz targets would not count as callers)")
if a.impl_links == 0:
errors.append("0 trait-impl links resolved (calls through a trait would never reach an impl)")
counts = {lv: sum(1 for v in a.level.values() if v == lv) for lv in ("L0", "L1", "live")}
print(f"compared: items {len(a.items)}, example refs {a.example_refs}, binding refs {a.binding_refs}, "
f"fuzz refs {a.fuzz_refs}, trait-impl links {a.impl_links}, external-trait impls {a.external_impls}, "
f"unindexed {len(a.unindexed)}, "
f"L0 {counts['L0']}, L1 {counts['L1']}, live {counts['live']}")
for e in errors:
print(f"error: {e}", file=sys.stderr)
if errors:
return 1
if args.write:
Path(args.write).write_text(report(a, baseline_unwired(root)), encoding="utf-8")
bpath = Path(args.baseline) if Path(args.baseline).is_absolute() else root / args.baseline
l0 = sorted(k for k, v in a.level.items() if v == "L0")
if args.write_baseline:
bpath.write_text(BASELINE_HEADER + "".join(f"{k}\n" for k in l0)
+ "".join(f"{UNINDEXED}{k}\n" for k in a.unindexed), encoding="utf-8")
print(f"wrote {len(l0)} L0 entries and {len(a.unindexed)} unindexed entries to {bpath}")
if args.check_baseline:
if not bpath.exists():
print(f"error: baseline {bpath} missing (run with --write-baseline)", file=sys.stderr)
return 1
lines = {ln.strip() for ln in bpath.read_text(encoding="utf-8").splitlines()
if ln.strip() and not ln.startswith("#")}
base = {ln for ln in lines if not ln.startswith(UNINDEXED)}
base_unindexed = {ln[len(UNINDEXED):] for ln in lines if ln.startswith(UNINDEXED)}
new_unindexed = [k for k in a.unindexed if k not in base_unindexed]
stale_unindexed = sorted(base_unindexed - set(a.unindexed))
for k in new_unindexed:
print(f"error: new unindexed item {k}: a pub item the SCIP index has no definition for "
"(a macro_rules body?), so its reach cannot be checked", file=sys.stderr)
for k in stale_unindexed:
print(f"error: stale unindexed entry {k}: remove the line", file=sys.stderr)
new = [k for k in l0 if k not in base]
stale = sorted(k for k in base if a.level.get(k) != "L0")
for k in new:
print(f"error: new L0 item {k}: no non-test code reaches it (add a caller or an example, "
"or remove it)", file=sys.stderr)
for k in stale:
lv = a.level.get(k, "gone")
print(f"error: stale baseline entry {k} (now {lv}): remove the line", file=sys.stderr)
print(f"baseline: {len(base)} entries, new L0 {len(new)}, stale {len(stale)}; "
f"unindexed {len(base_unindexed)} entries, new {len(new_unindexed)}, stale {len(stale_unindexed)}")
if new or stale or new_unindexed or stale_unindexed:
return 1
return 0
UNINDEXED = "unindexed: "
BASELINE_HEADER = """# L0 items (no non-test code reaches them) that existed when the ratchet was
# introduced. scripts/scip_reach.py --check-baseline fails on any L0 item not
# listed here, and on any line here that is no longer L0. Shrink this file;
# regenerate with --write-baseline only for an intended change.
# Lines starting with "unindexed: " are pub items the SCIP index has no
# definition for (macro_rules bodies); they have no level, the same ratchet
# applies to them.
"""
if __name__ == "__main__":
sys.exit(main())