This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-phase-guards.md
Sylpheed RE agent b636ac9d4e re: phase-guards for all 28 stages, and a 6/6 cross-check from an unrelated method
isl_report.py now accepts a directory, so the dominance analysis runs over the whole
disc: data/isl-phase-guards-all.txt, 177 phase exits, of which only 5 (2.8%) are
reachable from no static entry.  CFG reach ranges 69.5% (S26) to 95.8% (S25), median
about 4 dominating conditions per exit.

The lopsided number in the per-stage table was the six TUTORIAL stages, S18-S23, each
with exactly ONE exit and exactly ONE dominating condition.  That could have been a
degenerate result, so I looked: it is the same condition in all six,

    END_PHASE  <-  builtin104() != 1

and isl-builtins.md reached built-in 104 from call-site USAGE alone -- "S18-S23 only,
followed by wait_s 39/39, preceded by end_coroutine 37/39, a textbook poll loop".
Usage said 104 is the tutorial's polled test; dominance says it is the tutorial's
clear condition.  Two unrelated methods, six for six.

Stage 16 -- the corpus outlier whose script may be compiled C++ -- resolves as well:
read_freg(0) < 600, player_gauge0_test, player_gauge1_test, and two builtin141 calls
differing in a single argument (0 vs -4000), which is the shape of a position or zone
test.  builtin141 is unread, so it is not named.

Stage 02's separate artefact regenerates byte-identical.

Also added: an RLIMIT_AS cap in isl_report's entry point.  The dominator pass
OOM-killed a run earlier on this 15 GB box; a bad input should now fail the process
rather than the machine.

Still not settled and stated in the doc: dominance gives necessary, not sufficient,
conditions; the 5 unreachable exits need the trigger queue at phase+272; builtin104,
builtin141 and builtin7 all appear in clear conditions and are unread.
2026-08-27 06:19:44 +00:00

129 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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.
## ✅ All 28 stages — [`../data/isl-phase-guards-all.txt`](../data/isl-phase-guards-all.txt)
`isl_report.py <dir> phase-guards` runs the whole disc. **177 phase exits**, of
which only **5 (2.8 %)** are reachable from no static entry.
| | |
|---|---|
| CFG reach, best | Stage 25 — 95.8 % |
| CFG reach, worst | Stage 26 — 69.5 % |
| median conditions per exit | ~4 |
### ✅ An independent cross-check, 6 / 6
The six **tutorial** stages (S18S23) each have exactly **one** exit with exactly
**one** dominating condition, and it is the same one every time:
```
END_PHASE <- builtin104() != 1
```
[`isl-builtins.md`](isl-builtins.md) reached built-in **104** from a completely
different direction — call-site usage — and recorded it as *"S18S23 only …
followed by `wait_s` 39/39, preceded by `end_coroutine` 37/39 … a textbook poll
loop"*. Usage said it was the tutorial's polled test; dominance says it is the
tutorial's clear condition. **Two unrelated methods, six for six.** That the
1.0-condition uniformity turned out to be real rather than a degenerate result is
the check worth having run.
### Stage 16, the corpus outlier, also reads
`mission-script-ssb.md` flags S16 as the stage whose script may be compiled C++.
Its exits resolve anyway, and sensibly:
```
ph1 END_PHASE read_freg(0) < 600 ; player_gauge0_test(0) != 1 ; player_gauge1_test(0) != 1
ph1 FORCE_END_PHASE builtin141(TCN001, 1, 2, 0, 0, 0, 1000, 5, 100, 100) != 1 ;
builtin141(TCN001, 1, 2, 0, -4000, 0, 1000, 5, 100, 100) != 1 ; global[0] != 1
```
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.
* 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.