Files
Sylpheed/docs/re/isl-stream-is-flat.md
Sylpheed RE agent 228680edf0 re: the ISL stream is flat -- refute the "needs coroutine entry points" blocker
Two files (isl_report.py's docstring and structures/isl-builtins.md) recorded the
same blocker on a faithful per-phase condition listing: that it needs the coroutine
entry points from start_coroutine's operand.  Measured against isl.call_sites(),
which enumerates by scanning the encoding rather than by decoding and so is an
independent denominator:

  linear + jumps, stopping at ret (what the tool did)   133 / 2846 =   4.7%
  linear + jumps, continuing past ret                  2275 / 2846 =  79.9%
  ... + following start_coroutine (the recorded fix)   2355 / 2846 =  82.7%
  plain linear decode, no control flow at all          2846 / 2846 = 100.0%

Following the coroutine entries buys 2.8 points.  Disc-wide, a plain linear decode
from the first phase base reaches 25705/25705 call sites over all 28 stages, and
28/28 decode clean to code_end with no desync.

The real bug was isl.dis ending on `if op == 20: break`.  Op 20 is `ret`, but this
is a coroutine VM -- the thread suspends and resumes at the FOLLOWING instruction,
so code continues past it.  dis() now takes stop_at_ret (default True, preserving
the old output: data/isl-stage02.txt regenerates byte-identical) and
isl.linear_offsets() is the correct walk.

By-product, kept with its control: start_coroutine's target is staged slot 0 --
73/83 phase-1 sites land on a valid instruction, against a 38.7% chance rate for an
arbitrary 4-aligned offset.

New artefact data/isl-stage02-phase-ends.txt with a committed generator
(isl_report.py phase-ends).  It shows END_PHASE's call site is the WRONG place to
read a clear condition: all 12 Stage-02 sites sit in one stereotyped outro.  Not
settled, and stated as such: op10/op13/op14/op21/op23 are unread handlers, so the
condition in the poll loop upstream cannot be named yet.
2026-08-27 05:00:49 +00:00

107 lines
4.4 KiB
Markdown

# ✅ The ISL instruction stream is FLAT — the recorded blocker was my own decoder
Two files recorded the same blocker on producing a faithful per-phase condition
listing — `tools/re-capture/isl_report.py`'s docstring and
[`structures/isl-builtins.md`](structures/isl-builtins.md):
> *"neither `isl.resync` … nor a naive linear decode from the phase base reaches
> every call site, so producing that listing faithfully needs the **coroutine
> entry points**, which `start_coroutine`'s operand carries and this tool does
> not yet follow."*
**Refuted.** The coroutine entry points were never the problem.
## The measurement
`isl.call_sites()` enumerates calls by **scanning the encoding**, not by
decoding, so it is an independent denominator — exactly the control this needed.
Against it, for `Stage02.ssb` (2 846 call sites):
| decode strategy | call sites reached |
|---|---|
| linear + jumps, stopping at `ret`*what the tool did* | 133 — **4.7 %** |
| linear + jumps, continuing past `ret` | 2 275 — 79.9 % |
| … + following `start_coroutine`'s target — *the recorded fix* | 2 355 — **82.7 %** |
| **plain linear decode, no control flow at all** | **2 846 — 100.0 %** |
Following the coroutine entries — the thing both files said was required — buys
**2.8 points**. Ignoring control flow entirely gets everything.
### And it holds across the disc
Decoding linearly from the first phase base to `code_end`:
**28 files, 25 705 / 25 705 call sites = 100.00 %, and 28 / 28 decode clean to
the end with no desync.** Not one stage has a gap or a bad length.
So the stream is a flat, self-consistent instruction sequence. Reaching a call
site needs no control-flow reconstruction — only a correct length walk.
## The actual bug: `ret` is a YIELD
`isl.dis` ended its loop on `if op == 20: break`. Op 20 *is* `ret` — but this is
a **coroutine VM**: the handler "sets `r29=1` and takes the suspend path", the
thread parks, and it later **resumes at the following instruction**. Code
continues after a `ret`, so stopping there abandons the file at the first one.
For Stage 02 that is offset `0x1F0` of a `0x34C80` code region — 271 of 18 739
instructions.
This also silently truncated my own reading: the listing for the phase-3
`FORCE_END_PHASE` at `0x34A10` stopped four instructions short of the site,
at the `ret` at `0x34A0C`.
`dis` now takes `stop_at_ret` (default `True`, preserving the old output — the
committed `data/isl-stage02.txt` regenerates **byte-identical**), and
`isl.linear_offsets()` is the correct walk.
## ✅ By-product: `start_coroutine`'s target is staged slot 0
Worth keeping even though it was not the blocker. Of 83 `start_coroutine` sites
in phase 1, the only staged slot is **0**, and `phase_base + slot0` lands on a
valid instruction in **73/83 = 88 %**.
**Control:** an arbitrary 4-aligned offset in the code region looks like a valid
instruction **38.7 %** of the time (20 887 / 53 990). So 88 % is discriminating,
not the null result — the operand is a code offset relative to the phase base.
## 🟡 Where a phase actually ends — and where the condition is NOT
New artefact [`data/isl-stage02-phase-ends.txt`](data/isl-stage02-phase-ends.txt)
(generator: `isl_report.py phase-ends`). Stage 02 has **15** phase-ending calls:
12 `END_PHASE` (6) and 3 `FORCE_END_PHASE` (62).
**Every one of the 12 `END_PHASE` sites sits in the same stereotyped outro:**
```
call wait_cmds_drained
call fade_sound(3)
call builtin85(3)
call wait_s(3)
call END_PHASE
call end_coroutine
```
That is the phase *teardown*, not its condition — so **`END_PHASE`'s call site
is the wrong place to look for the clear condition.** The decision is upstream,
in the `op10`/`op13` pair that precedes the outro, which
[`structures/isl-builtins.md`](structures/isl-builtins.md) already flags as an
unread compare/branch (handlers `0x82271598` and `0x82271830`).
The phase-3 `FORCE_END_PHASE` region shows the shape a real condition takes —
a poll loop, not a straight line:
```
op10 global[20], 7
op14 -> 0xFEB4
call unit_state(ADT308)
op23
op10 special, 1
op13 -> 0xFEB4 ; branch back — poll until the test passes
call end_coroutine
```
🟡 **Not settled:** `op10`/`op13`/`op14`/`op21`/`op23` are still unread
handlers, so which way each branch goes is a guess, and I am not naming them
from a pattern — the corpus has paid for that twice already. Reading those five
handlers is what turns this listing into the per-phase clear condition.