re: the per-phase clear conditions, by dominance over the ISL CFG

Closes the backlog's "which condition guards each END_PHASE".  With the CFG from the
previous commit this is a graph query, not new machinery.

The obvious query is WRONG for this language, and I implemented it first: "one
successor reaches END_PHASE and the other does not" finds 1/62/1 guards across Stage
02's three phases, and the 1s are both the same read_freg(0) < 1200 timeout -- every
objective test missed.  The cause is the dominant idiom: a POLL LOOP's loop-back
branch also reaches the exit, one iteration later, so neither successor discriminates.
The asymmetric 1/62/1 is what exposed it; a uniform number would have read as
plausible.

Dominance has no such blind spot: a condition dominates an exit when every path from
an entry passes through it, so it is NECESSARY for the phase to end that way, and a
poll loop's test dominates its own exit by construction.  Iterative dominators
converge in 3 passes over 15670/18739 instructions (83.6%).

Result for Stage 02 -- every exit in all three phases is dominated by
unit_hp_pct(TCN001, Character_Player_Test) != 0, the player's ship being alive, which
falls out rather than being assumed.  Beyond that, phase 1's objective exit requires
hp_pct_test on ADT102, ADT107 and ADT113; phase 3's requires ADT301 and ADT302;
read_freg(0) gates at 210 / 300 and times out at 1200; random(3) and random(5)
dominate only the exits that pick one of several closing lines.

Two of the 15 exits are reachable from NO static entry, both FORCE_END_PHASE.  That
agrees with the independently measured 389 unreachable routines: they are started from
the trigger queue at phase+272, by data rather than code.

Practical note recorded: the first dominator run was OOM-killed -- 6743 nodes each
holding a Python set of up to 6743 elements.  Integer bitmasks run in seconds.

Not settled, and said so: dominance gives necessary, not sufficient, conditions; only
Stage 02's artefact is committed; one listed condition is still an unresolved
<unknown>; read_freg's units are inferred from the gate values, not read.

calls, phase-ends and conditions all regenerate byte-identical.
This commit is contained in:
Sylpheed RE agent
2026-08-27 06:12:41 +00:00
parent 5ea9e38b35
commit 4f95b98813
6 changed files with 387 additions and 3 deletions

58
tools/re-capture/guard.py Normal file
View File

@@ -0,0 +1,58 @@
import struct, collections, isl, isl_cfg
def edges(b, spawn=False):
"""off -> successors. `spawn` includes the coroutine a start_coroutine creates."""
offs = isl.linear_offsets(b)
nxt = {offs[i]: offs[i+1] for i in range(len(offs)-1)}
bases = isl.phase_bases(b)
E = collections.defaultdict(list)
loc, sp = {}, {}
for off in offs:
w = struct.unpack_from('>I', b, off)[0]
op, ln = w & 0xFF, (w >> 8) & 0xFF
sk, dk = (w >> 24) & 0xFF, (w >> 16) & 0xFF
words = [struct.unpack_from('>I', b, off+i)[0]
for i in range(4, max(ln,4), 4) if off+i+4 <= len(b)]
ph = sum(1 for x in bases if x <= off)
base = bases[ph-1] if ph else bases[0]
fall = nxt.get(off)
if op == 0 and len(words) >= 2:
v = words[1] if sk == 1 else sp.get(words[1]) if sk == 2 else None
d = sp if dk == 2 else loc
if v is None: d.pop(words[0], None)
else: d[words[0]] = v
elif op == 19 and words:
if words[0] == 11: fall = None # end_coroutine: thread dies
if spawn and words[0] == 1 and 0 in loc:
t = base + loc[0]
if t in nxt or t in offs: E[off].append(t)
loc = {}
elif op == 12 and words:
E[off].append(base + words[0]); fall = None
elif op in isl.REL and words:
E[off].append(base + words[0])
if fall: E[off].append(fall)
return E, nxt
def guards(b, spawn=False):
offs = isl.linear_offsets(b)
E, nxt = edges(b, spawn)
rev = collections.defaultdict(list)
for a, ss in E.items():
for s in ss: rev[s].append(a)
ends = {o for o, bid, _ in isl.call_sites(b) if bid in (6, 62)}
R, q = set(ends), collections.deque(ends)
while q:
n = q.popleft()
for p in rev.get(n, ()):
if p not in R: R.add(p); q.append(p)
out = []
s1, s2 = isl.symbols(b, 1), isl.symbols(b, 2)
for c in isl_cfg.conditions(b, s1, s2):
br = nxt.get(c['off'])
if br is None: continue
taken, fallth = c['target'], nxt.get(br)
t, f = taken in R, (fallth in R if fallth else False)
if t != f:
out.append({**c, 'ends_when': 'taken' if t else 'not-taken'})
return out, len(R), len(offs), len(ends)

