alice-physics 2.0.0

Deterministic physics engine: 128-bit fixed-point rigid bodies with bit-exact, replayable results, typed observation and rollback for search and planning, plus FEM, CFD and 3D-print analysis modules
Documentation
#!/usr/bin/env python3
"""Resolve the code symbols that known-defect reasons name, before anyone fixes them.

Every `#[ignore = "known defect: AUD-…: …"]` in `tests/` describes a defect in
prose, and the prose names code: `DebugDrawData::aabb`, `insert_hash()`,
`` `step_with_options` ``. A fix starts by finding those items. Doing that by
grepping for a word in the description is how a correct audit gets "corrected"
into a wrong one (a search for "wireframe" finds no function, although
`DebugDrawData::aabb` — documented as "Draw an AABB wireframe" — exists).

This script resolves each mention to its definition (`file:line`) with the
rust-analyzer SCIP indexes from `scripts/scip_index.sh`, falling back to the
source text for items SCIP does not index (methods generated by `macro_rules`,
type aliases).

  python3 scripts/audit_refs.py AUD-A-S4W3-008     # where is everything this defect names?
  python3 scripts/audit_refs.py --check            # every mention in every reason resolves

Mentions recognised: `a::b` paths, `name()` calls, and identifiers in
backticks. Bare words are not checked (they are English as often as code).
`--check` fails when a mention resolves to nothing, or when it compared nothing.
"""

from __future__ import annotations

import argparse
import importlib.util
import re
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import scip_reach  # noqa: E402

ID_RE = re.compile(r"AUD-[A-Z]-S\d+(?:W\d+)?-\d+")
PATH_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)::([A-Za-z_][A-Za-z0-9_]*)\b")
CALL_RE = re.compile(r"\b([a-z_][a-z0-9_]{2,})\(\)")
TICK_RE = re.compile(r"`([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)`")
# words that appear in `a::b` / backticks in prose but are not crate items
NOT_ITEMS = {"self", "Self", "super", "crate", "std", "core", "alloc", "f32", "f64", "i32", "i64", "u32", "u64",
             "usize", "isize", "u8", "u16", "i8", "i16", "u128", "i128", "bool", "Option", "Some", "None", "Ok",
             "Err", "Result", "Vec", "MAX", "MIN", "EPSILON", "NAN", "INFINITY", "NEG_INFINITY", "ZERO", "ONE",
             "true", "false", "NaN", "inf"}


def _load_oracle():
    spec = importlib.util.spec_from_file_location("gen_oracle_status", HERE / "gen-oracle-status.py")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def defects(root: Path) -> list[dict]:
    """Known-defect tests: id, test, file, reason."""
    oracle = _load_oracle()
    out = []
    for tf in sorted((root / "tests").glob("*.rs")):
        for t in oracle.extract_test_metadata(tf):
            r = t["ignore_reason"]
            if r.lower().startswith("known defect"):
                m = ID_RE.search(r)
                out.append({"id": m.group(0) if m else "", "test": t["name"], "file": t["file"], "reason": r})
    return out


FILE_REF_RE = re.compile(r"[A-Za-z0-9_/.-]+\.rs(?::\d+)?(?:::[A-Za-z_][A-Za-z0-9_]*)?")


def mentions(reason: str) -> list[str]:
    m = ID_RE.search(reason)
    body = reason[m.end():] if m else reason  # the description starts after the defect id
    body = FILE_REF_RE.sub(" ", body)  # `tests/foo.rs::test_name` points at a test, not at an item
    body = re.split(r"root:\s*external\b", body)[0]  # what follows names the dependency's code
    found: list[tuple[str, bool]] = []  # (mention, came from backticks)
    for m in TICK_RE.finditer(body):
        found.append((m.group(1), True))
    for a, b in PATH_RE.findall(body):
        found.append((f"{a}::{b}", False))
    for n in CALL_RE.findall(body):
        found.append((n, False))
    out = []
    for f, ticked in found:
        parts = f.split("::")
        if parts[0] in NOT_ITEMS:
            continue  # `f32::clamp`, `i64::try_from`, `Option::map`: std, not this crate
        if len(parts) == 1 and parts[0] in NOT_ITEMS:
            continue
        if ticked and len(parts) == 1 and parts[0].islower() and "_" not in parts[0]:
            continue  # a single lowercase word in backticks is as often prose as code
        if f not in out:
            out.append(f)
    return out


