feat(opcodes): register full AGE opcode ABI

This commit is contained in:
gamer147
2026-07-28 22:21:40 -04:00
parent 8f84f0b083
commit 77c7b85612
8 changed files with 6682 additions and 21 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -655,12 +655,21 @@ A cursory Kamidori boot probe on 2026-07-20 validated much of this boundary: its
archives loaded, `SYSTEM4.BIN` completed the initializer chain, call-script resolution entered
`TITLE.BIN`, and title graphics/resource commands resolved. The first hard stop was not a different
container or catalog. Kamidori's title loop reached opcode `0x1be` (`u0041D9D0`, argc 2) at bytecode
offset `0xd7`. That opcode exists in Kelebek's full AGE/SYS4 table, but it is absent from the generated
`build/opcodes.json` because that runtime artifact currently contains only opcodes observed in Himegari.
offset `0xd7`. That opcode existed in Kelebek's full AGE/SYS4 table, but was absent from the generated
`build/opcodes.json` because the runtime artifact contained only opcodes observed in Himegari.
`Sys4Loader` therefore stopped decoding at the unknown opcode; `TITLE.BIN` returned as if it had ended,
and Godot displayed its ordinary `— end —` marker instead of reporting an incompatibility.
The future multi-game design must keep three separate concepts:
**ABI-registry floor completed 2026-07-28.** `vm-map/opcodes.toml` and generated `build/opcodes.json`
now contain the complete 548-entry Kelebek AGE catalog: 248 instructions observed in Himegari and 300
catalog-only compatibility entries (including the already mapped but unused persistence opcode `0x19f`).
Each entry carries `observed_in_himegari`; per-game observation no longer limits decoding. A recognized
but unimplemented opcode reaches the VM's traced stub-and-advance fallback, so a probe continues beyond
it instead of losing the rest of the script. Catalog-only entries deliberately remain
`noop_headless=false`: skipping them is a temporary compatibility-probe behavior, not evidence that their
native effects are semantically safe to omit.
The multi-game design keeps three separate concepts:
1. A **version ABI registry** containing every known opcode number, operand count/shape, and version gate
needed to decode that SYS generation, regardless of whether the active game uses it.
@@ -669,11 +678,11 @@ The future multi-game design must keep three separate concepts:
3. **Semantic/runtime implementation coverage**, which may remain incomplete and should report a precise
unsupported-service error containing game/profile, script, opcode, and bytecode offset.
An opcode that is absent even from the selected version ABI should likewise be a structured decode error,
not a synthetic final instruction or natural script return. Supporting a new same-version game then means
selecting the complete shared ABI, measuring its corpus against existing semantics, and implementing only
the newly exercised services. It should not require cloning the VM or manufacturing a new parser table
from that game's corpus.
An opcode that is absent even from the selected version ABI should still become a structured decode error,
not a synthetic final instruction or natural script return; that diagnostic hardening remains open.
Supporting a new same-version game now means selecting the complete shared ABI, measuring its corpus
against existing semantics, and implementing only the newly exercised services. It does not require
cloning the VM or manufacturing a new parser table from that game's corpus.
The probe also exposed a separate profile/presentation concern: Kamidori creates a `1024x576` render
target while the current Himegari frontend assumes an `800x600` presentation. Logical canvas geometry,

View File

