fix(phase-29): reconcile_tu dropped the sibling declarators of a multi-symbol extern line

THE DEFECT (on the banking path — gate_stage runs reconcile_tu): its rewrite replaced the draft's
declaration LINE with the TU's declaration of the ONE conflicting symbol. A statement can declare
several: 'extern u16 D_80078EB2, D_8011F82A, D_8011F82C, D_80078EB4, D_8011F8C4;' where only EB4
conflicts became 'extern s16 D_80078EB4;' — four symbols silently gone.

WHY IT HID: the draft does not fail at the declaration. It fails later with 'D_8011F82A undeclared'
at a USE, several conflicts down a peeling chain, nowhere near the cause. I peeled four separate
'next conflicts' out of func_80176218 before dumping ALL cc1 errors in ONE build and seeing three
undeclared symbols that the tool itself had removed.

FIX: group the plan by STATEMENT rather than by symbol; re-emit EVERY declarator (TU's version for
the conflicting ones, the draft's own for the rest); note multi-declarator statements; and when a
statement cannot be re-parsed, say so loudly instead of emitting only the planned symbols.
VERIFIED: all 5 declarators survive, and the same draft now reconciles 3 symbols instead of 2 —
the dropped ones had been hiding a further conflict.

cookbook §95. The law (R32 again): a transform that REPLACES a syntactic unit must account for
everything that unit contained — the STATEMENT, not the line, is the unit of a C declaration.
Diagnostic: when a draft fails in a chain, stop peeling one error per gate cycle; splice once and
dump every cc1 error, because the shape of the whole set names the cause.
This commit is contained in:
Drew T
2026-07-27 16:33:52 -06:00
parent 1670fe293c
commit db620d4b8d
2 changed files with 72 additions and 5 deletions
+35
View File
@@ -6958,3 +6958,38 @@ copies. Byte-gate the lift ALONE first (it must be neutral — it was, `d19c9580
> **Cost of not doing this:** this family sat recorded as a bimodal "doesn't template" case across
> two sessions. The whole diagnosis, once pointed at one sibling's real stderr, took under an hour
> and was worth 137 members.
## §95 — `reconcile_tu` dropped the SIBLING declarators of a multi-symbol `extern` line (Phase 29 SESSION-21, `func_80176218`)
`reconcile_tu` conforms a draft's DATA declarations to what the target TU can see — the right
question (§ the retired `reconcile_decls` asked the fleet, which has no single answer). Its rewrite
replaced the draft's **declaration LINE** with the TU's declaration of the conflicting symbol. But a
declaration statement can declare SEVERAL symbols:
```c
extern u16 D_80078EB2, D_8011F82A, D_8011F82C, D_80078EB4, D_8011F8C4; /* only EB4 conflicts */
```
became
```c
extern s16 D_80078EB4; /* four symbols GONE */
```
**Why it was hard to see:** the draft does not fail at the declaration. It fails later with
`D_8011F82A undeclared (first use this function)` — pointing at a USE, several conflicts down a
peeling chain, nowhere near the cause. I peeled four separate "next conflicts" out of this one draft
before dumping ALL cc1 errors in a single build and seeing three undeclared symbols that the tool
itself had removed.
**Fix:** group the plan by STATEMENT rather than by symbol, then re-emit **every** declarator — the
TU's version for the ones that conflict, the draft's own for the rest — and note when a
multi-declarator statement is touched. When the statement cannot be re-parsed, say so loudly instead
of emitting only the planned symbols. After the fix the same draft reconciles **3** symbols rather
than 2: the dropped declarators had been hiding a further conflict.
> **The law (R32, again):** a transform that REPLACES a syntactic unit must account for everything
> that unit contained. Line-granular rewriting of C declarations is wrong by construction — the
> statement, not the line, is the unit, and a statement can hold N declarators.
>
> **Diagnostic worth reusing:** when a draft fails in a chain, stop peeling one error per gate cycle.
> Splice it once and dump EVERY cc1 error — the shape of the whole set names the cause
> (three `undeclared` symbols that share one original declaration line is not three problems).
+37 -5
View File
@@ -137,14 +137,46 @@ def fix(body, tu_path, fn):
if not plan:
return body, notes
# Group the plan by STATEMENT, not by symbol. A declaration statement can declare SEVERAL
# symbols — `extern u16 D_80078EB2, D_8011F82A, D_8011F82C, D_80078EB4, D_8011F8C4;` — and only
# one of them may conflict. Replacing the LINE with that one symbol's declaration silently
# DROPS the other four, and the draft then fails with `D_8011F82A undeclared` several conflicts
# later, pointing nowhere near the cause. Measured on func_80176218 (Phase 29 SESSION-21):
# 5 declarators in, 1 out, 4 lost. Re-emit every declarator — the TU's version for the ones that
# conflict, the draft's own for the rest.
by_stmt = {}
for _name, (_tu, _d, st) in plan.items():
by_stmt.setdefault(st.text, set()).add(_name)
out, done = [], set()
dropped_check = [0, 0] # (declarators seen, declarators emitted)
for line in body.split('\n'):
hit = next((s for s, (_t, _d, st) in plan.items()
if st.text.split('\n')[0].strip() == line.strip() and s not in done), None)
if hit: # the draft's decl line -> the TU's declaration
done.add(hit)
hit_stmt = next((s for s in by_stmt
if s.split('\n')[0].strip() == line.strip() and s not in done), None)
if hit_stmt: # the draft's decl line -> conform, preserving siblings
done.add(hit_stmt)
indent = re.match(r'^[ \t]*', line).group(0)
out.append(indent + plan[hit][0].declaration())
planned = by_stmt[hit_stmt]
try:
all_ds = [d for d in cdecl.parse(hit_stmt)
if not (d.is_definition or d.kind == 'func' or d.storage == 'typedef')]
except cdecl.CDeclError:
all_ds = []
if all_ds:
for d in all_ds:
dropped_check[0] += 1
out.append(indent + (plan[d.name][0].declaration() if d.name in planned
else d.declaration()))
dropped_check[1] += 1
if len(all_ds) > 1:
notes.append(f'multi-declarator statement preserved: '
f'{len(all_ds)} symbol(s), {len(planned)} conformed')
else: # unparseable — emit what we planned, and SAY so
notes.append(f'!! could not re-parse a planned decl statement; emitted only the '
f'{len(planned)} conformed symbol(s) — siblings may be lost: '
f'{line.strip()[:70]}')
for nm in sorted(planned):
out.append(indent + plan[nm][0].declaration())
continue
if re.match(r'\s*(extern|typedef)\b', line):
out.append(line) # never cast inside a declaration line