This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-bytecode.md
Sylpheed RE agent 7b445916bc re: ISL bytecode encoding decoded; phase-end call sites located in Stage02
Read the encoding off the interpreter rather than guessing: instruction is a
big-endian u32 whose LOW byte is the opcode (25 of them, table 0x822635FC),
byte[2] is the instruction length -- every handler advances the pc by it -- and
bytes[0..1] are operand kinds. Op 12 is a jump whose operand is relative to the
code base [phase+232], which settles that offsets are code-base-relative for
this opcode. Op 19 is the built-in call: id in word@+4, and word@+8 is a
monotonically increasing STATEMENT id (0x245, 0x248, 0x24A, ...).

Confirmed by disassembling Stage02.ssb: the stream decodes cleanly from the code
base and routines terminate on ret exactly where expected.

Scanning the code region on the call encoding: 2846 call sites, 73 of the 147
built-ins used. The phase-control ones are located -- built-in 6 (end phase) at
12 sites, 62 at 3, 39 (mark last phase) at 8 -- so a phase has several exit
paths, as a mission with win and lose branches should.

New tool tools/re-capture/isl.py with --calls and --to (resync-into-target,
needed because instructions are variable-length so you cannot walk backwards).

Not settled: the 147 built-ins are uncharacterised, so this is structure without
meaning -- we can see THAT a phase ends, not WHAT was tested.
2026-08-25 12:21:44 +00:00

112 lines
4.8 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 script bytecode — instruction encoding decoded
Status: ✅ the encoding, the 25-opcode table and the call form, read off the
interpreter and confirmed by disassembling `Stage02.ssb`; 🟡 most opcode
*semantics* are named only by their handler; ❔ the 147 built-ins are not yet
characterised.
Follows [mission-script-ssb](mission-script-ssb.md) (where the scripts live) and
[mission-phase-advance](../mission-phase-advance.md) (why they matter).
Tool: `tools/re-capture/isl.py`.
## ✅ Encoding
`ScriptPhase::Update` (`sub_82263408`) fetches one **big-endian u32** per
instruction:
```
0x822635D4 lwz r11,0(r31) ; the instruction word
0x822635D8 clrlwi r4,r11,24 ; OPCODE = the LOW byte
0x822635DC cmplwi 0x18 ; 25 opcodes
0x822635FC jump table, 25 absolute VAs
```
Every handler advances the pc with `lbz r11,2(r31); add r31,r11,r31`, so:
| byte | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| meaning | operand kind A | operand kind B | **instruction length in bytes** | **opcode** |
Operand words follow. The common form is 12 bytes (opcode word + two operands).
Operand kinds go through resolvers with their own 4-entry table
(`sub_82271D40` for integers, `cmplwi 0x3`), so there are **4 operand kinds**.
## ✅ The opcode table
| op | handler | what the handler does |
|---|---|---|
| 0 | `82263660` | integer assign — resolve rvalue (`82271D40`, kind byte[0], word@+8), resolve lvalue (`82272030`, kind byte[1], word@+4), `stw` |
| 1 | `8226369C` | float assign — same shape with `82271F10`/`82272120` and `stfd` |
| 2,4,6,8 | `822636D0` | → `822713E8` (a compare/branch family; four opcodes share one handler) |
| 3,5,7,9 | `822636E4` | → `822714D0` (the sibling family) |
| 10 | `822636F8` | → `82271598` |
| 11 | `8226370C` | → `822716E0` |
| **12** | `82263720` | **JUMP**`r31 = [phase+232] + word@+4` |
| 1318 | `82263738`… | → `82271830`, `822718C8`, `82271960`, `822719F8`, `82271AC8`, `82271B60` |
| **19** | `822637B0` | **CALL BUILT-IN**`sub_82272220` |
| 20 | `82263874` | `li r29,1` then the suspend path — **yield / return** |
| 21 | `822637C4` | `sub_82175C20(phase+44, phase+168)` |
| 22 | `822637E4` | `sub_82274BA0(phase+64, phase+184)` |
| 23,24 | `82263804`… | → `82271C30`, `82271CB8` |
Handler return codes drive the outer loop at `0x82263828`: **0** continue,
**1** suspend, **2**/**3** other exits.
### ✅ Jump operands are code-base-relative
Op 12 adds its operand to `[phase+232]`, the code base — i.e. the `.ssb`
header's code offset (`0x24` in every file). That settles, for this opcode, the
question `mission-script-ssb.md` left open about whether offsets are file- or
code-base-relative.
### ✅ The call form, and a statement counter
`sub_82272220` reads the **built-in id from word@+4** (`cmplwi 0x92` → 147
built-ins, table `0x8227226C`) and stores **word@+8** into `[phase+200]`.
That second word turns out to be a **monotonically increasing statement id**
`0x245, 0x248, 0x24A, 0x24B, 0x24C, 0x24D, 0x24E, 0x252…` along a routine. It is
a source-position counter, presumably for the script's own error traces.
## ✅ It decodes — Stage 02
Disassembling from the code base runs cleanly, and routines terminate on `ret`
(op 20) exactly where expected. Data in `data/isl-stage02.txt`.
Scanning the whole code region for the call encoding:
```
2846 call sites, 73 distinct built-ins used (of 147)
most used: 11 (×372), 69 (×255), 1 (×216), 64 (×213), 30 (×179), 20 (×167)
```
**The phase-control built-ins, located:**
| built-in | meaning | sites in Stage02 |
|---|---|---|
| **6** | end phase (`[ScriptPhase+196] = 1`) | **12** |
| **62** | force-end, skipping the end event | **3** |
| **39** | mark last phase (`[phase+300] = 2`) | **8** |
| 40 | `[phase+300] = 1` | 4 |
Twelve end-phase sites across three phases — so a phase has several exit paths,
which is what a mission with win *and* lose branches should look like.
Argument passing is visible in the disassembly: pairs of
`set.i k=01,02 <0> <value>` / `set.i k=02,03 <slot> <0>` stage arguments into
slots, then `call`. Floats are staged the same way — e.g. `40080000` = 3.0
immediately before several calls.
## ❔ What this does not settle
* **The 147 built-ins are uncharacterised.** Without them the disassembly is
structure without meaning: we can see *that* a phase ends here, not *what was
tested*. That is the remaining step to per-phase clear conditions.
* Opcodes 211 and 1318 are named only by handler address. The four-way sharing
(2/4/6/8 and 3/5/7/9) suggests the handler re-reads the opcode to pick a
comparison or a type, but that is not yet read.
* Operand *kinds* (4 of them) are not decoded — the `k=01,02` / `k=02,03` pairs
are recorded literally.
* The mission-level stream at `+0x24` of a `.ssb` — as opposed to this ISL
stream — is still only partly read.