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.
5.9 KiB
✅ The ISL branch ops — a condition-code machine, read off the handlers
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 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:
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
🔴 My error — the corpus already had it.op23(0x82271C30) is left unnamed.isl-bytecode.md, which owns the opcode table, names 21–24push.i/push.f/pop.i/pop.f.op23ispop.i, into[phase+168]=special[1]. Verified from the thunks and closed in isl-builtin-dispatch, 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 passesphase+168andphase+44, not the pc. That ispush.i: it pushesspecial[1]onto the deque atphase+44. The generic deque helper itself is still unread.- The bitset container at
phase+24.op10reaches it with0x822749B0(by address,phase+24) andop13with0x82274CC0(through a word loaded fromphase+32). Both land on the same bits, but the exact container layout is not established, sophase+32is 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.