Files
OpenMaidEngine/tools/validate_opcode_table_naive.py
2026-08-03 14:37:58 -04:00

76 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Validate the canonical AGE opcode table against Himegari SYS4 scripts.
Model recorded in vm-map/opcodes.toml:
code stream = sequence of instructions.
each instruction = <opcode:u32> then argument_count * <arg>, where each arg = <type:u32><value:u32>.
=> instruction length in dwords = 1 + 2*argument_count (uniform; type-2/0x64 args seek elsewhere, don't consume inline)
arg type 2 = inline string (value = dword offset into body). types: 0 imm,1 float,3 g-int,9 l-int, etc.
A clean decode consumes exactly code_len dwords with no unknown opcode and no arg overrun.
"""
import os, sys, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import paths
import sys4load
from age_opcodes import OPCODES
TABLE = {op: entry[1] for op, entry in OPCODES.items()}
LABEL = {op: entry[0] for op, entry in OPCODES.items()}
print(f"loaded {len(TABLE)} opcode defs from the canonical registry (max op 0x{max(TABLE):x})")
files = paths.scripts()
clean = dirty = parsefail = 0
unknown_ops = collections.Counter()
fail_examples = []
str_ok = str_bad = 0
opcode_use = collections.Counter()
for name, p in sorted(files.items()):
try:
scr = sys4load.load(p)
except Exception:
parsefail += 1; continue
dw = scr.dwords; cl = scr.code_len
i = 0; ok = True; reason = ""
while i < cl:
op = dw[i]
if op not in TABLE:
unknown_ops[op] += 1; ok = False; reason = f"unknown op 0x{op:x} @{i}"; break
opcode_use[op] += 1
argc = TABLE[op]
# check each arg's type; resolve strings
base = i + 1
if base + 2*argc > cl:
ok = False; reason = f"arg overrun op 0x{op:x} @{i} needs {argc} args"; break
for a in range(argc):
atype = dw[base + 2*a]; aval = dw[base + 2*a + 1]
if atype == 2: # inline string
if 0 <= aval < scr.nbody:
txt, nd = sys4load._decode_string(dw, aval)
if txt is None: str_bad += 1
else: str_ok += 1
else:
str_bad += 1
i = base + 2*argc
if ok and i == cl:
clean += 1
else:
dirty += 1
if len(fail_examples) < 15:
fail_examples.append(f" {name}: {reason} (stopped @{i}/{cl})")
print(f"\n== decode result over {len(files)} scripts ==")
print(f" clean (fully consumed, all opcodes known): {clean}")
print(f" dirty: {dirty}")
print(f" parse-fail (container): {parsefail}")
print(f" string args resolved ok / bad: {str_ok} / {str_bad}")
print(f"\ntop unknown opcodes (op: files affected):")
for op, c in unknown_ops.most_common(25):
print(f" 0x{op:x}: {c}")
print(f"\nsample dirty files:")
print("\n".join(fail_examples))
print(f"\ntop 25 opcodes actually used in Himegari (op label count):")
for op, c in opcode_use.most_common(25):
print(f" 0x{op:<4x} {LABEL.get(op,'?'):22} {c}")