View File

@@ -54,6 +54,97 @@ def _join(a, bst):
st = a[2] if a[2] == bst[2] else ()
return (m(a[0], bst[0]), m(a[1], bst[1]), st)
def edges(b, spawn=False):
"""off -> successors. `spawn` includes the coroutine a start_coroutine creates."""
offs = isl.linear_offsets(b)
nxt = {offs[i]: offs[i+1] for i in range(len(offs)-1)}
bases = isl.phase_bases(b)
E = collections.defaultdict(list)
loc, sp = {}, {}
for off in offs:
w = struct.unpack_from('>I', b, off)[0]
op, ln = w & 0xFF, (w >> 8) & 0xFF
sk, dk = (w >> 24) & 0xFF, (w >> 16) & 0xFF
words = [struct.unpack_from('>I', b, off+i)[0]
for i in range(4, max(ln,4), 4) if off+i+4 <= len(b)]
ph = sum(1 for x in bases if x <= off)
base = bases[ph-1] if ph else bases[0]
fall = nxt.get(off)
if op == 0 and len(words) >= 2:
v = words[1] if sk == 1 else sp.get(words[1]) if sk == 2 else None
d = sp if dk == 2 else loc
if v is None: d.pop(words[0], None)
else: d[words[0]] = v
elif op == 19 and words:
if words[0] == 11: fall = None # end_coroutine: thread dies
if spawn and words[0] == 1 and 0 in loc:
t = base + loc[0]
if t in nxt or t in offs: E[off].append(t)
loc = {}
elif op == 12 and words:
E[off].append(base + words[0]); fall = None
elif op in isl.REL and words:
E[off].append(base + words[0])
if fall: E[off].append(fall)
return E, nxt
def dominating_conditions(b):
"""For each END_PHASE / FORCE_END_PHASE site, the conditions that DOMINATE it.
A condition dominates an exit when EVERY path from an entry to that exit
passes through it -- so it is a NECESSARY condition for the phase to end.
That is what the port needs.
⚠️ The obvious query, "one branch reaches END_PHASE and the other does not",
is WRONG for this language and was tried first. The dominant shape here is a
POLL LOOP, where the loop-back branch also reaches the exit -- one iteration
later -- so neither successor discriminates. It found exactly ONE guard in
each of Stage 02's phases 1 and 3 (a `read_freg(0) < 1200` timeout) while
missing every objective test. Dominance has no such blind spot.
"""
import collections as _c
E, nxt = edges(b, spawn=True)
offs = isl.linear_offsets(b)
entries = {e for e in set(isl.phase_bases(b)) | set(coroutine_entries(b)) if e in nxt}
preds = _c.defaultdict(list)
for a, ss in E.items():
for s in ss: preds[s].append(a)
R, q = set(entries), _c.deque(entries)
while q:
n = q.popleft()
for s in E.get(n, ()):
if s not in R: R.add(s); q.append(s)
order = [o for o in offs if o in R]
idx = {o: i for i, o in enumerate(order)}
N = len(order); FULL = (1 << N) - 1
DOM = [(1 << i) if o in entries else FULL for i, o in enumerate(order)]
for _ in range(50):
changed = False
for i, o in enumerate(order):
if o in entries: continue
ps = [idx[p] for p in preds.get(o, ()) if p in idx]
if not ps: new = 1 << i
else:
acc = DOM[ps[0]]
for p in ps[1:]: acc &= DOM[p]
new = acc | (1 << i)
if new != DOM[i]: DOM[i] = new; changed = True
if not changed: break
conds = {c['off']: c for c in conditions(b, isl.symbols(b, 1), isl.symbols(b, 2))}
bases = isl.phase_bases(b)
out = []
for e, bid, _x in [(o, bid, x) for o, bid, x in isl.call_sites(b) if bid in (6, 62)]:
ph = sum(1 for x in bases if x <= e)
if e not in idx:
out.append({'end': e, 'phase': ph, 'builtin': bid, 'conds': None}); continue
m = DOM[idx[e]]
dc = [conds[order[i]] for i in range(N) if (m >> i) & 1 and order[i] in conds]
out.append({'end': e, 'phase': ph, 'builtin': bid,
'conds': sorted(dc, key=lambda c: c['off'])})
return out, len(R), len(offs)
def coroutine_entries(b):
"""Every `start_coroutine` target, found by a linear pre-pass."""
offs = isl.linear_offsets(b)

