Files
Sylpheed/docs/re/structures/isl-builtin-dispatch.md
Sylpheed RE agent 142b8d7200 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

4.8 KiB
Raw Blame History

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 — which owns the opcode table — already named ops 2124 (push.i / push.f / pop.i / pop.f) and ops 1318 as the six branches. My own 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 groupunit_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.