mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-10-02 16:00:27 -04:00
feat(resolver/aprop): decl-from-use wired into the A-prop drafter; TU-BROKEN classification; dup-def→extern candidates (P31 S61 follow-up, all zero-token)
aprop_autodraft: where the seed carries no decl, infer a minimal extern from the MEMBER'S OWN target .s (decl_from_use, negative-controlled 97.4%/4,702) and place it at BLOCK scope via insert_decls — file scope collides with the fleet's per-function loose-typing (the ov_SC04_018 lesson). Strictly additive: runs only where the old path refused; genuine refusals keep the old behaviour with the class named. integration_resolver: (1) a CC1/CPP verdict is billed to the DRAFT only after the split TU passes a TU-alone compile probe WITHOUT the draft — else TU-BROKEN, no demotion, auto-reopened when the TU's hash changes (S61: one broken TU was billed to 39 drafts; one-off sweep over the CC1 stock: 1 broken TU of 55, 39 verdicts reclassified). (2) dup-def→extern demotion: a draft that DEFINES data a still-stubbed sibling .s in the same TU also emits dies at the assembler with 'symbol already defined' — invisible to rtu (INCLUDE_ASM neutralized). Single-line file-scope defs whose symbol a sibling .s emits are demoted to extern as an ADDITIONAL candidate (new sha, so the ledger's unchanged-skip does not hide it). Smoke test: md_MAIN_003/func_800D3204 demoted D_800D3200 → rtu MATCH (12 ins); the whole-binary SHA remains the sole arbiter.
This commit is contained in:
@@ -517,6 +517,34 @@ def build_draft(body, seed_name, member_name, renames, seed_text, dest_text,
|
|||||||
missing.append(s)
|
missing.append(s)
|
||||||
continue
|
continue
|
||||||
decls.append(re.sub(rf'\b{re.escape(old)}\b', s, d))
|
decls.append(re.sub(rf'\b{re.escape(old)}\b', s, d))
|
||||||
|
if missing and member_asm:
|
||||||
|
# DECL-FROM-USE (P31 S61 follow-up). The seed has no decl, but the member's OWN target .s
|
||||||
|
# states how the symbol is accessed (widths, sign, index scale). Infer a MINIMAL C89 extern
|
||||||
|
# (negative-controlled at 97.4% kind-agreement over 4,702 banked triples) and place it at
|
||||||
|
# BLOCK SCOPE via insert_decls — file-scope decls conflict with the fleet's per-function
|
||||||
|
# loose-typing convention (the ov_SC04_018 lesson). Genuine refusals (STRUCT, conflicting
|
||||||
|
# width, function symbols, no evidence) keep the old refusal, now with the class named.
|
||||||
|
# Strictly additive: this branch runs only where the old code refused outright.
|
||||||
|
try:
|
||||||
|
import decl_from_use as _dfu
|
||||||
|
_ev = _dfu.collect(member_asm)
|
||||||
|
_blanked = _dfu.blank_decl_statements(new_body)
|
||||||
|
_got, _still = {}, []
|
||||||
|
for _s in missing:
|
||||||
|
try:
|
||||||
|
_d = _dfu.infer(_s, _ev, _dfu.usage_forms(_blanked, _s))
|
||||||
|
_got[_s] = _d["decl"]
|
||||||
|
except _dfu.Refuse as _r:
|
||||||
|
_still.append("%s(%s)" % (_s, _r.cls))
|
||||||
|
except Exception as _e:
|
||||||
|
_still.append("%s(ERR:%s)" % (_s, type(_e).__name__))
|
||||||
|
if _got and not _still:
|
||||||
|
new_body = _dfu.insert_decls(new_body, _got)
|
||||||
|
missing = []
|
||||||
|
else:
|
||||||
|
missing = _still or missing
|
||||||
|
except Exception as _e:
|
||||||
|
missing = ["(decl_from_use failed: %r)" % _e] + missing
|
||||||
if missing:
|
if missing:
|
||||||
# R32: a draft missing a declaration is a KNOWN-BAD draft. Don't spend a build on it.
|
# R32: a draft missing a declaration is a KNOWN-BAD draft. Don't spend a build on it.
|
||||||
return None, "no seed decl for " + ",".join(missing[:3])
|
return None, "no seed decl for " + ",".join(missing[:3])
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ REJECTS = ".run/reloc_rejects.jsonl"
|
|||||||
STAGE_DEFAULT = ".run/sweep_resolver"
|
STAGE_DEFAULT = ".run/sweep_resolver"
|
||||||
STAGEABLE_RELOC = ("AGREE", "UNRESOLVED", "COMPILE-FAIL") # COMPILE-FAIL = standalone-only (§ rtu_second_chance)
|
STAGEABLE_RELOC = ("AGREE", "UNRESOLVED", "COMPILE-FAIL") # COMPILE-FAIL = standalone-only (§ rtu_second_chance)
|
||||||
FINAL_NEG = ("DIFF", "CC1", "CPP", "MASPSX", "AS", "SYMBOL-MISMATCH", "GATE-REJECTED",
|
FINAL_NEG = ("DIFF", "CC1", "CPP", "MASPSX", "AS", "SYMBOL-MISMATCH", "GATE-REJECTED",
|
||||||
"NEEDS-TU-EDIT", "NO-OBJ-FN", "UNKNOWN", "RELOC-ERR", "TIMEOUT", "EDIT-FAIL")
|
"NEEDS-TU-EDIT", "NO-OBJ-FN", "UNKNOWN", "RELOC-ERR", "TIMEOUT", "EDIT-FAIL",
|
||||||
|
"TU-BROKEN") # re-opened automatically when the split TU (split_sha) changes
|
||||||
|
|
||||||
_print_lock = threading.Lock()
|
_print_lock = threading.Lock()
|
||||||
_stage_lock = threading.Lock()
|
_stage_lock = threading.Lock()
|
||||||
@@ -301,6 +302,71 @@ def stub_index(b):
|
|||||||
# ------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------
|
||||||
# judge one item
|
# judge one item
|
||||||
# ------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------
|
||||||
|
_tu_ok_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _tu_broken(binary, stub, split_sha):
|
||||||
|
"""True + first error when the split TU fails the TU-alone compile WITH NO DRAFT SPLICED.
|
||||||
|
A CC1 verdict then belongs to the TREE, not the draft (S61: one broken TU was billed to 39
|
||||||
|
drafts as `undeclared`). Cached per (binary, split_sha)."""
|
||||||
|
key = (binary, split_sha)
|
||||||
|
if key not in _tu_ok_cache:
|
||||||
|
try:
|
||||||
|
import decl_from_use as _dfu
|
||||||
|
ok, errs = _dfu.tu_compiles(binary, stub.path)
|
||||||
|
_tu_ok_cache[key] = (not ok, (errs[0] if errs else "")[:200])
|
||||||
|
except Exception as e:
|
||||||
|
_tu_ok_cache[key] = (False, "tu-probe failed: %r" % e)
|
||||||
|
return _tu_ok_cache[key]
|
||||||
|
|
||||||
|
|
||||||
|
_DATA_DEF_RE = re.compile(r"^(?P<lead>(?:const\s+|static\s+|volatile\s+)*[A-Za-z_]\w*(?:\s*\*+)?\s+)"
|
||||||
|
r"(?P<sym>[A-Za-z_]\w*_8[0-9A-Fa-f]{7})(?P<arr>\s*\[[^\]]*\])?"
|
||||||
|
r"\s*(?:=[^;]*)?;\s*$", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def dup_def_demote(binary, fn, stub, body, work):
|
||||||
|
"""The duplicate-symbol class (S61 live probe: assembler `symbol 'D_...' is already defined`):
|
||||||
|
the draft DEFINES data at file scope that a still-stubbed sibling .s in the SAME TU also emits.
|
||||||
|
rtu is blind to it (INCLUDE_ASM neutralized), so it surfaces only at the real build. Demote such
|
||||||
|
defs to extern (single-line defs only; the sibling .s keeps emitting the bytes; the whole-binary
|
||||||
|
SHA stays the sole arbiter). Returns a new candidate path, or None when nothing applies."""
|
||||||
|
try:
|
||||||
|
tu = open(stub.path, errors="replace").read()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
sibs = []
|
||||||
|
for m in re.finditer(r'INCLUDE_ASM\("([^"]+)",\s*(\w+)\)', tu):
|
||||||
|
if m.group(2) != fn:
|
||||||
|
sibs.append(os.path.join(m.group(1), m.group(2) + ".s"))
|
||||||
|
if not sibs:
|
||||||
|
return None
|
||||||
|
emitted = set()
|
||||||
|
for sp in sibs:
|
||||||
|
try:
|
||||||
|
for dm in re.finditer(r"^(?:dlabel|glabel)\s+([A-Za-z_]\w*)", open(sp, errors="replace").read(), re.M):
|
||||||
|
emitted.add(dm.group(1))
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
# only lines at brace depth 0 are file-scope defs
|
||||||
|
out, changed, depth = [], [], 0
|
||||||
|
for line in body.split("\n"):
|
||||||
|
m = _DATA_DEF_RE.match(line) if depth == 0 else None
|
||||||
|
if m and m.group("sym") in emitted and not line.lstrip().startswith("extern"):
|
||||||
|
out.append("extern " + m.group("lead").lstrip() + m.group("sym") + (m.group("arr") or "") + ";"
|
||||||
|
+ " /* dup-def demoted: a sibling .s still emits it (S61) */")
|
||||||
|
changed.append(m.group("sym"))
|
||||||
|
else:
|
||||||
|
out.append(line)
|
||||||
|
depth += line.count("{") - line.count("}")
|
||||||
|
if not changed:
|
||||||
|
return None
|
||||||
|
os.makedirs(work, exist_ok=True)
|
||||||
|
pth = os.path.join(work, fn + ".dupfix.c")
|
||||||
|
open(pth, "w").write("\n".join(out))
|
||||||
|
return pth
|
||||||
|
|
||||||
|
|
||||||
def resolve(item, a, ledger, stage):
|
def resolve(item, a, ledger, stage):
|
||||||
b, fn, stub = item["binary"], item["fn"], item["stub"]
|
b, fn, stub = item["binary"], item["fn"], item["stub"]
|
||||||
work = os.path.join(RUN, "work", "%s__%s" % (b, fn))
|
work = os.path.join(RUN, "work", "%s__%s" % (b, fn))
|
||||||
@@ -311,7 +377,13 @@ def resolve(item, a, ledger, stage):
|
|||||||
src_file = stub.path
|
src_file = stub.path
|
||||||
rows, best = [], None
|
rows, best = [], None
|
||||||
staged = None
|
staged = None
|
||||||
|
cands = list(item["candidates"])
|
||||||
for kind, draft in item["candidates"]:
|
for kind, draft in item["candidates"]:
|
||||||
|
if os.path.exists(draft):
|
||||||
|
dd = dup_def_demote(b, fn, stub, open(draft, errors="replace").read(), os.path.join(work, "dupfix"))
|
||||||
|
if dd:
|
||||||
|
cands.append(("dupfix:" + kind, dd))
|
||||||
|
for kind, draft in cands:
|
||||||
row = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "run": a.run_id, "binary": b, "fn": fn,
|
row = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "run": a.run_id, "binary": b, "fn": fn,
|
||||||
"kind": kind, "draft": draft, "draft_sha": sha(draft), "split_sha": split_sha,
|
"kind": kind, "draft": draft, "draft_sha": sha(draft), "split_sha": split_sha,
|
||||||
"stored_closeness": item.get("stored_closeness")}
|
"stored_closeness": item.get("stored_closeness")}
|
||||||
@@ -365,6 +437,11 @@ def resolve(item, a, ledger, stage):
|
|||||||
else:
|
else:
|
||||||
v = {"verdict": "RELOC-" + str(st), "nins": v["nins"], "ndiff": 0,
|
v = {"verdict": "RELOC-" + str(st), "nins": v["nins"], "ndiff": 0,
|
||||||
"note": str(rv.get("note") or rv.get("caveat") or "")[:200]}
|
"note": str(rv.get("note") or rv.get("caveat") or "")[:200]}
|
||||||
|
if not staged and v["verdict"] in ("CC1", "CPP"):
|
||||||
|
_broken, _terr = _tu_broken(b, stub, split_sha)
|
||||||
|
if _broken:
|
||||||
|
v = {"verdict": "TU-BROKEN", "nins": v.get("nins"), "ndiff": None,
|
||||||
|
"note": "the split TU fails WITHOUT the draft — a tree condition, not this draft: " + _terr}
|
||||||
row.update(verdict=("STAGED" if staged else v["verdict"]), ndiff=v.get("ndiff"),
|
row.update(verdict=("STAGED" if staged else v["verdict"]), ndiff=v.get("ndiff"),
|
||||||
nins=v.get("nins"), note=v.get("note", ""), via=via, body=(staged[0] if staged else body),
|
nins=v.get("nins"), note=v.get("note", ""), via=via, body=(staged[0] if staged else body),
|
||||||
reloc=(staged[2] if staged else None))
|
reloc=(staged[2] if staged else None))
|
||||||
@@ -671,6 +748,7 @@ def main():
|
|||||||
n_dem = 0
|
n_dem = 0
|
||||||
for r in final.values():
|
for r in final.values():
|
||||||
if r["verdict"] in ("DIFF", "CC1", "CPP", "SYMBOL-MISMATCH", "NEEDS-TU-EDIT"):
|
if r["verdict"] in ("DIFF", "CC1", "CPP", "SYMBOL-MISMATCH", "NEEDS-TU-EDIT"):
|
||||||
|
# TU-BROKEN is deliberately absent: a tree condition must not demote the draft
|
||||||
if r["verdict"] == "DIFF" and r.get("ndiff") == r.get("stored_closeness"):
|
if r["verdict"] == "DIFF" and r.get("ndiff") == r.get("stored_closeness"):
|
||||||
continue
|
continue
|
||||||
backlog.append_record({
|
backlog.append_record({
|
||||||
|
|||||||
Reference in New Issue
Block a user