re: close the ISL operand chain -- built-in result is special[0], via a vtable

Answers what the previous commit left open: naming the branches did not give a
clear condition, because that needs the operand chain feeding each compare.

First, a correction to my own work.  isl-bytecode.md -- which OWNS the opcode table
-- already named ops 21-24 push.i/push.f/pop.i/pop.f.  isl-branches.md, which I
wrote last iteration, said op21 and op23 were unread.  The stale file was mine.
Verified from the thunks rather than accepted: 21 pushes [phase+168] onto the deque
at phase+44, 22 pushes [phase+184] onto phase+64, and the 23/24 handlers touch only
r3+168 and r3+184.  So pop.i lands in special[1].

New: the 147-entry built-in table is a thin DISPATCH LAYER, not implementations.
Each stub resolves the local[] argument base and tail-calls a fixed ScriptPhase
vtable slot.  112 of 147 dispatch that way; 17 write [phase+164] inline; 0 write
+184.  Every named predicate is in the vtable group -- unit_state 184, unit_alive
188, hp_pct_test 64, dist_lt 56, is_engaged 252, timer_elapsed 372 -- which is the
control that the split separates engine queries from script bookkeeping.

The vtable is 0x820A84BC, derived from a known implementation rather than a stride:
MARK_LAST_PHASE is documented as [phase+300]=2; the function 0x8226B498 is exactly
that stub; it appears as a data word at exactly one address, 0x820A8570; built-in
39 uses slot 180.  The check NOT used in the derivation: built-in 40 mark_not_last
uses slot 176, and slot 176 holds the [phase+300]=1 stub.  Predicted and confirmed.
The db's own vptr_writes independently lists 0x820A84BC, written at 0x82261B80.

unit_state = slot 184 = 0x8226ADF0, which indexes [phase+324] by local[4] and writes
its answer to [phase+164] = special[0] at both exits.  The phase-3 poll loop now
reads end to end: unit_state(ADT308) -> special[0]; pop.i -> special[1]; cmp.i; beq.

isl.py names ops 21-24; the calls artefact regenerates with NO diff.

Left open and said so: the other 111 vtable slots, which comparand each site pushes,
the 35 non-vtable built-ins, and the vtable's length.
This commit is contained in:
Sylpheed RE agent
2026-08-27 05:21:10 +00:00
parent bad96eb54a
commit 142b8d7200
5 changed files with 142 additions and 17 deletions

View File

@@ -123,12 +123,16 @@ describe by shape.
## 🟡 Not settled
* **`op23`** (`0x82271C30`) is left unnamed. It indexes a container at `r3+44`
and stores a word to `[phase+168]`; it is almost certainly how a built-in's
return value reaches `special[]`, but "almost certainly" is how this corpus
acquired two names it later had to withdraw. Characterised, not named.
* ~~**`op23`** (`0x82271C30`) is left unnamed.~~ 🔴 **My error — the corpus
already had it.** [`isl-bytecode.md`](isl-bytecode.md), which owns the opcode
table, names 2124 `push.i`/`push.f`/`pop.i`/`pop.f`. `op23` is `pop.i`, into
`[phase+168]` = `special[1]`. Verified from the thunks and closed in
[isl-builtin-dispatch](isl-builtin-dispatch.md), which also settles where a
built-in's result goes: `[phase+164]` = `special[0]`.
* **`op21`** (`0x82175C20`) has a different shape from all of these — its thunk
passes `phase+168` and `phase+44`, not the pc — and was not read.
passes `phase+168` and `phase+44`, not the pc. That is `push.i`: it pushes
`special[1]` onto the deque at `phase+44`. The generic deque helper itself is
still unread.
* **The bitset container at `phase+24`.** `op10` reaches it with `0x822749B0`
(by address, `phase+24`) and `op13` with `0x82274CC0` (through a word loaded
from `phase+32`). Both land on the same bits, but the exact container layout

View File

