diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 014f932..5274298 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1396,11 +1396,25 @@ premise was wrong.** `beq/bne/blt/ble/bgt/bge` on those bits, targeting `[phase+232] + word@+4` exactly like `op12`. All six relations present, each once — the completeness is the check. Handler addresses come from the `bl` inside each dispatcher thunk; - guessing them at a fixed stride lands mid-function. ▶️ **Still open:** `op23` + guessing them at a fixed stride lands mid-function. ~~▶️ **Still open:** `op23` (`0x82271C30`, takes a built-in's result to `phase+168`) and `op21` (`0x82175C20`) are characterised but NOT named, and naming the branch does not yet give each stage's clear condition — that needs the operand chain feeding - each compare. The condition lives in the `op10`/`op13` poll loop upstream of + each compare.~~ ✅ **(2026-08-27) THE OPERAND CHAIN IS CLOSED — + [structures/isl-builtin-dispatch](structures/isl-builtin-dispatch.md).** + ⚠️ op21/op23 were already named in `isl-bytecode.md` (the owning file) as + `push.i`/`pop.i`; my `isl-branches.md` was the stale one. Verified and + reconciled. NEW: the 147-entry built-in table is a **thin dispatch layer** — + **112 of 147** stubs tail-call a fixed slot of the `ScriptPhase` vtable, and + every named predicate is in that group. The vtable is **`0x820A84BC`**, + derived from `MARK_LAST_PHASE`'s known `[phase+300]=2` stub and confirmed by + an unused-in-the-derivation prediction (slot 176 = the `=1` stub) plus the + db's own `vptr_writes`. `unit_state` = slot 184 = `0x8226ADF0`, which indexes + `[phase+324]` by `local[4]` and writes its answer to **`[phase+164]` = + `special[0]`**. So: **result → `special[0]`, comparand popped → `special[1]`, + then `cmp.i` + branch.** ▶️ **Still open:** the other 111 vtable slots are a + lookup but unread; which comparand each site pushes (needs `push.i` tracked + through the decode); the 35 non-vtable built-ins; the vtable's length. The condition lives in the `op10`/`op13` poll loop upstream of the outro — e.g. phase 3 polls `unit_state(ADT308)` and branches back to `0xFEB4` until it passes. Artefact: `data/isl-stage02-phase-ends.txt`. * 🐛 **(2026-08-25) The nav fix is NOT fully reliable.** `dialog_up.py` works on diff --git a/docs/re/data/isl-stage02-phase-ends.txt b/docs/re/data/isl-stage02-phase-ends.txt index b2e7269..f5de65f 100644 --- a/docs/re/data/isl-stage02-phase-ends.txt +++ b/docs/re/data/isl-stage02-phase-ends.txt @@ -52,7 +52,7 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 005834: 00000C13 call len=12 k=00,00 0000000B 00000278 end_coroutine 005840: 01021001 set.f len=16 k=01,02 00000000 406A4000 00000000 special[0] = imm 210 005850: 02020C01 set.f len=12 k=02,02 00000001 00000000 special[1] = special[0] - 00585C: 02000416 op22 len=4 k=02,00 + 00585C: 02000416 push.f len=4 k=02,00 005860: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 ## phase 1 — builtin 6 (END_PHASE) at 0x6010 @@ -76,7 +76,7 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 00601C: 00000C13 call len=12 k=00,00 0000000B 000002A5 end_coroutine 006028: 01021001 set.f len=16 k=01,02 00000000 4092C000 00000000 special[0] = imm 1200 006038: 02020C01 set.f len=12 k=02,02 00000001 00000000 special[1] = special[0] - 006044: 02000416 op22 len=4 k=02,00 + 006044: 02000416 push.f len=4 k=02,00 006048: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 ## phase 1 — builtin 6 (END_PHASE) at 0x6260 @@ -101,7 +101,7 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 006278: 00000414 ret len=4 k=00,00 00627C: 01020C00 set.i len=12 k=01,02 00000000 00000002 special[0] = imm 0x2 006288: 02020C00 set.i len=12 k=02,02 00000001 00000000 special[1] = special[0] - 006294: 02000415 op21 len=4 k=02,00 + 006294: 02000415 push.i len=4 k=02,00 ## phase 1 — builtin 62 (FORCE_END_PHASE) at 0x1482C 014770: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 @@ -292,14 +292,14 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 02C1EC: 00000C13 call len=12 k=00,00 0000000B 000002BA end_coroutine 02C1F8: 01021001 set.f len=16 k=01,02 00000000 4072C000 00000000 special[0] = imm 300 02C208: 02020C01 set.f len=12 k=02,02 00000001 00000000 special[1] = special[0] - 02C214: 02000416 op22 len=4 k=02,00 + 02C214: 02000416 push.f len=4 k=02,00 02C218: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 ## phase 3 — builtin 6 (END_PHASE) at 0x2CF74 02CEBC: 02020C00 set.i len=12 k=02,02 00000001 00000000 special[1] = special[0] - 02CEC8: 02000415 op21 len=4 k=02,00 + 02CEC8: 02000415 push.i len=4 k=02,00 02CECC: 00000C13 call len=12 k=00,00 00000063 00000306 builtin99 - 02CED8: 00020417 op23 len=4 k=00,02 + 02CED8: 00020417 pop.i len=4 k=00,02 02CEDC: 02020C0A cmp.i len=12 k=02,02 00000000 00000001 02CEE8: 0000080D beq len=8 k=00,00 000083B0 02CEF0: 01000C00 set.i len=12 k=01,00 00000074 00000002 global[116] = imm 0x2 @@ -316,7 +316,7 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 02CF80: 00000C13 call len=12 k=00,00 0000000B 0000030E end_coroutine 02CF8C: 01021001 set.f len=16 k=01,02 00000000 4092C000 00000000 special[0] = imm 1200 02CF9C: 02020C01 set.f len=12 k=02,02 00000001 00000000 special[1] = special[0] - 02CFA8: 02000416 op22 len=4 k=02,00 + 02CFA8: 02000416 push.f len=4 k=02,00 02CFAC: 01020C00 set.i len=12 k=01,02 00000000 00000000 special[0] = imm 0x0 ## phase 3 — builtin 6 (END_PHASE) at 0x2D1DC @@ -350,11 +350,11 @@ phase code bases: 0xe4 0x14aa8 0x24b4c 034994: 0000080E bne len=8 k=00,00 0000FEB4 03499C: 01020C00 set.i len=12 k=01,02 00000000 00000001 special[0] = imm 0x1 0349A8: 02020C00 set.i len=12 k=02,02 00000001 00000000 special[1] = special[0] - 0349B4: 02000415 op21 len=4 k=02,00 + 0349B4: 02000415 push.i len=4 k=02,00 0349B8: 01030C00 set.i len=12 k=01,03 00000000 00000001 local[0] = imm 0x1 0349C4: 01030C00 set.i len=12 k=01,03 00000004 00000049 local[4] = imm 0x49 0349D0: 00000C13 call len=12 k=00,00 00000045 0000072C unit_state(ADT308) - 0349DC: 00020417 op23 len=4 k=00,02 + 0349DC: 00020417 pop.i len=4 k=00,02 0349E0: 02020C0A cmp.i len=12 k=02,02 00000000 00000001 0349EC: 0000080D beq len=8 k=00,00 0000FEB4 0349F4: 00000C13 call len=12 k=00,00 0000000B 0000072D end_coroutine diff --git a/docs/re/structures/isl-branches.md b/docs/re/structures/isl-branches.md index 209b364..fecf9f1 100644 --- a/docs/re/structures/isl-branches.md +++ b/docs/re/structures/isl-branches.md @@ -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 21–24 `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 diff --git a/docs/re/structures/isl-builtin-dispatch.md b/docs/re/structures/isl-builtin-dispatch.md new file mode 100644 index 0000000..ae03572 --- /dev/null +++ b/docs/re/structures/isl-builtin-dispatch.md @@ -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 21–24 (`push.i` / `push.f` / `pop.i` / `pop.f`) and ops 13–18 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. diff --git a/tools/re-capture/isl.py b/tools/re-capture/isl.py index 40508a7..b90035b 100755 --- a/tools/re-capture/isl.py +++ b/tools/re-capture/isl.py @@ -131,7 +131,10 @@ OPS = { 10: 'cmp.i', 11: 'cmp.f', 12: 'jmp', 13: 'beq', 14: 'bne', 15: 'blt', 16: 'ble', 17: 'bgt', 18: 'bge', 19: 'call', 20: 'ret', - 21: 'op21', 22: 'op22', 23: 'op23', 24: 'op24', + # 21-24 name the deque ops -- see structures/isl-bytecode.md, verified from + # the thunks: 21 pushes [phase+168] onto the deque at phase+44, 22 pushes + # [phase+184] onto phase+64, and 23/24 pop back into +168 / +184. + 21: 'push.i', 22: 'push.f', 23: 'pop.i', 24: 'pop.f', }