View File

@@ -9,7 +9,8 @@ arguments) and once when three built-in names were corrected. Keeping the
generator in the tree is the point of this file.
isl_report.py <StageNN.ssb> phase-ends -> every END_PHASE with its context
isl_report.py <StageNN.ssb> conditions -> every condition site, comparand resolved
isl_report.py <StageNN.ssb> conditions -> every condition site, comparand resolved
isl_report.py <StageNN.ssb> phase-guards -> the NECESSARY conditions for each exit
The "needs the coroutine entry points" blocker recorded here is REFUTED: the
instruction stream is FLAT and `isl.linear_offsets` reaches 25705/25705 call
@@ -130,12 +131,43 @@ def emit_conditions(b, path):
% (c['off'], lhs, c['rel'], rhs, c['target']))
def emit_phase_guards(b, path):
"""The conditions that DOMINATE each phase exit — the per-phase clear condition."""
rows, reached, total = isl_cfg.dominating_conditions(b)
print('# %s — what each phase exit requires' % path)
print()
print('Generated by `tools/re-capture/isl_report.py phase-guards`.')
print()
print('A condition is listed when it DOMINATES the exit: every path from an')
print('entry to that `END_PHASE` passes through it, so it is NECESSARY for the')
print('phase to end that way. Reachability alone is the wrong query here —')
print('in a poll loop both successors reach the exit.')
print()
print('CFG reached %d of %d instructions (%.1f%%).' % (reached, total, 100.0 * reached / total))
for r in rows:
nm = isl.BUILTIN.get(r['builtin'], 'builtin%d' % r['builtin'])
if r['conds'] is None:
print()
print('## phase %d%s at 0x%06X: UNREACHABLE from any static entry'
% (r['phase'], nm, r['end']))
print(' (started from the trigger queue at `phase+272`, by data not code)')
continue
print()
print('## phase %d%s at 0x%06X: %d necessary condition(s)'
% (r['phase'], nm, r['end'], len(r['conds'])))
for c in r['conds']:
lhs = c['lhs'] if c['lhs'] is not None else '<unknown>'
rhs = c['rhs'] if c['rhs'] is not None else '<unknown>'
print(' 0x%06X %s %s %s' % (c['off'], lhs, c['rel'], rhs))
def main():
path = sys.argv[1]
b = isl.load(path)
name = path.replace('\\', '/').split('/')[-1]
{'calls': emit_calls, 'phase-ends': emit_phase_ends,
'conditions': emit_conditions}[sys.argv[2]](b, name)
'conditions': emit_conditions,
'phase-guards': emit_phase_guards}[sys.argv[2]](b, name)
if __name__ == '__main__':