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