@@ -36,7 +36,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
| Tool | Purpose | Run | Reads → Writes |
|---|---|---|---|
| `opcodes_build.py` | Generator + linter for the opcode reference. | `--build` · `--lint` · `--bootstrap` | `vm-map/opcodes.toml` → ⚙ `tools/age_opcodes_himegari.py`, ⚙ `build/opcodes.json`, ⚙ `docs/opcode-reference.md`, ⚙ `build/opcode-coverage.md` |
| `opcodes_build.py` | Generator + linter for the opcode reference. `--bootstrap` appends entries observed in the Himegari corpus; `--bootstrap-age` appends every missing opcode from the pristine 548-entry AGE catalog as an unobserved compatibility stub. Catalog stubs retain Kelebek's ABI label/argument count so other AGE scripts decode past them, but remain `noop_headless=false`: the VM currently traces/skips them, while coverage continues to report them as unresolved rather than semantically safe. | `--build` · `--lint` · `--bootstrap` · `--bootstrap-age` | `vm-map/opcodes.toml` → ⚙ `tools/age_opcodes_himegari.py`, ⚙ `build/opcodes.json`, ⚙ `docs/opcode-reference.md`, ⚙ `build/opcode-coverage.md` |
| `opcodes_model.py` | In-memory model + loader + linter (dangling-ref / confidence-ceiling / vocabulary / dependents). | *Imported by `opcodes_build.py`.* | `vm-map/opcodes.toml` → — |
| `test_opcodes.py` | Unit tests for the opcode tooling. | `test_opcodes.py` | — |
| `opcode_context.py` | Read-only evidence gatherer for classifying unnamed opcodes (frequency, argc, operand-type signature, neighbours, disasm snippets, Kelebek comment). | `--top 20` · `opcode_context.py 0x1f4 0x71 …` | corpus → stdout |

View File

