From 9a3e438ed9d930ceff10b64ae5523562ccb98295 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:28:40 -0600 Subject: [PATCH] fix(phase-30 S50): macro seeds carry only the DEFINITION, not the macro's decl layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on the 489-draft run: inline seeds banked 145/213 (68%), macro seeds 77/276 (28%), and the macro failures were decl collisions to a fault — 'parse error before *', 'too few arguments to function'. Cause: a de-macroized DEFINE_ block is decls + definition, and pasting it wholesale reintroduces the exact decl-agreement failure the minimal-preamble design exists to avoid. func_8016AB6C's block is 1,891 lines of which 108 are the function. - seed_body() now returns the DEFINITION as the body and keeps the macro block as the DECL SOURCE, so only the externs the definition actually references are emitted. - aprop_symfix.body_in_text(): text-based sibling of body_text (R33 — one implementation). Indented-definition fallback for macro blocks, confirmed by a '{' with no ';' before it, so an indented CALL can never be mistaken for a definition. --- tools/aprop_autodraft.py | 25 +++++++++++++++++-------- tools/aprop_symfix.py | 23 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/tools/aprop_autodraft.py b/tools/aprop_autodraft.py index b3742de19..46ccc139c 100644 --- a/tools/aprop_autodraft.py +++ b/tools/aprop_autodraft.py @@ -67,9 +67,14 @@ def seed_body(sb, seed_name, header="src/shared/engine_core.h"): if sb["kind"] == "macro": h = seed_file_text(sb["path"] or header) for nm in (sb.get("name"), seed_name): - b = macro_body(h, nm) if nm else None - if b: - return b, h, "macro" + block = macro_body(h, nm) if nm else None + if block: + # The DEFINITION only — the macro's own extern block becomes the DECL SOURCE, not + # part of the draft. Pasting it wholesale reintroduces the exact decl-agreement + # failure the minimal-preamble design exists to avoid: measured, macro seeds banked + # 77/276 (28%) that way against inline's 145/213 (68%), and the macro failures were + # `parse error before '*'` / `too few arguments` — decl collisions, every one. + return ASF.body_in_text(block, nm) or block, block, "macro" return None, "", "macro" for nm in (sb.get("name"), seed_name): b = ASF.body_text(sb["path"], nm) if nm else None @@ -123,8 +128,13 @@ def build_draft(body, seed_name, member_name, renames, seed_text, dest_text, pat = re.compile(r'\b(' + '|'.join(re.escape(k) for k in keys) + r')\b') if keys else None new_body = pat.sub(lambda m: renames[m.group(1)], body) if pat else body new_body = re.sub(rf'\b{re.escape(seed_name)}\b', member_name, new_body, flags=re.I) - if member_name not in new_body: - return None, "fn-name rename produced no definition" + # R32: assert the DEFINITION, not merely the name. The seed name used to be recovered by + # scanning the body for the first `func_XXXXXXXX(` token — which in a de-macroized body is the + # first EXTERN DECLARATION, not the definition. That renamed a callee to the member's name and + # left the definition under the seed's, and the draft linked with `undefined reference to + # `: a full binary build spent to learn what one regex knows. + if not re.search(rf'^[^\n=;]*\b{re.escape(member_name)}\s*\([^;]*\)\s*\{{', new_body, re.M): + return None, "no definition of the member after rename (seed-name misidentified?)" if already_self_contained: # A de-macroized body already carries its own externs; synthesizing a second set would @@ -200,10 +210,9 @@ def main(): skip["symbol_map: " + err.split("(")[0].strip()] += 1 continue dest = "".join(open(p).read() for p in sorted(glob.glob(f"src/{m['binary']}/*.c"))) - body_name = re.search(r'\b(func_[0-9A-Fa-f]{8})\s*\(', body) - draft, why = build_draft(body, body_name.group(1) if body_name else seed["name"], + draft, why = build_draft(body, sb.get("name") or seed["name"], m["name"], ren, seed_text, dest, - already_self_contained=(kind == "macro")) + already_self_contained=False) if draft is None: skip[why.split(" for ")[0]] += 1 continue diff --git a/tools/aprop_symfix.py b/tools/aprop_symfix.py index 6b02e3887..094fbcfb3 100644 --- a/tools/aprop_symfix.py +++ b/tools/aprop_symfix.py @@ -47,16 +47,31 @@ def draft_syms(path): def body_text(path, name): - """The C body of ONE function out of a file (a draft is one function; a seed's file is not). + """The C body of ONE function out of a FILE (thin wrapper over body_in_text).""" + if not path or not os.path.isfile(path): + return None + return body_in_text(open(path).read(), name) + + +def body_in_text(txt, name): + """The C body of ONE function out of a block of text (a draft is one function; a seed's file, + or a de-macroized DEFINE_ block, is not). Matches an `DEFINE_(` engine_core macro or a plain definition, then brace-matches. A whole-file scan would drag every OTHER function's symbols into the comparison and drown the signal, so callers annotating a SEED must pass through here.""" - if not path or not os.path.isfile(path): - return None - txt = _strip_comments(open(path).read()) + txt = _strip_comments(txt) + # Column-0 first (a file's definitions start there). The INDENTED fallback exists for a + # de-macroized DEFINE_ block, whose lines are all indented — but an indented `func_X(` is far + # more often a CALL, so that form must be confirmed by a `{` with no `;` before it. m = (re.search(rf'\bDEFINE_{re.escape(name)}\s*\(', txt) or re.search(rf'^[A-Za-z_][^\n=;]*\b{re.escape(name)}\s*\(', txt, re.M)) + if not m: + for cand in re.finditer(rf'^[ \t]*[A-Za-z_][^\n=;]*\b{re.escape(name)}\s*\(', txt, re.M): + i = txt.find('{', cand.start()) + if i >= 0 and ';' not in txt[cand.end():i]: + m = cand + break if not m: return None i = txt.find('{', m.start())