mirror of
https://github.com/open-goal/jak-project
synced 2026-09-05 10:57:28 -04:00
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.
This commit is contained in:
+529
@@ -0,0 +1,529 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user