Files
Sylpheed/docs/re/isl-stream-entry-points.md
Sylpheed RE agent 62f52a4543 re: sub_822737C8 is the coroutine spawner; the record's third word is the MAIN ENTRY
Asking who calls the function the timeline calls enumerates every way an ISL routine
can begin.  sub_822737C8(phase, base, offset) computes base + offset early on, and has
seven real call sites: the phase initialiser sub_82270DF8, the built-in stub region
(start_coroutine), the timeline walker sub_822748D0, TWICE inside sub_8226D740 -- the
per-frame engine->script drain -- and two unread, sub_82273910 and sub_82264058.

CORRECTION to my own write-up: the 0x1883 record's third word is the phase's MAIN
ENTRY, not a "size".  The initialiser hands it straight to the spawner:

    8227101C  or  r5, r22, r22     ; the record's third word
    82271020  or  r4, r26, r26     ; the code base
    82271030  bl  0x822737C8

44 of 44 records land on the instruction stream (100%) against a 25.0% control, and
all three Stage-02 targets open with the identical prologue
`special[0]=0 ; local[0]=0 ; call builtin116(0)` -- a routine entry, not a length.
So the record is 0x1883, base, MAIN_ENTRY, 0, code_end, force_end_handler.

Seeding the main entries moves no coverage number: every one was already among the
CFG's entry points by another route.  This corrects a field's meaning, not the graph.

Lead recorded rather than claimed: both of the drain's spawns take their offset from
[node+112], the first field of a drained node to be located, and the best remaining
angle on the ~15% of code nothing appears to start.  It is NOT shown that those nodes
come from the trigger queue at phase+272 -- that is precisely the over-reach
isl-builtins.md already made and withdrew, so it is not asserted here.

All artefacts regenerate byte-identical; this is documentation only.
2026-08-27 07:22:51 +00:00

