Reverse-engineering + open reimplementation workspace for Eushully's AGE/SYS4 engine (first target: Himegari). The repo root is age-reimpl/; the original game install and the extracted ALF data are siblings outside the repo and are never tracked. build/ (derived corpora) is gitignored and regenerated by the tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
Design: Living Opcode Reference (single-source-of-truth + provenance)
Status: approved (design) · Date: 2026-07-06 · Author: session work
Related: docs/phase-a-slice-plan.md, vm-map/himegari-opcode-notes.md, docs/PROJECT-STRUCTURE.md
Problem
Opcode knowledge for the AGE/SYS4 VM is currently spread across five artifacts:
| File | Role today | Consumed by |
|---|---|---|
tools/age_opcodes.py |
Kelebek table verbatim: {op: (label, argc)} + arg-types |
sys4load, vm0 |
tools/age_opcodes_himegari.py |
our INFERRED dict (name/category/noop/confidence/method/note) |
sys4load |
vm-map/opcodes-himegari.json |
data snapshot (arg-types, header fields, per-op array) | — (reference) |
vm-map/himegari-opcode-notes.md |
prose evidence, buckets A–F | — (humans) |
build/opcode-coverage.md |
coverage tiers | — (humans) |
The same fact lives in several places, so they drift (this session already found stale "6/8 scenes"
wording, and age_opcodes_himegari.py's method field duplicates prose in the notes doc). None of
them record why we believe a claim or what other claims it rests on — so when a reversal proves
one opcode wrong, there is no way to find the downstream inferences that depended on it.
Goals
- One canonical, hand-edited file as the single source of truth; every other opcode artifact is generated from it and can never disagree with it or with the VM.
- Provenance per claim — record the source (Kelebek / our harness / our investigation / Frida / Unicorn) and confidence, with the near-certain ABI facts kept separate from the fallible semantics.
- Dependency tracking — a semantic claim can declare the opcodes whose interpretation it rests on, so a correction's blast radius is visible and mechanically checkable.
- Zero disruption to working tooling — sys4load/vm0 keep importing the same Python interface, now generated.
- Complete map — an entry for every one of the 248 opcodes Himegari uses, so coverage is measurable and any opcode can be named as a dependency.
Non-goals
- Not touching
tools/age_opcodes.py(the Kelebek table): it stays pristine as the ABI baseline and the bootstrap input. - No per-call-site type modelling (arg types are per-instruction in the bytecode, not fixed per opcode).
- No live-capture work (Frida/Unicorn) here — this is the reference structure; those remain sources a future entry can cite.
- Not covering the ~300 Kelebek opcodes Himegari never uses.
Architecture
One canonical file; everything else is generated:
vm-map/opcodes.toml ← CANONICAL, hand-edited. [meta] + 248 [[opcode]] tables.
│
▼ tools/opcodes_build.py (generator + linter; stdlib tomllib read, no new dep)
├─ tools/age_opcodes_himegari.py GENERATED — exposes INFERRED (sys4load imports unchanged)
├─ build/opcodes.json GENERATED — language-neutral machine view (future C# VM)
├─ docs/opcode-reference.md GENERATED — human reference incl. reverse dependents index
└─ build/opcode-coverage.md GENERATED — counts by source / confidence / category
tools/age_opcodes.py(Kelebek) stays: ABI baseline + bootstrap input, never hand-edited.- Retired (content migrates into
opcodes.toml, then re-emitted): hand-maintainedage_opcodes_himegari.py(now generated),vm-map/opcodes-himegari.json(→build/opcodes.json),vm-map/himegari-opcode-notes.md(per-op evidence → each entry'sdetails; rendered intodocs/opcode-reference.md), and hand-maintainedbuild/opcode-coverage.md. - Editing loop: edit
opcodes.toml→ runopcodes_build.py→ tooling, machine view, human doc, and coverage all update together.
Canonical file format (vm-map/opcodes.toml)
[meta] (migrated from opcodes-himegari.json)
instruction_model— the1 + 2*argcdecode description (string).[meta.arg_types]— type-code → name (0x0→immediate,0x2→string,0x9→local-int, …).[meta.header_fields]— F0–F12 meanings.[meta.sources],[meta.confidence],[meta.categories]— controlled vocabularies (documented below), so the linter can reject unknown values.
[[opcode]] — ABI block (high trust)
| field | meaning |
|---|---|
op |
opcode number (TOML 0x..) |
label |
Kelebek engine address (e.g. "u0041BEB0") |
argc |
argument count — validated by our 481/481 clean decode (len = 1 + 2*argc) |
code_target_args |
1-based arg indices that are jump targets (from Kelebek's notes; optional) |
abi_source |
e.g. "kelebek+decode-validated" |
abi_note |
optional caveat |
[opcode.semantics] — our fallible layer (where cascades live)
| field | meaning |
|---|---|
name |
short mnemonic for the disassembler (the one field sys4load requires) |
category |
marker|structural|control|adv|draw|audio|input|compute|unknown |
summary |
one-line description |
noop_headless |
bool — safe for the headless VM v1 to skip/fall-through (no state/visible effect) |
source |
kelebek|harness|investigation|frida|unicorn|inference |
confidence |
high|med|low |
depends_on |
list of op ids whose interpretation this claim rests on (default []) |
evidence |
concise grounding (one line) |
details |
optional multi-line long-form evidence (migrated from the notes doc) |
confirm_by |
optional — what would raise confidence or falsify this |
[[opcode.semantics.args]] — per-arg roles (array of tables, optional)
| field | meaning |
|---|---|
i |
1-based arg index |
role |
semantic role ("x", "y", "target:click", "count", …) |
observed_types |
list of type names actually seen at this position (auto-filled from corpus) |
note |
optional |
Source vocabulary (grounding roots vs. fallible)
kelebek— from the Kelebek table (ABI trusted; semantics from a later AGE title → treat as a lead, not truth). A root.harness— confirmed by our dialogue-oracle diff (build/text/dialogue.jsonl). Empirically grounded root.investigation— our own static/corpus analysis (maydepends_onother ops).frida/unicorn— live runtime capture / micro-execution. Roots (direct observation).inference— reasoned from neighbours/context; weakest, usually hasdepends_on.
Generator + linter (tools/opcodes_build.py)
Subcommands:
--bootstrap— createopcodes.toml: the used-opcode set is computed by scanning the corpus withsys4load(authoritative, reproducible); seed each withop/label/argcfromage_opcodes.py,abi_source="kelebek+decode-validated"(argc is validated for all 248 by our 481/481 decode),semantics.source="kelebek",confidence=medif Kelebek names it elselow(bareu004xxx/dev_uknlabel →category="unknown"); auto-fillobserved_typesper arg position from the corpus scan. Idempotent: re-running preserves hand-edited entries (only fills missing).--build(default) — readopcodes.toml, run lint, then emit the four generated artifacts. The generatedage_opcodes_himegari.pyexposesINFERRED: dict[int, dict]with at leastname(plus category/noop/confidence/source for future consumers) — a drop-in for the current interface.--lint— run the three checks; nonzero exit on any error.
Generated-file guard: each emitted file carries a DO NOT EDIT — generated from vm-map/opcodes.toml
header.
Lint checks (the cascade mechanism)
- Dangling-ref (error): every id in a
depends_onmust be an existingop. No orphan dependencies. - Reverse index / dependents (report): build
A → [ops whose semantics depend on A]; render it indocs/opcode-reference.mdas a "depended on by: 0x…" line per opcode, so a revision's blast radius is visible. Also emitted to the lint output. - Confidence-ceiling (warning): an entry's
confidencemay not exceed the minimum confidence among itsdepends_ontargets (can't behighwhile resting on alow). Surfaces shaky foundations. - Vocabulary (error):
category/source/confidencemust be in the[meta]controlled lists.
Chains terminate at a kelebek/harness/frida/unicorn root or at direct evidence.
Bootstrap / migration procedure
- Implement
opcodes_build.py; run--bootstrap→opcodes.tomlwith 248 seeded skeletons + observed arg-type histograms. - Hand-migrate our real inferences into their entries: the ~26 in
age_opcodes_himegari.pyplus the evidence inhimegari-opcode-notes.md(buckets A–F, incl. the0x90/0x97deep-dive), each with propersource,depends_on,evidence,details. - Run
--build; confirm generatedage_opcodes_himegari.pyreproduces the current names, andsys4load … --validate+vm0.py --test+vm0.py --sweepare unchanged (differential check). - Delete the retired hand-maintained files; update
docs/PROJECT-STRUCTURE.mdand the memory index.
Testing / verification
- Regression (must be byte-for-byte where it matters): after migration,
sys4loaddisassembly of a few scripts (MENU, SC0830) shows the same mnemonics;vm0.py --testPASS and--sweepstill 282/294. This proves the generated shim is a true drop-in. - Generator round-trip:
--buildis deterministic; re-running produces no diff. - Lint unit checks: craft a tiny fixture TOML exercising dangling-ref (error), a confidence-ceiling violation (warning), and an unknown category (error).
- Coverage sanity: generated
opcode-coverage.mdtotals reconcile with 248 used opcodes.
Risks / open questions
- TOML verbosity for 248 entries — acceptable; entries are mostly skeletons and diffs read cleanly.
observed_typesstaleness — it's derived; re-run--bootstrap --refresh-observed(fills only that field) if the corpus set changes. Not load-bearing (evidence, not truth).- Deciding
confidencefor Kelebek-named ops — Kelebek names are ABI-labels from another title; default seed =medfor named, and we downgrade/confirm as the VM exercises them (harness). - git — workspace isn't a git repo, so the design/spec aren't committed; offer
git initlater.
Out of scope (future)
- Emitting a C#-native binding from
build/opcodes.json(A1 will consume the JSON directly). - A GUI/queryable browser over the reference.
- Automated "review-needed" flags on git-diff of
opcodes.toml(the dependents index already gives the manual signal).