forked from Keelrleek/Harbor
re_match_program: the whole-program LTCG build and check; progress counts its exact matches
This commit is contained in:
1 parent
d2c365d531
commit
5a0fb44c7d
3 files changed
+54
-2
No files matched your search
+17
-1
@@ -43,7 +43,8 @@ review queue, presence and stop flags. Protocol:
|
|||||||
12. Pick functions with next_functions() (easiest first in your bands). Before writing, read re_lookup (it states the
|
12. Pick functions with next_functions() (easiest first in your bands). Before writing, read re_lookup (it states the
|
||||||
calling convention read from the bytes) and re_similar (a readable near-duplicate is a head start). Before
|
calling convention read from the bytes) and re_similar (a readable near-duplicate is a head start). Before
|
||||||
submitting, run re_match on your worktree: 'match' or 'arguments in registers' is byte-identical, otherwise
|
submitting, run re_match on your worktree: 'match' or 'arguments in registers' is byte-identical, otherwise
|
||||||
similarity shows how close you are. After two failed attempts, defer(address, reason, evidence) instead of
|
similarity shows how close you are; re_match_program (slow, once before submitting) builds the whole
|
||||||
|
program the way the game was built, so register conventions reproduce. After two failed attempts, defer(address, reason, evidence) instead of
|
||||||
looping. Use re_struct_audit before changing a struct in types.h."""
|
looping. Use re_struct_audit before changing a struct in types.h."""
|
||||||
|
|
||||||
KindName = Literal["note", "question", "answer", "request", "finding", "retraction", "decision", "status", "handoff"]
|
KindName = Literal["note", "question", "answer", "request", "finding", "retraction", "decision", "status", "handoff"]
|
||||||
@@ -523,6 +524,21 @@ def re_match(targets: str, decomp: str | None = None) -> dict:
|
|||||||
raise ToolError(str(error)) from None
|
raise ToolError(str(error)) from None
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def re_match_program(targets: str = "", decomp: str | None = None) -> dict:
|
||||||
|
"""The exact-match check the way the game was built: every readable function of your worktree (decomp)
|
||||||
|
goes into one /GL + /LTCG image with the game's compiler, so register-argument conventions and inlining
|
||||||
|
reproduce (re_match compiles one file at a time and cannot). Slow (1.5 to 9 minutes): run it once before
|
||||||
|
submitting, with targets = the functions you changed. Statuses: exact, exact-relaxed (identical after
|
||||||
|
dropping leading stack-argument loads), exact-but-targets-differ, differs (with similarity and the
|
||||||
|
register inputs on both sides)."""
|
||||||
|
try:
|
||||||
|
from .reverse import match_program
|
||||||
|
return match_program(harbor().project.reverse, targets, decomp)
|
||||||
|
except (HarborError, projects.ProjectError) as error:
|
||||||
|
raise ToolError(str(error)) from None
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def re_similar(target: str, k: int = 10, min_score: float = 0.7) -> dict:
|
def re_similar(target: str, k: int = 10, min_score: float = 0.7) -> dict:
|
||||||
"""Functions whose instructions look like this one (normalized: addresses blanked, registers and small
|
"""Functions whose instructions look like this one (normalized: addresses blanked, registers and small
|
||||||
|
|||||||
+2
-1
@@ -205,7 +205,8 @@ class FunctionQueue:
|
|||||||
# ------------------------------------------------------------------ progress
|
# ------------------------------------------------------------------ progress
|
||||||
def progress(self):
|
def progress(self):
|
||||||
facts, statuses, deferred = self.r._facts(), self.statuses(), self.deferrals()
|
facts, statuses, deferred = self.r._facts(), self.statuses(), self.deferrals()
|
||||||
exact = {a for a, row in self._results("match_results").items() if str(row.get("status")) == "match"}
|
exact = {a for a, row in self._results("match_results").items()
|
||||||
|
if str(row.get("status")) in ("match", "exact", "exact-relaxed")}
|
||||||
verified = {a for a, row in self._results("diffrun_results").items() if str(row.get("verdict")) == "same"}
|
verified = {a for a, row in self._results("diffrun_results").items() if str(row.get("verdict")) == "same"}
|
||||||
library = set(self._results("library_results"))
|
library = set(self._results("library_results"))
|
||||||
ranges = self.library_ranges()
|
ranges = self.library_ranges()
|
||||||
|
|||||||
@@ -531,3 +531,38 @@ def run_toolkit(config, key, argv, json_to_file=False, timeout=900):
|
|||||||
return json.loads(text)
|
return json.loads(text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HarborError(f"{key} failed: " + (result.stdout + result.stderr).strip()[-800:]) from None
|
raise HarborError(f"{key} failed: " + (result.stdout + result.stderr).strip()[-800:]) from None
|
||||||
|
|
||||||
|
|
||||||
|
def match_program(config, targets=None, decomp=None):
|
||||||
|
"""Build every readable function into one /GL + /LTCG image the way the game was built (stand-in callers, stubs
|
||||||
|
for the rest, globals at their original addresses) and check the targets against the original bytes. Slow: a
|
||||||
|
build takes 1.5 to 9 minutes depending on load."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
config = config or {}
|
||||||
|
script = config.get("ltcg")
|
||||||
|
if not script or not Path(script).exists():
|
||||||
|
raise HarborError("this project has no ltcg script in its [reverse] table")
|
||||||
|
wanted = [t.strip() for t in re.split(r"[,\s]+", str(targets or "")) if t.strip()]
|
||||||
|
extra = ["--decomp", str(decomp)] if decomp else []
|
||||||
|
run = lambda argv, timeout: subprocess.run([sys.executable, "-B", str(script), *argv], capture_output=True,
|
||||||
|
text=True, encoding="utf-8", errors="replace", timeout=timeout,
|
||||||
|
cwd=str(Path(script).parent),
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||||
|
built = run(["build", *extra], 1800)
|
||||||
|
if built.returncode != 0:
|
||||||
|
raise HarborError("the whole-program build failed: " + (built.stdout + built.stderr).strip()[-800:])
|
||||||
|
with tempfile.TemporaryDirectory(dir=config.get("debugger_temp")) as tmp:
|
||||||
|
out = Path(tmp) / "check.json"
|
||||||
|
checked = run(["check", *extra, "--json", str(out)] + (["--only", ",".join(wanted)] if wanted else []), 900)
|
||||||
|
if not out.exists():
|
||||||
|
raise HarborError("the whole-program check failed: " + (checked.stdout + checked.stderr).strip()[-800:])
|
||||||
|
rows = json.loads(out.read_text(encoding="utf-8"))
|
||||||
|
counts = {}
|
||||||
|
for r in rows:
|
||||||
|
counts[r.get("status")] = counts.get(r.get("status"), 0) + 1
|
||||||
|
keep = ("address", "name", "status", "first_difference", "similarity", "registers_in", "ours_registers_in",
|
||||||
|
"ret_pops", "ours_ret_pops", "relocation_problems", "note")
|
||||||
|
return {"counts": counts, "results": [{k: r[k] for k in keep if k in r} for r in rows] if wanted else
|
||||||
|
[{k: r[k] for k in keep if k in r} for r in rows if r.get("status") != "differs"][:200]}
|
||||||
Reference in new issue
Block a user