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.
143 lines
5.9 KiB
Markdown
143 lines
5.9 KiB
Markdown
# ✅ The ISL branch ops — a condition-code machine, read off the handlers
|
||
|
||
[`BACKLOG.md`](../BACKLOG.md) had this as the last thing between the flat decode
|
||
and a per-phase clear condition: five unread handlers, and an explicit
|
||
instruction not to name a branch from a pattern. They are read now, from
|
||
`sylpheed.db`. Nothing below is inferred from usage.
|
||
|
||
## Getting the real handler addresses
|
||
|
||
The 25-entry jump table at `0x822635FC` holds **thunks** inside the dispatcher,
|
||
not the handlers. Each thunk is three register moves and a `bl`; the `bl` target
|
||
is the handler. Taking the addresses any other way gets them wrong — my first
|
||
attempt guessed them at a fixed stride and landed mid-function, which produced a
|
||
20-line "difference" that was pure misalignment.
|
||
|
||
| op | thunk | handler |
|
||
|---|---|---|
|
||
| 10 | `0x822636F8` | `0x82271598` |
|
||
| 11 | `0x8226370C` | `0x822716E0` |
|
||
| 12 | `0x82263720` | *inline in the thunk* |
|
||
| 13 | `0x82263738` | `0x82271830` |
|
||
| 14 | `0x8226374C` | `0x822718C8` |
|
||
| 15 | `0x82263760` | `0x82271960` |
|
||
| 16 | `0x82263774` | `0x822719F8` |
|
||
| 17 | `0x8226379C` | `0x82271AC8` |
|
||
| 18 | `0x82263788` | `0x82271B60` |
|
||
|
||
## ✅ op10 / op11 are COMPARE, and they write three condition bits
|
||
|
||
`0x82271598` resolves two operands and compares them:
|
||
|
||
```
|
||
822715B0 lwz r5, 8(r29) ; word@+8
|
||
822715B4 lbz r4, 0(r29) ; kind byte[0]
|
||
822715B8 bl 0x82271D40 ; integer operand resolver -> r28 = RHS
|
||
822715C0 lwz r5, 4(r29) ; word@+4
|
||
822715C8 lbz r4, 1(r29) ; kind byte[1]
|
||
822715CC bl 0x82271D40 -> r27 = LHS
|
||
822715D4 addi r31, r31, 24 ; the condition bitset lives at phase+24
|
||
822715D8 cmp cr6, 0, r27, r28 ; SIGNED
|
||
```
|
||
|
||
It then writes **three** bits, each set-or-cleared by its own `cmp`:
|
||
|
||
| bit | written at | condition | set / clear |
|
||
|---|---|---|---|
|
||
| **0** | `0x822715D8` | `EQ` | `or` if equal, `andc` if not |
|
||
| **1** | `0x8227162C` | `GT` | `or` / `andc` on `cr6+gt` |
|
||
| **2** | `0x82271678` | `LT` | `or` / `andc` on `cr6+lt` |
|
||
|
||
**`op11` is the same machine for floats** — `0x822716E0` resolves through the
|
||
float resolvers (`0x82271F10` / `0x82271E30`), issues `fcmpu cr6, f30, f31`, and
|
||
writes the same bitset at `phase+24`.
|
||
|
||
**Operand order matters and is easy to get backwards:** LHS is `(kind byte[1],
|
||
word@+4)` and RHS is `(kind byte[0], word@+8)`. So a listing line
|
||
|
||
```
|
||
cmp.i k=01,02 00000000 00000002
|
||
```
|
||
|
||
is *compare `special[0]` against immediate `2`* — `k=01` is the RHS kind
|
||
(`imm`), `k=02` the LHS kind (`special`).
|
||
|
||
## ✅ op13–op18 are the six relational branches
|
||
|
||
Every one of them tests bits in that same bitset and, when taken, sets the pc to
|
||
`[phase+232] + word@+4` — **the identical phase-relative target form as the
|
||
unconditional `op12`**.
|
||
|
||
| op | bits tested | taken when | name |
|
||
|---|---|---|---|
|
||
| 13 | 0 | set | **`beq`** |
|
||
| 14 | 0 | clear | **`bne`** |
|
||
| 15 | 2 | set | **`blt`** |
|
||
| 17 | 1 | set | **`bgt`** |
|
||
| 16 | 2, then 0 | either set | **`ble`** |
|
||
| 18 | 1, then 0 | either set | **`bge`** |
|
||
|
||
13, 14, 15 and 17 are **byte-identical to each other** apart from two
|
||
instructions — the bit index (`addi r5, r0, N`) and the polarity
|
||
(`cmpli r11, 0x1` vs `0x0`). 16 and 18 are longer because they test a second bit
|
||
after the first fails, which is exactly `<=` and `>=`.
|
||
|
||
🔑 **All six relations are present and each appears exactly once.** That
|
||
completeness is the check: a mis-read would not produce a closed, non-redundant
|
||
relational set.
|
||
|
||
## ✅ This closes the `op10`+`op13` "switch" question
|
||
|
||
[`isl-builtins.md`](isl-builtins.md) recorded it as 🟡 *"Consecutive small
|
||
immediates each paired with their own code offset is the shape of a case/branch
|
||
dispatch … **Not confirmed** — the handlers have not been read."* They are read
|
||
now, and the shape is what it looked like — a chain of compare-and-branch-if-equal:
|
||
|
||
```
|
||
cmp.i k=01,02 00000000 00000001
|
||
beq -> 0x4E2C
|
||
cmp.i k=01,02 00000000 00000002
|
||
beq -> 0x4ED4
|
||
cmp.i k=01,02 00000000 00000003
|
||
beq -> 0x4F7C
|
||
```
|
||
|
||
*"switch (special[0]) { case 1: … case 2: … case 3: … }"*, lowered to sequential
|
||
compares. Confirmed from the handlers, not from the pattern.
|
||
|
||
## ✅ And a phase-3 clear condition now reads as one
|
||
|
||
From [`../data/isl-stage02-phase-ends.txt`](../data/isl-stage02-phase-ends.txt):
|
||
|
||
```
|
||
0349D0 call unit_state(ADT308)
|
||
0349DC op23 ; takes the call result
|
||
0349E0 cmp.i k=02,02 special[0], special[1]
|
||
0349EC beq -> 0xFEB4 ; loop back while it holds
|
||
0349F4 call end_coroutine
|
||
```
|
||
|
||
A coroutine polling `unit_state` on unit `ADT308` and branching **back** while
|
||
the comparison is equal — the poll loop the previous iteration could only
|
||
describe by shape.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* ~~**`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. 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
|
||
is not established, so `phase+32` is *not* asserted to be its data pointer.
|
||
* **Which value `special[0]` holds at a given site.** Naming the branch does not
|
||
by itself give the clear condition for every stage — that needs the operand
|
||
chain feeding each compare, which is the next step.
|