T4.c1: restruct rung L head-fold mode; probe t38_4probe folds 4 defs, R22 green

This commit is contained in:
Drew T
2026-09-29 22:01:34 -06:00
parent f75e99c834
commit d87c35435d
6 changed files with 737 additions and 109 deletions
+21
View File
@@ -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 <types> -> 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:<name>@<fn|file>` (#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).
+2 -9
View File
@@ -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;
+21 -55
View File
@@ -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;
+20 -37
View File
@@ -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;
+669 -5
View File
@@ -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 <CanonType> … # the HEAD-FOLD rows (latest per unit): verdict -> cause -> units
(rung L with `--only <CanonType> …` = HEAD-FOLD mode, P38 T4: same-size other-layout definitions -> `typedef <Canon> <Local>;` + 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 <canonical type> …` 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 <Canon> <Local>;`, `struct
# <Tag>` 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:<name>@<function|file>` (#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"(?<![\w])(?:" + "|".join(alts) + r")(?![\w])")
crx = re.compile(r"^\(\s*(?:(?:const|volatile)\s+)*(?:" + "|".join(alts) + r")\s*(\**)\s*\)")
G = {} if block else _typed_vars(masked, glob_spans, trx, params=False)
acc = []
for lo, hi in lo_hi_fns:
V = dict(G)
V.update(_typed_vars(masked, [(lo, hi)], trx, params=not block))
for m in HF_ACCESS.finditer(masked, lo, hi):
if m.group(2) not in lfields:
continue
base = masked[_postfix_start(masked, m.start()):m.start()]
if _lvl(base, V, crx, lself) == (1 if m.group(1) == "->" 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 <heads>: 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):
+4 -3
View File
@@ -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"