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

@@ -69,6 +69,22 @@ def _detect_method_moves(old_m: list[Item], new_m: list[Item]) -> list[Item]:
return moves
def _dispatch_key(x: Item) -> Hashable:
return (x["owner"], x["id"])
def _dispatch_with_names(snap: Snapshot) -> list[Item]:
"""Attach the method name (from the methods axis, joined on owner+id) to each dispatch row,
so a body-level diff reads as 'SHOW body changed' rather than 'CMC_Animo id 1 changed'."""
name_by = {(m["owner"], m.get("id")): m["name"] for m in snap.methods}
out = []
for r in snap.method_dispatch:
rr = dict(r)
rr["name"] = name_by.get((r["owner"], r["id"]))
out.append(rr)
return out
def compute_diff(old: Snapshot, new: Snapshot) -> dict[str, Any]:
return {
"binary": {"from": old.binary, "to": new.binary},
@@ -78,6 +94,8 @@ def compute_diff(old: Snapshot, new: Snapshot) -> dict[str, Any]:
"fields": keyed_diff(old.fields, new.fields, _owner_name_key, ["type"]),
"struct_layout": keyed_diff(old.struct_layout, new.struct_layout, _layout_key,
["size", "is_vtable"]),
"method_dispatch": keyed_diff(_dispatch_with_names(old), _dispatch_with_names(new),
_dispatch_key, ["impl", "calls"]),
"method_inheritance": keyed_diff(old.method_inheritance, new.method_inheritance,
lambda x: x["runner"], ["base_runner"]),
"field_inheritance": keyed_diff(old.field_inheritance, new.field_inheritance,
@@ -90,7 +108,7 @@ def compute_diff(old: Snapshot, new: Snapshot) -> dict[str, Any]:
def _item_owner(axis: str, item: Item) -> str | None:
if axis == "types":
return item.get("cpp_class")
if axis in ("methods", "events", "fields", "struct_layout"):
if axis in ("methods", "events", "fields", "struct_layout", "method_dispatch"):
return item.get("owner")
if axis == "method_inheritance":
return item.get("runner")