76 lines
2.9 KiB
Python
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}")
|