From 5f1fc5b5ebbaea33f00db1ca0f6c9736cafad46d Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:27:24 -0600 Subject: [PATCH] =?UTF-8?q?feat(phase-29):=20caller=20pair=20banked=20(57,?= =?UTF-8?q?822=20templ=20ins)=20via=20K&R=20defs=20=E2=80=94=20=C2=A792's?= =?UTF-8?q?=20remedy=20corrected=20(=C2=A799)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit func_80175AB8 + func_80175DA8 both banked. R22 clean-fleet 140 passed / 0 failed of 140. §92 SAID these need "the §17a-1 caller pair, NOT a bare conform" — the diagnosis was right (conforming a narrow param changes argument promotion at every call site, measured PLUMBING -> DIFF) but the remedy was the expensive one. The actual fix touches NO declaration: convert the DEFINITION to K&R, where a narrow param PROMOTES to int (C89 6.3.2.2) and is therefore already compatible with the fleet's existing `s32` prototype, while still emitting narrow-param codegen. §43 applied to the def side. T0 draft-only, ZERO blast radius, versus a 524-site fleet conform. THREE reconcile_tu BUGS SURFACED, ONE OF THEM MINE: (a) BLIND TO BLOCK SCOPE. split_statements is depth-0 BY DESIGN, and §8d deliberately demotes data externs into the function body — so the tool saw one statement and no declarations, printing "reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0" for a draft cc1 rejected with `conflicting types for D_8011F7BC`. A silent skip (R32). Fixed: descend one level. (b) MY BUG, introduced by (a): descending into ANY `{` also enters struct/union/enum definitions, so MEMBERS parse as declarations and get conformed — `u32 code;` became the TU's `typedef void (*code)(unsigned short*);` INSIDE the struct, and `p->code` became `p->(*(u32 *)&code)`. Caught by DIFFING THE TOOL'S OUTPUT AGAINST ITS INPUT before trusting it; the byte-gate would have said PLUMBING and explained nothing. Guard: function bodies only. (c) LATENT since the tool was written: _cast_sub matched bare identifiers and rewrote MEMBER ACCESSES as globals. Unreachable until (a) existed. Guard: (?). cookbook §99. --- docs/matching-cookbook.md | 49 ++++ phase-ends/CURRENT_PHASE.md | 22 ++ src/ov_SC01_077/ov_SC01_077_jr_801734BC.c | 273 +++++++++++++++++++++- tools/reconcile_tu.py | 50 +++- 4 files changed, 390 insertions(+), 4 deletions(-) diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 3d73e5f35..c6fb76187 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -7142,3 +7142,52 @@ defining TU's own decl); definition + register pin intact; both drafts re-banked > binary **succeeded** — because it reused objects the clean run rebuilds. *An incremental pass does > not refute a clean-tree failure.* Every step of this diagnosis came from reading the real cc1/ld > error after a genuinely clean rebuild, never from reasoning about what the tool "should" do. + +## §99 — The narrow-param wall is a DEF-side problem with a ZERO-blast-radius fix: convert the definition to K&R (Phase 29 SESSION-22, `func_80175AB8` + `func_80175DA8`) + +**§92 said these two need "the §17a-1 caller pair, NOT a bare conform".** The diagnosis was right — +conforming the fleet's `void f(s32)` declarations to the byte-true `void f(s16)` narrows the +parameter, changing argument promotion at **every** call site, so the callers emit different code +(§92 measured exactly that: PLUMBING before the conform, **DIFF** after). But the prescribed remedy +was the expensive one. The actual fix touches **no declaration at all**: + +```c +void func_80175AB8(param_1) /* K&R: the narrow param PROMOTES to int (C89 6.3.2.2), */ + s16 param_1; /* so this is ALREADY compatible with the fleet's `s32` */ +{ /* prototype — while still emitting the narrow-param codegen. */ +``` + +That is §43 applied to the **definition** side instead of the declaration side. Cost: a draft-only +edit (**T0**, zero blast radius) versus a **524-site fleet conform**. Both banked, `d19c9580`, +R22 140/140 — **57,822 templated instructions for two draft-local rewrites.** + +> **The law:** when the byte-true signature has a NARROW scalar parameter and the fleet declares it +> wide, do NOT move the declarations. Move the DEFINITION to K&R and let C's promotion rule make the +> existing prototype correct. Conform only when the disagreement is a POINTER shape (caller-neutral, +> §85) or an arity/return change. + +### Two `reconcile_tu` bugs found underneath, one introduced while fixing the other + +**(a) It was blind to BLOCK-SCOPE declarations.** `split_statements` is depth-0 **by design**, so for +a draft whose body is one function definition it returns exactly ONE statement and every declaration +inside is invisible — and §8d (`scope_data_externs`) *deliberately demotes the data externs to block +scope*. C still requires a block-scope `extern` to agree with a file-scope declaration in scope, so +the conflict is real: the tool printed `reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0` +for a draft cc1 then rejected with `conflicting types for D_8011F7BC`. Fixed by descending one level. + +**(b) DESCENDING INTO ANY `{` CORRUPTS STRUCTS — a bug I introduced with (a) and caught by diffing +the tool's own output against its input.** Struct MEMBERS parse as declarations and get "conformed": +``` +- u32 code; /* 0x04 */ -> typedef void (*code)(unsigned short*); +- p->code = *(u32 *)src; -> p->(*(u32 *)&code) = *(u32 *)src; +``` +Guard: descend only into a FUNCTION body (a parameter list before the brace, and not a +`typedef|struct|union|enum` head). + +**(c) And it exposed a LATENT one:** `_cast_sub`'s regex matched a bare identifier, so it rewrote +**member accesses** as if they were the global. Broken since the tool was written; only reachable +once block-scope descent started finding such names. Guard: `(?)`. + +> **Process note worth keeping:** (b) was caught because the transform's output was diffed against +> its input *before* the result was trusted — not by a gate. The byte-gate would have reported +> PLUMBING and told me nothing about *why*, and the corrupted draft looked plausible. diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index eced16829..24c49afa9 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -6136,3 +6136,25 @@ closure-scope error) — on the tool that banked 543 members today. **Reverted, Restructuring a proven tool with blind string replaces at the end of a long session is how a working thing gets broken. Left as a specified next-session task; `sweep_parallel.py` already exists and is proven, so the work is *wiring*, not invention. + +## ✅ T22 — the caller pair BANKED (57,822 templ ins) with ZERO fleet edits; §92's remedy corrected + +**`func_80175AB8` + `func_80175DA8` both banked.** R22 clean-fleet **140/140**. + +**§92 prescribed the expensive remedy.** Its diagnosis was right (conforming a narrow param changes +argument promotion at every call site → DIFF), but the fix needs **no declaration touched**: convert +the DEFINITION to **K&R**, where the narrow param promotes to `int` and is therefore already +compatible with the fleet's existing `s32` prototype — §43 applied to the def side. **T0, zero blast +radius, versus a 524-site fleet conform.** → cookbook **§99**. + +**Three `reconcile_tu` bugs surfaced, and ONE WAS MINE:** +- **(a)** blind to **block-scope** declarations (`split_statements` is depth-0 by design, and §8d + *deliberately demotes* data externs into the function body) → reported `0 reconciled / 0 coverage + defects` for a draft cc1 rejected with `conflicting types for D_8011F7BC`. Fixed: descend one level. +- **(b) MY BUG, introduced by (a):** descending into ANY `{` also enters struct definitions, so + members parse as declarations and get conformed — it rewrote `u32 code;` into the TU's + `typedef void (*code)(...)` INSIDE the struct and mangled `p->code` → `p->(*(u32 *)&code)`. + **Caught by diffing the tool's output against its input before trusting it** — the gate would only + have said PLUMBING. Guard: descend only into a function body. +- **(c)** latent since the tool was written: `_cast_sub` matched bare identifiers, rewriting **member + accesses** as globals. Only reachable once (a) existed. Guard: `(?)`. diff --git a/src/ov_SC01_077/ov_SC01_077_jr_801734BC.c b/src/ov_SC01_077/ov_SC01_077_jr_801734BC.c index 6e1ec2635..bf61d6881 100644 --- a/src/ov_SC01_077/ov_SC01_077_jr_801734BC.c +++ b/src/ov_SC01_077/ov_SC01_077_jr_801734BC.c @@ -2998,9 +2998,278 @@ INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_801758F INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_801759D8); -INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80175AB8); +// @class: regalloc-order +// @stuck: none — MATCH (188/188), symcheck SYMS-OK (22 symbols) +// +// func_80175AB8 — ov_SC01_077 h_seq exemplar (188 ins, ×138 members). +// Zero file-scope footprint (§28/§43): typedef + every extern is block-scoped so the body +// lifts as one unit for dedup_propagate / family_sweep. +// +// FOUR byte-proven levers found here (all newly measured this session, closeness 15 -> 0): +// +// L1 base-pointer opacity WITHOUT killing the birthing boost (sched.c birthing_insn_p). +// $s2 must hold &D_8011F7F0 while `arr = base - 0x48` stays a SEPARATE addiu, so the +// symbol must not constant-fold. The obvious fence `__asm__("" : "=r"(base) : "0"(base))` +// works but sets `base` TWICE => reg_n_sets[base]==2 => birthing_insn_p() returns 0 => +// adjust_priority() skips the max_priority boost => the `la` is scheduled at the very top +// instead of after `sll $s3,$v0,2`. Fix: fence a SINGLE-set pseudo — +// __asm__("" : "=r"(base) : "0"(&D_8011F7F0)); +// `base` is now set once (by the asm) and the address temp once (by the `la`), so BOTH keep +// the birthing boost and land where the target puts them. (15 -> 5 mismatches.) +// +// L2 a volatile STORE is a memory barrier that register moves may cross; a +// `__asm__ __volatile__("" ::: "memory")` is a FULL barrier that they may not. +// sched.c's ASM_OPERANDS case adds a dependence on every reg's last set/use for a volatile +// asm, so the memory fence pinned `sw $s1,0x18($s4)` BEFORE the call's `addu $a0,$s1,$zero`. +// Writing the store as `*(volatile u32 *)&slot[6] = ...` keeps the store anchored (dropping +// the fence entirely let it sink 11 slots) while letting the arg move hoist past it. +// (5 -> 3 mismatches.) The SECOND memory fence (before func_80024054) is still required. +// +// L3 local-alloc.c block_alloc ties operand 0 to the first input operand that DIES +// (combine_regs). `sum = r1 + r2` therefore inherits r1's quantity — and r1 crosses a call, +// so the whole quantity is forced call-preserved ($s0) and the `move $5,ext` copy-suggestion +// is ignored. Neither variable reuse nor an extra post-addu use of r1 breaks the tie +// (sched1 runs first and re-sinks the use). A pin whose live range crosses no call does: +// register s32 sum __asm__("$5"); +// (§44-correction: a pin that does not span a `jal` is ×N-safe.) (3 -> 2 mismatches.) +// +// L4 pinning `sum` to $a1 makes gcc emit it before the $a0 arg move; pinning the $a0 arg too +// (`register u32 *a0v __asm__("$4"); a0v = p;`) restores the target order. (2 -> 0.) +// +// Also note the deliberate MIXED addressing of the same objects: (*(u16 *)&D_8011F7BC)/(*(u16 *)&D_8011F7BE) are read +// absolutely at the 3rd call (lui/lhu) and $s2-relatively (-0x34/-0x32) at the 4th/6th/7th. That +// is what the target does; writing either form uniformly breaks it. + +#include "common.h" + +void func_80175AB8(param_1) + s16 param_1; +{ + typedef struct { s32 g0; s32 pad[2]; } S_AF634; /* size 0x0C */ + extern S_AF634 D_800AF634[]; + extern S_AF634 D_800AF638[]; + + extern u8 D_8011F7F0; + extern u16 D_8011F7B4; + extern u16 D_8011F7B6; + extern s16 D_8011F7BC; + extern s16 D_8011F7BE; + extern s32 D_8011F804; + extern u16 D_8011F80A; + extern u16 D_8011F824; + extern u8 D_8011F832; + extern u8 D_8011F83A; + extern u8 *D_8018A23C[]; + + extern u32 *func_80176D94(void *param_1, u32 param_2, s16 param_3_); + extern u32 *func_801770E0(void *param_1, u32 param_2, s16 param_3_); + extern u32 func_801783D0(s32 a0, s32 a1); + extern u32 *func_80177EA4(u32 *param_1, s32 param_2, u32 param_3, s32 param_4); + extern u32 *func_80177B5C(u32 *a0, s32 a1, s32 a2, s32 a3, s32 a4); + extern void func_80177940(u32 *p, u32 a_, u32 b_, u32 c_); + extern u32 *func_80178298(u32 *param_1, u8 *param_2, short param_3, short param_4); + extern s32 func_80024054(u8*, u8*); + extern s32 func_8005A600(s32, s32, s32, s32, s32); + + u8 local[24]; + u8 *base; + u8 *arr; + u32 *slot; + u32 *p; + register u32 *a0v __asm__("$4"); + u32 uv; + s32 k; + s16 q; + s32 r1; + s32 a2v; + register s32 sum __asm__("$5"); + s32 r2; + s32 idx; + s16 sv; + + p = (u32 *)(D_800AF638[param_1].g0 + D_800AF634[param_1].g0 * 4); + __asm__("" : "=r"(base) : "0"(&D_8011F7F0)); /* L1 */ + arr = base - 0x48; + slot = (u32 *)(param_1 * 4 + (s32)arr); + *(volatile u32 *)&slot[6] = (u32)p; /* L2 */ + p = func_80176D94(p, (s16)(D_8011F7B4 - 0x71), (s16)(D_8011F7B6 + 0x51)); + p = func_801770E0(p, (s16)(D_8011F7B4 - 0x71), (s16)(D_8011F7B6 + 0x65)); + p = func_80177EA4(p, func_801783D0(D_8011F804, 0), + (s16)((*(u16 *)&D_8011F7BC) + 0x39), (s16)((*(u16 *)&D_8011F7BE) + 0x51)); + uv = D_8011F824; + k = 0x3E7; + if (uv < 1000) { + k = uv; + } + sv = (s16)func_801783D0(k, 4); + p = func_80177B5C(p, sv, D_8011F832, + (s16)(*(u16 *)(base - 0x34) + 0x39), + (s16)(*(u16 *)(base - 0x32) + 0x65)); + q = (s16)D_8011F80A / 15; + a2v = (q & 3) * 15; + r1 = func_801783D0((s32)(q << 16) >> 18, 8); + r2 = func_801783D0(a2v, 0); + a0v = p; /* L4 */ + sum = r1 + r2; /* L3 */ + p = ((u32 * (*)(u32 *, s32, s32, s32))func_80177940)( + a0v, (s16)sum, + (s16)(*(u16 *)(base - 0x34) + 0x76), + (s16)(*(u16 *)(base - 0x32) + 0x65)); + idx = D_8011F83A & 0x7F; + D_8011F83A = idx; + __asm__ __volatile__("" ::: "memory"); + ((void (*)(s32, u8 *))func_80024054)(((s32 *)D_8018A23C)[idx], local); + p = func_80178298(p, local, + (s16)(*(u16 *)(base - 0x34) + 0x39), + (s16)(*(u16 *)(base - 0x32) + 0x5B)); + func_8005A600((s32)p, 0, 0, 0x15, 0); + *p = (((u32)p - 0x14) & 0xFFFFFF) | 0x2000000; + slot[8] = (u32)p; + p += 5; + D_800AF634[param_1].g0 += ((s32)p - (s32)slot[6]) >> 2; +} + + +// @class: schedule +// @stuck: none — MATCH (231 ins) +#include "common.h" + +extern u8 D_8011F7F0; +extern u8 D_800B9A13; +typedef struct { s32 g0; s32 pad[2]; } S_AF634; /* size 0x0C */ +extern S_AF634 D_800AF634[]; +extern S_AF634 D_800AF638[]; +extern u16 D_8018A1DC[]; +extern u16 D_8018A22C[]; +extern u16 D_8018A238; +extern u8 D_8018A2B8[]; +extern u8 D_8018A2CC[]; +extern s32 D_8018A2E4[]; +extern u8 D_800D43D4; +extern u8 D_800D4414; +extern u8 D_800D45D4; +extern u32 *func_8017742C(u32 *a0, s32 a1, s32 a2); +extern s32 func_8005A600(s32 a0, s32 a1, s32 a2, s32 a3, s32 a4); +extern void func_800183E0(s32 a0); + +typedef struct { + u32 tag; /* 0x00 */ + u32 code; /* 0x04 */ + u16 x; /* 0x08 */ + u16 y; /* 0x0A */ + u32 uv; /* 0x0C */ + u32 wh; /* 0x10 */ +} Sp_80175DA8; /* 0x14 */ + +void func_80175DA8(param_1) + u16 param_1; +{ + u8 *base = &D_8011F7F0; + u8 *s = base - 0x48; + u16 *src = D_8018A1DC; + Sp_80175DA8 *p; + s16 i; + s32 arg; + s32 t; + s32 fl; + u16 v; + + p = (Sp_80175DA8 *)(D_800AF638[(s16)param_1].g0 + D_800AF634[(s16)param_1].g0 * 4); + *(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x28) = p; + i = 0; + do { + p->tag = ((u32)(p - 1) & 0xFFFFFF) | 0x4000000; + p->code = *(u32 *)src; + src += 2; + if (i < 2) { + p->x = *src++ + *(u16 *)(s + 0xC); + p->y = *src++ + *(u16 *)(s + 0xE); + } else if (i == 2) { + p->x = *src++ + *(u16 *)(s + 0x10); + p->y = *src++ + *(u16 *)(s + 0x12); + } else { + p->x = *src++ + *(u16 *)(s + 0x14); + p->y = *src++ + *(u16 *)(s + 0x16); + } + p->uv = *(u32 *)src; + src += 2; + p->wh = *(u32 *)src; + src += 2; + p++; + i++; + } while (i < 5); + + *(u8 *)(s + 7) = D_800B9A13; + p = (Sp_80175DA8 *)func_8017742C((u32 *)p, + (s16)(*(u16 *)(s + 0x10) - 0x98), + (s16)(*(u16 *)(s + 0x12) + 9)); + func_8005A600((s32)p, 0, 0, 0x16, 0); + p->tag = ((u32)(p - 1) & 0xFFFFFF) | 0x2000000; + *(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x30) = p; + p++; + { + s32 acc = D_800AF634[(s16)param_1].g0; + D_800AF634[(s16)param_1].g0 = + acc + (((s32)p - *(s32 *)(s + (s16)param_1 * 4 + 0x28)) >> 2); + } + + p = *(Sp_80175DA8 **)(s + (s16)param_1 * 4 + 0x28); + if (base[0x48] != 0) { + *((u8 *)p + 0xD) = 0; + } else { + *((u8 *)p + 0xD) = 0xA0; + } + + { + u8 *q1 = *(u8 **)(s + (s16)param_1 * 4 + 0x28); + *(u16 *)(q1 + 0x22) = 0x6CD6; + if (base[0x48] & 0x80) { + *(u16 *)(q1 + 0x20) = D_8018A238; + arg = (s32)&D_800D45D4; + } else { + u16 *tt = D_8018A22C; + s32 k = base[0x48]; + if (k != 0) { + k--; + tt += k; + } + *(u16 *)(q1 + 0x20) = *tt; + arg = D_8018A2E4[base[0x48]]; + } + } + func_800183E0(arg); + + { + u8 *q2 = *(u8 **)(s + (s16)param_1 * 4 + 0x28); + t = (s32)(*(u16 *)(base + 0x2E) << 16); + if (t != 0) { + q2[0x49] = D_8018A2CC[t >> 20]; + } else { + q2[0x49] = 0xA0; + } + } + + { + u8 *q3 = *(u8 **)(s + (s16)param_1 * 4 + 0x28); + v = *(u16 *)(base + 0x40); + if (v < 100) { + q3[0x5D] = D_8018A2B8[v / 5]; + } else { + q3[0x5D] = 0; + } + } + + fl = *(s16 *)(base + 0x1E) & 0x8000; + /* §5a zero-byte sched fence: without it sched1 hoists the `la D_800D43D4` + into the lh's load-delay slot, dropping the target's nop (-1 ins). */ + __asm__(""); + arg = (s32)&D_800D43D4; + if (fl != 0) { + arg = (s32)&D_800D4414; + } + func_800183E0(arg); +} -INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80175DA8); INCLUDE_ASM("asm/ov_SC01_077/nonmatchings/ov_SC01_077_jr_801734BC", func_80176144); diff --git a/tools/reconcile_tu.py b/tools/reconcile_tu.py index e5217a6a9..ea6a9ce7a 100644 --- a/tools/reconcile_tu.py +++ b/tools/reconcile_tu.py @@ -95,7 +95,11 @@ def _cast_sub(d, tu): `D_x[i]()` into `((u8 *)D_x)[i]()` — a dormant transform that fixing the parser would ARM. This tool handles the kind natively, which is why it supersedes that one rather than patching it.""" sym = d.name - rx = re.compile(rf'(&?)\b{re.escape(sym)}\b(\s*\[)?') + # `(?)` — a MEMBER ACCESS is not this symbol. Without it, a struct field that + # happens to share a global's name is rewritten at every use: `p->code` became + # `p->(*(u32 *)&code)`, which is not even valid C. Latent since the tool was written and only + # reachable once block-scope descent started finding such names (Phase 29 SESSION-22). + rx = re.compile(rf'(&?)(?){re.escape(sym)}\b(\s*\[)?') c_arr = tu.kind in ('array', 'fnptr_array') e = _elem(d) @@ -110,16 +114,58 @@ def _cast_sub(d, tu): return rx, lambda m: f'{m.group(1)}{acc}{m.group(2) or ""}' +def _draft_statements(body): + """Every declaration-bearing statement in the draft, at FILE scope AND at BLOCK scope, as + (Stmt-with-ABSOLUTE-spans, is_inner). + + WHY (Phase 29 SESSION-22, byte-witnessed on func_80175AB8). `split_statements` is depth-0 BY + DESIGN, so for a draft whose body is one function definition it returns exactly ONE statement — + the definition — and every declaration inside it is invisible. That is precisely where the data + externs live: §8d (`scope_data_externs`) DEMOTES them to block scope on purpose, because a + block-scope extern declares no global for the TU's own later decls to collide with. + + But C still requires a block-scope `extern` to be compatible with a file-scope declaration of the + same identifier that is in scope — so the conflict is real, and this tool was structurally blind + to it: it reported `reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0` for a draft + cc1 then rejected with `conflicting types for D_8011F7BC`. A silent skip that reads exactly like + "nothing to do" (R32). + + One level of descent is enough and is deliberate: the externs sit at the top of the function + body, not inside nested blocks, and recursing further would start parsing arbitrary code.""" + out = [] + for st in cdecl.split_statements(body): + out.append((st, False)) + m = re.search(r'\{', cdecl._mask(st.text)) + if not m: + continue + # DESCEND ONLY INTO A FUNCTION BODY. Descending into ANY `{` also enters struct/union/enum + # definitions, whose MEMBERS then parse as declarations and get "conformed" against the TU — + # byte-witnessed while writing this: a member `u32 code;` was rewritten to the TU's + # declaration of `code` (`typedef void (*code)(unsigned short*);`) INSIDE the struct, and the + # member ACCESS `p->code` became `p->(*(u32 *)&code)`. A function definition is distinguished + # by a parameter list before the brace and by not being a tag/typedef definition. + head = st.text[:m.start()] + if re.match(r'\s*(typedef|struct|union|enum)\b', head) or ')' not in head: + continue + off = st.start + m.end() # first byte INSIDE the body + inner_txt = body[off:st.end] + for s2 in cdecl.split_statements(inner_txt): + out.append((cdecl.Stmt(s2.text, off + s2.start, off + s2.end), True)) + return out + + def fix(body, tu_path, fn): """Conform the draft's DATA decls to the TU. Returns (new_body, notes).""" full = cdecl.tu_scope(tu_path) # CONFLICT domain: a decl BELOW still conflicts above = set(cdecl.tu_scope(tu_path, above=fn)) # ORDER, for cc1's no-prototype rule plan, notes = {}, [] - for st in cdecl.split_statements(body): + for st, inner in _draft_statements(body): try: ds = cdecl.parse(st.text) except cdecl.CDeclError as e: + if inner: + continue # ordinary CODE inside a function body — not a declaration, not a defect notes.append(f'!! UNPARSED (a coverage defect, not a no-op): {st.text[:60]} -> {e}') continue for d in ds: