phase11: tools/sf3_rank --fragments + cookbook 114 — worker A's fragment anomaly

Worker A found 0x800C3490 is not a matchable body: it starts mid-expression with sw v0,32(sp)
before any frame setup, and its identical tail also appears at 0x800C3470, so it is a
shared/jump-target block Ghidra promoted to a function -- inside no region, with the code
before it in no worklist, so nobody can match it standalone.

Its generalised rule is narrower than 'first instruction is not prologue-like', because a
function may legally start with beq/sh/move: a row is a FRAGMENT if its first instruction
touches the stack before any addiu sp,sp,-N, reads a stack slot, or uses a callee-saved
register that is never saved.

Implemented as a --fragments scan. Measured: 5 suspects across all four partitions, but 2
false positives across the 555 REGISTERED regions, so it is ADVISORY not an exclusion --
sufficient-but-not-complete like the trapping check. A hit means read before spending a
spelling, never skip.
This commit is contained in:
Christopher Williams
2026-09-24 10:17:38 -04:00
parent e4b6423774
commit 74199d1ab3
2 changed files with 92 additions and 2 deletions
+29
View File
@@ -1779,3 +1779,32 @@ it as taken. Worker A was wrongly blocked on exactly that address.
range, not whether it appears as an endpoint.** Worker A wrote `.run/p11/w-a/free.sh <addr>` to do
this correctly. **Any worker checking a candidate address against the registry must use a range
test** — this is the third defect found in a coordinator-written rule this phase.
### 114. A FRAGMENT row can never match — worker A's anomaly scan (advisory)
Worker A found `0x800C3490` (132 B, in its partition, unclaimed) **is not a matchable body: it starts
mid-expression.** Its first instruction is `sw v0,32(sp)` — a stack write before any frame setup,
using `v0`/`v1`/`t1` set by the code *before* the row. It ends with its own
`addiu sp,sp,48; jr ra`, and the identical tail also appears at `0x800C3470`–`0x800C348C`, so it is a
**shared/jump-target block that Ghidra promoted to a function**. It is inside no region and the code
before it is in no worklist, so **nobody can match it standalone**.
**Worker A's generalised rule, and the reason it is narrower than "first instruction is not
prologue-like":** a function may legally start with `beq a0,a1,...` (comparing two incoming
registers) or `sh` (storing an incoming register to `*a0`), so most such rows are fine. The real
signal is:
> **A row is a FRAGMENT if its first instruction writes to the stack (sp-relative) before any
> `addiu sp,sp,-N`, or reads a stack slot, or uses a callee-saved register that is never saved.**
Worker A scanned its 256 rows and got 11 first-instruction hits, of which **only `0x800C3490`**
trips the real rule.
**Implemented as `./tools/sf3_rank --fragments`, which scans any partition set.** Measured across
all four partitions it finds **5 suspects**; measured against the **555 registered regions it flags
2** (`0x800923E8`, `0x80099DC4`), so it is **advisory, not an exclusion** — sufficient-but-not-complete
in exactly the way the trapping check is. **A hit means "read this before spending a spelling", never
"skip it".**
**The value is the reading phase, not the match:** a fragment costs a full structural derivation and
can never close, so flagging it saves a worker's whole reading budget.
+63 -2
View File
@@ -77,6 +77,45 @@ def redundancy(words: list[int]) -> float:
return max(scores) if scores else 0.0
CALLEE_SAVED = frozenset(range(16, 24)) # s0..s7
def fragment_reason(insns: list[int]) -> str:
"""Worker A's fragment check: does the body use a frame it never set up?
A function may legally start with `beq a0,a1,...` or `sh a0,0(a0)`. It may NOT start by
touching a slot of a frame that does not exist yet. Worker A found `0x800C3490`, whose
first instruction is `sw v0,32(sp)` -- a stack write before any frame setup, using
registers set by the code BEFORE the row -- and whose identical tail also appears at
0x800C3470. It is a shared/jump-target block Ghidra promoted to a function, so it is
inside no region and nobody can match it standalone.
ADVISORY, NOT AN EXCLUSION: measured against the 555 registered regions this flags
**2** of them (`0x800923E8`, `0x80099DC4`, both "first insn touches sp+16"), so it is
sufficient-but-not-complete in exactly the way the trapping check is. Treat a hit as
"read this before spending a spelling", never as "skip".
"""
if not insns:
return "empty"
word = insns[0]
opcode = word >> 26
rs = (word >> 21) & 0x1F
rt = (word >> 16) & 0x1F
immediate = word & 0xFFFF
if opcode in (0x23, 0x2B, 0x0F, 0x20, 0x24, 0x25, 0x28, 0x29, 0x2C, 0x2D, 0x2E, 0x3F):
if (rs == 29 or rt == 29) and immediate != 0:
return f"first instruction touches sp+{immediate}"
used = {rs, rt}
if opcode == 0:
used.add((word >> 11) & 0x1F)
hit = used & CALLEE_SAVED
if hit and opcode != 0x09 and len(insns) > 8:
saves = any((x >> 26) == 0x2B and ((x >> 21) & 0x1F) == 29 for x in insns[:12])
if not saves:
return f"uses {sorted(hit)} with no save in the first 12 instructions"
return ""
def load_rows(paths: list[Path], payload: bytes, min_size: int, max_size: int) -> list[tuple[float, int, str]]:
rows: list[tuple[float, int, str]] = []
for path in paths:
@@ -93,11 +132,13 @@ def load_rows(paths: list[Path], payload: bytes, min_size: int, max_size: int) -
def main(argv: list[str]) -> int:
paths, top, min_size, max_size = [], 40, 0, 1 << 30
paths, top, min_size, max_size, fragments = [], 40, 0, 1 << 30, False
index = 0
while index < len(argv):
arg = argv[index]
if arg == "--top":
if arg == "--fragments":
fragments = True
elif arg == "--top":
index += 1
top = int(argv[index])
elif arg == "--min-size":
@@ -120,6 +161,26 @@ def main(argv: list[str]) -> int:
return 2
payload = EXE.read_bytes()
if fragments:
print("ADVISORY ONLY -- measured at 2 false positives across 555 registered regions.")
print(f"{'size':>5} address reason")
count = 0
for path in paths:
for line in path.read_text().splitlines():
if line.startswith("#") or not line.strip():
continue
fields = line.split("\t")
start, end = int(fields[1], 16), int(fields[2], 16)
if end - start < min_size or end - start > max_size:
continue
reason = fragment_reason(body_words(payload, start, end))
if reason:
count += 1
print(f"{end - start:5d} {fields[1]} {reason}")
print(f"\nsuspects={count}")
return 0
rows = load_rows(paths, payload, min_size, max_size)
rows.sort(key=lambda row: (-row[0], row[1], row[2]))