mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-10-02 16:00:27 -04:00
fix(cast_self_callers): --sync-decls must emit a declaration the TU can parse
`--sync-decls` copied the draft's parameter list verbatim into the TU. A draft
names types that are not in scope where the declaration sits, and both forms
of that broke the COMMITTED baseline build in one apply:
src/800.c:2631 extern void func_80015760(Obj_80015760 *obj, s32 *ot);
-> the type is draft-local; the TU has never heard of it
src/800c3.c:866 s32 func_8005E3AC(Ctx *s, s32 size);
-> `Ctx' is typedef'd at line 941, 75 lines BELOW the decl
src/800c3.c:866: parse error before `*'
src/800.c:2631: parse error before `*'
Caught by gate_main's BASELINE RED check with no draft substituted, so the
failure was attributed to the plumbing and not to seven innocent drafts.
THE FALLBACK FOLLOWS THE TOOL'S OWN DOCTRINE. Once the call sites are cast, the
declaration emits no code; it only has to be COMPATIBLE with the definition and
PARSE. `<ret> fn();` satisfies both without naming a type, and C89 6.5.4.3
makes it compatible with a prototyped definition exactly when no parameter is
affected by the default argument promotions. So the draft's own spelling is
still preferred — it is the byte-proven behaviour and it keeps the declaration
informative — and the no-proto form is used ONLY where that spelling cannot
parse at that line. Where it cannot parse AND a narrow parameter forbids
no-proto, the tool refuses loudly and names the type (R43).
NEGATIVE CONTROL (R39) over all 40 main recovery drafts: 68 edits before and
after, 65 byte-identical. The three that changed are exactly the declarations
naming an out-of-scope type — Obj_80015760, Ctx, and Slot54/Rec14 — and no
already-correct declaration is churned.
BASELINE PROOF: with all 14 plumbing edits applied and NO draft substituted,
main builds 143dbb89f34491258bbc27810d0a12ec8b43a8dd — byte-identical. The
casts move zero bytes, as the §20 fold predicts.
This commit is contained in:
+104
-1
@@ -149,6 +149,89 @@ def cast_sites(binary, fn, ret, apply_=False):
|
||||
return edits
|
||||
|
||||
|
||||
# THE DRAFT'S SPELLING IS NOT ALWAYS LEGAL WHERE THE DECLARATION SITS (P31 S77).
|
||||
# `--sync-decls` copied the draft's parameter list verbatim into the TU, but a draft names types the
|
||||
# TU does not have IN SCOPE AT THAT LINE. Byte-witnessed, both from one apply:
|
||||
# src/800.c:2631 extern void func_80015760(Obj_80015760 *obj, s32 *ot); -> the type is
|
||||
# draft-local; the TU has never heard of it => parse error before `*'
|
||||
# src/800c3.c:866 s32 func_8005E3AC(Ctx *s, s32 size); -> `Ctx' is typedef'd at
|
||||
# line 941, SEVENTY-FIVE LINES BELOW the declaration => parse error before `*'
|
||||
# Both broke the COMMITTED baseline, which is how gate_main caught them (BASELINE RED, no draft
|
||||
# substituted) rather than mis-attributing the failure to the drafts.
|
||||
#
|
||||
# THE FIX FOLLOWS THE TOOL'S OWN DOCTRINE. Once the call sites are cast (the step above), the
|
||||
# declaration emits no code; all it must do is be COMPATIBLE with the definition and PARSE. The
|
||||
# no-proto form `s32 func_X();` satisfies both without naming a single type — and C89 makes it
|
||||
# compatible with a prototyped definition exactly when no parameter is affected by the default
|
||||
# argument promotions. So: prefer no-proto whenever it is legal, fall back to the full prototype
|
||||
# only when a NARROW parameter forces it (the §378a case this flag exists for, e.g.
|
||||
# `void func_X(s16)`, which `--any-proto` cannot legally reach), and in that case REFUSE loudly
|
||||
# rather than emit a type the TU cannot parse (R43).
|
||||
NARROW = frozenset(("char", "short", "float", "s8", "u8", "s16", "u16", "signed", "unsigned"))
|
||||
PRIMITIVE = frozenset(("void", "int", "long", "double", "unsigned", "signed", "char", "short",
|
||||
"float", "const", "volatile", "struct", "union", "enum", "register",
|
||||
"s8", "u8", "s16", "u16", "s32", "u32", "s64", "u64", "f32", "f64"))
|
||||
|
||||
|
||||
def _param_types(params):
|
||||
"""[(text, is_pointer)] for each parameter in a verbatim parameter list."""
|
||||
out, depth, cur = [], 0, ""
|
||||
for ch in params:
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if ch == "," and depth == 0:
|
||||
out.append(cur)
|
||||
cur = ""
|
||||
else:
|
||||
cur += ch
|
||||
if cur.strip():
|
||||
out.append(cur)
|
||||
return [(p.strip(), "*" in p or "[" in p) for p in out if p.strip()]
|
||||
|
||||
|
||||
def noproto_is_legal(params):
|
||||
"""True when NO parameter is affected by the default argument promotions (C89 6.5.4.3).
|
||||
|
||||
Pointers and 32-bit-or-wider scalars are promotion-stable; char/short/float and the project's
|
||||
narrow typedefs are not. A `void` list is stable (there are no parameters to promote)."""
|
||||
for text, is_ptr in _param_types(params):
|
||||
if is_ptr:
|
||||
continue
|
||||
words = set(re.findall(r"[A-Za-z_]\w*", text))
|
||||
if words & NARROW:
|
||||
return False
|
||||
if not (words & PRIMITIVE) and words:
|
||||
return False # an unknown scalar typedef could be narrow -> do not assume
|
||||
return True
|
||||
|
||||
|
||||
def _unresolved_types(params, tu_text, line_idx):
|
||||
"""Type identifiers in `params` that are not visible in the TU ABOVE `line_idx` (0-based).
|
||||
|
||||
Textual and deliberately over-cautious: a name counts as visible only if the TU introduces it
|
||||
above the declaration as a typedef, tag, or macro. A false 'unresolved' costs a refusal; a
|
||||
false 'resolved' costs a broken baseline."""
|
||||
above = "".join(tu_text[:line_idx])
|
||||
bad = []
|
||||
for text, _ in _param_types(params):
|
||||
words = re.findall(r"[A-Za-z_]\w*", text)
|
||||
# The trailing identifier is the PARAMETER NAME, not a type — `s16 step` must not put
|
||||
# `step` up for resolution. (Left alone for a function-pointer parameter, whose name is
|
||||
# in the middle; an over-cautious refusal there is the safe direction.)
|
||||
if len(words) > 1 and "(" not in text:
|
||||
words = words[:-1]
|
||||
for w in words:
|
||||
if w in PRIMITIVE or w in NARROW:
|
||||
continue
|
||||
if re.search(r"\b(?:typedef\b[^;]*|struct|union|enum|#\s*define)\s+%s\b" % re.escape(w), above) \
|
||||
or re.search(r"\b%s\s*;" % re.escape(w), above):
|
||||
continue
|
||||
bad.append(w)
|
||||
return sorted(set(bad))
|
||||
|
||||
|
||||
def sync_decls(binary, fn, ret, params, apply_=False):
|
||||
"""Rewrite every forward DECLARATION of `fn` to the draft's exact signature.
|
||||
|
||||
@@ -165,9 +248,10 @@ def sync_decls(binary, fn, ret, params, apply_=False):
|
||||
prototype would convert the arguments at the call site and move the caller's bytes.
|
||||
"""
|
||||
edits = []
|
||||
refusals = []
|
||||
decl_re = re.compile(r"^([ \t]*(?:extern[ \t]+)?)([A-Za-z_][\w \t\*]*?\b)%s[ \t]*\([^;{]*\)[ \t]*;"
|
||||
% re.escape(fn))
|
||||
want = "%s %s(%s);" % (ret, fn, params)
|
||||
noproto = noproto_is_legal(params)
|
||||
for path in src_files(binary):
|
||||
try:
|
||||
lines = open(path).read().splitlines(keepends=True)
|
||||
@@ -182,6 +266,20 @@ def sync_decls(binary, fn, ret, params, apply_=False):
|
||||
continue
|
||||
if _kw_prefixed(line, fn): # `return func_X(a0);` is a CALL, not a declaration
|
||||
continue
|
||||
# The DRAFT'S OWN SPELLING FIRST — it is the byte-proven behaviour and it keeps the
|
||||
# declaration informative. Fall back only where that spelling cannot PARSE at this
|
||||
# line, so a correct declaration is never churned into a weaker one.
|
||||
unresolved = _unresolved_types(params, lines, i)
|
||||
if not unresolved:
|
||||
want = "%s %s(%s);" % (ret, fn, params)
|
||||
elif noproto:
|
||||
want = "%s %s();" % (ret, fn)
|
||||
else:
|
||||
refusals.append("%s: %s:%d — %s not in scope here, and a narrow parameter (%s) "
|
||||
"forbids the no-proto fallback"
|
||||
% (fn, os.path.relpath(path, REPO), i + 1,
|
||||
", ".join(unresolved), params))
|
||||
continue
|
||||
new = m.group(1) + want + "\n" # group(1) already carries the indent AND any `extern `
|
||||
if new == line:
|
||||
continue
|
||||
@@ -191,6 +289,11 @@ def sync_decls(binary, fn, ret, params, apply_=False):
|
||||
changed = True
|
||||
if changed and apply_:
|
||||
open(path, "w").write("".join(lines))
|
||||
if refusals:
|
||||
print("REFUSED %d declaration sync(s) (R43 — emitting a type the TU cannot parse would "
|
||||
"break the baseline build, not the draft):" % len(refusals), file=sys.stderr)
|
||||
for r in refusals:
|
||||
print(" " + r, file=sys.stderr)
|
||||
return edits
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user