@@ -1,19 +1,37 @@
using Age.Engine.Model;
using Age.Engine.Sys4;
using Xunit;
public class OpcodeTableTests
{
[Fact]
public void LoadsObservedOpcodesPlusMappedUnusedAbiEntries()
public void LoadsCompleteAgeCatalog()
{
var t = OpcodeTableJson.Load(Paths.OpcodesJson);
Assert.Equal(249, t.Count); // 248 corpus-observed + unused persistence ABI opcode 0x19f
Assert.Equal(548, t.Count); // 248 Himegari-observed + 300 broader AGE compatibility stubs
Assert.True(t.TryGet(0x55, out var label, out var argc));
Assert.Equal("mov", label);
Assert.Equal(2, argc);
Assert.Equal("u0041BEB0", t.Label(0x90));
Assert.Equal(7, t.Argc(0x90));
Assert.Equal(2, t.Argc(0x19f));
Assert.Equal(2, t.Argc(0x1be)); // Kamidori probe blocker; catalog-only in Himegari
Assert.Equal(-1, t.Argc(0x9999)); // absent -> -1
}
[Fact]
public void CatalogOnlyOpcodeDoesNotTruncateFollowingInstructions()
{
var t = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(t, "CROSS_GAME_STUB", new List<(int, Operand[])>
{
(0x1be, new[] { new Operand(0, 11), new Operand(0, 22) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
Assert.Equal(2, script.Instructions.Count);
Assert.Equal(0x1be, script.Instructions[0].Opcode);
Assert.Equal(2, script.Instructions[0].Args.Count);
Assert.Equal(0x2, script.Instructions[1].Opcode);
}
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Generator + linter for the living opcode reference (vm-map/opcodes.toml).
--bootstrap seed skeletons for every used opcode (append-only; preserves hand edits)
--bootstrap-age seed compatibility stubs for every opcode in the broader AGE catalog
--build emit age_opcodes_himegari.py + build/opcodes.json + docs/opcode-reference.md + build/opcode-coverage.md
--lint run the linter, print errors/warnings, exit nonzero on errors
See docs/superpowers/specs/2026-07-06-opcode-reference-design.md."""
@@ -77,14 +78,22 @@ def scan_corpus():
def _is_named(label: str) -> bool:
return not (label.startswith("u00") or label == "dev_ukn" or label.startswith("?"))
def skeleton_toml(op: int, label: str, argc: int, argtypes_for_op: dict) -> str:
def skeleton_toml(op: int, label: str, argc: int, argtypes_for_op: dict,
*, observed_in_himegari: bool = True) -> str:
conf = "med" if _is_named(label) else "low"
lines = ["[[opcode]]", f"op = 0x{op:x}", f'label = "{label}"', f"argc = {argc}",
'abi_source = "kelebek+decode-validated"', "", "[opcode.semantics]",
f'name = "{label}"', 'category = "unknown"', 'summary = ""',
abi_source = "kelebek+decode-validated" if observed_in_himegari else "kelebek"
summary = ("" if observed_in_himegari else
"Broader AGE-catalog compatibility stub; the port currently traces and skips it.")
evidence = ("" if observed_in_himegari else
"Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.")
lines = ["[[opcode]]", f"op = 0x{op:x}", f'label = "{label}"', f"argc = {argc}"]
if not observed_in_himegari:
lines.append("observed_in_himegari = false")
lines += [f'abi_source = "{abi_source}"', "", "[opcode.semantics]",
f'name = "{label}"', 'category = "unknown"', f'summary = "{summary}"',
"noop_headless = false", 'source = "kelebek"', f'confidence = "{conf}"',
"depends_on = []", 'evidence = ""']
for i in range(argc):
"depends_on = []", f'evidence = "{evidence}"']
for i in range(argc if observed_in_himegari else 0):
tnames = [TYPE_NAMES.get(t, "t%#x" % t) for t in sorted(argtypes_for_op.get(i, ()))]
obs = ", ".join('"%s"' % n for n in tnames)
lines += ["", "[[opcode.semantics.args]]", f"i = {i + 1}", 'role = ""',
@@ -107,6 +116,22 @@ def bootstrap(toml_path: Path) -> None:
f.write("\n".join(blocks))
print(f"bootstrap: {len(used)} used opcodes; appended {len(blocks)} new skeletons -> {toml_path}")
def bootstrap_age(toml_path: Path) -> None:
"""Append compatibility stubs for catalog opcodes absent from the canonical map."""
present = set(M.load(toml_path).opcodes) if toml_path.exists() else set()
blocks = [
skeleton_toml(op, label, argc, {}, observed_in_himegari=False)
for op, (label, argc) in sorted(OPCODES.items())
if op not in present
]
if not toml_path.exists():
toml_path.parent.mkdir(parents=True, exist_ok=True)
toml_path.write_text(META_TOML + "\n", encoding="utf-8")
with toml_path.open("a", encoding="utf-8") as f:
f.write("\n".join(blocks))
print(f"bootstrap-age: {len(OPCODES)} catalog opcodes; "
f"appended {len(blocks)} compatibility stubs -> {toml_path}")
GEN_HEADER = "# DO NOT EDIT -- generated from vm-map/opcodes.toml by tools/opcodes_build.py --build\n"
def emit_inferred_py(model: M.Model) -> str:
@@ -128,6 +153,7 @@ def emit_json(model: M.Model) -> str:
"dependents": {"0x%x" % k: ["0x%x" % d for d in v] for k, v in rev.items() if v}}
for op, oc in sorted(model.opcodes.items()):
e = {"op": "0x%x" % op, "label": oc.label, "argc": oc.argc,
"observed_in_himegari": oc.observed_in_himegari,
"code_target_args": oc.code_target_args, "abi_source": oc.abi_source}
s = oc.semantics
if s:
@@ -140,9 +166,12 @@ def emit_json(model: M.Model) -> str:
def emit_reference_md(model: M.Model) -> str:
rev = M.dependents(model)
observed = sum(oc.observed_in_himegari for oc in model.opcodes.values())
catalog_only = len(model.opcodes) - observed
L = ["<!-- DO NOT EDIT -- generated from vm-map/opcodes.toml by tools/opcodes_build.py --build -->",
"# Opcode Reference (generated)", "",
f"{len(model.opcodes)} opcodes used by Himegari. Source of truth: `vm-map/opcodes.toml`.", ""]
f"{len(model.opcodes)} AGE catalog opcodes: {observed} observed in Himegari and "
f"{catalog_only} compatibility stubs. Source of truth: `vm-map/opcodes.toml`.", ""]
by_cat = collections.defaultdict(list)
for op, oc in model.opcodes.items():
cat = oc.semantics.category if oc.semantics else "unknown"
@@ -182,8 +211,12 @@ def emit_coverage_md(model: M.Model) -> str:
by_cat[s.category] += 1
if s.name != oc.label:
named += 1
observed = sum(oc.observed_in_himegari for oc in model.opcodes.values())
L = ["<!-- DO NOT EDIT -- generated from vm-map/opcodes.toml -->", "# Opcode Coverage (generated)", "",
f"- opcodes: {len(model.opcodes)}", f"- given a distinct mnemonic: {named}", "",
f"- AGE catalog opcodes: {len(model.opcodes)}",
f"- observed in Himegari: {observed}",
f"- compatibility stubs: {len(model.opcodes) - observed}",
f"- given a distinct mnemonic: {named}", "",
"## by source", ""]
L += [f"- {k}: {v}" for k, v in sorted(by_src.items())]
L += ["", "## by confidence", ""] + [f"- {k}: {by_conf[k]}" for k in ("high", "med", "low")]
@@ -193,6 +226,7 @@ def emit_coverage_md(model: M.Model) -> str:
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--bootstrap", action="store_true")
ap.add_argument("--bootstrap-age", action="store_true")
ap.add_argument("--build", action="store_true")
ap.add_argument("--lint", action="store_true")
ap.add_argument("--toml", default=str(TOML_DEFAULT))
@@ -201,6 +235,9 @@ def main(argv=None):
if args.bootstrap:
bootstrap(tp)
return 0
if args.bootstrap_age:
bootstrap_age(tp)
return 0
if args.build:
model = M.load(tp)
errors, warnings = M.lint(model)
@@ -226,7 +263,7 @@ def main(argv=None):
print("error:", m)
print(f"lint: {len(errors)} errors, {len(warnings)} warnings")
return 1 if errors else 0
ap.error("no action (expected --bootstrap/--build/--lint)")
ap.error("no action (expected --bootstrap/--bootstrap-age/--build/--lint)")
if __name__ == "__main__":
sys.exit(main())

View File

@@ -29,6 +29,7 @@ class Opcode:
op: int
label: str
argc: int
observed_in_himegari: bool = True
code_target_args: list[int] = field(default_factory=list)
abi_source: str = "kelebek+decode-validated"
abi_note: str = ""
@@ -61,6 +62,7 @@ def load(path) -> Model:
)
ops[int(e["op"])] = Opcode(
op=int(e["op"]), label=e.get("label", ""), argc=int(e["argc"]),
observed_in_himegari=bool(e.get("observed_in_himegari", True)),
code_target_args=[int(x) for x in e.get("code_target_args", [])],
abi_source=e.get("abi_source", "kelebek+decode-validated"),
abi_note=e.get("abi_note", ""), semantics=sem,
@@ -73,6 +75,12 @@ def lint(model: Model) -> tuple[list[str], list[str]]:
errors: list[str] = []
warnings: list[str] = []
ops = model.opcodes
expected_observed = model.meta.get("opcodes_used_by_himegari")
actual_observed = sum(oc.observed_in_himegari for oc in ops.values())
if expected_observed is not None and int(expected_observed) != actual_observed:
errors.append(
"meta.opcodes_used_by_himegari="
f"{expected_observed} but {actual_observed} entries are marked observed")
for op, oc in sorted(ops.items()):
s = oc.semantics
if not s:

View File

@@ -118,6 +118,7 @@ def test_lint():
def test_bootstrap():
import opcodes_build as B
from age_opcodes import OPCODES
from pathlib import Path
fd, p = tempfile.mkstemp(suffix=".toml"); os.close(fd); os.remove(p)
tp = Path(p)
@@ -130,6 +131,16 @@ def test_bootstrap():
check(len(M.load(tp).opcodes) == n1, "second bootstrap adds no duplicates")
e, w = M.lint(m)
check(e == [], f"bootstrapped file lints clean (errors: {e[:3]})")
B.bootstrap_age(tp)
full = M.load(tp)
check(len(full.opcodes) == len(OPCODES), "bootstrap-age seeds the complete AGE catalog")
check(sum(o.observed_in_himegari for o in full.opcodes.values()) == len(m.opcodes),
"bootstrap-age marks only added catalog entries unobserved")
check(all(not o.semantics.noop_headless for o in full.opcodes.values()
if not o.observed_in_himegari),
"compatibility stubs are not misclassified as semantically safe no-ops")
e, w = M.lint(full)
check(e == [], f"full catalog lints clean (errors: {e[:3]})")
def test_emit_inferred():
import opcodes_build as B

File diff suppressed because it is too large Load Diff