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

143 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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`).
## ✅ 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@+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 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. 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.