re: three ISL built-in names were wrong, including the most-used one

All re-read twice — the handler, and the thing it calls — because each had
been named from its shape rather than its effect.

* id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ;
  stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from
  the active list and returns it to the free list. It destroys the thread.
  2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was.
* id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the
  thread parked at the target pc, or itself if the target is its own pc.
  It waits for nothing.
* id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger
  container and then frees every thread whose pc differs from the caller's
  — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage
  02 sites all sit in the phase terminator, next to timer_stop,
  clear_flag(-1) and MARK_LAST_PHASE.

One name recovered from the game's own text: opcode 992 prints
"RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message
(2683 sites).

Return codes documented properly: 1 = restart the coroutine from its entry
(previously not recorded at all), 3 = terminate. And the blocking set was
wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block;
its handler ends `b 0x822724F8`, so it always returns 0.

Unit-operand resolution settled from DATA over all 28 stages rather than by
reading 147 handlers: a slot qualifies only if every value is a valid
symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most
of the table — that last clause is what discriminates, since every small
integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one
at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the
table, and the resolver now declines rather than inventing a name.

New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and
128 take type 8 at slot 12 in 100% of their sites.

A downstream inference is withdrawn with it: the note reading the live
trigger counter attributed it to "the script arming watches as it goes" via
built-in 100. The measurement stands; the attribution does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-25 22:09:05 +00:00
parent 723fc9b890
commit 1ba5d0a4a3
5 changed files with 261 additions and 56 deletions

View File