143 lines
6.3 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.
# ✅ The mission-level `0x1883` record: one entry point and one code/data boundary
Every phase exit in all 28 stages is now reachable. `data/isl-phase-guards-all.txt`
went from **5 of 177 exits unreachable** to **0**.
## The hunt, including the part that failed
The first step was the cheap one: take each unreached routine's entry offset and
look for it as a word anywhere in the file. **It is not there**, in any encoding:
| entry encoded as | appears as a word | control (reached offsets) |
|---|---|---|
| phase-relative | 6.6 % | **11.5 %** |
| absolute | 1.6 % | 3.3 % |
| phase-relative / 4 | 1.6 % | 6.6 % |
| absolute / 4 | 0.0 % | 8.2 % |
Every variant sits **at or below** the control, so nothing in the bytecode names
these routines. That is a real negative, and it ruled out the whole family of
"some instruction operand points at them".
It also ruled out *dead code*: the 3 069 unreached instructions in Stage 02
contain **485 calls**, including `start_coroutine` ×75, `squadron_attack` ×59,
`set_group_speed` ×42 and `objective_marker` ×13. That is live mission logic.
## ✅ The answer is in the mission-level stream
`isl-bytecode.md` records that the stream at `+0x24` holds three `0x1883` records,
one per phase, whose operand is the phase's code base, and notes they each end
"in a pair of plausible ISL entry offsets". They are exactly that. The record is
```
0x1883, base_delta, size, 0, entry_a, entry_b ; entries PHASE-RELATIVE
```
~~**Measured over all 28 stages: 82 of 88 of those values land on a valid
instruction — 93.2 %, against a 38.6 % chance rate.**~~
⚠️ **That test was too weak** — see *What `entry_a` really is* below. A trailing
data record has length 8 and so passes "nonzero, even length"; the 93.2 % counted
data as code. The `entry_b` result stands on a different footing: those targets
were matched against `isl.call_sites()`, an independent enumeration.
And for Stage 02 the second entry of each record is the phase's force-end
handler:
```
base 0x0000E4 -> 0x014848, 0x01482C <- 0x1482C was an unreachable exit
base 0x014AA8 -> 0x024A0C, 0x0249F0
base 0x024B4C -> 0x034AC8, 0x034A10 <- 0x34A10 was an unreachable exit
```
Both of Stage 02's unreachable exits are named here, and phase 2's `0x249F0`
already reachable — is the same slot, which is the consistency check.
Seeding these as CFG entries: **exits unreachable 5 → 0** across the disc.
🔑 It also *reads*: those exits now show **0 necessary conditions**, because they
are entry points rather than code reached through tests. A handler the engine can
enter directly, with no preconditions, is what `FORCE_END_PHASE` should look like.
## ⚠️ Seeded in one place, and the count did not move
The first attempt added the entries to `conditions()` and reported reach
**85.0 % → 85.0 %**, exits **5 → 5**. Nothing moved, which is the signal.
`dominating_conditions()` builds its **own** entry set and did not use the one I
had patched — the same fix-the-instance-not-the-class mistake that cost 22× its
own size earlier in this corpus. With both patched: exits **5 → 0**.
## 🟡 Not settled
* **Reach only went 85.0 % → 85.2 %.** These records name a handful of routines,
not the ~15 % of code still unreached. What starts *the rest* is still open —
and the negative above says it is not an operand in the file.
* **`entry_a` is unidentified.** `entry_b` is the force-end handler in the three
Stage-02 records; what the first entry of each pair is has not been checked.
* **6 of 88 values do not land on an instruction.** Not investigated — they may
be absent phases, a different record variant, or a decode edge.
## 🔴 What `entry_a` really is — NOT an entry point
Disassembling the three Stage-02 `entry_a` targets shows **opcodes 0x19 and
0x1A**, and the dispatcher's table has 25 entries (`cmplwi 0x18`). Those are not
instructions at all:
```
014848: 00000819 tag 19 00000019 int 25
014850: 00000819 tag 19 00002B20 int
014858: 0000081A tag 1A 00000000 float 0.0
014860: 00000819 tag 19 00000005 int 5
014868: 00000819 tag 19 00002B88 int
014870: 0000081A tag 1A 3F000000 float 0.5
```
Each phase region **ends with a trailing DATA table** of 8-byte typed records —
tag `0x19` = int, tag `0x1A` = IEEE float — and `entry_a` is where it starts.
**Confirmed across the disc: in 44 of 44 phases, the first offset whose opcode
exceeds 0x18 is exactly that phase's `entry_a`. Zero exceptions.** Only two tags
ever appear: 1 394 × `0x19` and 675 × `0x1A`.
So the record is
```
0x1883, base, MAIN_ENTRY, 0, code_end, force_end_handler
```
🔴 **The third word is NOT a size** — that was my label and it was wrong. The
phase initialiser `sub_82270DF8` passes it straight to the coroutine spawner:
```
8227101C or r5, r22, r22 ; r22 = the record's third word
82271020 or r4, r26, r26 ; r26 = the code base
82271030 bl 0x822737C8 ; spawn(phase, base, word3)
```
Measured: **44 of 44 records land on the instruction stream (100 %) against a
25.0 % control**, and all three of Stage 02's targets open with the identical
prologue `special[0]=0 ; local[0]=0 ; call builtin116(0)` — a routine entry, not
a length.
🟡 The table's contents are **not decoded**. Its int values do land on the
instruction stream more often than chance (46/51 vs 29.5 %), but **0 of them are
unreached run-starts** — so this is not what starts the unreachable code either.
## ✅ And the decoder was reading that data as code
`isl.linear_offsets` was decoding all **2 069** of those records as instructions,
1.23 % of the stream. Fixed by stopping each phase's walk at its boundary:
| | before | after |
|---|---|---|
| decoded instructions | 168 251 | **166 182** = 168 251 2 069 |
| records with opcode > 0x18 | 2 069 | **0** |
| **call sites covered** | 25 705 / 25 705 | **25 705 / 25 705** |
| exits unreachable | 0 | 0 |
| conditions with unknown LHS | 400 | 400 |
⚠️ **The first attempt at that fix destroyed 36 % of the stream** — 168 251 →
107 596, exits 0 → 74 — because `linear_offsets` is one global walk from the
first phase base, so stopping at phase 1's table lost every later phase. It has
to *skip* the region and resume at the next base. A count moving hard in the
wrong direction is the same signal as one that will not move.