Files
jak-project/test-script.py
T
Tyler Wilding 9c86c86c74 d/jx: More files (#4360)
Breaks the 300 file milestone, also includes the majority of mips2c
functions carried forward. Some functions need some fixes in the c++
code to mips2c properly, and a small handful of the mips2c functions
require manual patching -- all spots were marked with `TODO - fix` that
had to be commented out to get things compiling.
2026-09-04 09:27:06 -04:00

529 lines
9.6 KiB
Python

#!/usr/bin/env python3
import difflib
import os
import re
import sys
import termios
import tty
from dataclasses import dataclass
from io import StringIO
OLD_FILE = "ir2-jak3-test_ir2.asm"
NEW_FILE = "ir2-jakx-test_ir2.asm"
# ============================================================================
# Colors
# ============================================================================
class Color:
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
DIM = "\033[2m"
BOLD = "\033[1m"
def color(text, c):
return f"{c}{text}{Color.RESET}"
# ============================================================================
# Data Structures
# ============================================================================
@dataclass
class Operation:
number: int
asm: str
mnemonic: str
normalized: str
@dataclass
class Function:
name: str
stack_total: int
operations: list[Operation]
has_errors: bool = False
# ============================================================================
# Parsing
# ============================================================================
FUNC_RE = re.compile(r";\s*\.function\s+(.+)")
STACK_RE = re.compile(r";stack:\s*total\s+(0x[0-9a-fA-F]+|\d+)")
OP_RE = re.compile(r"^\s*(.*?)\s*;;\s*\[\s*(\d+)\]")
ERROR_RE = re.compile(r";;\s*ERROR:\s*")
REGISTER_RE = re.compile(
r"\b("
r"r0|at|v[01]|a[0-3]|t[0-9]|s[0-7]|gp|sp|fp|ra|hi|lo"
r")\b"
)
def normalize_registers(asm):
mapping = {}
next_id = 0
def repl(match):
nonlocal next_id
reg = match.group(1)
if reg not in mapping:
mapping[reg] = f"R{next_id}"
next_id += 1
return mapping[reg]
return REGISTER_RE.sub(repl, asm)
def parse_file(path):
functions = {}
current = None
with open(path) as f:
for line in f:
m = FUNC_RE.search(line)
if m:
if current:
functions[current.name] = current
current = Function(
name=m.group(1),
stack_total=0,
operations=[],
)
continue
if current is None:
continue
if ERROR_RE.search(line):
current.has_errors = True
m = STACK_RE.search(line)
if m:
current.stack_total = int(m.group(1), 0)
continue
m = OP_RE.match(line)
if m:
asm = m.group(1).rstrip()
current.operations.append(
Operation(
number=int(m.group(2)),
asm=asm,
mnemonic=asm.split()[0] if asm else "",
normalized=normalize_registers(asm),
)
)
if current:
functions[current.name] = current
return functions
# ============================================================================
# Analysis
# ============================================================================
def similarity(a, b):
return difflib.SequenceMatcher(None, a, b).ratio()
def is_decompiled(func):
return not func.has_errors
def compare_exact(old, new):
return (
len(old.operations) == len(new.operations)
and all(
a.asm == b.asm
for a, b in zip(
old.operations,
new.operations
)
)
)
def classify(old, new):
if compare_exact(old, new):
return "IDENTICAL"
mnemonic_score = similarity(
[x.mnemonic for x in old.operations],
[x.mnemonic for x in new.operations],
)
normalized_score = similarity(
[x.normalized for x in old.operations],
[x.normalized for x in new.operations],
)
if normalized_score > .995:
return "REGISTER CHANGES"
if mnemonic_score > .98:
return "NEAR MATCH"
if mnemonic_score > .75:
return "MODIFIED"
return "DIFFERENT"
CLASS_COLOR = {
"IDENTICAL": Color.GREEN,
"REGISTER CHANGES": Color.GREEN,
"NEAR MATCH": Color.YELLOW,
"MODIFIED": Color.YELLOW,
"DIFFERENT": Color.RED,
}
CLASS_ORDER = {
"DIFFERENT": 0,
"MODIFIED": 1,
"NEAR MATCH": 2,
"REGISTER CHANGES": 3,
"IDENTICAL": 4,
}
# ============================================================================
# Diff
# ============================================================================
def print_diff(old, new, out):
matcher = difflib.SequenceMatcher(
None,
[x.normalized for x in old.operations],
[x.normalized for x in new.operations],
)
print("Diff:\n", file=out)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == "equal":
continue
print(
color(
f"@@ {tag} @@",
Color.CYAN
),
file=out
)
if tag in ("replace", "delete"):
for op in old.operations[i1:i2]:
print(
color(
f"- [{op.number:4}] {op.asm}",
Color.RED
),
file=out
)
if tag in ("replace", "insert"):
for op in new.operations[j1:j2]:
print(
color(
f"+ [{op.number:4}] {op.asm}",
Color.GREEN
),
file=out
)
print(file=out)
def analyze(old, new):
out = StringIO()
def p(*args):
print(*args, file=out)
classification = classify(old,new)
p("=" * 100)
p(old.name)
p("=" * 100)
p()
p(
"Stack:"
)
stack_match = old.stack_total == new.stack_total
p(f" old : 0x{old.stack_total:x}")
p(f" new : 0x{new.stack_total:x}")
p(
" ",
color(
"MATCH" if stack_match else "DIFFERENT",
Color.GREEN if stack_match else Color.RED
)
)
p()
p("Operations:")
p(f" old : {len(old.operations)}")
p(f" new : {len(new.operations)}")
p(f" diff: {len(new.operations)-len(old.operations):+}")
mnemonic = similarity(
[x.mnemonic for x in old.operations],
[x.mnemonic for x in new.operations],
)
normalized = similarity(
[x.normalized for x in old.operations],
[x.normalized for x in new.operations],
)
p()
p("Similarity:")
p(f" Exact Assembly : {compare_exact(old,new)}")
p(f" Mnemonic : {mnemonic:.3%}")
p(f" Register Normalized : {normalized:.3%}")
p()
p(
"Classification:",
color(
classification,
CLASS_COLOR[classification]
)
)
if classification != "IDENTICAL":
p()
print_diff(
old,
new,
out
)
return out.getvalue()
# ============================================================================
# Pager
# ============================================================================
def clear_screen():
os.system(
"clear" if os.name != "nt"
else "cls"
)
def read_key():
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
key = sys.stdin.read(1)
if key == "\x1b":
key += sys.stdin.read(2)
return key
finally:
termios.tcsetattr(
fd,
termios.TCSADRAIN,
old
)
# ============================================================================
# Main
# ============================================================================
old_functions = parse_file(OLD_FILE)
new_functions = parse_file(NEW_FILE)
functions = []
for name in set(old_functions) & set(new_functions):
old = old_functions[name]
new = new_functions[name]
functions.append(
(
old,
new,
analyze(old,new)
)
)
functions.sort(
key=lambda x:
(
1 if is_decompiled(x[0]) else 0,
CLASS_ORDER.get(
classify(x[0],x[1]),
99
),
x[0].name
)
)
show_decompiled = False
index = 0
while True:
visible = [
f for f in functions
if show_decompiled or not is_decompiled(f[0])
]
if not visible:
break
index = min(index, len(visible)-1)
clear_screen()
print(
color(
f"Functions: {len(functions)} | "
f"Review: {len(functions)-sum(is_decompiled(x[0]) for x in functions)} | "
f"Decompiled: {sum(is_decompiled(x[0]) for x in functions)}",
Color.CYAN
)
)
print()
for i,(old,new,_) in enumerate(visible):
cls = classify(old,new)
marker = "> " if i == index else " "
print(
marker
+ f"{old.name:<40}"
+ color(
cls,
CLASS_COLOR[cls]
)
)
print()
print(visible[index][2])
print(
color(
"[↑↓/jk] move [d] show decompiled [q] quit",
Color.DIM
)
)
key = read_key()
if key.lower() == "q":
break
elif key in ("\x1b[A","k","K"):
index = max(index-1,0)
elif key in ("\x1b[B","j","J"):
index = min(index+1,len(visible)-1)
elif key.lower() == "d":
show_decompiled = not show_decompiled
index = 0