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:
@@ -1472,7 +1472,20 @@ premise was wrong.**
|
|||||||
+ the unit's live object, pushes it on a queue via the `push.i` helper, and
|
+ the unit's live object, pushes it on a queue via the `push.i` helper, and
|
||||||
returns 1 (or 0 if the unit is absent). Finding that **exposed a much bigger
|
returns 1 (or 0 if the unit is absent). Finding that **exposed a much bigger
|
||||||
bug in my own condition tracker** — see the entry below.
|
bug in my own condition tracker** — see the entry below.
|
||||||
which condition guards each `END_PHASE` (needs the control flow between them);
|
~~which condition guards each `END_PHASE` (needs the control flow between them);~~
|
||||||
|
✅ **(2026-08-27) DONE — [structures/isl-phase-guards](structures/isl-phase-guards.md)**
|
||||||
|
(`isl_report.py phase-guards`, `data/isl-stage02-phase-guards.txt`). Uses
|
||||||
|
DOMINANCE over the CFG, not reachability. 🔴 The obvious query — "one branch
|
||||||
|
reaches `END_PHASE`, the other doesn't" — is **wrong for this language** and was
|
||||||
|
tried first: poll loops have BOTH successors reaching the exit, so it found
|
||||||
|
1/62/1 guards in Stage 02's three phases, the 1s being a `read_freg(0) < 1200`
|
||||||
|
timeout, missing every objective test. Dominators converge in 3 passes over
|
||||||
|
15670/18739 instructions. Result: **every exit in all 3 phases requires
|
||||||
|
`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
|
||||||
|
reachable from NO static entry — consistent with the trigger queue at `phase+272`.
|
||||||
the vtable's length. The condition lives in the `op10`/`op13` poll loop upstream of
|
the vtable's length. The condition lives in the `op10`/`op13` poll loop upstream of
|
||||||
the outro — e.g. phase 3 polls `unit_state(ADT308)` and branches back to
|
the outro — e.g. phase 3 polls `unit_state(ADT308)` and branches back to
|
||||||
`0xFEB4` until it passes. Artefact: `data/isl-stage02-phase-ends.txt`.
|
`0xFEB4` until it passes. Artefact: `data/isl-stage02-phase-ends.txt`.
|
||||||
|
|||||||
102
docs/re/data/isl-stage02-phase-guards.txt
Normal file
102
docs/re/data/isl-stage02-phase-guards.txt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Stage02.ssb — what each phase exit requires
|
||||||
|
|
||||||
|
Generated by `tools/re-capture/isl_report.py phase-guards`.
|
||||||
|
|
||||||
|
A condition is listed when it DOMINATES the exit: every path from an
|
||||||
|
entry to that `END_PHASE` passes through it, so it is NECESSARY for the
|
||||||
|
phase to end that way. Reachability alone is the wrong query here —
|
||||||
|
in a poll loop both successors reach the exit.
|
||||||
|
|
||||||
|
CFG reached 15670 of 18739 instructions (83.6%).
|
||||||
|
|
||||||
|
## phase 1 — END_PHASE at 0x0051E4: 2 necessary condition(s)
|
||||||
|
0x004D70 unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## phase 2 — FORCE_END_PHASE at 0x0249F0: 2 necessary condition(s)
|
||||||
|
0x0247E4 hp_pct_test(ADN202, 0) != 1
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## phase 3 — FORCE_END_PHASE at 0x034A10: UNREACHABLE from any static entry
|
||||||
|
(started from the trigger queue at `phase+272`, by data not code)
|
||||||
88
docs/re/structures/isl-phase-guards.md
Normal file
88
docs/re/structures/isl-phase-guards.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ✅ What each phase exit requires — the per-phase clear conditions
|
||||||
|
|
||||||
|
This is what the ISL thread was for. `BACKLOG.md` framed it as *"which condition
|
||||||
|
guards each `END_PHASE`"*, and with the CFG from
|
||||||
|
[isl-conditions](isl-conditions.md) it is a graph query rather than new
|
||||||
|
machinery. Artefact:
|
||||||
|
[`../data/isl-stage02-phase-guards.txt`](../data/isl-stage02-phase-guards.txt),
|
||||||
|
generator `isl_report.py phase-guards`.
|
||||||
|
|
||||||
|
## 🔴 The obvious query is WRONG here — tried first, and it fails quietly
|
||||||
|
|
||||||
|
The natural definition of a guard is *one successor reaches `END_PHASE` and the
|
||||||
|
other does not*. I implemented that first. It reports, for Stage 02:
|
||||||
|
|
||||||
|
| phase | 1 | 2 | 3 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| "guards" found | **1** | 62 | **1** |
|
||||||
|
|
||||||
|
and the single condition it finds in phases 1 and 3 is the same one —
|
||||||
|
`read_freg(0) < 1200`, a timeout. Every objective test is missed.
|
||||||
|
|
||||||
|
**The reason is the shape of the language.** The dominant idiom here is a **poll
|
||||||
|
loop**: `if still-alive: jump back`. The loop-back branch reaches the exit too —
|
||||||
|
one iteration later — so *neither* successor discriminates and the clear
|
||||||
|
condition is invisible to a reachability test. The asymmetric 1 / 62 / 1 is what
|
||||||
|
exposed it; a uniform number would have looked plausible and been wrong.
|
||||||
|
|
||||||
|
## ✅ Dominance has no such blind spot
|
||||||
|
|
||||||
|
A condition **dominates** an exit when *every* path from an entry to that
|
||||||
|
`END_PHASE` passes through it — so it is a **necessary** condition for the phase
|
||||||
|
to end that way. A poll loop's test dominates its own exit, so the idiom that
|
||||||
|
defeats reachability is handled by construction.
|
||||||
|
|
||||||
|
Iterative dominators over the CFG, converging in **3 passes**, reaching
|
||||||
|
**15 670 of 18 739** instructions (83.6 %).
|
||||||
|
|
||||||
|
> Practical note: the first run was **OOM-killed**. 6 743 reachable nodes each
|
||||||
|
> carrying a Python `set` of up to 6 743 elements is ~45 M objects. Integer
|
||||||
|
> bitmasks fit in seconds.
|
||||||
|
|
||||||
|
## ✅ What Stage 02 actually requires
|
||||||
|
|
||||||
|
**Every exit in all three phases** is dominated by
|
||||||
|
|
||||||
|
```
|
||||||
|
unit_hp_pct(TCN001, Character_Player_Test) != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
— the player's own ship being alive. That is the universal precondition, and it
|
||||||
|
falls out of the analysis rather than being assumed.
|
||||||
|
|
||||||
|
Then, per phase:
|
||||||
|
|
||||||
|
| phase | exit at | necessary conditions beyond the player being alive |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `0x0051E4` | `random(3) == 0` |
|
||||||
|
| 1 | `0x005828` | `hp_pct_test(TCN004, 0) != 1`, `random(5) == 0` |
|
||||||
|
| 1 | `0x006010` | `hp_pct_test(TCN004…)`, `read_freg(0) <= 210`, **`hp_pct_test(ADT102, 0) != 1`**, **`ADT107`**, **`ADT113`** |
|
||||||
|
| 1 | `0x006260` | `hp_pct_test(TCN004…)`, `read_freg(0) <= 210`, `read_freg(0) < 1200` |
|
||||||
|
| 2 | `0x019934` | `hp_pct_test(TCT206, 0) != 1` |
|
||||||
|
| 2 | `0x01AC44` | `TCT206`, `builtin7(TCT206, 1, Route_TCT206_p2S, …, 500) == 1`, `global[4] == 0`, `global[4] != 1` |
|
||||||
|
| 2 | `0x0249F0` | `hp_pct_test(ADN202, 0) != 1`, `unit_state(TCT206) != 1` |
|
||||||
|
| 3 | `0x02C1E0` | `hp_pct_test(TCN004…)`, `global[112] < 4` |
|
||||||
|
| 3 | `0x02CF74` | + `read_freg(0) <= 300`, **`hp_pct_test(ADT301, 0) != 1`**, **`ADT302`** |
|
||||||
|
| 3 | `0x02D1DC` | + `read_freg(0) < 1200` |
|
||||||
|
|
||||||
|
The `hp_pct_test(ADTnnn, 0) != 1` chains are the objective kills; `read_freg(0)`
|
||||||
|
is a phase clock (`<= 210`, `<= 300` gates, `< 1200` the timeout); `random(3)`
|
||||||
|
and `random(5)` dominate only the exits that pick one of several closing lines.
|
||||||
|
|
||||||
|
## 🟡 Two exits are unreachable, and that is informative
|
||||||
|
|
||||||
|
`0x01482C` and `0x034A10` — both `FORCE_END_PHASE` — are reachable from **no
|
||||||
|
static entry**. That agrees with the independently measured 389 unreachable
|
||||||
|
routines: they are started from the **trigger queue at `phase+272`**, by data
|
||||||
|
rather than code. So a purely static reading cannot say what forces those exits.
|
||||||
|
|
||||||
|
## 🟡 Not settled
|
||||||
|
|
||||||
|
* **Only Stage 02 is committed.** The other 27 generate from the same command.
|
||||||
|
* **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.
|
||||||
|
* 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.
|
||||||
58
tools/re-capture/guard.py
Normal file
58
tools/re-capture/guard.py
Normal 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)
|
||||||
@@ -54,6 +54,97 @@ def _join(a, bst):
|
|||||||
st = a[2] if a[2] == bst[2] else ()
|
st = a[2] if a[2] == bst[2] else ()
|
||||||
return (m(a[0], bst[0]), m(a[1], bst[1]), st)
|
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):
|
def coroutine_entries(b):
|
||||||
"""Every `start_coroutine` target, found by a linear pre-pass."""
|
"""Every `start_coroutine` target, found by a linear pre-pass."""
|
||||||
offs = isl.linear_offsets(b)
|
offs = isl.linear_offsets(b)
|
||||||
|
|||||||
@@ -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.
|
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> 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
|
The "needs the coroutine entry points" blocker recorded here is REFUTED: the
|
||||||
instruction stream is FLAT and `isl.linear_offsets` reaches 25705/25705 call
|
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']))
|
% (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():
|
def main():
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
b = isl.load(path)
|
b = isl.load(path)
|
||||||
name = path.replace('\\', '/').split('/')[-1]
|
name = path.replace('\\', '/').split('/')[-1]
|
||||||
{'calls': emit_calls, 'phase-ends': emit_phase_ends,
|
{'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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user