mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 21:36:06 -04:00
207559daad
Wave P drafted at 97% and cost A DOZEN clean rebuilds to bank, and not one of those rebuilds failed on a matching problem -- every one failed on a TEXTUAL property of the substituted file that a grep could have reported instantly. This is that grep. gate_main's resolve_conflicts cannot answer it, and not from carelessness: it inspects the DRAFTS while the compiler sees the FILE THEY LAND IN -- after typedef stripping and renaming, at each draft's own insertion offset, interleaved with declarations the file already had. Those transformations run AFTER the conflict check passes. So substitute() gained write=False and this tool checks the artifact itself. Five checks, each earned by a rebuild lost this session (§176h): typedef used above its definition; type never defined anywhere; duplicate typedef with different bodies; one symbol declared two incompatible ways; definition contradicting a visible prototype. CALIBRATED AGAINST THE COMPILER, NOT AGAINST C89 PEDANTRY -- and this mattered. The first version reported 4 hard FAILUREs on the slate that had just built BYTE-IDENTICAL: - it ignored SCOPE, but the project deliberately uses block-scope extern blocks, and a declaration inside one function cannot conflict with a definition elsewhere. Now brace-depth aware. - it split `void f()` from `void f(void)`, which gcc-2.7.2 accepts. Normalized. - it called every def-vs-decl mismatch fatal, but gcc-2.7.2 accepted `void f(void*,s32)` against a `void f(s8*,s32)` definition and even `G3P *f(...)` against `G4P *f(...)`. What it REJECTED was a void/non-void RETURN split (func_8001ABBC). That split alone is FAIL; the rest are WARN. Comments are masked via cdecl before any use-site scan (an unmasked scan reported 7 phantom hits). R39 controls: the slate that banked is FAIL-free (exit 0, 3 informative warnings); four synthetic defects each reported at FAIL; a clean text reports nothing; a block-scope extern does not conflict; `short` vs `s16` does not conflict; array-vs-scalar does.
407 lines
22 KiB
Python
407 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""Gate a BATCH of main-EXE drafts the only way main can be gated: a CLEAN rebuild.
|
|
|
|
WHY THIS EXISTS (P31, 2026-08-15). `gate_lane`/`gate_stage` build INCREMENTALLY. That is fine
|
|
for overlays, but main's `make extract` runs the EXE-only `psyq_integrate` + `ld_interleave`
|
|
steps which REWRITE THE LINKER SCRIPT. An incremental build after a source change re-runs that
|
|
on an already-rewritten `.ld` and produces a FALSE DIFF -- exactly the trap R22's own rationale
|
|
describes. This cost the campaign a night: 4 byte-correct main drafts gated 0/4, and I wrote up
|
|
a nonexistent "linker defect" before the null-draft control exposed it (with NO draft
|
|
substituted at all, `make build BINARY=main` still produced the same wrong hash).
|
|
|
|
So main is gated like this instead:
|
|
substitute every draft -> make extract BINARY=main -> make build BINARY=main -> compare SHA
|
|
ONE clean build verifies the WHOLE BATCH, which is what makes this cheap: 34 functions banked in
|
|
a single rebuild. On failure the batch is bisected so one bad draft cannot sink the rest.
|
|
|
|
TWO MAIN-SPECIFIC HAZARDS THIS TOOL HANDLES:
|
|
|
|
1. IN-TU CROSS-DRAFT DECLARATION CONFLICTS. Batching N drafts into one .c means their `extern`s
|
|
must agree WITH EACH OTHER, not merely with the file: wave J hit `D_800A4ED4` declared s16 by
|
|
one draft and u16 by another, and `func_8001C9D0` as void / void* / s32 across three. C
|
|
rejects the TU. We resolve greedily (keep in order, drop the incompatible) and REPORT the
|
|
dropped set -- those drafts are usually correct and recoverable with a cast-at-use.
|
|
Compatibility compares TYPE SIGNATURES ONLY: parameter NAMES are irrelevant, and a checker
|
|
that compares them wrongly discards good work (R39 -- I made exactly that mistake first).
|
|
|
|
2. STALE .s AFTER A REVERT. Once a function becomes C, splat stops emitting its .s. If you then
|
|
revert src/, `corpus.stubs()` raises because the tree and source disagree. Always
|
|
`make extract BINARY=main` BEFORE resolving stubs.
|
|
|
|
Usage:
|
|
gate_main.py <slate.json> [--apply] [--no-bisect]
|
|
slate.json: [{"fn": ..., "draft": ...}, ...] (binary is assumed main)
|
|
default is a DRY RUN that reports what would be substituted and any conflicts.
|
|
--apply performs the substitution + clean rebuild and leaves banked drafts in the tree.
|
|
"""
|
|
import argparse, collections, functools, json, re, subprocess, sys
|
|
sys.path.insert(0, 'tools')
|
|
import corpus
|
|
|
|
# stdout is BUFFERED when redirected to a file -- a long run then looks hung with an
|
|
# empty log (measured: 16 min of silence during a bisect). Always flush.
|
|
print = functools.partial(print, flush=True)
|
|
|
|
GOOD = '143dbb89f34491258bbc27810d0a12ec8b43a8dd'
|
|
TYPES = {'void','char','short','int','long','unsigned','signed','float','double','const',
|
|
'volatile','s8','u8','s16','u16','s32','u32','f32','s64','u64','struct','union'}
|
|
# Same trailing-comment blindness as the typedef patterns had: `;\s*$` misses
|
|
# `extern u8 D_800A4640[]; /* the flag table */`, so the destination TU's own declaration went
|
|
# UNSEEN and a contradicting draft reached the compiler. Seventh instance of one root cause in
|
|
# this file -- a pattern that anchors on end-of-line silently under-reports on commented code,
|
|
# and agents comment nearly everything they declare.
|
|
DECL = re.compile(r'^\s*extern\s+([^;]+?)\s*;[ \t]*(?://[^\n]*|/\*(?:[^*]|\*(?!/))*\*/[ \t]*)?$',
|
|
re.M)
|
|
|
|
def sym_of(d):
|
|
m = re.search(r'\b(D_[0-9A-Fa-f]{8}|func_[0-9A-Fa-f]{8}|[A-Za-z_]\w*)\s*(?:\[|\()', d)
|
|
if m: return m.group(1)
|
|
m = re.search(r'\b(D_[0-9A-Fa-f]{8}|func_[0-9A-Fa-f]{8})\b', d)
|
|
return m.group(1) if m else None
|
|
|
|
# The project's scalar typedefs. `short` and `s16` are THE SAME TYPE, so two drafts spelling one
|
|
# symbol both ways do not conflict -- but a textual comparison calls them different and drops a
|
|
# good draft (R39 over-refusal; wave O hit it with `extern short D_800B9A02`). Signedness is NOT
|
|
# normalized away: u16 vs s16 is a genuine conflict and must stay one.
|
|
_ALIASES = {
|
|
'char': 's8', 'signed char': 's8', 'unsigned char': 'u8',
|
|
'short': 's16', 'signed short': 's16', 'short int': 's16',
|
|
'unsigned short': 'u16', 'unsigned short int': 'u16',
|
|
'int': 's32', 'signed int': 's32', 'long': 's32', 'long int': 's32', 'signed long': 's32',
|
|
'unsigned': 'u32', 'unsigned int': 'u32', 'unsigned long': 'u32', 'float': 'f32',
|
|
}
|
|
|
|
|
|
def _alias(t):
|
|
"""Normalize a type string's spelling, preserving qualifiers, pointers and signedness."""
|
|
t = ' '.join(t.split())
|
|
t = re.sub(r'^\s*extern\b', '', t).strip()
|
|
quals = []
|
|
for q in ('const', 'volatile'):
|
|
if re.match(r'\b%s\b' % q, t) or (' %s ' % q) in (' ' + t + ' '):
|
|
quals.append(q)
|
|
t = re.sub(r'\b%s\b' % q, '', t).strip()
|
|
stars = ''
|
|
while t.endswith('*'):
|
|
stars = '*' + stars
|
|
t = t[:-1].strip()
|
|
t = ' '.join(t.split())
|
|
t = _ALIASES.get(t, t)
|
|
return ' '.join(quals + ([t + stars] if stars else [t])).strip()
|
|
|
|
|
|
def typesig(d):
|
|
"""Type signature only. Parameter NAMES do not affect C compatibility (R39: comparing them
|
|
dropped 2 good drafts before I fixed it) -- but the DECLARATOR SUFFIX absolutely does.
|
|
|
|
`u8 D_x` and `u8 D_x[]` are INCOMPATIBLE; an earlier version of this function split on the
|
|
symbol and kept only the prefix, so both reduced to ('u8', None) and a real conflict slipped
|
|
through into the build (wave K, D_80078D98: one draft scalar, two array -> compile error
|
|
AFTER the batch had reported BYTE-IDENTICAL). Too-coarse and too-strict are both defects."""
|
|
d = ' '.join(d.split()); sym = sym_of(d) or ''
|
|
m = re.search(r'\((.*)\)\s*$', d)
|
|
ret = _alias(d.split(sym)[0].strip() if sym and sym in d else d)
|
|
if not m:
|
|
# data decl: keep the declarator suffix ('' vs '[]' vs '[N]' -> normalized to '[]')
|
|
tail = d.split(sym, 1)[1].strip() if sym and sym in d else ''
|
|
tail = '[]' if tail.startswith('[') else tail
|
|
return (ret, tail)
|
|
params = tuple(_alias(' '.join(t for t in re.findall(r'[A-Za-z_]\w*|\*', p) if t in TYPES or t == '*'))
|
|
for p in m.group(1).split(','))
|
|
return (ret, params)
|
|
|
|
def run(cmd, **kw):
|
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
|
|
|
|
def sha():
|
|
r = run("sha1sum build/us/SLUS_007.26")
|
|
return r.stdout.split()[0] if r.returncode == 0 and r.stdout else None
|
|
|
|
def resolve_conflicts(slate):
|
|
"""Drop drafts whose externs contradict (a) the destination TU's OWN existing declarations,
|
|
or (b) an earlier draft landing in the SAME file.
|
|
|
|
Two defects fixed here after the S52 recovery pass (both cost verified-correct drafts):
|
|
|
|
(a) THE TABLE STARTED EMPTY. Only draft-vs-draft was compared, so a draft contradicting a
|
|
declaration ALREADY IN the .c sailed through to the rebuild and only surfaced as a
|
|
compile error + bisect. Real case: src/800.c carries `extern void func_8001C9D0(void);`
|
|
(from banked func_8001C2C4) while three wave-J drafts declared it (s32) / (void *).
|
|
The TU is the arbiter (wave law 2) -- so the TU seeds the table.
|
|
(b) ONE NAMESPACE FOR ALL FILES. `seen` was global across the slate, so two drafts landing in
|
|
DIFFERENT .c files could not legally disagree about a symbol -- but they can; separate
|
|
TUs are separate namespaces. Now keyed per destination file (R39: a refusal check that
|
|
discards good work is worse than one that lets a failure through).
|
|
|
|
Recovery for a real (a)-class drop is the call-site cast: adopt the TU's declaration verbatim
|
|
and cast at the use site -- including through a function pointer when the TU's prototype takes
|
|
no argument and your call passes one: ((void (*)(s32))func_8001C9D0)(a0) (byte-identical;
|
|
verified on all 3 of the above)."""
|
|
stubs = {st.symbol: st for st in corpus.stubs('main').values()}
|
|
kept, dropped = [], []
|
|
seen_by_file, from_tu = {}, {}
|
|
|
|
def table(path):
|
|
if path not in seen_by_file:
|
|
t = {}
|
|
try:
|
|
for d in DECL.findall(open(path).read()):
|
|
s = sym_of(d)
|
|
if s: t[s] = typesig(d)
|
|
except OSError:
|
|
pass
|
|
seen_by_file[path] = t
|
|
from_tu[path] = set(t) # so the report can say WHO it clashed with
|
|
return seen_by_file[path]
|
|
|
|
for e in slate:
|
|
st = stubs.get(e['fn'])
|
|
path = st.path if st else '<unknown>'
|
|
seen = table(path)
|
|
body = open(e['draft']).read()
|
|
ds = [(sym_of(d), typesig(d)) for d in DECL.findall(body)]
|
|
ds = [(s, t) for s, t in ds if s]
|
|
# A DRAFT'S OWN DEFINITION IS A DECLARATION TOO (§20 / wave law 3, the DEF-side wall).
|
|
# Only `extern` lines were being compared, so a draft defining `s32 func_X(...)` against a
|
|
# TU (or sibling draft) prototyping it `void func_X(...)` sailed past the checker and blew
|
|
# up mid-build -- one wasted clean rebuild per occurrence, three of them in wave P alone.
|
|
dm = re.search(r'^\s*([A-Za-z_][\w \t\*]*?)\s*\b%s\s*\(([^;{]*)\)\s*\{' % re.escape(e['fn']),
|
|
body, re.M)
|
|
if dm:
|
|
ds.append((e['fn'], typesig('%s %s(%s)' % (dm.group(1).strip(), e['fn'], dm.group(2)))))
|
|
clash = [(s, seen[s], t) for s, t in ds if s in seen and seen[s] != t]
|
|
if clash:
|
|
sym = clash[0][0]
|
|
dropped.append({'fn': e['fn'], 'symbol': sym, 'file': path,
|
|
'against': 'the TU itself' if sym in from_tu.get(path, ()) else 'an earlier draft',
|
|
'kept': str(clash[0][1]), 'this': str(clash[0][2])})
|
|
continue
|
|
for s, t in ds: seen[s] = t
|
|
kept.append(e)
|
|
return kept, dropped
|
|
|
|
# A TRAILING COMMENT MUST NOT DEFEAT THESE. Both patterns used to demand `;[ \t]*\n`, so
|
|
# `typedef struct { s16 vx, vy, vz, pad; } SVEC2; /* 0x08 */` matched NEITHER the destination
|
|
# file's copy nor the draft's -- the TU's definition went unseen, the draft's duplicate was never
|
|
# stripped, and the build died on a C89 duplicate typedef. Agents comment their struct sizes as a
|
|
# matter of habit, so this was hitting the commonest possible spelling. (S52, cost 1 rebuild.)
|
|
_EOL = r'[ \t]*(?://[^\n]*|/\*(?:[^*]|\*(?!/))*\*/[ \t]*)?\n'
|
|
TYPEDEF_BLOCK = re.compile(
|
|
r'^[ \t]*typedef\s+(?:struct|union|enum)?[^;{]*\{[^{}]*\}\s*(\w+)\s*;' + _EOL, re.M)
|
|
TYPEDEF_PLAIN = re.compile(r'^[ \t]*typedef\s+[\w\s\*]+?\s(\w+)\s*;' + _EOL, re.M)
|
|
|
|
def strip_dup_typedefs(body, already, suffix=''):
|
|
"""Make a draft's typedef names unique against the destination TU and the rest of the batch.
|
|
|
|
Each draft is written to compile STANDALONE, so it carries its own `typedef struct {...}
|
|
SVECTOR;`. Once one such function is banked, that typedef lives in the .c forever and every
|
|
later draft defining its own collides -- a C89 duplicate-typedef error, not a byte miss.
|
|
|
|
BODY-AWARE (S52). Two wrong strategies were tried before this one, each costing a rebuild:
|
|
|
|
* STRIP every duplicate -- assumes the surviving definition sits ABOVE the insertion point.
|
|
It need not: the destination file's `Rsc24` lived BELOW where a draft was substituted, so
|
|
dropping the draft's copy left the name undefined there, gcc fell back to implicit-int,
|
|
and the file's later declaration collided ("previous declaration of D_800A4640").
|
|
* RENAME every duplicate -- wrong when several drafts share an IDENTICAL typedef, because
|
|
giving each its own name makes their `extern <T> D_x[]` declarations mutually
|
|
incompatible. I shipped that one and it broke three drafts at once.
|
|
|
|
So decide by BODY, not by name:
|
|
* identical definition already known -> STRIP this copy and reuse the existing name;
|
|
* same name, DIFFERENT definition -> RENAME this draft's copy (private to the draft, so
|
|
it cannot change an emitted byte).
|
|
`already` maps name -> normalized definition text. Returns (body, names_now_defined)."""
|
|
# SINGLE PASS, NO RESCAN. An earlier version re-scanned after each edit; the rescan then found
|
|
# the definition it had just RENAMED, saw the new name already in `defined` with identical
|
|
# text, and STRIPPED it -- leaving references to a type that no longer existed
|
|
# ("parse error before `*'"). Decide every typedef once, against a snapshot, then apply.
|
|
defined, spans, renames = {}, [], {}
|
|
hits = []
|
|
for pat in (TYPEDEF_BLOCK, TYPEDEF_PLAIN):
|
|
hits.extend(pat.finditer(body))
|
|
for m in sorted(hits, key=lambda x: x.start()):
|
|
name, text = m.group(1), ' '.join(m.group(0).split())
|
|
known = already.get(name, defined.get(name))
|
|
if known is None:
|
|
defined[name] = text
|
|
elif known == text:
|
|
spans.append((m.start(), m.end())) # exact duplicate, reuse the visible one
|
|
else:
|
|
new = '%s_%s' % (name, suffix) # same name, different shape
|
|
renames[name] = new
|
|
defined[new] = text.replace(name, new)
|
|
for s, e in sorted(spans, reverse=True):
|
|
body = body[:s] + body[e:]
|
|
for old, new in renames.items():
|
|
body = re.sub(r'\b%s\b' % re.escape(old), new, body)
|
|
return body, defined
|
|
|
|
def substitute(entries, write=True):
|
|
"""Replace each INCLUDE_ASM stub line with its draft body.
|
|
|
|
write=False produces the substituted text WITHOUT touching the tree, which is what
|
|
tools/pregate_check.py needs: every batch failure this project has hit is a textual property
|
|
of the file that will be compiled, so it can be checked in ~2s instead of a 5-minute rebuild.
|
|
Returns (count, {path: text})."""
|
|
stubs = {st.symbol: st for st in corpus.stubs('main').values()}
|
|
byfile = collections.defaultdict(list)
|
|
for e in entries:
|
|
st = stubs.get(e['fn'])
|
|
if st: byfile[st.path].append((st.addr, e['fn'], st.asm_dir, e['draft']))
|
|
n, texts = 0, {}
|
|
for path, items in byfile.items():
|
|
t = open(path).read()
|
|
# What the destination file already defines: name -> normalized definition text, so a
|
|
# draft carrying an IDENTICAL typedef can reuse it (strip) while a draft carrying a
|
|
# DIFFERENT shape under the same name gets renamed instead of silently colliding.
|
|
# ...and WHERE it defines them. A typedef may only be reused by a draft substituted BELOW
|
|
# it; the file's own copy is frequently further down the .c than the stub being replaced
|
|
# (src/800.c defines `Rec14`/`Rsc24` hundreds of lines after the INCLUDE_ASM lines that
|
|
# now want them). Reusing one from below yields `parse error before '*'` at the draft.
|
|
# So the visible set is recomputed per draft against its own insertion offset.
|
|
def defs_above(text, at):
|
|
"""Typedefs defined strictly ABOVE offset `at` in the CURRENT text.
|
|
|
|
Recomputed per draft on purpose: `t` grows with every substitution, so offsets
|
|
captured once go stale and understate where a definition really sits -- which would
|
|
mark a below-the-draft typedef as reusable, the exact bug this guards against. It also
|
|
naturally picks up typedefs contributed by drafts already substituted above."""
|
|
out = {}
|
|
for p in (TYPEDEF_BLOCK, TYPEDEF_PLAIN):
|
|
for mm in p.finditer(text):
|
|
if mm.start() < at:
|
|
out.setdefault(mm.group(1), ' '.join(mm.group(0).split()))
|
|
return out
|
|
# PROCESS IN FILE ORDER (= address order), not slate order. strip_dup_typedefs keeps the
|
|
# FIRST definition it sees and drops later duplicates, so if the drafts are walked in
|
|
# slate order the surviving typedef can end up BELOW a draft that uses it -> "syntax error
|
|
# before D_800A651C" at the earlier draft's line. Each stub is substituted at its own
|
|
# position in the .c, so the walk must follow those positions. (S52, cost 2 rebuilds.)
|
|
for _addr, fn, asmdir, draft in sorted(items):
|
|
body = "\n".join(l for l in open(draft).read().splitlines()
|
|
if not l.strip().startswith('#include'))
|
|
old = f'INCLUDE_ASM("{asmdir}", {fn});'
|
|
if old not in t:
|
|
continue
|
|
at = t.index(old)
|
|
body, _newly = strip_dup_typedefs(body, defs_above(t, at), suffix=fn.split('_')[-1])
|
|
t = t[:at] + body + t[at + len(old):]
|
|
n += 1
|
|
texts[path] = t
|
|
if write:
|
|
open(path, 'w').write(t)
|
|
return n, texts
|
|
|
|
def clean_build():
|
|
"""The ONLY trustworthy main verification: extract (rewrites the .ld) then build.
|
|
|
|
DELETE THE OUTPUT FIRST, AND CHECK THE RETURN CODE. sha() reads build/us/SLUS_007.26 off
|
|
disk; if `make build` FAILS (e.g. a compile error) the PREVIOUS successful binary is still
|
|
sitting there, so sha() returns the GOOD hash and this tool reports BYTE-IDENTICAL for a
|
|
build that never ran -- a FALSE PASS. That is exactly how it once claimed "43 banked" on a
|
|
batch whose TU did not compile; the clean-fleet R22 caught it afterwards. A verifier that
|
|
can pass without building is worse than no verifier."""
|
|
run("rm -f build/us/SLUS_007.26")
|
|
run("make extract BINARY=main")
|
|
r = run("make build BINARY=main")
|
|
if r.returncode != 0:
|
|
return None, r # build failed -> no hash, and never a pass
|
|
return sha(), r
|
|
|
|
def try_batch(entries):
|
|
run("git checkout -- src/")
|
|
run("make extract BINARY=main") # regenerate .s for the reverted stubs (hazard 2)
|
|
substitute(entries)
|
|
got, r = clean_build()
|
|
return got == GOOD, got, r
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('slate'); ap.add_argument('--apply', action='store_true')
|
|
ap.add_argument('--no-bisect', action='store_true')
|
|
a = ap.parse_args()
|
|
|
|
slate = json.load(open(a.slate))
|
|
kept, dropped = resolve_conflicts(slate)
|
|
print(f"slate {len(slate)} -> {len(kept)} compatible, {len(dropped)} dropped for in-TU decl conflict")
|
|
for d in dropped:
|
|
print(f" DROP {d['fn']}: {d['symbol']} clashes with {d.get('against','?')} "
|
|
f"in {d.get('file','?')} kept={d['kept']} this={d['this']}")
|
|
if dropped:
|
|
print(" (dropped drafts are usually CORRECT -- recover with a cast-at-use: adopt the")
|
|
print(" other declaration verbatim and adapt at the use site, e.g. (&D_x)[i].)")
|
|
if not a.apply:
|
|
print("\nDRY RUN. Re-run with --apply to substitute and clean-rebuild.")
|
|
return
|
|
|
|
ok, got, r = try_batch(kept)
|
|
if ok:
|
|
print(f"\nBANKED {len(kept)} main functions -- {got} BYTE-IDENTICAL")
|
|
json.dump([e['fn'] for e in kept], open('.run/gate_main_banked.json', 'w'))
|
|
return
|
|
# A COMPILE error names its own culprit -- read it instead of bisecting. Bisection here costs
|
|
# a FULL CLEAN REBUILD per step (~2-4 min), so it is worst-case hours; the compiler already
|
|
# told us the symbol and line. (Measured the hard way: a 41-draft bisect ran 28+ min with no
|
|
# output.) Only a byte MISMATCH with a clean compile genuinely needs bisection.
|
|
err = (r.stderr or '') + (r.stdout or '')
|
|
m = re.search(r'^(.*?):(\d+): previous declaration of `([^\']+)\'', err, re.M)
|
|
if m:
|
|
print(f"\nCOMPILE conflict on `{m.group(3)}' at {m.group(1)}:{m.group(2)} —"
|
|
f" NOT bisecting; drop or reconcile the drafts declaring it and re-run.")
|
|
offenders = [e['fn'] for e in kept
|
|
if re.search(rf"\b{re.escape(m.group(3))}\b", open(e['draft']).read())]
|
|
print(" drafts declaring it:", offenders)
|
|
run("git checkout -- src/")
|
|
return
|
|
# A BUILD failure (sha None) is not a byte mismatch, and bisecting it costs a full clean
|
|
# rebuild per step to rediscover what the compiler/linker already printed. The named-culprit
|
|
# path above only recognizes ONE error shape ("previous declaration of"); everything else --
|
|
# undefined reference, redefinition, conflicting types, parse error -- used to fall straight
|
|
# through to a silent bisect. So: always SHOW the error, and try to name the offending drafts
|
|
# for the common shapes first. (S52: a 46-draft bisect started on an error the log never
|
|
# printed. R32/R35 -- an instrument must report what it saw, not just that it failed.)
|
|
if got is None:
|
|
# SELECT the error lines; do NOT tail the stream. `err` is stderr+stdout concatenated, so
|
|
# a blind tail shows only make's trailing "CC ..." progress chatter and hides the actual
|
|
# message (S52: the first version of this printer did exactly that and reported nothing
|
|
# useful). Grep both streams for the shapes that mean failure.
|
|
pat = re.compile(r'error|Error|undefined|conflict|redefinition|parse error|No rule|\*\*\*'
|
|
r'|previous declaration|warning: .*implicit', re.I)
|
|
hits = [l for l in (err.splitlines()) if pat.search(l)]
|
|
print("\nBUILD FAILED (no binary produced). Error lines from the build:")
|
|
for l in (hits[:40] or ["(no line matched the error patterns — showing stderr tail)"]):
|
|
print(" ", l)
|
|
if not hits:
|
|
for l in [x for x in (r.stderr or '').splitlines() if x.strip()][-25:]:
|
|
print(" ", l)
|
|
for pat, label in ((r"undefined reference to `([^']+)'", "undefined reference"),
|
|
(r"redefinition of `([^']+)'", "redefinition"),
|
|
(r"conflicting types for `([^']+)'", "conflicting types")):
|
|
syms = set(re.findall(pat, err))
|
|
for s in syms:
|
|
owners = [e['fn'] for e in kept
|
|
if re.search(rf"\b{re.escape(s)}\b", open(e['draft']).read())]
|
|
print(f" {label} `{s}' -> drafts referencing it: {owners or '(none in slate)'}")
|
|
print(f"\nbatch FAILED (sha {got}); {'not bisecting' if a.no_bisect else 'bisecting'}")
|
|
if a.no_bisect:
|
|
run("git checkout -- src/"); return
|
|
good = []
|
|
lo = kept
|
|
while lo:
|
|
half = max(1, len(lo)//2)
|
|
head, lo = lo[:half], lo[half:]
|
|
ok, got, _ = try_batch(good + head)
|
|
if ok: good += head
|
|
elif len(head) == 1:
|
|
print(f" reject {head[0]['fn']}")
|
|
else:
|
|
lo = head + lo # split further
|
|
ok, got, _ = try_batch(good)
|
|
print(f"\nBANKED {len(good)} of {len(kept)} after bisection -- {got}"
|
|
f"{' BYTE-IDENTICAL' if ok else ' *** STILL MISMATCHED ***'}")
|
|
json.dump([e['fn'] for e in good], open('.run/gate_main_banked.json', 'w'))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|