@@ -0,0 +1,104 @@
# ✅ How a built-in's result reaches a condition — the operand chain
The previous iteration named the branches and stated plainly that this was still
missing: *"naming the branch does not by itself give the clear condition — that
needs the operand chain feeding each compare."* It is read now, and it closes.
## ⚠️ First: the corpus already had half of it, and I had written the stale file
[`isl-bytecode.md`](isl-bytecode.md) — which **owns** the opcode table — already
named ops 2124 (`push.i` / `push.f` / `pop.i` / `pop.f`) and ops 1318 as the
six branches. My own [`isl-branches.md`](isl-branches.md), written one iteration
earlier, said `op23` and `op21` were unread. **The stale file was mine.**
Verified rather than assumed, from the thunks and handlers:
| op | moves | verified by |
|---|---|---|
| 21 `push.i` | `[phase+168]` → deque at `phase+44` | thunk: `addi r4,r30,168` / `addi r3,r30,44` |
| 22 `push.f` | `[phase+184]` → deque at `phase+64` | thunk: `addi r4,r30,184` / `addi r3,r30,64` |
| 23 `pop.i` | → `[phase+168]` | handler `0x82271C30`, only r3-offset touched is **168** |
| 24 `pop.f` | → `[phase+184]` | handler `0x82271CB8`, only r3-offset touched is **184** |
With `isl-bytecode.md`'s operand-kind table (`special[0] = [phase+164]`,
`special[1] = [phase+168]`), `pop.i` lands in **`special[1]`**.
## ✅ The built-in table is a thin dispatch layer over a vtable
Each of the 147 entries is a **stub**, not an implementation. The stub resolves
the `local[]` argument base and tail-calls a fixed slot of the `ScriptPhase`
vtable at `[phase+0]`:
```
82272DFC addi r3, r31, 20 ; local[] base
82272E00 bl 0x82454A40 ; resolve
82272E04 lwz r11, 0(r31) ; the vptr
82272E10 lwz r11, 184(r11) ; <- fixed slot, one per built-in
82272E14 b 0x822724F0 ; mtspr CTR / bcctrl, then return 0
```
Measured over all 147:
| | count |
|---|---|
| dispatched through a `ScriptPhase` vtable slot | **112** |
| write `[phase+164]` (`special[0]`) inline in the stub | 17 |
| write `[phase+184]` inline | 0 |
**Every named predicate is in the vtable group**`unit_state` 184,
`unit_alive` 188, `hp_pct_test` 64, `dist_lt` 56, `unit_hp_pct` 256,
`is_engaged` 252, `group_ratio_pct` 196, `timer_elapsed` 372,
`player_gauge0/1_test` 396/400 — which is the control: the split is not
arbitrary, it separates engine queries from script-local bookkeeping.
## ✅ The vtable is at `0x820A84BC`, derived self-checkingly
Not guessed from a stride — the trap this corpus already paid for. Derived from
a **known implementation**:
1. `isl-bytecode.md` documents built-in **39** `MARK_LAST_PHASE` as `[phase+300] = 2`.
2. The function `0x8226B498` is exactly `addi r11,r0,2 ; stw r11,300(r3) ; blr`.
3. It appears as a data word at exactly **one** address: `0x820A8570`.
4. Built-in 39's stub uses slot **180** → base = `0x820A8570 180` = **`0x820A84BC`**.
**The check, which was not used in the derivation:** built-in **40**
`mark_not_last` (`[phase+300] = 1`) uses slot **176**, so the base predicts
`0x820A84A8`… and slot 176 holds `0x8226B4A8`, which is
`addi r11,r0,1 ; stw r11,300(r3) ; blr` — the `= 1` stub sitting immediately
after the `= 2` one. Predicted and confirmed.
**Third, independent:** the database's own `vptr_writes` lists
`0x820A84BC` as a vtable, written at `0x82261B80`.
## ✅ And the result lands in `special[0]`
`unit_state` is slot 184 → **`0x8226ADF0`**. It indexes `[phase+324]` — the unit
array `isl-builtins.md` already documents — by `local[4]`, reads the record, and
writes its answer to **`[phase+164]` = `special[0]`** at both its normal exit
(`0x8226AEC4`) and its early exit (`0x8226AF48`).
That completes the chain, and the phase-3 poll loop now reads end to end:
```
call unit_state(ADT308) ; result -> special[0]
pop.i ; special[1] <- the pushed comparand
cmp.i special[0], special[1]
beq -> 0xFEB4 ; loop back while they are equal
```
**A built-in's return value is `special[0]`; the comparand is popped into
`special[1]`; the compare and branch do the rest.** That is the shape of every
clear condition in the corpus.
## 🟡 Not settled
* **The other 111 vtable slots are not read.** The base is now known, so each is
a lookup rather than a search — but knowing where `hp_pct_test` lives is not
the same as having read it.
* **Which comparand each site pushes.** The loop above compares against whatever
`push.i` put on the deque; recovering that per site needs the push tracked
through the decode, which `isl.py` does not do.
* **The 35 non-vtable built-ins**, and `op21`/`op22`'s generic deque helpers
(`0x82175C20`, `0x82274BA0`), were not read — only their arguments.
* **The vtable's length.** `0x820A84BC` is the base and slot 404 is in use, so it
is at least 102 entries; I did not establish where it ends.