From db620d4b8d070e62dd089d4c2429c5d18dc50211 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:33:52 -0600 Subject: [PATCH] fix(phase-29): reconcile_tu dropped the sibling declarators of a multi-symbol extern line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/matching-cookbook.md | 35 ++++++++++++++++++++++++++++++++ tools/reconcile_tu.py | 42 ++++++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index 7814c48b83..217a3f3a3a 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -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). diff --git a/tools/reconcile_tu.py b/tools/reconcile_tu.py index 42252962c5..12c394d760 100644 --- a/tools/reconcile_tu.py +++ b/tools/reconcile_tu.py @@ -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