@@ -22,9 +22,28 @@ Arguments do **not** live in the instruction. Every handler starts
**packed operand blob**, which is what the `local[]` staging in
[isl-bytecode](isl-bytecode.md) fills.
Return codes: **0** continue, **2** yield (re-execute next frame), **3**
coroutine control. Five built-ins skip the pc advance on 2 and so genuinely
**block**: 97, 120, 137, 142, 143.
### ✅ Return codes — read off `ScriptPhase::Update` (`0x82263830`)
The dispatcher switches on `r3` exactly four ways:
| r3 | address | effect |
|---|---|---|
| **0** | `0x82263850` | continue to the next thread in the same frame |
| **1** | `0x82263878` | `[thread+4] = [thread+0]`**restart this coroutine from its entry** |
| **2** | `0x82263888` | `[thread+4] = saved pc` — resume next frame |
| **3** | `0x82263894` | `sub_8226EA20` erases the thread from the active list `[+216]`, then `sub_8226EAB8` returns it to the free list `[+204]`**terminate this coroutine** |
Two tails do the pc bookkeeping: `0x822724F8` is `li r3,0` then advance;
`0x822724FC` advances only, preserving `r3`. Advance is `pc += [insn+2]`, the
length byte — the same field [isl-bytecode](isl-bytecode.md) decodes. So **every
handler that ends `b 0x822724F0` returns 0**, and its only output is
`[phase+164]`/`[phase+176]`.
**CORRECTED — the blocking set was wrong in two places.** The blocking form is
`bctrl ; cmpwi r3,2 ; bne 0x822724FC`, and it appears at **102, 120, 137, 142,
143**. This file previously listed **97**, which does not block: its handler
`0x8227313C` ends `b 0x822724F8`, so it *always* returns 0. And **102** was
missing. All six handlers re-read to confirm.
## ✅ `ScriptPhase` state layout
@@ -65,7 +84,7 @@ two tables already parsed in [mission-script-ssb](mission-script-ssb.md).
| **132134** | player gauges | speed/boost ratios and a player byte |
| **73, 123127** | timer family | start / resume / stop / reset / read elapsed / read limit |
| **8 / 9 / 93** | `set_flag` / `read_freg` / `clear_flag` | latch a result into the 32-entry files |
| **100 / 115** | `push_trigger` / `named_event` | the engine→script edge |
| **100 / 115** | `reset_phase_threads` / `named_event` | ❌ 100 is **not** `push_trigger` — see below |
**The state machine is therefore:** a trigger fires a coroutine → the coroutine
tests one of the predicates → it latches the answer with `set_flag` → some later
@@ -80,9 +99,63 @@ Two spot-checks I ran against the disassembly rather than taking on trust:
a NULL object and state 1, then calls `823011B0` (initial, packed
`hi<<16|lo`) and `82301118` (current). Exactly as described.
## ❌ Three built-in names WITHDRAWN
Each re-read twice — the handler, and the thing it calls — because all three had
been named from their shape rather than their effect.
| id | was | **is** | evidence |
|---|---|---|---|
| **11** | `yield` | **`end_coroutine`** | `0x82272624` is `li r11,1 ; li r3,3 ; stw r11,164(r31)`. Return 3 **destroys the thread**. It is the single most-used built-in in the game — 2945 sites, 372 in Stage 02 alone — so this was the most load-bearing wrong name in the file. |
| **5** | `await_label` | **`kill_coroutine(label)`** | `sub_82273B08` computes `target = [phase+232] + blob[0]`; if that equals the **caller's own** pc it returns 3 (kill self), otherwise it finds the thread parked at `target` in `[phase+220]` and moves it to the free list. It does not wait for anything. |
| **100** | `push_trigger` | **`reset_phase_threads`** | the handler calls vtable slot 2 (clears the trigger container at `[phase+272]`) and then `sub_82273BE8`, which walks `[phase+220]` and frees **every thread whose pc differs from the caller's**. It drops queued triggers and terminates every *other* coroutine — the opposite of pushing one. |
One name is newly **recovered**, from the game's own text: interpreter opcode 992
prints `★RequestScriptMessage %s` (`0x820A5700`), so **id 64 is
`request_script_message`** — 2683 sites, and the second most-used built-in.
## ✅ Which operands are unit indices — settled from the data
`tools/re-capture/isl.py` resolved a symbol-table-2 name only for 11 built-ins,
at slot 4. The real set is much larger, and it was established by **measurement
over all 28 stages** rather than by reading 147 handlers:
> a slot qualifies only if every observed value is a valid symtab-2 index, it
> takes ≥15 distinct values, **and its maximum reaches most of the table**.
That last clause is what makes the test work. Symbol table 2 tops out at 122
entries, so a slot carrying something else overruns it; plain range-checking
cannot separate an index from a bool, because every small integer is in range.
* **unit index at slot 4** — 2, 3, 7, 12, 15, 16, 18, 19, 20, 24, 25, 26, 28,
29, 30, 47, 48, 56, 57, 58, 63, 69, 70, 79, 91, 92, 95, 105, 108, 128, 143
* **a second at slot 12** — 2, 18, 47, 48, 56, 79, 95, 128
* **a third at slot 20** — 128
Every one of these is 100.0 % in range across its call sites (the largest, id 20,
over 2930 of them).
The test also **refuted** a tempting entry: `set_flag`'s slot 0 passes the range
and spread checks but its maximum *exceeds* the table — flag indices run 0..31
against symbol tables as small as 40 — so it is excluded, and the disassembler
now declines to resolve it rather than printing an invented name.
**New, unexplained:** symbol table 2 holds **two types**, 2 (1160 entries
disc-wide) and 8 (249), and they are not interchangeable. Built-ins **95** and
**128** take a type-2 unit at slot 4 and, at slot 12, an operand that is type 8
in **100 %** of its 90 and 152 call sites. What separates the two classes is not
established.
## ✅ What Stage 02 actually uses — and it settles a standing question
Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`):
Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`, regenerated by
`tools/re-capture/isl_report.py calls`):
⚠️ The sibling artefact `data/isl-stage02-conditions.txt` **predates the name
corrections above** — it still prints `yield`, `await_label` and `push_trigger`,
and its operand rendering predates the staging fix. It has no committed
generator; reproducing it needs the coroutine entry points, which
`start_coroutine`'s operand carries and the tool does not yet follow.
| built-in | sites |
|---|---|
@@ -91,7 +164,7 @@ Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`):
| `dist_lt` | **92** |
| `unit_alive` | **71** |
| `unit_relation` | **52** |
| `set_flag` / `clear_flag` / `push_trigger` | 12 each |
| `set_flag` / `clear_flag` / `reset_phase_threads` | 12 each |
| `END_PHASE` / `MARK_LAST_PHASE` / `FORCE_END_PHASE` | 12 / 8 / 3 |
**Not used at all in Stage 02:** `squad_survival_pct`, `group_ratio_pct`,
@@ -285,9 +358,16 @@ Read from a running Stage 02 mission (`ScriptPhase 0xBE14DD80`, container at
```
**`+20` moves, 0 → 1 → 2**, while the phase ordinal stays 1. So it is a real
counter of **currently registered triggers** — the script arming watches as it
goes (Stage 02 has 12 `push_trigger` sites) — and it is readable live with no
debugger. That is the first direct view of *what the script is waiting for*.
counter of **currently registered triggers**, readable live with no debugger —
the first direct view of *what the script is waiting for*.
⚠️ The *measurement* stands; its attribution did not. This paragraph used to add
"the script arming watches as it goes (Stage 02 has 12 `push_trigger` sites)",
pointing at built-in 100. Built-in 100 is `reset_phase_threads` — it **clears**
the trigger container, it does not arm one. The 12 sites are real, but they are
12 places where Stage 02 *tears the trigger set down*, which is close to the
opposite reading. ❔ What actually arms a trigger is now open again; built-ins
19 and 25 both queue into `[phase+272]` and are the first place to look.
🟡 **`+12` is not a list head after all**, or not only that: it reads
`0x000A0009`, which is not a pointer. The `addi r31, r30, 12` in the push made