Method dispatch axis: map id -> body via Runner::run switch

Recovers how a script method id maps to its implementation, the foundation for
body-level normalisation. Each CMC_*_Runner::run is a switch(id) (vtable slot 17);
every case is the method body — inline (MSVC6) or a tail-call to a separate
show()/load() (MSVC8). The extractor parses the jump table at the disassembly
level (Ghidra's decompiler jump-table recovery silently dropped the big runners),
fingerprints each case by its ordered CALL anchors (Class::method / vtbl+0xNN),
and expands thin wrappers one level so MSVC8 lines up with MSVC6.

Validated on the golden pair: Animo SHOW..RESUME (id 1-4) yield identical leaves
(getAnimo + vtbl+0xa0/0xa4/0x4c/0x50) across both compilers. Coverage 30/32
runners; Piklib 475 / BlooMoo 619 dispatch rows.

- extract_engine_surface.py: extract_method_dispatch (schema_version -> 4)
- snapshots regenerated with the method_dispatch axis
- ams: Snapshot.method_dispatch; diff axis keyed (owner,id) on [impl,calls] with
  method-name join; render METHOD BODIES section; cli --only dispatch; owner filter
- UI: "Ciała metod" diff axis + browse tab
- tests: body-change unit + cross-compiler vtbl assertion -> 29/29

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Patryk Gensch
2026-05-31 13:15:58 +02:00
parent 38be932abc
commit 27399a52b1
10 changed files with 15843 additions and 13 deletions

View File

@@ -72,6 +72,26 @@ def test_field_type_change_and_owner_filter():
assert d["fields"]["added"] == [] and d["fields"]["removed"] == []
def test_method_dispatch_body_change():
old = _snap(
methods=[{"owner": "CMC_Animo", "name": "SHOW", "id": 1}],
method_dispatch=[{"owner": "CMC_Animo", "id": 1, "impl": None,
"impl_addr": "0x1", "calls": ["CMC_Animo::getAnimo", "vtbl+0xa0"]}],
)
new = _snap(
methods=[{"owner": "CMC_Animo", "name": "SHOW", "id": 1}],
method_dispatch=[{"owner": "CMC_Animo", "id": 1, "impl": None,
"impl_addr": "0x1", "calls": ["CMC_Animo::getAnimo", "vtbl+0xa4"]}],
)
d = compute_diff(old, new)["method_dispatch"]
assert len(d["changed"]) == 1
ch = d["changed"][0]
assert ch["item"]["name"] == "SHOW" # name joined from the methods axis on (owner, id)
assert ch["changes"]["calls"] == [["CMC_Animo::getAnimo", "vtbl+0xa0"],
["CMC_Animo::getAnimo", "vtbl+0xa4"]]
assert "METHOD BODIES" in render_text(compute_diff(old, new))
def test_render_no_diff():
out = render_text(compute_diff(_snap(), _snap()))
assert "(no differences)" in out
@@ -101,3 +121,10 @@ def test_golden_pair_piklib_to_bloomoo():
# rendering must not raise and must mention the new types
text = render_text(d)
assert "GRBUFFER" in text and "MOUSE" in text
# method bodies recovered cross-compiler: Animo SHOW (id 1) maps to the same vtable leaf
# despite MSVC6 inlining it and MSVC8 keeping it as a separate show() function
disp_old = {(r["owner"], r["id"]): r for r in old.method_dispatch}
disp_new = {(r["owner"], r["id"]): r for r in new.method_dispatch}
assert disp_old[("CMC_Animo", 1)]["calls"][-1] == "vtbl+0xa0"
assert disp_new[("CMC_Animo", 1)]["calls"][-1] == "vtbl+0xa0"