mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-29 23:24:32 -04:00
cdd535e45b
The S74 checkpoint's "one unfixed defect that is actively costing banks"
(reconcile_tu manufacturing declaration conflicts), run to ground — plus the
harness gap that produced a false carve-corruption verdict.
reconcile_tu.py — three defects, measured against the real gcc-2.7.2 front end
(cdecl._cc1_accepts, the oracle cdecl.compatible was validated with; R33):
* The premise "a decl BELOW still conflicts" is TRUE at file scope and FALSE
at block scope. cc1 ACCEPTS a block-scope extern against a TU decl below it
(pedwarn "type mismatch with previous external decl"); conforming it is
destructive, because the TU's decl names the TU's TYPE and a type declared
below the splice point is not in scope AT it -- the emitted result gets
"syntax error before 'D_x'". Byte-witnessed on resident:func_800D06E8 (344
ins), whose block-scoped `extern Blk80078E78` became `extern
Struct80078E78`, typedef 388 lines lower. That construct is what this
ladder's OWN scope_demote_drafts (§8d) rung emits on purpose, and three
already-banked functions in that TU use it: one rung undoing another.
* The cast pass rewrote COMMENT PROSE -- 8 rewrites inside one header comment,
including inside a quoted cc1 diagnostic. Now matches on cdecl._mask
(length-preserving, so a mask offset is a source offset) and splices into
the original.
* `&sym` emitted `&` applied to a cast: legal for the scalar arm, `invalid
lvalue in unary '&'` (measured) for the array/fnptr/fnptr_array arms. `&`
now selects a pointer form and consumes itself -- but ONLY with no trailing
subscript, because `&sym[i]` is the address of ELEMENT i and the old code
had that case right. That last clause exists because the R39 negative
control caught the fold as a regression in the first cut of this fix.
gate_stage.py — `--skip-stages` / `GATE_SKIP_STAGES` (loud when used). Stage 0
gates raw drafts first, so a broken rung can only cost a RECOVERY, which is
exactly what makes it invisible: the function it destroys was already failing,
so its DIFF reads as a fact about the function.
verify_worktree.py / jr_isolate_all.py / parallel_gate.py — provision() now
symlinks every .run/sig.*.jsonl (main clone 259, provisioned worktree 0), the
third member of the class holding extracted/ and .run/obj40. parallel_gate was
fixed for this identical bug in S69: two provisioners, no shared list, found
twice; they now cross-reference each other. jr_isolate_all no longer swallows
the resulting FileNotFoundError into `except: continue` -- that turned a missing
index into a confident carve-CORRUPTION verdict over 2,603 of 2,603 functions
(R54). Adds _assert_scan_covered: attempted == raised means the scan measured
nothing, so its zero is an artifact, not a finding (R32).
Verification:
* 4 cc1 probes (the table above), each run on the pinned front end.
* R39 negative control over the stored-draft corpus: 661 adjudicated, 652
IDENTICAL, 9 CHANGED and every one an intended class. 4,173 of 4,864 drafts
unadjudicable (filenames that are not func_<ADDR>) -- stated, not hidden.
* jr_isolate_all ov_SC03_105 --dry-run: unchanged in the main tree.
* make clean/extract/build BINARY=resident -> 8e17e02f... BYTE-IDENTICAL.
Docs ship with the change (R21): cookbook §442/§443, index regenerated (1,112
sections), 3 docs/SETUP.md rows, CURRENT_PHASE S75 log.
390 lines
22 KiB
Python
390 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""reconcile_tu.py — conform a draft's DATA declarations to what the TARGET TU can actually SEE.
|
|
|
|
THE SUCCESSOR TO reconcile_decls.py, and the reason is structural, not a typo.
|
|
|
|
reconcile_decls asks "what does the FLEET call this symbol?"
|
|
C asks "what does THIS TRANSLATION UNIT declare?"
|
|
|
|
Those are different questions, and only the second one has an answer. This engine is loosely typed:
|
|
the same address is legitimately declared with incompatible types in different overlays, so a single
|
|
fleet-wide answer is **wrong for some TU by construction** — and it is worse than a silent skip,
|
|
because it hands back an ACTIVELY WRONG declaration that then collides with the very TU it was
|
|
supposed to conform to. Measured on ov_SC01_077's 12 TUs (Phase 26-A):
|
|
|
|
the fleet oracle AGREES with the TU's own declaration ...... 2883
|
|
the fleet oracle CONFLICTS with it (cc1 REJECTS the result) . 548 <- 16%
|
|
the TU declares it and the fleet oracle has NO answer ....... 357
|
|
|
|
and it is live: it rewrote 60 of 196 drafts in the current batch.
|
|
|
|
WHAT THIS DOES INSTEAD
|
|
======================
|
|
* Ask cpp what the TU actually declares (`cdecl.tu_scope` — so macro-injected `DEFINE_func_*`
|
|
externs are visible; a raw text scan cannot see them, §8c / cookbook §51g LAW 7).
|
|
* Ask cc1 whether the draft's declaration can coexist with it (`cdecl.compatible`, validated
|
|
against the real gcc-2.7.2 front end on 1,485 live pairs — NOT against the C standard and NOT
|
|
against modern gcc, which give different answers; §51g LAW 9).
|
|
* NO visible declaration -> no conflict is possible -> LEAVE THE DRAFT ALONE. Its extern types are
|
|
load-bearing (%lo-folding, access width, alignment all key off the declared type), and
|
|
scope_data_externs (§8d) will demote it to block scope so it establishes no global that the TU's
|
|
own later block-scope externs would have to agree with.
|
|
* A visible declaration that is BELOW the splice point while the draft's is at BLOCK SCOPE ->
|
|
cc1 ACCEPTS that (pedwarn "type mismatch with previous external decl"), so there is nothing to
|
|
fix and everything to lose: the TU's declaration names the TU's TYPE, and a type declared below
|
|
the splice point is not in scope AT it, so "conforming" replaces a construct cc1 compiles with
|
|
one it cannot parse. LEAVE IT ALONE (R43). Measured on resident:func_800D06E8, P31 S75.
|
|
* A visible, COMPATIBLE declaration -> nothing to do.
|
|
* A visible, CONFLICTING declaration -> **the TU wins** (it is the environment; we are the guest).
|
|
Rewrite the draft's decl to the TU's, and CAST AT EVERY USE so the access the draft intended is
|
|
preserved exactly. gcc-2.7.2 folds a compile-time cast of a known symbol, so the emitted bytes
|
|
are unchanged — and the whole-binary byte-gate (G3/P9) remains the sole arbiter either way.
|
|
|
|
WHICH TU? DERIVED, NEVER HAND-PASSED (§51g LAW 10)
|
|
==================================================
|
|
`--src-file` was an OPTIONAL flag defaulting to `src/<ov>/<ov>.c`. ov_SC01_077 has **263 open stubs
|
|
across 12 TUs and only 13 of them are in the main .c**, so 95% of drafts were being reconciled
|
|
against a translation unit that would never compile them. The INCLUDE_ASM line is self-describing;
|
|
`corpus.stubs()` reads it. Ask, don't assume.
|
|
|
|
Usage:
|
|
tools/reconcile_tu.py --overlay ov_SC01_077 --in <drafts> --out <dir> [--src-file <tu.c>]
|
|
from reconcile_tu import fix; body, notes = fix(body, tu_path, fn)
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import importlib.util
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def _load(mod, rel):
|
|
spec = importlib.util.spec_from_file_location(mod, os.path.join(REPO, rel))
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
cdecl = _load('cdecl', 'tools/cdecl.py')
|
|
corpus = _load('corpus', 'tools/corpus.py')
|
|
|
|
|
|
def tu_for(overlay, fn, override=None):
|
|
"""The TU this draft is spliced into — DERIVED from the corpus (§51g LAW 10)."""
|
|
if override:
|
|
return override
|
|
try:
|
|
addr = int(fn[5:], 16) # a curated (non-func_ADDR) name has no address here
|
|
except ValueError:
|
|
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
|
|
# CorpusError PROPAGATES (R32/R35, P30 S1e) — see the identical note in cast_call_sites.tu_for.
|
|
# A swallow here silently reconciles the draft against the DEFAULT `<ov>.c` instead of the jr/-O0
|
|
# split TU that actually compiles it, which is a §51g LAW 10 violation dressed up as a gate refusal.
|
|
st = corpus.stubs(overlay).get(addr)
|
|
if st:
|
|
return os.path.join(REPO, st.path)
|
|
return os.path.join(REPO, f'src/{overlay}/{overlay}.c')
|
|
|
|
|
|
# ---- the byte-neutral access casts ---------------------------------------------------------------
|
|
def _elem(d):
|
|
"""The element/base type the DRAFT believes the symbol has."""
|
|
return (d.base + (' *' if d.kind == 'ptr' else '')).strip()
|
|
|
|
|
|
def _cast_sub(d, tu):
|
|
"""(regex, repl) reproducing the DRAFT's intended access to the symbol under the TU's storage.
|
|
gcc folds a compile-time cast of a known symbol, so the emitted access is unchanged.
|
|
|
|
NOTE the fn-ptr arms. reconcile_decls' `data_access_subs` has no fn-ptr kind, so the moment its
|
|
parser is taught to SEE `extern void (*D_x[])(void);` it would happily rewrite a call-through
|
|
`D_x[i]()` into `((u8 *)D_x)[i]()` — a dormant transform that fixing the parser would ARM. This
|
|
tool handles the kind natively, which is why it supersedes that one rather than patching it.
|
|
|
|
TWO FORMS, and the `&` prefix SELECTS between them (P31 S75). The leading `&` used to be
|
|
captured and re-emitted VERBATIM in front of the value form, which yields `&` applied to a cast
|
|
— and for three of the four arms that is not merely ugly, it is a hard error. Measured on the
|
|
real cc1:
|
|
|
|
&(*(E *)&sym) scalar/struct/ptr ACCEPT (legal; the fold is cosmetic)
|
|
&((E *)&sym) array REJECT `invalid lvalue in unary '&'`
|
|
&((E (*)(P))sym) fnptr REJECT same
|
|
&((E (**)(P))&sym) fnptr_array REJECT same
|
|
|
|
So `&sym` now emits a POINTER form built from the symbol's address directly, which is valid for
|
|
every arm, is the same link-time constant, and keeps the drafted view's pointee type.
|
|
|
|
⚠️ ONLY WHEN NO SUBSCRIPT FOLLOWS. `&sym[i]` is not `&` applied to the symbol at all — `[]`
|
|
binds tighter, so it is the address of ELEMENT i, and `&((E *)&sym)[i]` is both legal and
|
|
correct. Folding it to a pointer form silently changes what the expression MEANS. Caught by the
|
|
R39 negative control over the stored-draft corpus (`ov_SC02_005:func_8018DFC4`, whose
|
|
`(s32)&((Pair8 *)D_801E4998)[idx]` would have become `(s32)((Pair8 (*)[])D_801E4998)[idx]`) —
|
|
the trailing-`[` case is exactly the one the old code got RIGHT."""
|
|
sym = d.name
|
|
# `(?<![.\w])(?<!->)` — a MEMBER ACCESS is not this symbol. Without it, a struct field that
|
|
# happens to share a global's name is rewritten at every use: `p->code` became
|
|
# `p->(*(u32 *)&code)`, which is not even valid C. Latent since the tool was written and only
|
|
# reachable once block-scope descent started finding such names (Phase 29 SESSION-22).
|
|
rx = re.compile(rf'(&?)(?<![.\w])(?<!->){re.escape(sym)}\b(\s*\[)?')
|
|
c_arr = tu.kind in ('array', 'fnptr_array')
|
|
e = _elem(d)
|
|
p = ", ".join(d.params or [])
|
|
at = sym if c_arr else f'&{sym}' # the symbol's ADDRESS under TU storage
|
|
|
|
if d.kind == 'fnptr': # draft: void (*D_x)(P)
|
|
val = f'(({e} (*)({p})){sym})'
|
|
ptr = f'(({e} (**)({p})){at})'
|
|
elif d.kind == 'fnptr_array': # draft: void (*D_x[])(P)
|
|
val = f'(({e} (**)({p})){at})'
|
|
ptr = f'(({e} (***)({p})){at})'
|
|
elif d.kind == 'array': # draft: E D_x[]
|
|
val = f'(({e} *){at})'
|
|
ptr = f'(({e} (*)[]){at})'
|
|
else: # scalar / struct / pointer
|
|
val = f'(*({e} *){at})'
|
|
ptr = f'(({e} *){at})'
|
|
# group(1) = a leading `&`, group(2) = a trailing `[`. The pointer form is right ONLY for a bare
|
|
# `&sym`; with a subscript the `&` belongs to the element, so re-emit it in front of the value
|
|
# form exactly as before (see the docstring's ⚠️).
|
|
def rep(m):
|
|
if m.group(1) and not m.group(2):
|
|
return ptr # `&sym` -> pointer form, `&` consumed
|
|
return f'{m.group(1)}{val}{m.group(2) or ""}' # `sym`, `sym[i]`, `&sym[i]` -> unchanged shape
|
|
return rx, rep
|
|
|
|
|
|
def _draft_statements(body):
|
|
"""Every declaration-bearing statement in the draft, at FILE scope AND at BLOCK scope, as
|
|
(Stmt-with-ABSOLUTE-spans, is_inner).
|
|
|
|
WHY (Phase 29 SESSION-22, byte-witnessed on func_80175AB8). `split_statements` is depth-0 BY
|
|
DESIGN, so for a draft whose body is one function definition it returns exactly ONE statement —
|
|
the definition — and every declaration inside it is invisible. That is precisely where the data
|
|
externs live: §8d (`scope_data_externs`) DEMOTES them to block scope on purpose, because a
|
|
block-scope extern declares no global for the TU's own later decls to collide with.
|
|
|
|
But C still requires a block-scope `extern` to be compatible with a file-scope declaration of the
|
|
same identifier that is in scope — so the conflict is real, and this tool was structurally blind
|
|
to it: it reported `reconciled: 0 draft(s), 0 data symbol(s); coverage defects: 0` for a draft
|
|
cc1 then rejected with `conflicting types for D_8011F7BC`. A silent skip that reads exactly like
|
|
"nothing to do" (R32).
|
|
|
|
One level of descent is enough and is deliberate: the externs sit at the top of the function
|
|
body, not inside nested blocks, and recursing further would start parsing arbitrary code."""
|
|
out = []
|
|
for st in cdecl.split_statements(body):
|
|
out.append((st, False))
|
|
m = re.search(r'\{', cdecl._mask(st.text))
|
|
if not m:
|
|
continue
|
|
# DESCEND ONLY INTO A FUNCTION BODY. Descending into ANY `{` also enters struct/union/enum
|
|
# definitions, whose MEMBERS then parse as declarations and get "conformed" against the TU —
|
|
# byte-witnessed while writing this: a member `u32 code;` was rewritten to the TU's
|
|
# declaration of `code` (`typedef void (*code)(unsigned short*);`) INSIDE the struct, and the
|
|
# member ACCESS `p->code` became `p->(*(u32 *)&code)`. A function definition is distinguished
|
|
# by a parameter list before the brace and by not being a tag/typedef definition.
|
|
head = st.text[:m.start()]
|
|
if re.match(r'\s*(typedef|struct|union|enum)\b', head) or ')' not in head:
|
|
continue
|
|
off = st.start + m.end() # first byte INSIDE the body
|
|
inner_txt = body[off:st.end]
|
|
for s2 in cdecl.split_statements(inner_txt):
|
|
out.append((cdecl.Stmt(s2.text, off + s2.start, off + s2.end), True))
|
|
return out
|
|
|
|
|
|
def fix(body, tu_path, fn):
|
|
"""Conform the draft's DATA decls to the TU. Returns (new_body, notes)."""
|
|
# CONFLICT domain. A decl BELOW the splice point still conflicts — AT FILE SCOPE. At BLOCK
|
|
# scope cc1 accepts it with a pedwarn, which is why `above` is consulted again in the loop
|
|
# rather than only for argument order (P31 S75; the four-probe measurement is quoted there).
|
|
full = cdecl.tu_scope(tu_path)
|
|
above = set(cdecl.tu_scope(tu_path, above=fn)) # ORDER, for cc1's no-prototype rule
|
|
|
|
plan, notes = {}, []
|
|
for st, inner in _draft_statements(body):
|
|
try:
|
|
ds = cdecl.parse(st.text)
|
|
except cdecl.CDeclError as e:
|
|
if inner:
|
|
continue # ordinary CODE inside a function body — not a declaration, not a defect
|
|
notes.append(f'!! UNPARSED (a coverage defect, not a no-op): {st.text[:60]} -> {e}')
|
|
continue
|
|
for d in ds:
|
|
if d.is_definition or d.kind == 'func' or d.storage == 'typedef':
|
|
continue
|
|
# BLOCK SCOPE: only a real `extern` DECLARATION counts (Phase 29 SESSION-22, second cut).
|
|
# Descending into function bodies also feeds ordinary STATEMENTS to cdecl.parse, and some
|
|
# of them parse without raising into a declarator with an EMPTY base type and the
|
|
# statement's symbol as its name. That fake entry then overwrote the genuine plan row for
|
|
# the same symbol (`plan[d.name] = …`), so the span rewrite landed on a statement instead
|
|
# of the declaration — and the R32 completion check still passed, because the conformed
|
|
# text it looks for did appear somewhere. Byte-witnessed: `D_80126B5C` planned twice
|
|
# ("draft 's32'" and "draft ''"), output unchanged, gate PLUMBING.
|
|
if inner and not (st.text.lstrip().startswith('extern') and d.base.strip()):
|
|
continue
|
|
tu = full.get(d.name)
|
|
if tu is None:
|
|
continue # not declared here -> no conflict is possible
|
|
# BLOCK SCOPE + TU DECLARATION BELOW -> cc1 ACCEPTS. Do not touch it. (P31 S75)
|
|
#
|
|
# This module's own premise — "a decl BELOW still conflicts" (see `full` above) — is
|
|
# TRUE at file scope and FALSE at block scope, and the difference is not cosmetic. The
|
|
# four-probe measurement against the real cc1:
|
|
#
|
|
# block-scope extern, TU decl BELOW -> ACCEPT (warning: "type mismatch with
|
|
# previous external decl" — a pedwarn, not an error)
|
|
# block-scope extern, TU decl ABOVE -> REJECT `conflicting types`
|
|
# file-scope extern, TU decl BELOW -> REJECT `conflicting types` <- the real job
|
|
# the conformed output this rung emitted -> REJECT `syntax error before 'D_x'`
|
|
#
|
|
# That last line is why this is a refusal and not an optimisation. The TU's declaration
|
|
# names the TU's TYPE, and a type declared BELOW the splice point is not in scope AT it:
|
|
# conforming to it substitutes a construct cc1 accepts for one it cannot even parse.
|
|
# Byte-witnessed on resident:func_800D06E8 (344 ins, match_one closeness 0) — a
|
|
# deliberate block-scoped `extern Blk80078E78 D_80078E78;` rewritten to
|
|
# `extern Struct80078E78 D_80078E78;` whose typedef is declared 388 lines LOWER.
|
|
#
|
|
# The block-scope extern is not an accident to be cleaned up, either: it is what THIS
|
|
# LADDER'S OWN `scope_demote_drafts` (§8d) rung produces on purpose, and three already-
|
|
# banked functions in that TU use it. R43 — refuse the class, name it, leave the draft.
|
|
if inner and d.name not in above:
|
|
notes.append(f'-- {d.name}: block-scope extern vs a TU decl BELOW the splice point '
|
|
f'-> cc1 accepts (pedwarn only); LEFT ALONE (conforming it would name '
|
|
f'a type that is not yet in scope)')
|
|
continue
|
|
a, b = (tu, d) if d.name in above else (d, tu)
|
|
if cdecl.compatible(a, b):
|
|
continue # cc1 accepts both -> leave the draft's types
|
|
plan[d.name] = (tu, d, st)
|
|
notes.append(f'{d.name}: draft {d.type!r} vs TU {tu.type!r} -> TU wins + cast at use')
|
|
|
|
if not plan:
|
|
return body, notes
|
|
|
|
# Locate each planned declaration by its SOURCE SPAN — never by re-finding its text.
|
|
# `split_statements` preserves spans precisely because drafts get rewritten, and the text it
|
|
# hands back is comment-STRIPPED: `extern u8 D_80078E78;` for a line that actually reads
|
|
# `extern u8 D_80078E78; /* cur base ($s5) */`. Matching statement-text against line-text
|
|
# therefore misses EVERY commented declaration — and it misses it in the worst possible way,
|
|
# SILENTLY: the uses are still cast to the draft's intended view while the declaration stays
|
|
# unconformed, so cc1 reports `conflicting types` at the very decl the tool believed it had
|
|
# just fixed, and the tool reports success. (Measured on func_80176218, Phase 29 SESSION-22.)
|
|
#
|
|
# 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 statement 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_span = {}
|
|
for _name, (_tu, _d, st) in plan.items():
|
|
by_span.setdefault((st.start, st.end), set()).add(_name)
|
|
|
|
pieces, pos = [], 0
|
|
seen = emitted = 0 # declarators IN vs OUT — asserted below (R32)
|
|
for (a, b) in sorted(by_span):
|
|
planned, stmt = by_span[(a, b)], body[a:b]
|
|
ls = body.rfind('\n', 0, a) + 1 # this statement's own indentation
|
|
indent = body[ls:a] if not body[ls:a].strip() else ''
|
|
try:
|
|
all_ds = [d for d in cdecl.parse(stmt)
|
|
if not (d.is_definition or d.kind == 'func' or d.storage == 'typedef')]
|
|
except cdecl.CDeclError:
|
|
all_ds = []
|
|
if all_ds:
|
|
decls = [(plan[d.name][0] if d.name in planned else d).declaration() for d in all_ds]
|
|
seen += len(all_ds)
|
|
else: # unparseable — emit what we planned, and SAY so
|
|
notes.append(f'!! could not re-parse a planned decl statement; emitting only the '
|
|
f'{len(planned)} conformed symbol(s) — siblings may be lost: {stmt[:70]}')
|
|
decls = [plan[nm][0].declaration() for nm in sorted(planned)]
|
|
seen += len(planned)
|
|
emitted += len(decls)
|
|
if len(decls) > 1:
|
|
notes.append(f'-- multi-declarator statement preserved: {len(decls)} symbol(s), '
|
|
f'{len(planned)} conformed')
|
|
pieces.append(body[pos:a])
|
|
pieces.append(('\n' + indent).join(decls))
|
|
pos = b
|
|
pieces.append(body[pos:])
|
|
conformed = ''.join(pieces)
|
|
|
|
# R32 — ASSERT THE CONFORM LANDED. The bug this replaced was not a crash but a reported success,
|
|
# so the counter that would have caught it has to be COMPARED, not merely incremented.
|
|
if emitted < seen:
|
|
notes.append(f'!! dropped {seen - emitted} declarator(s) while conforming')
|
|
for nm, (tu, _d, _st) in plan.items():
|
|
if tu.declaration() not in conformed:
|
|
notes.append(f'!! {nm}: planned conform to {tu.type!r} did NOT land in the output '
|
|
f'(its uses would be cast against a declaration that was never rewritten)')
|
|
|
|
# Cast every USE to the draft's intended view — but ONLY in CODE.
|
|
#
|
|
# This pass used to run `rx.sub` over every non-declaration LINE, which is every line of the
|
|
# draft's doc comment too. Byte-witnessed on resident:func_800D06E8 (P31 S75): a 40-line header
|
|
# comment explaining the block-scope idiom had `D_80078E78` rewritten to
|
|
# `(*(Blk80078E78 *)&D_80078E78)` in its PROSE, eight times, including inside a quoted cc1
|
|
# diagnostic. Harmless to the bytes and corrosive to the only artifact that explains WHY the
|
|
# function is written the way it is — and it made a mechanical rewrite look like an authored one.
|
|
#
|
|
# `cdecl._mask` blanks comments and string/char literals LENGTH-PRESERVINGLY, so an offset into
|
|
# the mask is an offset into the original: match on the mask, splice into the source (R33 — the
|
|
# masker already exists and is the same one `split_statements` trusts for brace counting).
|
|
masked = cdecl._mask(conformed)
|
|
edits = []
|
|
for _s, (tu, d, _st) in plan.items():
|
|
rx, rep = _cast_sub(d, tu)
|
|
for m in rx.finditer(masked):
|
|
ls = conformed.rfind('\n', 0, m.start()) + 1
|
|
le = conformed.find('\n', m.start())
|
|
if re.match(r'\s*(extern|typedef)\b', conformed[ls:le if le >= 0 else len(conformed)]):
|
|
continue # never cast inside a declaration line
|
|
edits.append((m.start(), m.end(), rep(m)))
|
|
pieces, pos = [], 0
|
|
for a, b, text in sorted(edits):
|
|
if a < pos:
|
|
notes.append(f'!! overlapping cast rewrites at {a}; the later one was skipped')
|
|
continue
|
|
pieces.append(conformed[pos:a]); pieces.append(text); pos = b
|
|
pieces.append(conformed[pos:])
|
|
return ''.join(pieces), notes
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--overlay', default='ov_SC01_077')
|
|
ap.add_argument('--in', dest='indir', required=True)
|
|
ap.add_argument('--out', dest='outdir', required=True)
|
|
ap.add_argument('--src-file', dest='src_file', default=None,
|
|
help='override the derived TU (normally unnecessary — it is derived per draft)')
|
|
ap.add_argument('--strict', action='store_true', help='exit non-zero on a coverage defect')
|
|
a = ap.parse_args()
|
|
|
|
override = a.src_file and os.path.join(REPO, a.src_file)
|
|
os.makedirs(os.path.join(REPO, a.outdir), exist_ok=True)
|
|
drafts = touched = syms = defects = 0
|
|
for p in sorted(glob.glob(os.path.join(REPO, a.indir, '*.c'))):
|
|
fn = os.path.basename(p)[:-2]
|
|
new, notes = fix(open(p).read(), tu_for(a.overlay, fn, override), fn)
|
|
open(os.path.join(REPO, a.outdir, os.path.basename(p)), 'w').write(new)
|
|
drafts += 1
|
|
n = sum(1 for x in notes if not x.startswith(('!!', '--'))) # '--' = informational, not a symbol
|
|
defects += sum(1 for x in notes if x.startswith('!!'))
|
|
if n:
|
|
touched += 1
|
|
syms += n
|
|
print(f'drafts: {drafts}; reconciled: {touched} draft(s), {syms} data symbol(s); '
|
|
f'coverage defects: {defects}')
|
|
if a.strict and defects:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|