diff --git a/tools/diff_optrace.py b/tools/diff_optrace.py index 718f6a8..77c2a80 100644 --- a/tools/diff_optrace.py +++ b/tools/diff_optrace.py @@ -46,6 +46,15 @@ def first_divergence(a: list, b: list) -> dict: "b": b[n] if n < len(b) else None} +def operand_filter(offsets: list, argc_by_off: dict) -> list: + """Keep only offsets whose instruction has >=1 operand — the subsequence an OPERAND-mode engine + capture (vm_operand_fetch hook, deduped per-pc) can see. Zero-operand ops (stmt-begin/end and other + markers) never trigger an operand fetch, so they are absent from the engine trace; filtering the VM + trace the same way makes the two directly comparable. (Markers don't branch, so control flow is + preserved.) Offsets not in the map (shouldn't happen for a valid scene) are dropped.""" + return [o for o in offsets if argc_by_off.get(o, 0) >= 1] + + def pick_scene_codebase(entries: list, vm_offsets: list): """The codebase whose in-order offset sequence shares the longest common prefix with `vm_offsets` — identifies which loaded-script instance in the engine trace is the scene we ran in the VM. Returns @@ -95,12 +104,13 @@ def _load_scene_disasm(scene: str): scr = sys4load.load(paths.scripts()[key]) sys4load.decode_code(scr) op_by_off = {ins.offset: ins.opcode for ins in scr.instructions} + argc_by_off = {ins.offset: len(ins.args) for ins in scr.instructions} line_by_off = {} for line in sys4load.render_listing(scr).splitlines(): m = re.match(r"\s*0x([0-9a-fA-F]+):\s*(.*)", line) if m: line_by_off[int(m.group(1), 16)] = m.group(2).rstrip() - return op_by_off, line_by_off + return op_by_off, argc_by_off, line_by_off def _fmt(off, op_by_off, line_by_off) -> str: @@ -111,15 +121,21 @@ def _fmt(off, op_by_off, line_by_off) -> str: return f"0x{off:05x}: {line}" + (f" [op 0x{op:x}]" if op is not None else " [op ?]") -def report(scene, entries, vm_offsets): +def report(scene, entries, vm_offsets, operand_mode=True): + op_by_off, argc_by_off, line_by_off = _load_scene_disasm(scene) + # An operand-hook engine capture only sees ops with >=1 operand; filter the VM trace to match so the + # two are the same subsequence. --full turns this off (for a tick-mode engine capture, which sees all). + if operand_mode: + vm_offsets = operand_filter(vm_offsets, argc_by_off) + cb = pick_scene_codebase(entries, vm_offsets) if cb is None: print(f"[!] could not identify {scene}'s codebase in the engine trace " f"({len(entries)} entries, {len({e['codebase'] for e in entries})} codebases) — " - f"no shared leading offset with the VM trace.") + f"no shared leading offset with the VM trace" + f"{' (operand-filtered)' if operand_mode else ''}.") return 2 engine_seq = [e["offset"] for e in entries if e["codebase"] == cb] - op_by_off, line_by_off = _load_scene_disasm(scene) d = first_divergence(engine_seq, vm_offsets) print(f"=== differential offset-path oracle: {scene} ===") @@ -173,7 +189,7 @@ def main(argv=None): entries = _load_engine(engine_path) vm_offsets = json.loads(vm_path.read_text(encoding="utf-8"))["offsets"] - return report(scene, entries, vm_offsets) + return report(scene, entries, vm_offsets, operand_mode="--full" not in argv) if __name__ == "__main__": diff --git a/tools/test_diff_optrace.py b/tools/test_diff_optrace.py index 54e6370..f4495d5 100644 --- a/tools/test_diff_optrace.py +++ b/tools/test_diff_optrace.py @@ -1,6 +1,6 @@ # tools/test_diff_optrace.py (plain runner) import sys -from diff_optrace import first_divergence, pick_scene_codebase +from diff_optrace import first_divergence, pick_scene_codebase, operand_filter FAILS=[] def check(c,m): (FAILS.append(m) or print("FAIL:",m)) if not c else print("ok:",m) @@ -21,8 +21,13 @@ def test_pick_codebase_by_longest_common_prefix(): {"codebase":200,"offset":0},{"codebase":200,"offset":1},{"codebase":200,"offset":2}] check(pick_scene_codebase(entries,[0,1,2])==200, "codebase matching VM prefix chosen") +def test_operand_filter_drops_zero_operand_ops(): + # argc: 0x0->2, 0x5->0 (marker), 0xa->1, 0xf->0 => keep 0x0 and 0xa + argc = {0x0:2, 0x5:0, 0xa:1, 0xf:0} + check(operand_filter([0x0,0x5,0xa,0xf,0x5], argc)==[0x0,0xa], "operand_filter keeps only argc>=1 ops") + def main(): test_equal_no_divergence(); test_first_divergence_point(); test_prefix_shorter_vm() - test_pick_codebase_by_longest_common_prefix() + test_pick_codebase_by_longest_common_prefix(); test_operand_filter_drops_zero_operand_ops() print("FAILURES:",len(FAILS)); return 1 if FAILS else 0 if __name__=="__main__": sys.exit(main())