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:
@@ -78,6 +78,40 @@ def _section_owned(out: list[str], title: str, block: dict, fmt: Callable[[dict]
|
||||
name_of(it), _fmt_changes(change_by_id[id(it)]["changes"])))
|
||||
|
||||
|
||||
def _dispatch_name(r: dict) -> str:
|
||||
return r.get("name") or "id {0}".format(r.get("id"))
|
||||
|
||||
|
||||
def _section_dispatch(out: list[str], block: dict) -> None:
|
||||
"""Method-body fingerprints (per owner+id). `calls` deltas are summarised by length so the
|
||||
line stays readable; the full anchor lists live in the JSON."""
|
||||
out.append("")
|
||||
out.append("{0:<16} {1}".format("METHOD BODIES", _counts(block)))
|
||||
owner_of = lambda r: r["owner"]
|
||||
added = _group_by(block["added"], owner_of)
|
||||
removed = _group_by(block["removed"], owner_of)
|
||||
changed = _group_by([c["item"] for c in block["changed"]], owner_of)
|
||||
change_by_id = {id(c["item"]): c for c in block["changed"]}
|
||||
for owner in sorted(set(added) | set(removed) | set(changed)):
|
||||
out.append(" {0}".format(owner))
|
||||
for it in sorted(added.get(owner, []), key=_dispatch_name):
|
||||
out.append(" + {0}".format(_dispatch_name(it)))
|
||||
for it in sorted(removed.get(owner, []), key=_dispatch_name):
|
||||
out.append(" - {0}".format(_dispatch_name(it)))
|
||||
for it in sorted(changed.get(owner, []), key=_dispatch_name):
|
||||
ch = change_by_id[id(it)]["changes"]
|
||||
bits = []
|
||||
if "impl" in ch:
|
||||
bits.append("impl {0} -> {1}".format(ch["impl"][0], ch["impl"][1]))
|
||||
if "calls" in ch:
|
||||
a, b = ch["calls"]
|
||||
bits.append("calls {0} -> {1}".format(len(a or []), len(b or [])))
|
||||
out.append(" ~ {0:<22} {1}".format(_dispatch_name(it), "; ".join(bits)))
|
||||
|
||||
|
||||
_EMPTY = {"added": [], "removed": [], "changed": []}
|
||||
|
||||
|
||||
def _is_empty(block: dict) -> bool:
|
||||
return not (block["added"] or block["removed"] or block["changed"])
|
||||
|
||||
@@ -107,6 +141,8 @@ def render_text(diff: dict[str, Any], only: set[str] | None = None) -> str:
|
||||
if want("layout") and not _is_empty(diff["struct_layout"]):
|
||||
_section_owned(out, "STRUCT LAYOUT", diff["struct_layout"], _fmt_layout,
|
||||
lambda x: x["owner"], lambda x: "@{0:#x}".format(x["offset"]))
|
||||
if want("dispatch") and not _is_empty(diff.get("method_dispatch", _EMPTY)):
|
||||
_section_dispatch(out, diff["method_dispatch"])
|
||||
if want("methods") and diff["moved_methods"]:
|
||||
out.append("")
|
||||
out.append("MOVED METHODS {0}".format(len(diff["moved_methods"])))
|
||||
@@ -114,7 +150,8 @@ def render_text(diff: dict[str, Any], only: set[str] | None = None) -> str:
|
||||
out.append(" {0}: {1} -> {2}".format(
|
||||
m["name"], ",".join(m["from_owners"]), ",".join(m["to_owners"])))
|
||||
|
||||
if all(_is_empty(diff[a]) for a in ("types", "methods", "events", "fields", "struct_layout")):
|
||||
if all(_is_empty(diff.get(a, _EMPTY))
|
||||
for a in ("types", "methods", "events", "fields", "struct_layout", "method_dispatch")):
|
||||
out.append("")
|
||||
out.append("(no differences)")
|
||||
return "\n".join(out)
|
||||
|
||||
Reference in New Issue
Block a user