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:
@@ -1498,7 +1498,20 @@ premise was wrong.**
|
||||
`unit_hp_pct(TCN001, Character_Player_Test) != 0`** (the player alive); phase 1's
|
||||
objective exit additionally requires `hp_pct_test(ADT102/ADT107/ADT113, 0) != 1`,
|
||||
phase 3's requires `ADT301`/`ADT302`, and `read_freg(0)` gates at 210/300/1200.
|
||||
🟡 Dominance gives NECESSARY not sufficient conditions. 🟡 2 of 15 exits are
|
||||
~~🟡 Dominance gives NECESSARY not sufficient conditions.~~ ✅ **(2026-08-27)
|
||||
THE SUFFICIENT SIDE IS DONE.** `isl_cfg.must_reach_exit()` = a least fixpoint of
|
||||
nodes from which `END_PHASE` is unavoidable (n qualifies when it has successors
|
||||
and ALL qualify — conservative, so a loop never enters, which is correct because
|
||||
a poll loop's exit depends on a LIVENESS property). A dominating condition is a
|
||||
**TRIGGER** when its satisfying successor is in that set. **732 dominating
|
||||
conditions, 234 triggers (31.97 %)**, and the artefacts now tag every line
|
||||
`precond` / `TRIGGER`. Stage 02 phase 1's objective exit = 6 preconditions plus
|
||||
ONE trigger, `hp_pct_test(ADN101, 0) != 1` — destroying ADN101 is what fires it.
|
||||
Per-exit: **89 of 172 have exactly one trigger**, 42 have none (those are the
|
||||
timeout exits — time passing is not a graph property, so declining to call it a
|
||||
trigger is correct), 41 have several. 🟡 "the FIRST trigger is the point of no
|
||||
return" holds **33/41**, not universally — the listing is ordered by file offset,
|
||||
which is not execution order — so it is recorded as a heuristic, not a rule. 🟡 2 of 15 exits are
|
||||
reachable from NO static entry — consistent with the trigger queue at `phase+272`.
|
||||
✅ **(2026-08-27) EXTENDED TO ALL 28 STAGES** — `data/isl-phase-guards-all.txt`
|
||||
(`isl_report.py <dir> phase-guards`). **177 exits, only 5 (2.8 %) unreachable**;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,94 +9,99 @@ in a poll loop both successors reach the exit.
|
||||
|
||||
CFG reached 15670 of 18739 instructions (83.6%).
|
||||
|
||||
`precond` = necessary: the exit cannot happen unless it holds.
|
||||
`TRIGGER` = also SUFFICIENT: once it holds the exit is unavoidable.
|
||||
An exit with no TRIGGER is not fired by a branch — a timeout exit is
|
||||
fired by time passing, which is not a property of the graph.
|
||||
|
||||
## phase 1 — END_PHASE at 0x0051E4: 2 necessary condition(s)
|
||||
0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x005110 random(3) == 0
|
||||
precond 0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
TRIGGER 0x005110 random(3) == 0
|
||||
|
||||
## phase 1 — END_PHASE at 0x005828: 3 necessary condition(s)
|
||||
0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
0x00572C random(5) == 0
|
||||
precond 0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
TRIGGER 0x00572C random(5) == 0
|
||||
|
||||
## phase 1 — END_PHASE at 0x006010: 7 necessary condition(s)
|
||||
0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
0x005888 read_freg(0) <= 210
|
||||
0x0058DC hp_pct_test(ADT102, 0) != 1
|
||||
0x005930 hp_pct_test(ADT107, 0) != 1
|
||||
0x005984 hp_pct_test(ADT113, 0) != 1
|
||||
0x005B3C hp_pct_test(ADN101, 0) != 1
|
||||
precond 0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
precond 0x005888 read_freg(0) <= 210
|
||||
precond 0x0058DC hp_pct_test(ADT102, 0) != 1
|
||||
precond 0x005930 hp_pct_test(ADT107, 0) != 1
|
||||
precond 0x005984 hp_pct_test(ADT113, 0) != 1
|
||||
TRIGGER 0x005B3C hp_pct_test(ADN101, 0) != 1
|
||||
|
||||
## phase 1 — END_PHASE at 0x006260: 4 necessary condition(s)
|
||||
0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
0x005888 read_freg(0) <= 210
|
||||
0x006070 read_freg(0) < 1200
|
||||
precond 0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x00523C hp_pct_test(TCN004, 0) != 1
|
||||
precond 0x005888 read_freg(0) <= 210
|
||||
precond 0x006070 read_freg(0) < 1200
|
||||
|
||||
## phase 1 — FORCE_END_PHASE at 0x01482C: UNREACHABLE from any static entry
|
||||
(started from the trigger queue at `phase+272`, by data not code)
|
||||
|
||||
## phase 2 — END_PHASE at 0x019640: 2 necessary condition(s)
|
||||
0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x01956C random(3) == 0
|
||||
precond 0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
TRIGGER 0x01956C random(3) == 0
|
||||
|
||||
## phase 2 — END_PHASE at 0x019934: 2 necessary condition(s)
|
||||
0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x019698 hp_pct_test(TCT206, 0) != 1
|
||||
precond 0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x019698 hp_pct_test(TCT206, 0) != 1
|
||||
|
||||
## phase 2 — END_PHASE at 0x01AC44: 9 necessary condition(s)
|
||||
0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x019698 hp_pct_test(TCT206, 0) != 1
|
||||
0x0199C8 builtin7(TCT206, 1, Route_TCT206_p2S, 4294967295, 500) == 1
|
||||
0x019A8C <unknown> <= 240
|
||||
0x01A1E4 global[4] == 0
|
||||
0x01A2B0 global[4] != 1
|
||||
0x01A924 hp_pct_test(TCT206, 89.9) != 0
|
||||
0x01AA38 global[104] != 0
|
||||
0x01AAF8 global[4] == 0
|
||||
precond 0x0191CC unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x019698 hp_pct_test(TCT206, 0) != 1
|
||||
precond 0x0199C8 builtin7(TCT206, 1, Route_TCT206_p2S, 4294967295, 500) == 1
|
||||
precond 0x019A8C <unknown> <= 240
|
||||
precond 0x01A1E4 global[4] == 0
|
||||
TRIGGER 0x01A2B0 global[4] != 1
|
||||
TRIGGER 0x01A924 hp_pct_test(TCT206, 89.9) != 0
|
||||
TRIGGER 0x01AA38 global[104] != 0
|
||||
TRIGGER 0x01AAF8 global[4] == 0
|
||||
|
||||
## phase 2 — FORCE_END_PHASE at 0x0249F0: 2 necessary condition(s)
|
||||
0x0247E4 hp_pct_test(ADN202, 0) != 1
|
||||
0x024848 unit_state(TCT206) != 1
|
||||
precond 0x0247E4 hp_pct_test(ADN202, 0) != 1
|
||||
TRIGGER 0x024848 unit_state(TCT206) != 1
|
||||
|
||||
## phase 3 — END_PHASE at 0x02B96C: 2 necessary condition(s)
|
||||
0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x02B898 random(3) == 0
|
||||
precond 0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
TRIGGER 0x02B898 random(3) == 0
|
||||
|
||||
## phase 3 — END_PHASE at 0x02BFB0: 3 necessary condition(s)
|
||||
0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
0x02BEB4 random(5) == 0
|
||||
precond 0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
TRIGGER 0x02BEB4 random(5) == 0
|
||||
|
||||
## phase 3 — END_PHASE at 0x02C1E0: 3 necessary condition(s)
|
||||
0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
0x02BFC8 global[112] < 4
|
||||
precond 0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
precond 0x02BFC8 global[112] < 4
|
||||
|
||||
## phase 3 — END_PHASE at 0x02CF74: 16 necessary condition(s)
|
||||
0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
0x02BFC8 global[112] < 4
|
||||
0x02C240 read_freg(0) <= 300
|
||||
0x02C294 hp_pct_test(ADT301, 0) != 1
|
||||
0x02C2E8 hp_pct_test(ADT302, 0) != 1
|
||||
0x02C33C hp_pct_test(ADT303, 0) != 1
|
||||
0x02C390 hp_pct_test(ADT304, 0) != 1
|
||||
0x02C3E4 hp_pct_test(ADT305, 0) != 1
|
||||
0x02C438 hp_pct_test(ADT306, 0) != 1
|
||||
0x02C48C hp_pct_test(ADT307, 0) != 1
|
||||
0x02C4E0 hp_pct_test(ADT308, 0) != 1
|
||||
0x02C5C4 global[112] != 0
|
||||
0x02CC0C global[112] != 0
|
||||
0x02CDC8 global[4] == 0
|
||||
0x02CE9C global[112] != 0
|
||||
precond 0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
precond 0x02BFC8 global[112] < 4
|
||||
precond 0x02C240 read_freg(0) <= 300
|
||||
precond 0x02C294 hp_pct_test(ADT301, 0) != 1
|
||||
precond 0x02C2E8 hp_pct_test(ADT302, 0) != 1
|
||||
precond 0x02C33C hp_pct_test(ADT303, 0) != 1
|
||||
precond 0x02C390 hp_pct_test(ADT304, 0) != 1
|
||||
precond 0x02C3E4 hp_pct_test(ADT305, 0) != 1
|
||||
precond 0x02C438 hp_pct_test(ADT306, 0) != 1
|
||||
precond 0x02C48C hp_pct_test(ADT307, 0) != 1
|
||||
precond 0x02C4E0 hp_pct_test(ADT308, 0) != 1
|
||||
TRIGGER 0x02C5C4 global[112] != 0
|
||||
TRIGGER 0x02CC0C global[112] != 0
|
||||
TRIGGER 0x02CDC8 global[4] == 0
|
||||
TRIGGER 0x02CE9C global[112] != 0
|
||||
|
||||
## phase 3 — END_PHASE at 0x02D1DC: 5 necessary condition(s)
|
||||
0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
0x02BFC8 global[112] < 4
|
||||
0x02C240 read_freg(0) <= 300
|
||||
0x02CFD4 read_freg(0) < 1200
|
||||
precond 0x02B4F8 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond 0x02B9C4 hp_pct_test(TCN004, 0) != 1
|
||||
precond 0x02BFC8 global[112] < 4
|
||||
precond 0x02C240 read_freg(0) <= 300
|
||||
precond 0x02CFD4 read_freg(0) < 1200
|
||||
|
||||
## phase 3 — FORCE_END_PHASE at 0x034A10: UNREACHABLE from any static entry
|
||||
(started from the trigger queue at `phase+272`, by data not code)
|
||||
|
||||
@@ -119,10 +119,68 @@ The two `builtin141` calls differ in one argument (`0` vs `-4000`), which is the
|
||||
shape of a position or zone test — but it is unread, so it is not named.
|
||||
|
||||
## 🟡 Not settled
|
||||
* **Dominance gives necessary, not sufficient, conditions.** A dominator set does
|
||||
not say the phase ends *when* they all hold, only that it cannot end unless
|
||||
they do. Turning these into a simulator's exit test needs the sufficient side.
|
||||
* ~~**Dominance gives necessary, not sufficient, conditions.**~~ ✅ **Done** — see
|
||||
*Necessary vs sufficient* below.
|
||||
* One condition in the phase-2 list still prints `<unknown> <= 240` — one of the
|
||||
402 sites the CFG cannot resolve.
|
||||
* `builtin7` and `read_freg`'s units are unread; `read_freg(0)` behaves like
|
||||
seconds against the 210 / 300 / 1200 gates but that is not established.
|
||||
|
||||
|
||||
## ✅ Necessary vs sufficient — the exits now name their TRIGGER
|
||||
|
||||
Dominance says the exit *cannot* happen unless a condition holds. A port also
|
||||
needs the other half: once it holds, does the exit *have* to happen?
|
||||
|
||||
That is a **must-reach** set — nodes from which an `END_PHASE` is unavoidable —
|
||||
computed as a 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, because a poll loop reaches its exit only if the polled
|
||||
predicate eventually becomes true, and that is a **liveness** property, not a
|
||||
graph one.
|
||||
|
||||
A dominating condition is then a **TRIGGER** when the successor it branches to on
|
||||
being satisfied lies in that set.
|
||||
|
||||
**Over all 28 stages: 732 dominating conditions, 234 of them triggers (31.97 %).**
|
||||
|
||||
And the split lands exactly where it should. Stage 02's phase-1 objective exit:
|
||||
|
||||
```
|
||||
precond unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||
precond hp_pct_test(TCN004, 0) != 1
|
||||
precond read_freg(0) <= 210
|
||||
precond hp_pct_test(ADT102, 0) != 1
|
||||
precond hp_pct_test(ADT107, 0) != 1
|
||||
precond hp_pct_test(ADT113, 0) != 1
|
||||
TRIGGER hp_pct_test(ADN101, 0) != 1
|
||||
```
|
||||
|
||||
Six preconditions and **one thing that actually fires it** — destroying `ADN101`.
|
||||
That is a sentence a port can implement.
|
||||
|
||||
### Triggers per exit, 172 reachable exits
|
||||
|
||||
| triggers | exits |
|
||||
|---|---|
|
||||
| **0** | 42 |
|
||||
| **1** | **89** |
|
||||
| 2–4 | 35 |
|
||||
| 5–13 | 6 |
|
||||
|
||||
**The 42 with none are not a failure** — they are the exits no branch fires. Stage
|
||||
02's `0x006260` is one: its last necessary condition is `read_freg(0) < 1200`, a
|
||||
timeout. Time passing is not a property of the graph, so a graph analysis
|
||||
correctly declines to call it a trigger.
|
||||
|
||||
### 🟡 "The first trigger is the point of no return" — 33 / 41, not a rule
|
||||
|
||||
Where an exit has several triggers, the natural reading is that they form a
|
||||
forced tail and the **first** is where the outcome is decided. Tested on the 41
|
||||
multi-trigger exits: **33 hold, 8 do not** — 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. So the
|
||||
reading is a useful heuristic and **not** a property, and it is recorded that way
|
||||
rather than asserted.
|
||||
@@ -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