From d87c35435d6bf282c8ee970bf191b7e57c289d18 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:01:34 -0600 Subject: [PATCH] T4.c1: restruct rung L head-fold mode; probe t38_4probe folds 4 defs, R22 green --- phase-ends/current/logs/T4.c1.md | 21 + src/md_MAIN_044/md_MAIN_044.c | 11 +- src/ov_MAIN_012/ov_MAIN_012_jr_801458E0.c | 76 +-- src/ov_SC01_000/ov_SC01_000_after.c | 57 +- tools/restruct.py | 674 +++++++++++++++++++++- tools/restruct_cycle.sh | 7 +- 6 files changed, 737 insertions(+), 109 deletions(-) create mode 100644 phase-ends/current/logs/T4.c1.md diff --git a/phase-ends/current/logs/T4.c1.md b/phase-ends/current/logs/T4.c1.md new file mode 100644 index 0000000000..7c5888fed3 --- /dev/null +++ b/phase-ends/current/logs/T4.c1.md @@ -0,0 +1,21 @@ +# T4.c1 — restruct rung L HEAD-FOLD mode +Changed: +- tools/restruct.py: head-fold block after work_file_L (HEAD_FOLD_EXCLUDE, head_split, head_info, hf_cands, hf_keys, hf_settled, _typed_vars, _postfix_start, _split_binary, _lvl, _hf_file_edits, hf_unit, hf_units, _inflight_add, work_file_HF, fold_report); plan_HF + plan_L(rows, redraw) branch; describe_plan head-fold line; apply_batch routes to work_file_HF when --only names canonical types; --plan prints unit keys; --status --rung L --only -> fold_report; docstring lines; selftest fixture HF_FIX_H/HF_FIX_TU + 13 checks. +- tools/restruct_cycle.sh: commit msg `${TASK}: batch …` (no phase-37); CURRENT_PHASE.md append + git add skipped when the file is absent. +- src: probe batch t38_4probe (3 files, 4 defs folded). +Design: +- unit key `fold:@` (#k on repeat), rows carry head=, canon=, fields{local:canon}, files[]; settled = latest FOLDED/FOLD-REFUSED unless --redraw VERDICT[:cause] or defn name. +- type-aware renames: vars/params/globals declared with Local/`struct Tag` (level = stars+dims), base expression walked back from `->`/`.` and levelled (casts, index, `*`/`&`, +/-, self-typed field chains). +- refusal up front: referenced local field whose offset has no canonical field or only a padNN (pad treated as "no field"), local array field larger than the canonical field there; typedef with ptr names; non-typedef with variables. +- header file-scope unit: includer .c TUs (recipes()[2]) that mention the name get renames/tag edits; functions redefining the name at block scope are skipped; includers added to inflight.json before write. +Verified: +- `.venv/bin/python tools/restruct.py --selftest` -> 70/70 OK. +- plan .c: `50 of 153 files · 68 of 171 definitions · {80078E78/block 4, 80078E78/file 2, 800B5CB8/file 34, 800B5CB8/block 131} · excluded {Blk152 12, Fr_ 1}` (.run/logs/t4c1_plan.log). +- plan --headers: `3 of 3 files · 29 of 29 definitions` (engine_types.h 27, func_8014D820__*.h 2), excluded Blk152 1, Fr_ 1. +- oracle was stale (calibrated 54c26d08a): ran `delever_oracle.py --snapshot-baseline` (7428 objs at f75e99c83) + `--calibrate ov_SC04_011 ov_SC03_015 md_SC07_004 main -j 16` -> OK. +- probe `--apply --rung L --batch 3 --label t38_4probe --only Unkstruct_800B5CB8 ov_SC01_000 ov_MAIN_012 md_MAIN_044 -j 12` -> 4 folded (43 renames) / 0 refused, final 3/3 identical, written 3 (.run/logs/t4c1_probe.log). FOLDED: md_MAIN_044.c fold:Ent_CD894@file; ov_SC01_000_after.c fold:Ent@func_8014D820; ov_MAIN_012_jr_801458E0.c 2 units. +- R22 `make clean && make extract-all JOBS=16 && make check-all JOBS=16` -> `extract-all: 217 extracted, 0 failed of 217`, `check-all: 218 passed, 0 failed of 218` (.run/logs/t4c1_r22.log). +Notes for expert: +- .c count 171 vs brief's ~165 (scope = canonical files + own defs, not all include/). +- restruct_cycle.sh edit only `bash -n` checked, not run. +- header units untested on the real fleet (judge = every includer; engine_types.h has many includers — small batches). diff --git a/src/md_MAIN_044/md_MAIN_044.c b/src/md_MAIN_044/md_MAIN_044.c index 86cacd120c..2d971d4d09 100644 --- a/src/md_MAIN_044/md_MAIN_044.c +++ b/src/md_MAIN_044/md_MAIN_044.c @@ -529,14 +529,7 @@ extern void func_80015978(s32 a0, s32 *a1); extern s32 func_80135260(s32 a0, s32 a1, s32 a2, s32 a3); extern u8 D_801202A0[]; -typedef struct { - u16 unk00; - u8 pad02[0x1E]; - s32 unk20; - u8 pad24[0x34]; - s32 unk58; - u8 pad5C[0xB0]; -} Ent_CD894; +typedef Unkstruct_800B5CB8 Ent_CD894; s32 func_800CD894(void *a0) { u8 sp10[8]; @@ -550,7 +543,7 @@ s32 func_800CD894(void *a0) { for (i = 0; i < 0x60; i++) { Ent_CD894 *e = base + i; tag = 0x200; - if (e->unk00 == tag) { + if (e->unk0 == tag) { s32 f58 = e->unk58; if (f58 != 0) { s32 f20 = e->unk20; diff --git a/src/ov_MAIN_012/ov_MAIN_012_jr_801458E0.c b/src/ov_MAIN_012/ov_MAIN_012_jr_801458E0.c index 4536b87ad5..314d692b20 100644 --- a/src/ov_MAIN_012/ov_MAIN_012_jr_801458E0.c +++ b/src/ov_MAIN_012/ov_MAIN_012_jr_801458E0.c @@ -2915,24 +2915,7 @@ void func_8014D738(void) #include "../shared/ov/func_8014D790.h" - typedef struct EntD820_8014D820 - { - u16 f0; - u16 f2; - u16 f4; - u16 x; - u16 f8; - u16 y; - u16 fC; - u16 z; - u8 p10[0x10]; - s32 f20; - u8 p24[0x34]; - s32 f58; - u16 f5C; - u16 f5E; - u8 p60[0x10C - 0x60]; - } Ent_8014D820; + typedef Unkstruct_800B5CB8 Ent_8014D820; typedef struct { u16 x; @@ -3061,24 +3044,7 @@ s32 func_8014D820(s32 a0, u16 *a1, u16 *a2x) u16 z; u16 w; } V4_8014D820; - typedef struct EntD820_8014D820 - { - u16 f0; - u16 f2; - u16 f4; - u16 x; - u16 f8; - u16 y; - u16 fC; - u16 z; - u8 p10[0x10]; - s32 f20; - u8 p24[0x34]; - s32 f58; - u16 f5C; - u16 f5E; - u8 p60[0x10C - 0x60]; - } Ent_8014D820; + typedef Unkstruct_800B5CB8 Ent_8014D820; int new_var2; s16 new_var3; u16 *a2; @@ -3122,24 +3088,24 @@ a0v = a0; desc.y = a2[1] - a1[1]; desc.z = a2[2] - a1[2]; ent = *((Ent_8014D820 **) (a0v + 0x170)); - if (ent->f0 == 0) + if (ent->unk0 == 0) { goto fail; } - if ((ent->f5C & 0x1000) == 0) + if ((ent->unk5C & 0x1000) == 0) { goto fail; } - if (ent->f58 == 0) + if (ent->unk58 == 0) { goto fail; } - if (((s32 (*)(s32 a0, s32 a1, s32 *a2, s32 a3))func_80135A4C)(ent->f20, ent->f58, (s32) a1, a2x) == 0) + if (((s32 (*)(s32 a0, s32 a1, s32 *a2, s32 a3))func_80135A4C)(ent->unk20, ent->unk58, (s32) a1, a2x) == 0) { goto fail; } - ex = ent->x - (*((u16 *) (a0v + 6))); - ez = ent->z - (*((u16 *) (a0v + 0xE))); + ex = ent->unk6 - (*((u16 *) (a0v + 6))); + ez = ent->unkE - (*((u16 *) (a0v + 0xE))); r1 = (s16) ratan2((s16) dz, new_var3); r2 = (s16) ratan2((s16) ez, (s16) ex); r3 = (s16) func_80012A60(r1, r2); @@ -3150,11 +3116,11 @@ a0v = a0; desc.f20 = 0; desc.f1C = 0; desc.f18 = 0; - x0 = ent->x; + x0 = ent->unk6; pos.x = x0; - y0 = ent->y; + y0 = ent->unkA; pos.y = y0; - z0 = ent->z; + z0 = ent->unkE; tx = x0 + desc.x; pos.z = z0; tz = z0 + desc.z; @@ -3170,19 +3136,19 @@ a0v = a0; { continue; } - if (p->f0 == 0) + if (p->unk0 == 0) { continue; } - if (p->f5C == 0) + if (p->unk5C == 0) { continue; } - if (p->f58 == 0) + if (p->unk58 == 0) { continue; } - func_80135888(p->f20, p->f58, (s32) (&pos), (s32) (&desc)); + func_80135888(p->unk20, p->unk58, (s32) (&pos), (s32) (&desc)); } } @@ -3207,15 +3173,15 @@ a0v = a0; { continue; } - if (p->f0 == 0) + if (p->unk0 == 0) { continue; } - if (p->f5C == 0) + if (p->unk5C == 0) { continue; } - if (p->f58 == 0) + if (p->unk58 == 0) { continue; } @@ -3229,9 +3195,9 @@ a0v = a0; r = func_80133784(0, &pos, (s32) (&desc)); if ((r == 0) || ((r == 0x2000) && (((*(u16 *) D_801152A8) | ((u16) D_801152AC)) == 0))) { - ent->x = desc.x - desc.f18; - ent->y = desc.y; - ent->z = desc.z - desc.f20; + ent->unk6 = desc.x - desc.f18; + ent->unkA = desc.y; + ent->unkE = desc.z - desc.f20; desc.x -= pos.x; desc.y -= pos.y; desc.z -= pos.z; diff --git a/src/ov_SC01_000/ov_SC01_000_after.c b/src/ov_SC01_000/ov_SC01_000_after.c index 246b85fcab..7c1b21444c 100644 --- a/src/ov_SC01_000/ov_SC01_000_after.c +++ b/src/ov_SC01_000/ov_SC01_000_after.c @@ -1559,24 +1559,7 @@ s32 func_8014D820(s32 a0, u16 *a1, u16 *a2) u16 z; u16 w; } V4; - typedef struct EntD820 - { - u16 f0; - u16 f2; - u16 f4; - u16 x; - u16 f8; - u16 y; - u16 fC; - u16 z; - u8 p10[0x10]; - s32 f20; - u8 p24[0x34]; - s32 f58; - u16 f5C; - u16 f5E; - u8 p60[0x10C - 0x60]; - } Ent; + typedef Unkstruct_800B5CB8 Ent; int new_var2; s16 new_var3; V4 out[3]; @@ -1618,24 +1601,24 @@ a0v = a0; desc.y = a2[1] - a1[1]; desc.z = a2[2] - a1[2]; ent = *((Ent **) (a0v + 0x170)); - if (ent->f0 == 0) + if (ent->unk0 == 0) { goto fail; } - if ((ent->f5C & 0x1000) == 0) + if ((ent->unk5C & 0x1000) == 0) { goto fail; } - if (ent->f58 == 0) + if (ent->unk58 == 0) { goto fail; } - if (func_80135A4C(ent->f20, ent->f58, (s32 *) a1, (s32) a2) == 0) + if (func_80135A4C(ent->unk20, ent->unk58, (s32 *) a1, (s32) a2) == 0) { goto fail; } - ex = ent->x - (*((u16 *) (a0v + 6))); - ez = ent->z - (*((u16 *) (a0v + 0xE))); + ex = ent->unk6 - (*((u16 *) (a0v + 6))); + ez = ent->unkE - (*((u16 *) (a0v + 0xE))); r1 = (s16) ratan2((s16) dz, new_var3); r2 = (s16) ratan2((s16) ez, (s16) ex); r3 = (s16) func_80012A60(r1, r2); @@ -1646,11 +1629,11 @@ a0v = a0; desc.f20 = 0; desc.f1C = 0; desc.f18 = 0; - x0 = ent->x; + x0 = ent->unk6; pos.x = x0; - y0 = ent->y; + y0 = ent->unkA; pos.y = y0; - z0 = ent->z; + z0 = ent->unkE; tx = x0 + desc.x; pos.z = z0; tz = z0 + desc.z; @@ -1666,19 +1649,19 @@ a0v = a0; { continue; } - if (p->f0 == 0) + if (p->unk0 == 0) { continue; } - if (p->f5C == 0) + if (p->unk5C == 0) { continue; } - if (p->f58 == 0) + if (p->unk58 == 0) { continue; } - func_80135888(p->f20, p->f58, (s32) (&pos), (s32) (&desc)); + func_80135888(p->unk20, p->unk58, (s32) (&pos), (s32) (&desc)); } } @@ -1703,15 +1686,15 @@ a0v = a0; { continue; } - if (p->f0 == 0) + if (p->unk0 == 0) { continue; } - if (p->f5C == 0) + if (p->unk5C == 0) { continue; } - if (p->f58 == 0) + if (p->unk58 == 0) { continue; } @@ -1725,9 +1708,9 @@ a0v = a0; r = func_80133784(0, &pos, (s32) (&desc)); if ((r == 0) || ((r == 0x2000) && (((*(u16 *) D_801152A8) | ((u16) D_801152AC)) == 0))) { - ent->x = desc.x - desc.f18; - ent->y = desc.y; - ent->z = desc.z - desc.f20; + ent->unk6 = desc.x - desc.f18; + ent->unkA = desc.y; + ent->unkE = desc.z - desc.f20; desc.x -= pos.x; desc.y -= pos.y; desc.z -= pos.z; diff --git a/tools/restruct.py b/tools/restruct.py index d1640d56ff..24b71201a8 100644 --- a/tools/restruct.py +++ b/tools/restruct.py @@ -9,6 +9,8 @@ # TU + every TU declaring F, one judged unit (the 24 parked patches) tools/restruct.py --restore # every in-flight file back from inflight.json (never git checkout, R102) tools/restruct.py --status # the ledger's totals per rung + 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 --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) @@ -51,6 +53,7 @@ outer gate (the clean fleet run, R22) which tools/restruct_cycle.sh runs before import argparse import collections import difflib +import fnmatch import hashlib import json import os @@ -2447,6 +2450,551 @@ def work_file_L(tu, label, calib_id, log, pool=None, done=frozenset()): out["final_text"] = text return out +# ---------------------------------------------------------------------------------------------------------------------- +# rung L HEAD-FOLD mode (P38 T4): `--only …` folds every SAME-SIZE, OTHER-LAYOUT struct definition (file or block +# scope, .c and — with --headers — src/**/*.h) onto the named canonical type: the definition becomes `typedef ;`, `struct +# ` uses become the typedef name, and member accesses on bases OF THE LOCAL TYPE (typed vars/params/arrays, `((Local *)e)->f`, +# `v[i].f`, chains through self-typed fields) are renamed by offset. A header unit's renames reach its includer TUs (one judged unit). +# Width/sign/pointer mismatches are not refused up front: the judge decides. +# ---------------------------------------------------------------------------------------------------------------------- +HEAD_FOLD_EXCLUDE = {"Blk152*": "struct-copy blob, 800_b.c:311", "Fr_*": "stack frame"} # name glob -> cause (never a candidate) +HF_INCLUDERS_OVERRIDE = None # the selftest sets {header: [tu]}; None = delever.includers() through recipes() +_HF_STATS = {} # plan_L's head-fold counts for describe_plan +HF_DECLARATOR = re.compile(r"^\s*(\**)\s*(?:const\s+|volatile\s+)*([A-Za-z_]\w*)\s*((?:\[[^\]]*\]\s*)*)$") +HF_ACCESS = re.compile(r"(->|\.)\s*([A-Za-z_]\w*)") + + +def head_split(only): + """(heads, rest): the --only items naming a canonical type (head-fold mode) and the others (file/alias filters as today).""" + if not only: + return [], [] + can = canon_types() + return [o for o in only if o in can], [o for o in only if o not in can] + + +def head_info(heads): + """{head: dict(size, lhash, file, cf{off: (name, size)})} — the canonical side of a head fold.""" + can = canon_types() + out = {} + for h in heads: + d, res, fm = can[h] + lay = res.layout_of_fields(d["fields"], d["kind"], packed=("packed" in (d.get("attrs") or ""))) + if not lay: + continue + cf = {} + for nm, off in sl.field_offsets(res, d): + cf.setdefault(off, (nm, ((fm or {}).get(off) or {}).get("width"))) + out[h] = dict(size=lay[0], lhash=sl.layout_hash(lay), file=d["file"], cf=cf) + return out + + +def hf_names(d): + return d["names"] + ([d["tag"]] if d["tag"] else []) + + +def hf_cands(rel, defs, hinfo): + """[(d, head, resolver, excluded_cause | None)] — the definitions of one file sized like a head with another layout.""" + if rel.startswith("include/") or not hinfo or rel in {hi["file"] for hi in hinfo.values()}: # plan_HF draws src/ only + return [] + can = canon_types() + scope = dict(next(iter(can.values()))[1].defs) + for d in defs: + for n in hf_names(d): + scope.setdefault(n, d) + res = sl.Resolver(scope) + out = [] + for d in defs: + if d["kind"] == "enum" or not d["fields"]: + continue + lay = res.layout_of_fields(d["fields"], d["kind"], packed=("packed" in (d.get("attrs") or ""))) + if not lay: + continue + for h, hi in hinfo.items(): + if lay[0] == hi["size"] and sl.layout_hash(lay) != hi["lhash"]: + ex = next((c for g, c in HEAD_FOLD_EXCLUDE.items() if any(fnmatch.fnmatchcase(n, g) for n in hf_names(d))), None) + out.append((d, h, res, ex)) + break + return out + + +def hf_keys(cands): + """The ledger unit key per candidate: `fold:@` (#k on a repeat) — stable across line shifts.""" + seen, keys = collections.Counter(), [] + for d, *_ in cands: + k = "fold:" + (hf_names(d) or ["?"])[0] + "@" + (d["fn"] or "file") + seen[k] += 1 + keys.append(k if seen[k] == 1 else f"{k}#{seen[k]}") + return keys + + +def hf_settled(rows, redraw=()): + """{(tu, unit)} whose LATEST head-fold row is FOLDED / FOLD-REFUSED, minus the --redraw keys (VERDICT[:cause-substring] or a + definition name).""" + keys = [(k.split(":", 1)[0], k.split(":", 1)[1] if ":" in k else None) for k in redraw] + out = set() + for (rung, tu, unit), r in ledger_latest(rows).items(): + if rung != "L" or not r.get("head") or r.get("verdict") not in ("FOLDED", "FOLD-REFUSED"): + continue + v, c = r.get("verdict"), r.get("cause") or "" + if any((v == kv and (ks is None or ks in c)) or r.get("defn") == kv for kv, ks in keys): + continue + out.add((tu, unit)) + return out + + +def _fn_spans(text, rel, masked): + ls = dl.line_starts(masked) + out = [] + for r in sc.scan_text(text, rel, shared_defs=None): + if r["form"] == "def": + out.append((r["name"], ls[r["line"] - 1], ls[r["end"]] if r["end"] < len(ls) else len(masked))) + return out + + +def _match_close(s, i): + """s[i] in '([' -> the index of its closer (depth over both kinds), -1 if none.""" + depth = 0 + for k in range(i, len(s)): + if s[k] in "([": + depth += 1 + elif s[k] in ")]": + depth -= 1 + if depth == 0: + return k + return -1 + + +def _match_open(s, j): + """s[j] in ')]' -> the index of its opener, -1 if none.""" + depth = 0 + for k in range(j, -1, -1): + if s[k] in ")]": + depth += 1 + elif s[k] in "([": + depth -= 1 + if depth == 0: + return k + return -1 + + +def _typed_vars(masked, spans, trx, params): + """{name: level} of the declarators spelled with the local type in the spans (level = pointer stars + array dims; 0 = a value). + `params`: also a type right after `(`/`,` (a parameter; its first declarator only) — never at file level (prototypes).""" + V = {} + for lo, hi in spans: + for m in trx.finditer(masked, lo, hi): + prev = masked[lo:m.start()].rstrip()[-1:] + as_param = prev in ("(", ",") + if as_param and not params: + continue + depth, k, cur, parts = 0, m.end(), m.end(), [] + while k < hi: + c = masked[k] + if c in "([": + depth += 1 + elif c in ")]": + if depth == 0: + break + depth -= 1 + elif c in ";{" and depth == 0: + break + elif c == "," and depth == 0: + parts.append(masked[cur:k]); cur = k + 1 + if as_param: + break + k += 1 + parts.append(masked[cur:k]) + for p in (parts[:1] if as_param else parts): + dm = HF_DECLARATOR.match(p.split("=", 1)[0]) + if dm: + V[dm.group(2)] = len(dm.group(1)) + dm.group(3).count("[") + return V + + +def _postfix_start(s, i): + """The start of the postfix expression that ends right before s[i] (the base of a `->`/`.` at i).""" + k = i + while True: + j = k - 1 + while j >= 0 and s[j] in " \t\r\n": + j -= 1 + if j < 0: + return k + c = s[j] + if c.isalnum() or c == "_": + st = j + while st > 0 and (s[st - 1].isalnum() or s[st - 1] == "_"): + st -= 1 + p = st - 1 + while p >= 0 and s[p] in " \t\r\n": + p -= 1 + if p >= 0 and s[p] == ".": + k = p; continue + if p >= 1 and s[p - 1:p + 1] == "->": + k = p - 1; continue + return st + if c in ")]": + o = _match_open(s, j) + if o < 0: + return k + if c == "]": + k = o; continue + p = o - 1 + while p >= 0 and s[p] in " \t\r\n": + p -= 1 + if p >= 0 and (s[p].isalnum() or s[p] == "_"): + k = o; continue # a call: its callee continues the expression (judged None below) + return o + return k + + +def _split_binary(t): + """The operands of t's top-level binary + / - (None when another top-level binary operator is present).""" + depth, parts, cur, prevc = 0, [], 0, "" + k = 0 + while k < len(t): + c = t[k] + if c in "([": + depth += 1 + elif c in ")]": + depth -= 1 + elif depth == 0 and not c.isspace(): + nxt = t[k + 1:k + 2] + binary = bool(prevc) and (prevc.isalnum() or prevc in "_)]") + if c == "-" and nxt == ">": + prevc = ">"; k += 2; continue + if c in "+-" and nxt == c: + prevc = ")"; k += 2; continue # ++ / -- (postfix keeps the operand's level) + if c in "+-" and binary: + parts.append(t[cur:k]); cur = k + 1 + elif c in "*/%&|^<>=!?:" and binary: + return None + if not c.isspace(): + prevc = c if depth == 0 or c in ")]" else prevc + k += 1 + parts.append(t[cur:]) + return parts + + +def _lvl(t, V, crx, lself): + """The local-type level of expression text t (1 = a pointer to the local type, 0 = a value), None when not the local type.""" + t = t.strip() + if not t: + return None + parts = _split_binary(t) + if parts is None: + return None + if len(parts) > 1: + ls_ = [_lvl(p, V, crx, lself) for p in parts] + ptr = [x for x in ls_ if x] + return ptr[0] if len(ptr) == 1 and ls_[0] else None + t = re.sub(r"(\+\+|--)\s*$", "", t).strip() + if t.startswith(("++", "--")): + return _lvl(t[2:], V, crx, lself) + if t[0] == "*": + x = _lvl(t[1:], V, crx, lself) + return x - 1 if x else None + if t[0] == "&": + x = _lvl(t[1:], V, crx, lself) + return x + 1 if x is not None else None + cm = crx.match(t) + if cm and t[cm.end():].strip(): + return len(cm.group(1)) + if t[0] == "(": + c = _match_close(t, 0) + if c < 0: + return None + rest = t[c + 1:].lstrip() + if rest and not rest.startswith(("[", ".", "->", "(")): + return None # a cast to another type + lvl, rest = _lvl(t[1:c], V, crx, lself), t[c + 1:] + else: + m = re.match(r"[A-Za-z_]\w*", t) + if not m: + return None + lvl, rest = V.get(m.group(0)), t[m.end():] + while True: + rest = rest.lstrip() + if not rest or lvl is None: + return lvl + if rest[0] == "[": + c = _match_close(rest, 0) + if c < 0: + return None + lvl, rest = (lvl - 1 if lvl else None), rest[c + 1:] + elif rest.startswith("->") or rest[0] == ".": + need = 1 if rest.startswith("->") else 0 + m = re.match(r"(?:->|\.)\s*([A-Za-z_]\w*)", rest) + if not m: + return None + lvl, rest = (lself.get(m.group(1)) if lvl == need else None), rest[m.end():] + else: + return None + + +def _hf_file_edits(masked, lo_hi_fns, glob_spans, lnames, tag, lfields, lself, block): + """(accesses[(start, end, local field)], tag_edits[(start, end, repl)]) over one file's view of the local type. + lo_hi_fns: the function spans judged (a block unit: the one span after its definition); glob_spans: file-level text (the + globals of the type; empty for a block unit).""" + alts = [re.escape(n) for n in lnames] + ([r"struct\s+" + re.escape(tag)] if tag else []) + trx = re.compile(r"(?" else 0): + acc.append((m.start(2), m.end(2), m.group(2))) + tag_edits = [] + if tag: + for lo, hi in list(lo_hi_fns) + list(glob_spans): + for m in re.compile(r"\bstruct\s+" + re.escape(tag) + r"\b(\s*;)?").finditer(masked, lo, hi): + tag_edits.append((m.start(), m.end(), "" if m.group(1) else lnames[0])) + return acc, tag_edits + + +def _complement(spans, n): + out, last = [], 0 + for a, b in sorted(spans): + if a > last: + out.append((last, a)) + last = max(last, b) + if last < n: + out.append((last, n)) + return out + + +def hf_unit(rel, text, masked, d, head, hi, res, fns, defs): + """One head-fold unit: dict(defn, line, fn, kind, canon, edits{rel: [(s, e, repl)]}, renames, refused).""" + defn = (hf_names(d) or ["?"])[0] + u = dict(defn=defn, line=d["line"], fn=d["fn"], kind="head-fold", canon=head, edits={}, renames=0, refused=None) + + def refuse(cause): + u["refused"] = cause + return u + ls = dl.line_starts(masked) + lim = ls[d["line"]] if d["line"] < len(ls) else len(masked) + m = sl.DEF_START.search(masked, ls[d["line"] - 1]) + if not m or m.start() >= lim: + return refuse("the definition was not found at its line") + close = sl._match_brace(masked, m.end() - 1) + semi = masked.find(";", close) if close != -1 else -1 + if semi == -1: + return refuse("the definition's end was not found") + tag = d["tag"] + if d["typedef"]: + if d["ptr_names"]: + return refuse(f"the typedef also declares pointer/array names {d['ptr_names'][:3]}") + if not d["names"]: + return refuse("a typedef without a plain name") + lnames = d["names"] + else: + if ATTR_STRIP.sub("", masked[close + 1:semi]).strip(): + return refuse("the definition declares variables (struct T {…} v;)") + if not tag: + return refuse("an anonymous definition without a name") + lnames = [tag] + lf = dict(sl.field_offsets(res, d)) + lsize, lself = {}, {} + self_types = {re.sub(r"\s+", " ", n) for n in lnames} | ({f"struct {tag}"} if tag else set()) + for f in d["fields"]: + if not f["name"] or f["name"] not in lf: + continue + if f.get("nested"): + nd = f["nested"] + r = res.layout_of_fields(nd["fields"], nd["kind"], packed=("packed" in (nd.get("attrs") or ""))) if nd["kind"] != "enum" else (4, 4, []) + else: + r = res.layout(f["type"], f["stars"], f["dims"]) + if re.sub(r"\s+", " ", f["type"] or "").strip() in self_types: + lself[f["name"]] = f["stars"] + len(f["dims"] or []) + lsize[f["name"]] = (r[0] if r else None, bool(f.get("dims"))) + span = (m.start(), semi + 1) + edits = collections.defaultdict(list) + edits[rel].append((span[0], span[1], " ".join(f"typedef {head} {n};" for n in lnames))) + views = [] # (rel, masked, fn spans, global spans, block?) + if d["scope"] == "block": + fs = next(((lo, hi_) for (nm, lo, hi_) in fns if nm == d["fn"] and lo <= span[0] < hi_), None) + if fs is None: + return refuse(f"the enclosing function {d['fn']} was not found") + views.append((rel, masked, [(span[1], fs[1])], [], True)) + else: + def file_view_hf(r_, msk, fns_, defs_, skip): + shadow = {x["fn"] for x in defs_ if x["scope"] == "block" and set(hf_names(x)) & set(lnames)} + fsp = [(lo, hi_) for (nm, lo, hi_) in fns_ if nm not in shadow] + glob = _complement([(lo, hi_) for (_, lo, hi_) in fns_] + ([skip] if skip else []), len(msk)) + return (r_, msk, fsp, glob, False) + views.append(file_view_hf(rel, masked, fns, defs, span)) + if rel.endswith(".h"): + incs = (HF_INCLUDERS_OVERRIDE or {}).get(rel) if HF_INCLUDERS_OVERRIDE is not None else recipes()[2].get(rel, ()) + pat = re.compile(r"\b(?:" + "|".join(re.escape(n) for n in lnames + ([tag] if tag else [])) + r")\b") + for inc in sorted(set(incs or ())): + if inc == rel or not inc.endswith(".c") or not (REPO / inc).exists(): + continue + itext = (REPO / inc).read_text(errors="surrogateescape") + if not pat.search(itext): + continue + imask = dl.same_len_mask(itext) + views.append(file_view_hf(inc, imask, _fn_spans(itext, inc, imask), tc.walk_file(itext, inc)["definitions"], None)) + renames = {} + for r_, msk, fsp, glob, block in views: + acc, tag_edits = _hf_file_edits(msk, fsp, glob, lnames, tag, set(lf), lself, block) + for (s, e, nm) in acc: + if nm not in renames: + off = lf[nm] + c = hi["cf"].get(off) + if c is None or re.match(r"^pad[0-9A-F]+$", c[0]): + return refuse(f"no canonical field at {off:#x} for local `{nm}`" + (f" (only {c[0]})" if c else "")) + sz, is_arr = lsize.get(nm, (None, False)) + if is_arr and sz and c[1] and sz > c[1]: + return refuse(f"indexed local array field `{nm}` [{off:#x}+{sz:#x}] spans several canonical fields (`{c[0]}` is {c[1]:#x})") + renames[nm] = c[0] + if renames[nm] != nm: + edits[r_].append((s, e, renames[nm])) + u["renames"] += 1 + edits[r_].extend(tag_edits) + u["edits"] = {k: sorted(v) for k, v in edits.items() if v} + u["fields"] = {k: v for k, v in renames.items() if k != v} + return u + + +ATTR_STRIP = re.compile(r"__attribute__\s*\(\((?:[^()]|\([^()]*\))*\)\)") + + +def hf_units(tu, text, heads, settled=frozenset()): + """The TU's head-fold units (the excluded and the settled left out), in source order.""" + hinfo = head_info(heads) + w = tc.walk_file(text, tu) + cands = hf_cands(tu, w["definitions"], hinfo) + if not cands: + return [] + masked = dl.same_len_mask(text) + fns = _fn_spans(text, tu, masked) + units = [] + for (d, h, res, ex), key in zip(cands, hf_keys(cands)): + if ex or (tu, key) in settled: + continue + u = hf_unit(tu, text, masked, d, h, hinfo[h], res, fns, w["definitions"]) + u["key"] = key + units.append(u) + return units + + +def _inflight_add(files): + """Extra files a header unit writes (its includers) join inflight.json BEFORE the write, so --restore reaches them (R102).""" + if JUDGE_STUB is not None or not INFLIGHT.exists(): + return + with _LOCK: + d = json.loads(INFLIGHT.read_text()) + for rel, raw in files.items(): + d["files"].setdefault(rel, raw) + INFLIGHT.write_text(json.dumps(d)) + + +def work_file_HF(tu, label, calib_id, log, pool=None, heads=(), settled=frozenset()): + """Rung L head-fold on one file (a .c or a header whose units reach its includers): the ladder over its units, the winners written.""" + path = REPO / tu + raw = path.read_text(errors="surrogateescape") + short = hashlib.sha1(tu.encode()).hexdigest()[:6] + rows, out = [], dict(tu=tu, rows=[], written=False, final="", compiles=0, seconds=0.0, folded=0, refused=0, renames=0, units=0) + out["rows"] = rows + if not recs_for(tu) and JUDGE_STUB is None: + out["final"] = "NO-RECIPE" + return out + units = hf_units(tu, raw, heads, settled) + out["units"] = len(units) + + def row(u, verdict, **kw): + return dict(ts=time.strftime("%Y-%m-%d %H:%M:%S"), label=label, rung="L", calib=calib_id, tu=tu, unit=u["key"], defn=u["defn"], kind=u["kind"], + head=u["canon"], canon=u["canon"], fn=u["fn"], renames=u["renames"], fields=u.get("fields", {}), files=sorted(u["edits"]), + verdict=verdict, nhash_before="-", nhash_after="-", sites=[], **kw) + live = [] + for u in units: + if u["refused"]: + rows.append(row(u, "FOLD-REFUSED", cause=u["refused"])) + out["refused"] += 1 + else: + live.append(u) + if not live: + out["final"] = "UNCHANGED" + return out + raws = {tu: (raw, path.stat())} + for u in live: + for rel in u["edits"]: + if rel not in raws: + p = REPO / rel + raws[rel] = (p.read_text(errors="surrogateescape"), p.stat()) + + def compose(acc): + per = collections.defaultdict(list) + for u in acc: + for rel, es in u["edits"].items(): + per[rel].extend(es) + return {rel: compose_text(raws[rel][0], es) for rel, es in per.items()} + accepted, per, compiles, secs, rung = ladder_units(live, compose, lambda files, tag: judge_files(files, tag, pool=pool), f"{short}H", log=log) + out["compiles"] += compiles; out["seconds"] += secs + acc_ids = {id(u) for u in accepted} + for i, u in enumerate(live): + if id(u) in acc_ids: + rows.append(row(u, "FOLDED")) + out["folded"] += 1; out["renames"] += u["renames"] + else: + v, err, _ = per[i] + rows.append(row(u, "FOLD-REFUSED", cause=f"{v}: {err[:160]}")) + out["refused"] += 1 + if not accepted: + out["final"] = "UNCHANGED" + return out + files = compose(accepted) + if rung == "A": + fr = dict(verdict="IDENTICAL", compiles=0, seconds=0.0, err="", linked=[]) + else: + fr = judge_files(files, f"{short}HF", pool=pool) + out["compiles"] += fr["compiles"]; out["seconds"] += fr["seconds"] + if fr["verdict"] != "IDENTICAL": + for rel, (r0, st) in raws.items(): + dl.restore_file(REPO / rel, r0, st) + out["final"] = f"COMBINATION-FAILED ({fr['verdict']})" + for r_ in rows: + r_["verdict"] = "COMBINATION-FAILED" + out["folded"] = 0 + return out + out["final"] = "IDENTICAL" + if JUDGE_STUB is None: + _inflight_add({rel: raws[rel][0] for rel in files if rel != tu}) + for rel, t in files.items(): + (REPO / rel).write_text(t, errors="surrogateescape") + out["written"] = True + out["final_text"] = files.get(tu, raw) + out["final_texts"] = files + return out + + +def _norm_cause(c): + c = re.sub(r"`[^`]*`", "`…`", c) + c = re.sub(r"\[[^\]]*\]", "[…]", c) + c = re.sub(r"0x[0-9a-fA-F]+", "0x…", c) + return re.sub(r"\b(src/\S+?):\d+", r"\1:…", c)[:120] + + +def fold_report(heads): + """--status --rung L --only : the head-fold rows (latest per unit) bucketed verdict -> cause -> units.""" + lat = [r for (rung, _, _), r in ledger_latest(load_ledger()).items() if rung == "L" and r.get("head") in heads] + by = collections.defaultdict(lambda: collections.defaultdict(list)) + for r in lat: + cause = r.get("cause") or (f"onto {r.get('canon')}" if r.get("verdict") == "FOLDED" else "-") + by[r.get("verdict")][_norm_cause(cause)].append(f"{r['tu']}:{r['unit']}") + print(f"restruct --status L head-fold {' '.join(heads)}: {len(lat)} units (latest row each)") + for v, causes in sorted(by.items(), key=lambda x: -sum(len(u) for u in x[1].values())): + print(f" {v:20s} {sum(len(u) for u in causes.values())}") + for c, us in sorted(causes.items(), key=lambda x: -len(x[1])): + print(f" {len(us):5d} {c} e.g. {', '.join(us[:2])}") + return 0 + # ---------------------------------------------------------------------------------------------------------------------- # planning and --apply (the batch: files in leverage order, workers own whole TUs, headers serial and last) # ---------------------------------------------------------------------------------------------------------------------- @@ -2539,7 +3087,40 @@ def settled_units(rows, redraw=()): return {k for k, (v, c) in latest.items() if v in DONE_VERDICTS and not lifted(v, c)} -def plan_L(headers, batch, only): +def plan_HF(headers, batch, heads, rest, settled=frozenset()): + """Head-fold mode: [(rel, [unit keys])] — the files holding same-size, other-layout definitions of the heads, most first.""" + global _HF_STATS + hinfo = head_info(heads) + w = walk() + cands, excl, kinds, n_settled = [], collections.Counter(), collections.Counter(), 0 + for rel, r in sorted(w["results"].items()): + if not r or rel.endswith(".h") != headers or not rel.startswith("src/"): + continue + if rest and not matches_only(rest, rel, ""): + continue + cs = hf_cands(rel, r["definitions"], hinfo) + keys = [] + for (d, h, _, ex), key in zip(cs, hf_keys(cs)): + if ex: + excl[ex] += 1 + continue + if (rel, key) in settled: + n_settled += 1 + continue + keys.append(key) + kinds[f"{h}/{d['scope']}"] += 1 + if keys: + cands.append((len(keys), rel, keys)) + cands.sort(key=lambda x: (-x[0], x[1])) + plan = [(rel, keys) for _, rel, keys in cands[:batch]] + _HF_STATS = dict(heads=heads, defs=sum(n for n, _, _ in cands), excluded=dict(excl), kinds=dict(kinds), settled=n_settled) + return plan, len(cands) + + +def plan_L(headers, batch, only, rows=None, redraw=()): + heads, rest = head_split(only) + if heads: + return plan_HF(headers, batch, heads, rest, hf_settled(rows if rows is not None else load_ledger(), redraw)) w = walk() by_l = _canon_by_layout() cands = [] @@ -2567,6 +3148,10 @@ def describe_plan(rung, plan, total, skipped=None): + (f" · skipped bases {dict(skipped)}" if skipped else "")) if rung == "D": return f"restruct --plan D: {len(plan)} of {total} TUs with lying declarations · {sum(len(c) for _, c in plan)} callees" + if plan and plan[0][1] is not None: + s = _HF_STATS + return (f"restruct --plan L head-fold {' '.join(s['heads'])}: {len(plan)} of {total} files · {sum(len(k) for _, k in plan)} of {s['defs']} " + f"definitions · by head/scope {s['kinds']} · excluded {s['excluded']} · settled {s['settled']}") return f"restruct --plan L: {len(plan)} of {total} files with file-scope definitions" @@ -2640,8 +3225,10 @@ def apply_batch(a): plan, total = plan_D(a.headers, a.batch, a.only, settled_units(rows, a.redraw)) print(describe_plan("D", plan, total), flush=True) else: - plan, total = plan_L(a.headers, a.batch, a.only) + plan, total = plan_L(a.headers, a.batch, a.only, rows, a.redraw) print(describe_plan("L", plan, total), flush=True) + hf_heads = head_split(a.only)[0] if a.rung == "L" else [] + hf_set = hf_settled(rows, a.redraw) if hf_heads else frozenset() if not plan: print(f"restruct --apply {a.rung}: nothing to do (no drawable file)") return 0 @@ -2668,6 +3255,8 @@ def apply_batch(a): r = work_file_S(tu, payload, a.label, calib_id, log, pool=hdr_pool, allow_local=False, want_sa=not a.no_sa, want_recipes=a.recipes, done=done) elif a.rung == "D": r = work_file_D(tu, a.label, calib_id, log, pool=hdr_pool, callees=payload, done=done) + elif hf_heads: + r = work_file_HF(tu, a.label, calib_id, log, pool=hdr_pool, heads=hf_heads, settled=hf_set) else: r = work_file_L(tu, a.label, calib_id, log, pool=hdr_pool, done=done) except Exception as ex: # noqa: BLE001 — a worker's crash is a row, never a silent gap (R55) @@ -3407,6 +3996,40 @@ void fx_b(s32 a0) { } ''' +HF_FIX_H = '''typedef struct { s32 h0; s32 h4; u32 h8; s32 hC; } HdrT; +''' + +HF_FIX_TU = '''#include "common.h" +#include "hf.h" +typedef struct { s32 f4; } Oth; +typedef struct { s32 w0; s32 w4; s32 w8; s32 wC; } Fr_TEST; +extern HdrT D_80078E50; +extern Oth D_80078E60; + +void hf_a(s32 a0) { + typedef struct EntT { s16 f0; s16 f2; s32 f4; u16 f8; u16 fA; struct EntT *fC; } Ent; + Ent *e; + Ent v, arr[2]; + struct EntT *q; + Oth o; + e = (Ent *)a0; + e->f0 = 1; + v.f4 = e->f8; + arr[1].f8 = ((Ent *)a0)->f4; + q = e->fC->fC; + q->f4 = 0; + o.f4 = 2; + D_80078E60.f4 = 3; + D_80078E50.h4 = 4; +} + +void hf_b(s32 a0) { + typedef struct { s16 g0; s16 g2; s32 g4; s32 g8; s32 gC; } Bad; + Bad *b = (Bad *)a0; + b->g2 = 1; +} +''' + FIXTURE_BASES = {"param:a0": dict(type="Unkstruct_TEST", sites=12), "param:a1": dict(type="Unkstruct_TEST", sites=1), "gaddr:D_80078E20": dict(type="Unkstruct_TEST", sites=1), "global:D_80078E40": dict(type="Unkstruct_TEST", sites=1)} @@ -3472,7 +4095,7 @@ def macro_probe(): def selftest(real=False): - global JUDGE_STUB, DEFS_OVERRIDE, _canon, _map_cache, VISIBLE_OVERRIDE + global JUDGE_STUB, DEFS_OVERRIDE, _canon, _map_cache, VISIBLE_OVERRIDE, HF_INCLUDERS_OVERRIDE checks = [] def ok(name, cond, note=""): @@ -3605,6 +4228,44 @@ def selftest(real=False): ok("work_file_L: folds land under an IDENTICAL stub", rL["folded"] >= 1 and rL["final"] == "IDENTICAL", f"{rL['folded']} {rL['refused']} {rL['final']}") if rL.get("final_text"): ok("work_file_L: the alias replaces the definition", "typedef Blk8_TEST Loc8;" in rL["final_text"] or "typedef Twin_TEST Loc8;" in rL["final_text"]) + # ---- rung L HEAD-FOLD (P38 T4): block-scope fold with type-aware renames, a header unit reaching its includer, a refusal, an exclusion + (dirp / "ov" / "hf.h").write_text(HF_FIX_H) + (dirp / "ov" / "hf.c").write_text(HF_FIX_TU) + hf_h, hf_c = (dirp / "ov" / "hf.h").relative_to(REPO).as_posix(), (dirp / "ov" / "hf.c").relative_to(REPO).as_posix() + HF_INCLUDERS_OVERRIDE = {hf_h: [hf_c]} + ok("head_split: a canonical name is a head, an alias filters", head_split(["Unkstruct_TEST", "ov_SC01_000"]) == (["Unkstruct_TEST"], ["ov_SC01_000"])) + hf_text = HF_FIX_TU + cands = hf_cands(hf_c, tc.walk_file(hf_text, hf_c)["definitions"], head_info(["Unkstruct_TEST"])) + cby = {(hf_names(d_) or ["?"])[0]: ex for (d_, _, _, ex) in cands} + ok("HF candidates: same size other layout (Ent, Bad, Fr_TEST); Oth (0x4) not", set(cby) == {"Ent", "Bad", "Fr_TEST"}, str(cby)) + ok("HF exclusion: Fr_* carries its cause and makes no unit", cby.get("Fr_TEST") == "stack frame" and all(u_["defn"] != "Fr_TEST" for u_ in hf_units(hf_c, hf_text, ["Unkstruct_TEST"]))) + hu = {u_["defn"]: u_ for u_ in hf_units(hf_c, hf_text, ["Unkstruct_TEST"])} + ok("HF refusal: a referenced field with no canonical field at its offset", (hu["Bad"]["refused"] or "").startswith("no canonical field at 0x2 for local `g2`"), str(hu["Bad"]["refused"])) + ok("HF block unit: 8 member renames, key by function", hu["Ent"]["refused"] is None and hu["Ent"]["renames"] == 8 and hu["Ent"]["key"] == "fold:Ent@hf_a", f"{hu['Ent']['renames']} {hu['Ent'].get('key')}") + JUDGE_STUB = lambda files, tag: ("IDENTICAL", "") + rH = work_file_HF(hf_c, "st", dict(head="x"), logs.append, heads=["Unkstruct_TEST"]) + ft = rH.get("final_text", "") + ok("work_file_HF: Ent FOLDED, Bad FOLD-REFUSED", rH["folded"] == 1 and rH["refused"] == 1 and rH["final"] == "IDENTICAL", f"{rH['folded']} {rH['refused']} {rH['final']}") + ok("HF text: the definition becomes the typedef, struct Tag uses the name", + "typedef Unkstruct_TEST Ent;" in ft and "Ent *q;" in ft and "struct EntT" not in ft) + ok("HF text: renames on every typed base form (ptr var, value, array index, cast, self-typed chain)", + all(s_ in ft for s_ in ("e->unk0 = 1;", "v.unk4 = e->unk8;", "arr[1].unk8 = ((Ent *)a0)->unk4;", "q = e->unkC->unkC;", "q->unk4 = 0;")), + ft[ft.find("e->"):][:200]) + ok("HF text: same-named fields of OTHER types untouched (type-aware, not textual)", "o.f4 = 2;" in ft and "D_80078E60.f4 = 3;" in ft and "b->g2 = 1;" in ft) + ok("HF: the stub never writes the tree", (REPO / hf_c).read_text() == hf_text) + rHh = work_file_HF(hf_h, "st", dict(head="x"), logs.append, heads=["Unkstruct_TEST"]) + fts = rHh.get("final_texts", {}) + ok("HF header unit: the header folds and its includer's access is renamed in the same unit", + rHh["folded"] == 1 and "typedef Unkstruct_TEST HdrT;" in fts.get(hf_h, "") and "D_80078E50.unk4 = 4;" in fts.get(hf_c, ""), f"{rHh['folded']} {sorted(fts)}") + JUDGE_STUB = lambda files, tag: ("DIFFERS", "") if "typedef Unkstruct_TEST Ent;" in files.get(hf_c, "") else ("IDENTICAL", "") + rHd = work_file_HF(hf_c, "st", dict(head="x"), logs.append, heads=["Unkstruct_TEST"]) + ok("HF: a DIFFERS judgement is FOLD-REFUSED with the verdict as cause", any(r_["verdict"] == "FOLD-REFUSED" and r_["cause"].startswith("DIFFERS") and r_["defn"] == "Ent" for r_ in rHd["rows"])) + st_rows = [dict(rung="L", tu=hf_c, unit="fold:Ent@hf_a", verdict="FOLDED", head="Unkstruct_TEST", defn="Ent"), + dict(rung="L", tu=hf_c, unit="fold:Bad@hf_b", verdict="FOLD-REFUSED", cause="no canonical field at 0x2", head="Unkstruct_TEST", defn="Bad")] + ok("HF settled: FOLDED/FOLD-REFUSED settle; --redraw FOLD-REFUSED lifts", hf_settled(st_rows) == {(hf_c, "fold:Ent@hf_a"), (hf_c, "fold:Bad@hf_b")} + and hf_settled(st_rows, ["FOLD-REFUSED"]) == {(hf_c, "fold:Ent@hf_a")}) + ok("HF settled units make no unit", [u_["defn"] for u_ in hf_units(hf_c, hf_text, ["Unkstruct_TEST"], hf_settled(st_rows))] == []) + HF_INCLUDERS_OVERRIDE = None # ---- recipes (text transforms) site_glob = next(s for s in bsites if s.get("bclass") == "global") r1 = recipe_ptr_global_member(text, tu, "fx_a", site_glob) @@ -3715,6 +4376,8 @@ def main(): if a.restore: sys.exit(restore()) if a.status: + if a.rung == "L" and head_split(a.only)[0]: + sys.exit(fold_report(head_split(a.only)[0])) sys.exit(status()) if a.check_ledger: sys.exit(check_ledger(a)) @@ -3753,10 +4416,11 @@ def main(): plan, total = plan_D(a.headers, a.batch, a.only, settled_units(load_ledger(), a.redraw)) print(describe_plan("D", plan, total)) else: - plan, total = plan_L(a.headers, a.batch, a.only) + plan, total = plan_L(a.headers, a.batch, a.only, None, a.redraw) print(describe_plan("L", plan, total)) for tu, payload in plan[:40]: - print(f" {tu} " + (f"{len(payload)} bodies" if a.rung == "S" else f"{len(payload)} callees" if a.rung == "D" else "")) + print(f" {tu} " + (f"{len(payload)} bodies" if a.rung == "S" else f"{len(payload)} callees" if a.rung == "D" else + f"{len(payload)} definitions {payload[:3]}" if payload is not None else "")) return if a.apply: if a.callee and not (a.signature or a.body): diff --git a/tools/restruct_cycle.sh b/tools/restruct_cycle.sh index f9d18524e1..7605e4e320 100644 --- a/tools/restruct_cycle.sh +++ b/tools/restruct_cycle.sh @@ -62,7 +62,7 @@ for k in $(seq "$START" "$END"); do [ -n "$cline" ] || { echo "cycle: the census table carries no type_census line after batch $label"; exit 1; } $PY tools/readability_progress.py --snapshot "$label" > /dev/null 2>&1 || echo "cycle: readability snapshot refused (a stale census?) — continuing" fi - TASK=${TASK:-T4} $PY - "$label" "$vline" "$line" "$cline" "$(grep -oE 'wall=[0-9.]+ s' "$r22" | head -1)" "$MARKER" <<'PYEOF' || exit 1 + [ -f phase-ends/CURRENT_PHASE.md ] && { TASK=${TASK:-T4} $PY - "$label" "$vline" "$line" "$cline" "$(grep -oE 'wall=[0-9.]+ s' "$r22" | head -1)" "$MARKER" <<'PYEOF' import os, pathlib, re, sys, time, subprocess label, vline, r22line, cline, wall, marker = sys.argv[1:7] task = os.environ.get("TASK", "T4") @@ -81,13 +81,14 @@ for i, l in enumerate(lines): p.write_text("\n".join(lines)) print("log:", label) PYEOF + } || [ ! -f phase-ends/CURRENT_PHASE.md ] || exit 1 for i in 1 2 3 4 5 6; do [ -e .git/index.lock ] || break if [ -z "$(ps -eo cmd | grep -E '^git |/git ' | grep -v grep)" ]; then echo "cycle: stale .git/index.lock (no git process) — removed"; rm -f .git/index.lock; break; fi sleep 10 done - git add -A src && git add .run/P37/restruct .run/P37/census .run/P37/baseline phase-ends/CURRENT_PHASE.md docs/readability.md docs/readability-progress.tsv 2>/dev/null; git add .run/P36/delever/calibration.json 2>/dev/null - git commit -q -m "src(phase-37): ${TASK:-T4} batch $label — $vline | R22 $line | $cline (restruct_cycle)" || exit 1 + git add -A src && git add .run/P37/restruct .run/P37/census .run/P37/baseline $([ -f phase-ends/CURRENT_PHASE.md ] && echo phase-ends/CURRENT_PHASE.md) docs/readability.md docs/readability-progress.tsv 2>/dev/null; git add .run/P36/delever/calibration.json 2>/dev/null + git commit -q -m "${TASK:-T4}: batch $label — $vline | R22 $line | $cline (restruct_cycle)" || exit 1 echo "cycle: batch $label committed $(git log --oneline -1 | cut -c1-9) — $vline | $line" done echo "cycle: batches $START..$END done"