Files
OpenMaidEngine/tools/scene_opcode_coverage.py
gamer147 b94a20b9ec feat(tools): per-scene opcode completeness tracker + 0x259 marker fix
Add tools/scene_opcode_coverage.py: histograms a scene's static opcodes and
classifies each vs the C# VM as impl / safe-noop / GAP (effectful op the VM
silently stubs). Implemented set is parsed live from VirtualMachine.cs case arms
(no drift); metadata from build/opcodes.json. Makes a half-rendered scene legible
("N ops still stubbed") instead of implying everything runs.

SC0000 baseline: 129 distinct ops, ~94.8% instruction-weighted handled, 68 GAP.
The tracker cross-checks opcodes.toml vs VM behavior and surfaced 0x259
(script-entry marker) missing its noop_headless flag -> reconciled in opcodes.toml
and rebuilt (regen: age_opcodes_himegari.py, opcode-reference.md).

Docs: tools-reference.md (tool row), phase-a-slice-plan.md (completeness gauge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:57:25 -04:00

236 lines
9.3 KiB
Python
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.
#!/usr/bin/env py -3.11 -X utf8
"""Per-scene opcode coverage / completeness tracker.
Answers "how complete SHOULD this scene be?" by histogramming the opcodes a
scene's bytecode actually contains and classifying each against what the C# VM
implements. A half-rendered scene is far less alarming once we can see it still
has effectful ops sitting un-implemented (a GAP) versus everything being handled.
Three tiers per opcode:
impl - the VM has an explicit handler arm (real effect OR deliberate
no-op like set-font/comment). Correct treatment.
safe-noop - no VM arm, but opcodes.toml marks it noop_headless (statement /
block markers, 0x258/0x259/0x1f4...). Skipping it is correct.
GAP - no VM arm AND not a safe no-op -> an effectful op the VM silently
skips (default stub, pc+1). THESE are the completeness holes.
Single sources of truth (no hand-maintained duplicate list):
- implemented labels <- parsed from engine/Age.Engine/Vm/VirtualMachine.cs
(every `case "label":` arm)
- opcode metadata <- build/opcodes.json (label/name/category/noop_headless)
- scene opcodes <- sys4load.load(<SCENE>.BIN).instructions (canonical loader)
Usage:
py -3.11 -X utf8 tools/scene_opcode_coverage.py # SC0000 (default)
py -3.11 -X utf8 tools/scene_opcode_coverage.py SC0240 SP0062 # specific scenes
py -3.11 -X utf8 tools/scene_opcode_coverage.py --build # regen the SC0000 report doc
Writes build/scene-opcode-coverage/<SCENE>.md and prints a summary to stdout.
"""
from __future__ import annotations
import sys
import re
import json
import collections
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import paths
import sys4load
VM_SRC = paths.REPO / "engine" / "Age.Engine" / "Vm" / "VirtualMachine.cs"
OPCODES = paths.BUILD / "opcodes.json"
CALLNAME = paths.BUILD / "callscript-names.json"
OUT_DIR = paths.BUILD / "scene-opcode-coverage"
def implemented_labels() -> set[str]:
"""Every opcode label the VM dispatches with an explicit `case` arm."""
src = VM_SRC.read_text(encoding="utf8")
return set(re.findall(r'case\s+"([^"]+)"\s*:', src))
def opcode_meta() -> dict[int, dict]:
"""op(int) -> {label,name,category,noop_headless,confidence} from opcodes.json."""
doc = json.loads(OPCODES.read_text(encoding="utf8"))
out: dict[int, dict] = {}
for e in doc["opcodes"]:
op = int(e["op"], 16)
sem = e.get("semantics", {})
out[op] = {
"label": e.get("label", f"op_{op:#x}"),
"name": sem.get("name", ""),
"category": sem.get("category", "unknown"),
"noop_headless": bool(sem.get("noop_headless", False)),
"confidence": sem.get("confidence", "low"),
"summary": sem.get("summary", ""),
}
return out
def callscript_names() -> dict[int, str]:
if not CALLNAME.exists():
return {}
doc = json.loads(CALLNAME.read_text(encoding="utf8"))
# file maps str(id) -> name
return {int(k): v for k, v in doc.items()}
def classify(op: int, meta: dict, impl: set[str]) -> str:
label = meta.get(op, {}).get("label", "")
if label in impl:
return "impl"
if meta.get(op, {}).get("noop_headless", False):
return "safe-noop"
return "GAP"
def analyze(scene: str, meta: dict[int, dict], impl: set[str], cnames: dict[int, str]):
scripts = paths.scripts()
key = scene.upper()
if not key.endswith(".BIN"):
key += ".BIN"
if key not in scripts:
raise SystemExit(f"scene not found in corpus: {key}")
scr = sys4load.load(scripts[key])
hist = collections.Counter()
callscript_ids = collections.Counter()
for ins in scr.instructions:
hist[ins.opcode] += 1
m = meta.get(ins.opcode, {})
if m.get("label") == "call-script" and ins.args:
a0 = ins.args[0]
# only resolve immediate ids (arg type 0); dynamic ids left unresolved
if getattr(a0, "type", None) == 0:
callscript_ids[a0.value] += 1
rows = []
for op, n in hist.items():
m = meta.get(op, {"label": f"op_{op:#x}", "name": "", "category": "unknown",
"confidence": "low", "summary": ""})
rows.append({
"op": op, "count": n, "label": m["label"], "name": m["name"],
"category": m["category"], "confidence": m["confidence"],
"summary": m["summary"], "status": classify(op, meta, impl),
})
total_ins = sum(hist.values())
by_status = collections.Counter(r["status"] for r in rows)
ins_by_status = collections.Counter()
for r in rows:
ins_by_status[r["status"]] += r["count"]
return {
"scene": key, "rows": rows, "total_ins": total_ins,
"distinct": len(rows), "by_status": by_status,
"ins_by_status": ins_by_status, "callscript_ids": callscript_ids,
"cnames": cnames,
}
STATUS_ORDER = {"GAP": 0, "impl": 1, "safe-noop": 2}
def render_md(a: dict) -> str:
L = []
L.append(f"# Opcode coverage — {a['scene']}")
L.append("")
L.append("> Generated by `tools/scene_opcode_coverage.py` — do NOT hand-edit.")
L.append("> Static histogram of the scene's bytecode vs the C# VM's implemented ops.")
L.append("")
d, t = a["distinct"], a["total_ins"]
bs, ib = a["by_status"], a["ins_by_status"]
def pct(x, whole):
return f"{100.0*x/whole:.1f}%" if whole else ""
L.append("## Summary")
L.append("")
L.append(f"- **{d} distinct opcodes**, {t} instructions total.")
L.append(f"- **impl**: {bs['impl']} ops ({pct(bs['impl'], d)}) / "
f"{ib['impl']} instrs ({pct(ib['impl'], t)}) — VM has a handler.")
L.append(f"- **safe-noop**: {bs['safe-noop']} ops ({pct(bs['safe-noop'], d)}) / "
f"{ib['safe-noop']} instrs ({pct(ib['safe-noop'], t)}) — markers, correct to skip.")
L.append(f"- **GAP**: {bs['GAP']} ops ({pct(bs['GAP'], d)}) / "
f"{ib['GAP']} instrs ({pct(ib['GAP'], t)}) — effectful, silently stubbed.")
handled = bs["impl"] + bs["safe-noop"]
L.append(f"- **Correctly handled (impl + safe-noop): {handled}/{d} ops "
f"= {pct(handled, d)}**; remaining {bs['GAP']} are the completeness holes below.")
L.append("")
def table(title, rows):
L.append(f"## {title}")
L.append("")
if not rows:
L.append("_(none)_")
L.append("")
return
L.append("| op | count | label | name | category | conf | summary |")
L.append("|----|------:|-------|------|----------|------|---------|")
for r in rows:
summ = r["summary"].replace("|", "\\|")
L.append(f"| `{r['op']:#x}` | {r['count']} | `{r['label']}` | "
f"{r['name']} | {r['category']} | {r['confidence']} | "
f"{summ} |")
L.append("")
rows = sorted(a["rows"], key=lambda r: (STATUS_ORDER[r["status"]], -r["count"]))
gaps = [r for r in rows if r["status"] == "GAP"]
impl = [r for r in rows if r["status"] == "impl"]
noop = [r for r in rows if r["status"] == "safe-noop"]
table("GAP — effectful ops the VM silently skips (the completeness holes)", gaps)
table("impl — handled by the VM", impl)
table("safe-noop — markers / structural (correct to skip headless)", noop)
if a["callscript_ids"]:
L.append("## call-script targets (immediate ids in this scene)")
L.append("")
L.append("Subroutines this scene invokes statically — their own opcodes are NOT")
L.append("counted above (run the tracker on them too for full-depth coverage).")
L.append("")
L.append("| id | name | sites |")
L.append("|----|------|------:|")
for cid, n in sorted(a["callscript_ids"].items(), key=lambda kv: -kv[1]):
nm = a["cnames"].get(cid, "?")
L.append(f"| `{cid:#x}` | {nm} | {n} |")
L.append("")
return "\n".join(L)
def print_summary(a: dict):
bs, ib, d, t = a["by_status"], a["ins_by_status"], a["distinct"], a["total_ins"]
handled = bs["impl"] + bs["safe-noop"]
print(f"\n{a['scene']}: {d} distinct opcodes, {t} instructions")
print(f" impl {bs['impl']:3d} ops / {ib['impl']:6d} instrs")
print(f" safe-noop {bs['safe-noop']:3d} ops / {ib['safe-noop']:6d} instrs")
print(f" GAP {bs['GAP']:3d} ops / {ib['GAP']:6d} instrs <- effectful, stubbed")
print(f" handled {handled}/{d} ops ({100.0*handled/d:.1f}%)")
if bs["GAP"]:
gaps = sorted((r for r in a["rows"] if r["status"] == "GAP"),
key=lambda r: -r["count"])
print(" GAP ops:", ", ".join(
f"{r['label']}({r['op']:#x})×{r['count']}" for r in gaps))
def main(argv: list[str]):
scenes = [x for x in argv if not x.startswith("--")]
if not scenes:
scenes = ["SC0000"]
meta = opcode_meta()
impl = implemented_labels()
cnames = callscript_names()
OUT_DIR.mkdir(parents=True, exist_ok=True)
for scene in scenes:
a = analyze(scene, meta, impl, cnames)
out = OUT_DIR / f"{a['scene'].replace('.BIN', '')}.md"
out.write_text(render_md(a), encoding="utf8")
print_summary(a)
print(f" -> {out.relative_to(paths.REPO)}")
if __name__ == "__main__":
main(sys.argv[1:])