class Index:
    """Definitions in src/: by name, by (container, name), and from the source text."""

    def __init__(self, root: Path, scip_paths: list[Path]):
        self.by_name: dict[str, list[str]] = {}
        self.by_pair: dict[tuple[str, str], list[str]] = {}
        for sp in scip_paths:
            for doc in scip_reach.load_scip(sp):
                if not doc["path"].startswith("src/"):
                    continue
                for o in doc["occ"]:
                    s = o["symbol"]
                    if not (o["roles"] & 1) or not s or s.startswith("local "):
                        continue
                    loc = f"{doc['path']}:{o['range'][0] + 1}"
                    desc = s.split(" ", 4)[-1]
                    segs = [x for x in re.split(r"[/#.()\[\]`<>]+", desc) if x]
                    if not segs:
                        continue
                    name = segs[-1]
                    self.by_name.setdefault(name, []).append(loc)
                    for c in segs[:-1]:
                        self.by_pair.setdefault((c, name), []).append(loc)
        # text fallback: items that SCIP does not index (macro_rules bodies, aliases)
        self.text_fn: dict[str, list[str]] = {}
        self.text_type: dict[str, set[str]] = {}  # type name -> files that declare it
        for p in sorted((root / "src").rglob("*.rs")):
            rel = p.relative_to(root).as_posix()
            for i, line in enumerate(p.read_text(encoding="utf-8", errors="replace").split("\n"), 1):
                for m in re.finditer(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)", line):
                    self.text_fn.setdefault(m.group(1), []).append(f"{rel}:{i}")
                for m in re.finditer(r"\b(?:struct|enum|type|trait|union|mod)\s+([A-Za-z_][A-Za-z0-9_]*)", line):
                    self.text_type.setdefault(m.group(1), set()).add(rel)
                for m in re.finditer(r"\b[a-z_]+!\(\s*([A-Z][A-Za-z0-9_]*)\s*,", line):
                    self.text_type.setdefault(m.group(1), set()).add(rel)  # impl_foo!(Name, ...) declares a type

    def resolve(self, mention: str) -> tuple[str, list[str]]:
        """(how, locations): how is 'scip', 'text', or '' when unresolved."""
        parts = mention.split("::")
        if len(parts) == 1:
            n = parts[0]
            if n in self.by_name:
                return "scip", self.by_name[n]
            if n in self.text_fn:
                return "text", self.text_fn[n]
            return ("text", []) if n in self.text_type else ("", [])
        a, b = parts[-2], parts[-1]
        if (a, b) in self.by_pair:
            return "scip", self.by_pair[(a, b)]
        if b in NOT_ITEMS or b.isupper():  # `Fix128::MAX`, `Type::CONST`
            if a in self.by_name or a in self.text_type:
                return "scip", self.by_name.get(a, [])
        # prose shortens type names (`Laplace::new` for `LaplaceNoise::new`): a type whose
        # name starts with `a` and has a SCIP definition of `b` is that definition
        pref = sorted({loc for (t, n), locs in self.by_pair.items() if n == b and t.startswith(a) for loc in locs})
        if pref:
            return "scip", pref
        # a type generated by a macro (no SCIP definition) whose method is in the macro
        # body: only a `fn b` in a file that declares the type counts. Any `fn b` in the
        # crate used to count, so `Laplace::new` resolved to two unrelated `new`s
        files = {f for t, fs in self.text_type.items() if t == a or t.startswith(a) for f in fs}
        files |= {loc.rsplit(":", 1)[0] for loc in self.by_name.get(a, [])}
        locs = [loc for loc in self.text_fn.get(b, []) if loc.rsplit(":", 1)[0] in files]
        if locs:
            return "text", locs
        return "", []


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
    ap.add_argument("ids", nargs="*", help="defect IDs (or test names) to look up")
    ap.add_argument("--root", default=str(HERE.parent))
    ap.add_argument("--scip", default="target/scip")
    ap.add_argument("--check", action="store_true", help="fail when any mention does not resolve")
    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"]
    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
    idx = Index(root, paths)
    ds = defects(root)
    if args.ids:
        ds = [d for d in ds if d["id"] in args.ids or d["test"] in args.ids]
        if not ds:
            print(f"error: no known defect with id {args.ids}", file=sys.stderr)
            return 1
    n_mentions, unresolved = 0, []
    for d in ds:
        ms = mentions(d["reason"])
        n_mentions += len(ms)
        if args.ids:
            print(f"{d['id']}  {d['file']}::{d['test']}")
            print(f"  {d['reason']}")
        for m in ms:
            how, locs = idx.resolve(m)
            if not how:
                unresolved.append((d, m))
            if args.ids:
                where = ", ".join(sorted(set(locs))[:4]) + (" …" if len(set(locs)) > 4 else "")
                print(f"  {m:40s} {'NOT FOUND' if not how else how + ': ' + where}")
    print(f"compared: defects {len(ds)}, mentions {n_mentions}, unresolved {len(unresolved)}")
    for d, m in unresolved:
        print(f"unresolved: {d['id'] or '(no id)'} ({d['file']}::{d['test']}) names `{m}`, defined nowhere in src/",
              file=sys.stderr)
    if args.check:
        if not ds or n_mentions == 0:
            print("error: compared nothing (no known-defect reasons, or none names code)", file=sys.stderr)
            return 1
        return 1 if unresolved else 0
    return 0


if __name__ == "__main__":
    sys.exit(main())