diff --git a/decomp-architect/corpus/tools/P10/head_types.py b/decomp-architect/corpus/tools/P10/head_types.py index d859502436..083db5a9df 100644 --- a/decomp-architect/corpus/tools/P10/head_types.py +++ b/decomp-architect/corpus/tools/P10/head_types.py @@ -8,10 +8,12 @@ write the per-offset decision record. --header / --decisions rewrite only the te created with its hand-written skeleton. Flat structs only (the rewriter spells flat members; no unions, no nesting). .venv/bin/python tools/head_types.py [--type NAME ...] [--map PATH] [--header PATH] [--decisions PATH] [--selftest] + .venv/bin/python tools/head_types.py --header PATH --add-pads CANDIDATES.json # additive: pad splits only (P39 T3.c6) """ import argparse import json import pathlib +import re import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) @@ -186,6 +188,77 @@ def splice(path, skeleton, begin, end, body): return True +PAD_RE = re.compile(r"^(\s*)/\* (0x[0-9a-f]+) \*/ u8 pad[0-9A-F]+\[(0x[0-9a-f]+)\];$") + + +def add_pads(region, cands): + """Additive mode (P39 T3.c6): split pad arrays of the region's typedefs to insert candidate fields; every other line + is kept byte-for-byte. cands = [{type, off, width, sign}]. -> (new region, [added], [(cand, why skipped)]).""" + want = {} + for c in cands: + want.setdefault(c["type"], []).append(c) + out, added, skipped, block = [], [], [], [] + for line in region.split("\n"): + block.append(line) + m = re.match(r"^\} (\w+);", line) + if not m: + continue + cs, used = want.pop(m.group(1), []), set() + for k, bl in enumerate(block): + pm = PAD_RE.match(bl) + if not pm: + out.append(bl) + continue + ind, p0, pn = pm.group(1), int(pm.group(2), 16), int(pm.group(3), 16) + ins = sorted((c for c in cs if p0 <= c["off"] and c["off"] + c["width"] <= p0 + pn), key=lambda c: c["off"]) + cur, new = p0, [] + for c in ins: + off, w, sg = c["off"], c["width"], c["sign"] + if off < cur or (w, sg) not in sl.TYPE_OF or off % min(w, 4): + skipped.append((c, "overlap" if off < cur else "width/sign/align")) + continue + if off > cur: + new.append(f"{ind}/* {cur:#06x} */ u8 pad{cur:X}[{off - cur:#x}];") + new.append(f"{ind}/* {off:#06x} */ {sl.TYPE_OF[(w, sg)]} unk{off:X};") + added.append(c) + used.add(id(c)) + cur = off + w + if cur == p0: + out.append(bl) + continue + if cur < p0 + pn: + new.append(f"{ind}/* {cur:#06x} */ u8 pad{cur:X}[{p0 + pn - cur:#x}];") + out.extend(new) + skipped += [(c, "not in a pad") for c in cs if id(c) not in used and not any(c is s[0] for s in skipped)] + block = [] + out.extend(block) + skipped += [(c, "type not in region") for cs in want.values() for c in cs] + return "\n".join(out), added, skipped + + +def region_of(path): + p = REPO / path if not pathlib.Path(path).is_absolute() else pathlib.Path(path) + old = p.read_text() + i, j = old.find(C_BEGIN), old.find(C_END) + if i < 0 or j < i: + sys.exit(f"head_types: {path} lacks the markers {C_BEGIN} … {C_END}") + return old[i + len(C_BEGIN) + 1:j] + + +def selftest_add_pads(): + reg = ("typedef struct {\n /* c */\n /* 0x0000 */ u16 unk0;\n /* 0x0002 */ u8 pad2[0xa];\n" + " /* 0x000c */ s32 unkC;\n} Unkstruct_T; /* size = 0x10 */\n") + cands = [dict(type="Unkstruct_T", off=4, width=4, sign="s"), dict(type="Unkstruct_T", off=9, width=1, sign="u"), + dict(type="Unkstruct_T", off=0, width=1, sign="u"), dict(type="Unkstruct_T", off=6, width=2, sign="u")] + new, added, skipped = add_pads(reg, cands) + want = ("typedef struct {\n /* c */\n /* 0x0000 */ u16 unk0;\n /* 0x0002 */ u8 pad2[0x2];\n" + " /* 0x0004 */ s32 unk4;\n /* 0x0008 */ u8 pad8[0x1];\n /* 0x0009 */ u8 unk9;\n" + " /* 0x000a */ u8 padA[0x2];\n /* 0x000c */ s32 unkC;\n} Unkstruct_T; /* size = 0x10 */\n") + ok = new == want and len(added) == 2 and sorted(s[1] for s in skipped) == ["not in a pad", "overlap"] + print("add_pads OK" if ok else f"add_pads FAIL\n{new}\n{skipped}") + return ok + + def selftest(): rec = dict(name="Unkstruct_TEST", sites=10, bodies=2, size_hint=10, evidence={}, globals_at=[], layout=[[0, 4, "s", 5], [2, 2, "u", 4], [4, 2, "s", 3], [6, 2, "u", 3], [7, 1, "u", 9], [8, 4, "s", 2]], @@ -195,6 +268,7 @@ def selftest(): # best: 0:2u3 + 2:2u4 + 4:2s3 + 7:1u9 + 8:4s2 = 21 vs 0:4s5 + 4:2s3 + 7:1u9 + 8:4s2 = 19 want = "fields=5 kept_sites=21 dropped_sites=10 conflict_offsets=4 overlaps=4 misaligned=2 union_candidates=0" ok = summary == want and "/* 0x0007 */ u8 unk7;" in txt and "/* 0x0006 */ u8 pad6[0x1];" in txt and "size = 0xc" in txt + ok = selftest_add_pads() and ok print("OK" if ok else f"FAIL\n{summary}\n{txt}\n" + "\n".join(lines)) return 0 if ok else 1 @@ -205,10 +279,24 @@ def main(): ap.add_argument("--map", default=".run/P37/census/struct_map.json") ap.add_argument("--header", help="C header to rewrite between /* head_types:begin */ … /* head_types:end */") ap.add_argument("--decisions", help="markdown to rewrite between … ") + ap.add_argument("--add-pads", metavar="CANDIDATES.json", + help="additive mode: keep the --header region's fields byte-for-byte, only split pad arrays to insert " + "the candidate fields [{type, off, width, sign}] (no struct map read)") ap.add_argument("--selftest", action="store_true") - a = ap.parse_args() + a =ap.parse_args() if a.selftest: return selftest() + if a.add_pads: + if not a.header: + sys.exit("head_types: --add-pads needs --header") + new, added, skipped = add_pads(region_of(a.header), json.loads((REPO / a.add_pads).read_text())) + for c in added: + print(f"added {c['type']} 0x{c['off']:X} {c['width']}{c['sign']}") + for c, why in skipped: + print(f"skipped {c['type']} 0x{c['off']:X} {c['width']}{c['sign']}: {why}") + ch = splice(a.header, C_SKELETON, C_BEGIN, C_END, new) + print(f"head_types: {a.header} {'written' if ch else 'unchanged'} (add-pads: {len(added)} added, {len(skipped)} skipped)") + return 0 smap = json.loads((REPO / a.map).read_text()) by = {t["name"]: t for t in smap["types"]} chunks, md = [], [] diff --git a/decomp-architect/corpus/tools/P10/restruct.py b/decomp-architect/corpus/tools/P10/restruct.py index 567e513e58..92d58ef6c4 100644 --- a/decomp-architect/corpus/tools/P10/restruct.py +++ b/decomp-architect/corpus/tools/P10/restruct.py @@ -13,6 +13,8 @@ tools/restruct.py --status --rung L --only … # the HEAD-FOLD rows (latest per unit): verdict -> cause -> units (rung L with `--only …` = HEAD-FOLD mode, P38 T4: same-size other-layout definitions -> `typedef ;` + renames) tools/restruct.py --check-ledger # every MEMBER site still a leaf of its (current) canonical type + tools/restruct.py --rung S --residue --label [--only X …] # T3.c7: a no-edit RESIDUAL() row per undrawn body (ledger only) + tools/restruct.py --check-residue [--only X …] # P/X census sites (default ov_*) with no latest rung-S row; exit 1 if any tools/restruct.py --audit-types # the naming invariant over the canonical type files (struct_layout.audit_definition) tools/restruct.py --write-types --top N | --type NAME … [--space ov] # the WRITER: canonical definitions rendered from the struct map (a preview) tools/restruct.py --selftest [--real] # the fixture (a stub oracle); --real adds the calibration + known-true controls @@ -1186,6 +1188,20 @@ def s_prior_rows(tu, fn): S_NO_EDIT_VERDICTS = ("NO-SITE", "TYPE-NOT-VISIBLE", "TYPE-NOT-CANONICAL") +_NB_DEREF = re.compile(r"\*\s*\(\s*[A-Za-z_][\w\s]*\*+\s*\)\s*\(\s*([^()]*?)\s*\+\s*0x([0-9A-Fa-f]+)\s*\)") +_NB_MEMBER = re.compile(r"\(\s*\(\s*[A-Za-z_]\w*\s*\*\s*\)\s*([^()]*?)\s*\)\s*->\s*unk([0-9A-Fa-f]+)") + + +def s_base_norm(key): + """T3.c8: a site base key with its offset accesses spelling-normalised — `*(T *)(X + 0xNN)` and `((S *)X)->unkNN` both become + `@[X+NN]` (innermost first) — so a KEPT entry keyed on the pre-respell text still matches the base the census spells after a + respell of a nested base inside the macro (t3_S28 func_8017EA20: `*(s32 *)(param_1 + 0x64)` → `((Unkstruct_800B5CB8 *)param_1)->unk64`).""" + hx = lambda m: f"@[{m.group(1).strip()}+{int(m.group(2), 16):x}]" + prev = None + while prev != key: + prev, key = key, _NB_MEMBER.sub(hx, _NB_DEREF.sub(hx, key)) + return re.sub(r"\s+", "", key) + def s_macro_rows(tu, text, rows): """T3.c4: every kept-cast macro site (census form K) of a drawn body in the FINAL text gets its own KEPT() row in the @@ -1208,8 +1224,10 @@ def s_macro_rows(tu, text, rows): n = 0 for s in ks: want, key = f"KEPT({s['cls']})", f"{s['bclass']}:{s['base']}" + nkey = s_base_norm(key) # T3.c8: a respelled nested base still matches (was: exact base or line) src = next((lb for lb, e in cands if e.get("verdict") == want and (e.get("line") == s["line"] if s["cls"] == "REINTERP" else - e.get("off") == s["off"] and (e.get("base") == key or e.get("line") == s["line"]))), None) + e.get("off") == s["off"] and (e.get("base") == key or e.get("line") == s["line"] + or s_base_norm(e.get("base") or "") == nkey))), None) if src is None: continue e_ = site_row(s, want, tname=next((b.get("type") for b in row.get("bases") or [] if b.get("base") == key), None), cause=s["cls"]) @@ -3351,11 +3369,13 @@ def matches_only(only, tu, unit, extra=()): hay = [tu, unit] + list(extra) if tu and unit and f"{tu}:{unit}" in only: # T3.c4: `TU:FN` names one body (overlay fn names repeat) return True + if "main" in only and tu and (tu.count("/") == 1 or tu.startswith("src/resident/")): # P39 T4: main-executable TUs (as :1718/:4137) + return True globs =[o for o in only if any(c in o for c in "*?[")] # P39 T3: `ov_*` against the alias / src dir (and the path) if globs and tu and any(fnmatch.fnmatchcase(tu, o) or (tu.startswith("src/") and fnmatch.fnmatchcase(tu.split("/")[1], o)) for o in globs): return True return any(o == h or h.endswith("/" + o) or h.startswith("src/" + o + "/") or ("/" + o + "/") in h - or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only for h in hay if h) + or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only if o != "main" for h in hay if h) # exact: not src/shared/main/ def s_finished(tu, fns, latest, done=frozenset()): @@ -3411,6 +3431,101 @@ def plan_S(headers, batch, only, done, latest=None, data=None): return [(tu, files[tu]) for tu in ordered[:batch]], len(ordered), skipped +# T3.c7: the residue — plan_S skips every body none of whose bases is a visible canonical type (34131 ov bodies had no rung-S row). +# `--rung S --residue` ledgers one no-edit row per such body (no source touched, no compile); `--check-residue` counts what is left. +RESIDUE_FILE_UNIT = "" # the unit of census sites outside any function (site fn null) +RESIDUE_DEFAULT_ONLY = ["ov_*"] + + +def residue_cause(s, can=None): + """Why an undrawn P/X census site stays raw: FILE-SCOPE (fn null), NO-TYPE-IN-MAP (untyped), TYPE-NOT-CANONICAL, TYPE-NOT-VISIBLE, + else site_class's cause for a typed base (MISALIGNED / NO-EDIT: a visible canonical type plan_S's bbt did not pair with the base).""" + can = canon_types() if can is None else can + t = s.get("type") + if not s.get("fn"): + return "FILE-SCOPE" + if not t: + return "NO-TYPE-IN-MAP" + if t not in can: + return "TYPE-NOT-CANONICAL" + if not type_visible(s["tu"], t): + return "TYPE-NOT-VISIBLE" + return site_class(s, None, typed=True) + + +def residue_sites(only, latest, path=None): + """{(tu, unit): [census P/X site]} over the TUs matching `only` (default ov_*) — the bodies with no latest rung-S row (R100).""" + only = only or RESIDUE_DEFAULT_ONLY + out = collections.defaultdict(list) + with open(path or CENSUS / "sites.jsonl") as f: + for l in f: + if not l.startswith(('{"form": "P"', '{"form": "X"')): + continue + s = json.loads(l) + unit = s.get("fn") or RESIDUE_FILE_UNIT + if ("S", s["tu"], unit) in latest or not matches_only(only, s["tu"], unit): + continue + out[(s["tu"], unit)].append(s) + return out + + +def residue_rows(groups, label, calib_id): + """One rung-S no-edit row per (tu, unit) of residue_sites, sorted (tu, unit): nhash_before == nhash_after == the body's current nhash + (the masked file's for RESIDUE_FILE_UNIT; None when the body is gone); every site RESIDUAL(); verdict the shared cause when it + is TYPE-NOT-CANONICAL / TYPE-NOT-VISIBLE (S_NO_EDIT_VERDICTS), else RESIDUAL. Bases carry the cause as status: never s_judged.""" + can = canon_types() + by_tu = collections.defaultdict(list) + for tu, unit in groups: + by_tu[tu].append(unit) + rows = [] + for tu in sorted(by_tu): + path = REPO / tu + nh, file_nh = {}, None + if path.exists(): + text = path.read_text(errors="surrogateescape") + masked = dl.same_len_mask(text) + for d in sc.scan_text(text, tu, shared_defs=None): + if d["form"] == "def": + nh.setdefault(d["name"], body_nhash(masked, d)) + file_nh = lc.norm_hash(masked) + for unit in sorted(by_tu[tu]): + sites = [] + for s in sorted(groups[(tu, unit)], key=lambda x: (x.get("line") or 0, x.get("pos") or 0)): + c = residue_cause(s, can) + sites.append(dict(form=s.get("form"), line=s.get("line"), base=f"{s.get('bclass')}:{s.get('base')}", off=s.get("off"), + type=s.get("type"), verdict=f"RESIDUAL({c})", cause=c)) + causes = {x["cause"] for x in sites} + verdict = next(iter(causes)) if len(causes) == 1 and causes <= set(S_NO_EDIT_VERDICTS) else "RESIDUAL" + bases = {} + for x in sites: + bases.setdefault(x["base"], dict(base=x["base"], type=x["type"], status=x["cause"])) + fs = unit == RESIDUE_FILE_UNIT + h = file_nh if fs else nh.get(unit) + rows.append(dict(ts=time.strftime("%Y-%m-%d %H:%M:%S"), label=label, rung="S", calib=calib_id, tu=tu, unit=unit, + fn=None if fs else unit, addr=None if fs else dl.fn_addr(unit, tu), header=tu.endswith(".h"), + nhash_before=h, nhash_after=h, bases=list(bases.values()), skips={}, sites=sites, verdict=verdict)) + return rows + + +def residue(a): + """--rung S --residue: append the residue rows (ledger only) and print the per-cause site counts.""" + calib = json.loads(oracle.CALIB.read_text()) if oracle.CALIB.exists() else {} + rows = residue_rows(residue_sites(a.only, ledger_latest(load_ledger())), a.label, dict(head=calib.get("head"), stamp=calib.get("stamp"))) + ledger_append(rows) + cause = collections.Counter(x["cause"] for r in rows for x in r["sites"]) + verd = collections.Counter(r["verdict"] for r in rows) + print(f"restruct --residue {a.label}: {len(rows)} rows · {sum(cause.values())} sites · causes {dict(sorted(cause.items()))} · verdicts {dict(sorted(verd.items()))}") + return 0 + + +def check_residue(a): + """--check-residue: the P/X census sites (TUs matching --only, default ov_*) whose body has no latest rung-S row; exit 1 if any.""" + g = residue_sites(a.only, ledger_latest(load_ledger())) + n = sum(len(v) for v in g.values()) + print(f"uncovered {n} sites / {len(g)} bodies") + return 1 if n else 0 + + def argcheck_rows(): """argcheck's lying-declaration rows [{tu, callee, …}] (every callee spelled other than its definition), cached on the src stamp.""" import argcheck @@ -4707,12 +4822,27 @@ def selftest(real=False): line5 = tc.check_casts_verdict({}, mk5, {("S", tu3, "fx_e"): row5}, [], set())[1] ok("re-judge carries the earlier KEPT rows in the census key: KEPT, unbacked 0", row5["verdict"] == "KEPT" and "(unbacked 0)" in line5 and {e["macro_of"] for e in row5["sites"] if e.get("macro_of")} == {"t3_S1"}, f"{row5['verdict']} {line5}") + # T3.c8: func_8017EA20 shape — the earlier KEPT(NON-STRUCT) row keyed the pre-respell nested base on another line + txt6 = FIXTURE_TU_S3.replace("void func_8FF00000_impl(s32 param_1) __asm__", "void fx_f(s32 a0) {\n s32 v;\n" + " v = CAST_NONSTRUCT(u16, *(s32 *)(((Unkstruct_TEST *)a0)->unk4 + 0x20), 0x12);\n}\n\n" + "void func_8FF00000_impl(s32 param_1) __asm__", 1) + mk6 = [s_ for s_ in file_view(tu3, txt6)[4] if s_["form"] == "K" and s_.get("fn") == "fx_f"] + S_PRIOR = {(tu3, "fx_f"): [dict(rung="S", tu=tu3, unit="fx_f", label="t3_S13", verdict="KEPT", sites=[ + dict(base="nested:*(s32 *)(*(s32 *)(a0 + 0x4) + 0x20)", off=0x12, line=999, verdict="KEPT(NON-STRUCT)")])]} + row6 = dict(fn="fx_f", verdict="MEMBERS", sites=[], bases=[]) + s_macro_rows(tu3, txt6, [row6]) + line6 = tc.check_casts_verdict({}, mk6, {("S", tu3, "fx_f"): row6}, [], set())[1] + ok("carry: a nested base respelled under a kept macro still matches its earlier KEPT row (unbacked 0)", len(mk6) == 1 + and "(unbacked 0)" in line6 and [e.get("macro_of") for e in row6["sites"]] == ["t3_S13"], f"{[(s_['bclass'], s_['base']) for s_ in mk6]} {line6}") S_PRIOR = {} ok("respell_edit: a lower-precedence p or a non-literal k stays raw", respell_edit("*(s32 *)(a << 2 + 4)", "*(s32 *)(a << 2 + 4)", dict(form="P", pos=0, end=20, base=None, off=4), "SIGN") is None and respell_edit("*(s32 *)(a + i)", "*(s32 *)(a + i)", dict(form="P", pos=0, end=15, base="a", off=0), "SIGN") is None) ok("--only glob: ov_* matches the src dir, not main", matches_only(["ov_*"], "src/ov_SC05_010/x.c", "f") and not matches_only(["ov_*"], "src/main/x.c", "f") and matches_only(["src/ov_SC05_010/x.c"], "src/ov_SC05_010/x.c", "f") and matches_only(["src/a/x.c:func_1"], "src/a/x.c", "func_1") and not matches_only(["src/a/x.c:func_1"], "src/b/y.c", "func_1")) + ok("--only main: top-level src/*.c and src/resident/, not md_/ov_", matches_only(["main"], "src/800.c", "f") and matches_only(["main"], "src/resident/x.c", "f") + and not matches_only(["main"], "src/md_A/x.c", "f") and not matches_only(["main"], "src/ov_A/x.c", "f") + and not matches_only(["main"], "src/shared/main/x.h", "f")) # ---- rung D on the fixture DEFS_OVERRIDE = {"fx_callee": [("src/ov/fx_defs.c", "void", "s32 a0, s32 a1", False)], "fx_kr": [("src/ov/fx_defs.c", "s32", "s32 a, s32 b", True)]} units = d_units(tu, text) @@ -4915,6 +5045,25 @@ def selftest(real=False): _map_cache = map_keep_ ok("apply: the planned pinned base is CANONICAL with edits though the struct map lacks the name", [(b_[0], b_[1], b_[2]) for b_ in pb_] == [("param:a0", "Unkstruct_TEST", "CANONICAL")] and len(pb_[0][3]) > 0, f"{[b_[:3] for b_ in pb_]}") + # T3.c7: --residue rows (ledger only) and --check-residue over a fixture sites.jsonl + rs_p = dirp / "residue_sites.jsonl" + rs_p.write_text("".join(json.dumps(x) + "\n" for x in [ + dict(form="P", tu=tu, fn="fx_a", line=9, base="a0", bclass="param", off=0, type="Unkstruct_TEST"), + dict(form="X", tu=tu, fn="fx_b", line=20, base="a0", bclass="param", off=4, type=None), + dict(form="P", tu=tu, fn="fx_z", line=30, base="D_1", bclass="global", off=0, type="Unkstruct_GONE"), + dict(form="P", tu=tu, fn=None, line=2, base="D_2", bclass="gaddr", off=0, type=None), + dict(form="K", tu=tu, fn="fx_c", line=40, base="a0", bclass="param", off=0, type=None)])) + lat_ = ledger_latest([dict(rung="S", tu=tu, unit="fx_a", verdict="MEMBERS", nhash_after=nh_, bases=b_can_)]) + rr_ = residue_rows(residue_sites([tu], lat_, rs_p), "rtest", {}) + db_ = next(x for x in defs_ if x["name"] == "fx_b") + ok("--residue: one no-edit row per undrawn body (sorted), causes FILE-SCOPE/NO-TYPE-IN-MAP/TYPE-NOT-CANONICAL, never judged", + [(r["unit"], r["verdict"], r["sites"][0]["verdict"]) for r in rr_] == [("", "RESIDUAL", "RESIDUAL(FILE-SCOPE)"), + ("fx_b", "RESIDUAL", "RESIDUAL(NO-TYPE-IN-MAP)"), ("fx_z", "TYPE-NOT-CANONICAL", "RESIDUAL(TYPE-NOT-CANONICAL)")] + and rr_[1]["nhash_before"] == rr_[1]["nhash_after"] == body_nhash(m_, db_) and not any(s_judged(r) for r in rr_) + and ledger_index(rr_)[0] == set(), f"{[(r['unit'], r['verdict'], r['nhash_after']) for r in rr_]}") + ok("--check-residue: the residue rows cover every P/X site; without them 3 bodies / 3 sites stay uncovered", + residue_sites([tu], ledger_latest(list(lat_.values()) + rr_), rs_p) == {} + and sum(len(v) for v in residue_sites([tu], lat_, rs_p).values()) == 3) ok("the selftest never touches the live inflight.json", True) ok("struct_layout selftest", sl.selftest() == 0) JUDGE_STUB = None @@ -4967,6 +5116,8 @@ def main(): ap.add_argument("--restore", action="store_true") ap.add_argument("--status", action="store_true") ap.add_argument("--check-ledger", action="store_true") + ap.add_argument("--residue", action="store_true", help="rung S: one no-edit row per undrawn body with P/X census sites (ledger only; --only, default ov_*)") + ap.add_argument("--check-residue", action="store_true", help="P/X census sites (--only, default ov_*) whose body has no latest rung-S row; exit 1 if any") ap.add_argument("--audit-types", action="store_true") ap.add_argument("--write-types", action="store_true") ap.add_argument("--type", nargs="*") @@ -4998,6 +5149,12 @@ def main(): sys.exit(status()) if a.check_ledger: sys.exit(check_ledger(a)) + if a.residue: + if a.rung != "S": + sys.exit("restruct --residue is rung S only") + sys.exit(residue(a)) + if a.check_residue: + sys.exit(check_residue(a)) if a.audit_types: sys.exit(audit_types(a)) if a.write_types: diff --git a/tools/restruct.py b/tools/restruct.py index 0e27f0d2dc..92d58ef6c4 100644 --- a/tools/restruct.py +++ b/tools/restruct.py @@ -3369,11 +3369,13 @@ def matches_only(only, tu, unit, extra=()): hay = [tu, unit] + list(extra) if tu and unit and f"{tu}:{unit}" in only: # T3.c4: `TU:FN` names one body (overlay fn names repeat) return True + if "main" in only and tu and (tu.count("/") == 1 or tu.startswith("src/resident/")): # P39 T4: main-executable TUs (as :1718/:4137) + return True globs =[o for o in only if any(c in o for c in "*?[")] # P39 T3: `ov_*` against the alias / src dir (and the path) if globs and tu and any(fnmatch.fnmatchcase(tu, o) or (tu.startswith("src/") and fnmatch.fnmatchcase(tu.split("/")[1], o)) for o in globs): return True return any(o == h or h.endswith("/" + o) or h.startswith("src/" + o + "/") or ("/" + o + "/") in h - or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only for h in hay if h) + or (o in h and o.startswith(("Unkstruct_", "func_", "D_"))) for o in only if o != "main" for h in hay if h) # exact: not src/shared/main/ def s_finished(tu, fns, latest, done=frozenset()): @@ -4838,6 +4840,9 @@ def selftest(real=False): ok("--only glob: ov_* matches the src dir, not main", matches_only(["ov_*"], "src/ov_SC05_010/x.c", "f") and not matches_only(["ov_*"], "src/main/x.c", "f") and matches_only(["src/ov_SC05_010/x.c"], "src/ov_SC05_010/x.c", "f") and matches_only(["src/a/x.c:func_1"], "src/a/x.c", "func_1") and not matches_only(["src/a/x.c:func_1"], "src/b/y.c", "func_1")) + ok("--only main: top-level src/*.c and src/resident/, not md_/ov_", matches_only(["main"], "src/800.c", "f") and matches_only(["main"], "src/resident/x.c", "f") + and not matches_only(["main"], "src/md_A/x.c", "f") and not matches_only(["main"], "src/ov_A/x.c", "f") + and not matches_only(["main"], "src/shared/main/x.h", "f")) # ---- rung D on the fixture DEFS_OVERRIDE = {"fx_callee": [("src/ov/fx_defs.c", "void", "s32 a0, s32 a1", False)], "fx_kr": [("src/ov/fx_defs.c", "s32", "s32 a, s32 b", True)]} units = d_units(tu, text)