re: the sufficient side -- each phase exit now names the condition that FIRES it
Dominance said a phase cannot end unless X. A port also needs "once X holds, it must end", and that is a must-reach set: nodes from which END_PHASE is unavoidable, as a least fixpoint where n qualifies when it has successors and ALL of them qualify. The conservatism is deliberate and is the honest answer: a loop never enters the set, because a poll loop reaches its exit only if the polled predicate eventually becomes true, which is a liveness property rather than a graph one. A dominating condition is a TRIGGER when the successor it takes on being satisfied lies in that set. Over all 28 stages: 732 dominating conditions, 234 triggers (31.97%). isl_report.py phase-guards now tags every line precond / TRIGGER. The split lands where it should. Stage 02's phase-1 objective exit is six preconditions -- player alive, TCN004 destroyed, t <= 210, ADT102/ADT107/ADT113 destroyed -- and exactly ONE trigger: hp_pct_test(ADN101, 0) != 1. Destroying ADN101 is what fires the phase. That is a sentence a port can implement. Per-exit distribution over 172 reachable exits: 89 have exactly one trigger, 42 have none, 41 have several. The 42 with none are not a failure -- they are the exits no branch fires; Stage 02's 0x006260 ends on read_freg(0) < 1200, a timeout, and time passing is not a property of the graph, so declining to call it a trigger is correct. Recorded as a heuristic rather than a rule: "the first trigger is the point of no return" holds for 33 of the 41 multi-trigger exits, with 8 counterexamples where a precondition appears after a trigger. The likely cause is that the listing is ordered by file offset, which is not execution order -- coroutines and jumps let a lower offset run later. Not asserted. calls, phase-ends and conditions all regenerate byte-identical; the two phase-guards artefacts change only by gaining the tags.
This commit is contained in:
@@ -89,6 +89,32 @@ def edges(b, spawn=False):
|
||||
return E, nxt
|
||||
|
||||
|
||||
def must_reach_exit(b):
|
||||
"""Nodes from which an `END_PHASE` is UNAVOIDABLE.
|
||||
|
||||
Least fixpoint: n qualifies when it has successors and ALL of them qualify.
|
||||
Deliberately conservative -- a loop never enters the set, which is the honest
|
||||
answer: a poll loop reaches its exit only if the polled predicate eventually
|
||||
becomes true, and that is a LIVENESS property, not a graph one.
|
||||
"""
|
||||
import collections as _c
|
||||
E, _nxt = edges(b, spawn=True)
|
||||
ends = {o for o, bid, _x in isl.call_sites(b) if bid in (6, 62)}
|
||||
rev = _c.defaultdict(list)
|
||||
for a, ss in E.items():
|
||||
for s in ss: rev[s].append(a)
|
||||
A = set(ends)
|
||||
work = _c.deque(ends)
|
||||
while work:
|
||||
n = work.popleft()
|
||||
for pnode in rev.get(n, ()):
|
||||
if pnode in A: continue
|
||||
ss = E.get(pnode, ())
|
||||
if ss and all(x in A for x in ss):
|
||||
A.add(pnode); work.append(pnode)
|
||||
return A
|
||||
|
||||
|
||||
def dominating_conditions(b):
|
||||
"""For each END_PHASE / FORCE_END_PHASE site, the conditions that DOMINATE it.
|
||||
|
||||
@@ -132,6 +158,9 @@ def dominating_conditions(b):
|
||||
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))}
|
||||
A = must_reach_exit(b)
|
||||
for c in conds.values():
|
||||
c['sufficient'] = c['target'] in A
|
||||
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)]:
|
||||
|
||||
@@ -146,6 +146,11 @@ def emit_phase_guards(b, path):
|
||||
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))
|
||||
print()
|
||||
print('`precond` = necessary: the exit cannot happen unless it holds.')
|
||||
print('`TRIGGER` = also SUFFICIENT: once it holds the exit is unavoidable.')
|
||||
print('An exit with no TRIGGER is not fired by a branch — a timeout exit is')
|
||||
print('fired by time passing, which is not a property of the graph.')
|
||||
for r in rows:
|
||||
nm = isl.BUILTIN.get(r['builtin'], 'builtin%d' % r['builtin'])
|
||||
if r['conds'] is None:
|
||||
@@ -160,7 +165,8 @@ def emit_phase_guards(b, path):
|
||||
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))
|
||||
tag = 'TRIGGER ' if c.get('sufficient') else 'precond '
|
||||
print(' %s0x%06X %s %s %s' % (tag, c['off'], lhs, c['rel'], rhs))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user