Files
Sylpheed/docs/re/structures/isl-branches.md
Sylpheed RE agent 0287a78633 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.
2026-08-27 05:21:10 +00:00

5.9 KiB
Raw Permalink Blame History

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 floats0x822716E0 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 2k=01 is the RHS kind (imm), k=02 the LHS kind (special).

op13op18 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@+4the 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

  • op23 (0x82271C30) is left unnamed. 🔴 My error — the corpus already had it. 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, 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.