Skip to main content

brink_format/
opcode.rs

1use core::fmt;
2
3use alloc::vec::Vec;
4
5use crate::codec::{
6    read_def_id, read_f32, read_i32, read_u8, read_u16, read_u32, write_def_id, write_f32,
7    write_i32, write_u8, write_u16, write_u32,
8};
9use crate::id::DefinitionId;
10
11// ── Discriminant bytes ──────────────────────────────────────────────────────
12
13// Stack & literals
14const PUSH_INT: u8 = 0x01;
15const PUSH_FLOAT: u8 = 0x02;
16const PUSH_BOOL: u8 = 0x03;
17const PUSH_STRING: u8 = 0x04;
18const PUSH_LIST: u8 = 0x05;
19const PUSH_DIVERT_TARGET: u8 = 0x06;
20const PUSH_NULL: u8 = 0x07;
21const POP: u8 = 0x08;
22const DUPLICATE: u8 = 0x09;
23
24// Arithmetic
25const ADD: u8 = 0x10;
26const SUBTRACT: u8 = 0x11;
27const MULTIPLY: u8 = 0x12;
28const DIVIDE: u8 = 0x13;
29const MODULO: u8 = 0x14;
30const NEGATE: u8 = 0x15;
31
32// Comparison
33const EQUAL: u8 = 0x20;
34const NOT_EQUAL: u8 = 0x21;
35const GREATER: u8 = 0x22;
36const GREATER_OR_EQUAL: u8 = 0x23;
37const LESS: u8 = 0x24;
38const LESS_OR_EQUAL: u8 = 0x25;
39
40// Logic
41const NOT: u8 = 0x28;
42const AND: u8 = 0x29;
43const OR: u8 = 0x2A;
44
45// Global vars
46const GET_GLOBAL: u8 = 0x30;
47const SET_GLOBAL: u8 = 0x31;
48
49// Temp vars
50const DECLARE_TEMP: u8 = 0x34;
51const GET_TEMP: u8 = 0x35;
52const SET_TEMP: u8 = 0x36;
53const GET_TEMP_RAW: u8 = 0x37;
54
55// Variable pointers
56const PUSH_VAR_POINTER: u8 = 0x38;
57const PUSH_TEMP_POINTER: u8 = 0x39;
58
59// Control flow
60const JUMP: u8 = 0x40;
61const JUMP_IF_FALSE: u8 = 0x41;
62const GOTO: u8 = 0x42;
63const GOTO_IF: u8 = 0x43;
64const GOTO_VARIABLE: u8 = 0x44;
65
66// Container flow
67const ENTER_CONTAINER: u8 = 0x48;
68const EXIT_CONTAINER: u8 = 0x49;
69
70// Functions / tunnels
71const CALL: u8 = 0x50;
72const RETURN: u8 = 0x51;
73const TUNNEL_CALL: u8 = 0x52;
74const TUNNEL_RETURN: u8 = 0x53;
75const TUNNEL_CALL_VARIABLE: u8 = 0x54;
76const CALL_VARIABLE: u8 = 0x55;
77
78// Threads
79const THREAD_CALL: u8 = 0x57;
80const THREAD_START: u8 = 0x58;
81const THREAD_DONE: u8 = 0x59;
82
83// Output
84const EMIT_LINE: u8 = 0x60;
85const EMIT_VALUE: u8 = 0x61;
86const EMIT_NEWLINE: u8 = 0x62;
87const SPRING: u8 = 0x67;
88const GLUE: u8 = 0x63;
89const BEGIN_TAG: u8 = 0x64;
90const END_TAG: u8 = 0x65;
91const EVAL_LINE: u8 = 0x66;
92const BEGIN_FRAGMENT: u8 = 0x68;
93const END_FRAGMENT: u8 = 0x69;
94const ATTACH_ELEMENT: u8 = 0x6A;
95const END_ELEMENT_RUN: u8 = 0x6B;
96
97// Choices
98const BEGIN_CHOICE: u8 = 0x72;
99const END_CHOICE: u8 = 0x73;
100// Sequences
101const SEQUENCE: u8 = 0x78;
102const SEQUENCE_BRANCH: u8 = 0x79;
103
104// Intrinsics
105const VISIT_COUNT: u8 = 0x80;
106const TURNS_SINCE: u8 = 0x81;
107const TURN_INDEX: u8 = 0x82;
108const CHOICE_COUNT: u8 = 0x83;
109const RANDOM: u8 = 0x84;
110const SEED_RANDOM: u8 = 0x85;
111const CURRENT_VISIT_COUNT: u8 = 0x86;
112
113// Casts / math
114const CAST_TO_INT: u8 = 0x90;
115const CAST_TO_FLOAT: u8 = 0x91;
116const FLOOR: u8 = 0x92;
117const CEILING: u8 = 0x93;
118const POW: u8 = 0x94;
119const MIN: u8 = 0x95;
120const MAX: u8 = 0x96;
121
122// External fns
123const CALL_EXTERNAL: u8 = 0xA0;
124
125// The fn-value verb layer (`docs/stdlib-spec.md` §4, issue #1679) — the pure
126// quartet `map`/`filter`/`fold`/`filter_map` plus the effectful spellings
127// `each`/`map_each`. ONE discriminant byte with a [`SeqVerbOp`] kind
128// immediate, the `Tower`/`Collect` economy applied a third time, sized for the
129// whole ruled family.
130//
131// **Why 0xA1 rather than the high tail.** The stdlib blocks above walked
132// `0xF7`-`0xFD` down to a single free byte (`0xFF`); `0xCB`/`0xCC` are held
133// for `StoreVarIfNew`/`EqVars`. `0xFF` is deliberately left unclaimed as the
134// escape byte a future extended-opcode prefix would want, so this family takes
135// the first byte of the next contiguous free run instead — `0xA1`-`0xAF`,
136// immediately after `CallExternal`.
137//
138//   0xA1 SeqVerb(kind)  the kind byte selects the verb; see [`SeqVerbOp`] for
139//                       each one's stack shape. Every kind takes its callback
140//                       as a function value (`FnRef`/`Closure`) and evaluates
141//                       it re-entrantly, exactly like `SeqSortedBy`.
142const SEQ_VERB: u8 = 0xA1;
143
144// v4 collection opcodes (`docs/format-v4-rfc.md` §3 "Collections (T1a)") —
145// numeric assignments frozen by the §9 one-bump rule, contiguous and
146// adjacent to the existing List ops block below. Live as of T1b-2 (#570):
147// `Opcode` variants + VM execution exist for the whole block, though only a
148// subset (`ArrayNew`, `MapNew`, `IndexGet`, `IndexSet`, `Len`, `Keys`,
149// `PushLiteral`) is emitted by the compiler in T1b-2 — the map-mutator ops
150// (`MapGet`, `MapInsert`, `MapRemove`, `MapContains`, `Values`) become
151// compiler-reachable when the stdlib slice (T1b-3, `docs/t1b-surface-spec.md`
152// §5) lands, matching the RFC's "inert until each milestone's compiler work
153// emits them" discipline.
154//   0xBE ArrayNew(n)     0xBF MapNew(n)        0xC0 IndexGet
155//   0xC1 IndexSet        0xC2 Len              0xC3 MapGet
156//   0xC4 MapInsert       0xC5 MapRemove        0xC6 MapContains
157//   0xC7 Keys            0xC8 Values           0xC9 PushLiteral(u32)
158const ARRAY_NEW: u8 = 0xBE;
159const MAP_NEW: u8 = 0xBF;
160const INDEX_GET: u8 = 0xC0;
161const INDEX_SET: u8 = 0xC1;
162const COLLECTION_LEN: u8 = 0xC2;
163const MAP_GET: u8 = 0xC3;
164const MAP_INSERT: u8 = 0xC4;
165const MAP_REMOVE: u8 = 0xC5;
166const MAP_CONTAINS: u8 = 0xC6;
167const COLLECTION_KEYS: u8 = 0xC7;
168const COLLECTION_VALUES: u8 = 0xC8;
169const PUSH_LITERAL: u8 = 0xC9;
170// `PushLiteral(u32)` is the T1b `LiteralPool` reference opcode (RFC §2).
171// `PushList`/`ListLiterals` are unaffected by this PR — the RFC's absorption
172// of `ListLiterals` into `LiteralPool` is a separate, larger migration (see
173// PR description scopeNotes) that would require regenerating every checked-in
174// oracle `.inkb` fixture; out of scope here by construction (nothing in this
175// PR touches `PushList`/`ListLiterals` emission or decoding).
176//
177// Sharing-discipline ops (`TakeVar`, `StoreVarIfNew`, `EqVars`) and later
178// Tier-1 groups (functions, handles, projections, records) remain named in
179// the RFC but out of this reservation — each gets its own contiguous block,
180// numbered when its own issue lands.
181
182// v4 sharing-discipline opcodes (`docs/format-v4-rfc.md` §3 "Sharing
183// discipline (T1a)"; semantics in `docs/value-model-spec.md` §5/§6) —
184// numeric assignments frozen by the §9 one-bump rule, contiguous and
185// adjacent to the collection block above. Live as of T1b-4 (#576):
186//   0xCA TakeGlobal(DefinitionId)   0xCB StoreVarIfNew (reserved)
187//   0xCC EqVars(a, b) (reserved)    0xCD TakeTemp(u16)
188// `TakeGlobal`/`TakeTemp` are the RFC's generic `TakeVar(slot)` split into
189// its two concrete slot kinds (global `DefinitionId` vs temp `u16` — they
190// don't share an operand encoding, so one opcode can't cover both): each
191// moves the slot's current value out and leaves `Value::Null` behind — the
192// take-half of the take → `make_mut` → write-back RMW discipline (spec §5)
193// that closes the indexed-write COW cliff. `TakeTemp` mirrors `GetTemp`'s
194// pointer auto-dereference (`ref` params): if the temp holds a
195// `VariablePointer`/`TempPointer`, the *pointed-to* location is taken, not
196// the pointer value itself (see `vm.rs`'s `Opcode::TakeTemp` arm).
197// `TakeGlobal` does not auto-dereference — `GetGlobal`/`SetGlobal` don't
198// either, since ref-params live in temps, never in globals themselves.
199// `0xCD` is claimed fresh, adjacent to this block, rather than reusing
200// `0xCB`/`0xCC` — those stay reserved for `StoreVarIfNew`/`EqVars` exactly
201// as the RFC named them; splitting `TakeVar` doesn't touch their
202// numbering. `StoreVarIfNew` and `EqVars` remain reserved (comments only,
203// no `Opcode` variants — `decode`'s catch-all keeps rejecting both bytes)
204// — the optional ref-collapsing sites from spec §6 (store-time keep-old-Arc
205// cutoff; fused compare with optional collapse on structural equality),
206// pure peephole optimizations, never required for correctness. Later
207// Tier-1 groups (functions, handles, projections, records) remain named in
208// the RFC but out of this reservation — each gets its own contiguous
209// block, numbered when its own issue lands.
210const TAKE_GLOBAL: u8 = 0xCA;
211const TAKE_TEMP: u8 = 0xCD;
212
213// v4 record opcodes (TM-4, `docs/typed-mode-spec.md` §6; named but
214// numerically unallocated in `docs/format-v4-rfc.md` §3 — "design the exact
215// encoding against the reserved space" — assigned here) — contiguous and
216// adjacent to the sharing-discipline block above:
217//   0xCE RecordNew(ShapeId)      0xCF RecordGetDyn(NameId)
218//   0xD0 RecordSetDyn(NameId)    0xD1 RecordGet(offset)
219//   0xD2 RecordSet(offset)
220// `RecordNew`/`RecordGetDyn`/`RecordSetDyn` (PR #620/TM-4 foundation) are the
221// by-name field ops every dialect can use correctly. `RecordGet`/`RecordSet`
222// (TM-4c, #666) are the static-offset field ops, the strict-mode-only
223// performance payoff typed-mode-spec §6 anticipates: `brink-ir`'s LIR
224// lowering only emits them when a field access's record shape is proven at
225// compile time (see `docs/typed-mode-spec.md` §6 and the TM-4c PR
226// description) — the operand is a flat `u16` index into the record's own
227// field vector, checked only against that vector's bounds at runtime (no
228// shape re-verification — the "offset" payoff is skipping exactly that
229// lookup), so out-of-range is a turn-terminating fault
230// (`RuntimeError::RecordFieldOffsetOutOfRange`), never UB/panic.
231const RECORD_NEW: u8 = 0xCE;
232const RECORD_GET_DYN: u8 = 0xCF;
233const RECORD_SET_DYN: u8 = 0xD0;
234const RECORD_GET: u8 = 0xD1;
235const RECORD_SET: u8 = 0xD2;
236
237// TM-3 completion conversion intrinsics (`docs/typed-mode-spec.md` §4,
238// maintainer ruling 2026-07-13, issue #659) — contiguous and adjacent to the
239// record block above, this PR's own reservation (no prior RFC allocation for
240// these three; the record block's own "assigned here" precedent applies).
241const CONVERT_INT: u8 = 0xD3;
242const CONVERT_FLOAT: u8 = 0xD4;
243const CONVERT_STRING: u8 = 0xD5;
244
245// Function-value opcodes (T1c, `docs/format-v4-rfc.md` §3 "Functions" —
246// named there, numerically unallocated; assigned here, contiguous and
247// adjacent to the conversion block above, this PR's own reservation). First
248// live emission of the reserved function-value surface (`docs/t1c-spec.md`
249// §11 T1c-2).
250//   0xD6 PushFnRef(DefinitionId)          0xD7 MakeClosure(env descriptor)
251//   0xD8 CallValue(argc)                   0xD9 BindValue(argc)
252// `PushFnRef` pushes the zero-bound `Value::FnRef`. `MakeClosure`'s operand is
253// the target `DefinitionId` then a u16-counted descriptor of `{NameId, kind
254// u8 (0=val,1=ref)}` entries — one per bound arg, in declared order — and it
255// pops that many values off the stack (bound in order) to build a
256// `Value::Closure`. `CallValue(argc)` pops the callee (top of stack) then the
257// `argc` supplied (val-only) args below it, dispatching through the function
258// value (`docs/t1c-spec.md` §3): non-function callee / wrong arity /
259// rehydration mismatch / cross-flow ref-`#@local` are turn-terminating faults.
260// `BindValue(argc)` (T1c-3, `bind(f, args…)` stdlib intrinsic) pops the callee
261// (top of stack) then the `argc` supplied (val-only) args below it and returns
262// a *new* function value with those args appended to the callee's bound-arg
263// row (val-only currying — consuming the head of the remaining param row). The
264// newly bound entries take their name/mode from the target's own signature at
265// the appended positions (always `val`, since `ref` params are bound away at
266// creation). Faults (turn-terminating): callee is not a function value;
267// binding more args than the target has remaining params.
268const PUSH_FN_REF: u8 = 0xD6;
269const MAKE_CLOSURE: u8 = 0xD7;
270const CALL_VALUE: u8 = 0xD8;
271const BIND_VALUE: u8 = 0xD9;
272
273// Projection opcodes (T1e, `docs/format-v4-rfc.md` §3 "Projections" — named
274// there, numerically unallocated; assigned here, contiguous and adjacent to
275// the function-value block above, this PR's own reservation). First live
276// emission of the reserved projection surface (`docs/t1e-spec.md` §3/§8
277// T1e-2).
278//   0xDA MakeProjection(root, segment_count)   0xDB ProjRead
279//   0xDC ProjWrite
280// `MakeProjection` pops `segment_count` values off the stack (pushed by
281// codegen in source order; the VM's LIFO pop collects them reversed, then
282// reverses once more to restore source order — same shape `MakeClosure`'s
283// bound-arg row uses) and classifies each into a `ProjSegment` (`Int` →
284// `Index`, else → `Key`,
285// `docs/format-v4-rfc.md` §1), building a `Value::Projection` rooted at
286// `root`. `ProjRead`/`ProjWrite` implement the spec's root-cell RMW
287// discipline (take root → walk → `make_mut` spine → write → store back);
288// both fault `RuntimeError::ProjectionInvalidated` on a path that no longer
289// resolves (shrunk array, missing key, removed struct field — spec §1(2)).
290// The compiler's own emission path for dereferencing a projection-bound
291// `ref` parameter reuses the *same* underlying walk (`brink_runtime::proj_ops`)
292// through `GetTemp`/`SetTemp`/`TakeTemp`'s additive `Value::Projection`
293// dispatch arm rather than interleaving these bytes at every param access —
294// see those opcodes' VM dispatch for the shared implementation. `ProjRead`/
295// `ProjWrite` remain real, independently encodable/dispatchable opcodes.
296const MAKE_PROJECTION: u8 = 0xDA;
297const PROJ_READ: u8 = 0xDB;
298const PROJ_WRITE: u8 = 0xDC;
299
300// `char_at(s, i)` stdlib pure function (T1b stdlib slice 1 completion, issue
301// #857) — contiguous and adjacent to the projection block above, this PR's
302// own reservation (no prior RFC allocation for this one; same "assigned
303// here" precedent as the record/conversion/function-value/projection blocks
304// above it). Pops `i` then `s`, pushes the single-character `String` at
305// Unicode-scalar-value index `i` (chars, not UTF-8 bytes — author sanity per
306// the issue). Turn-terminating faults (value-model-spec §11c): `s` isn't a
307// `String` (`RuntimeError::NotIndexable`); `i` isn't an `Int`
308// (`RuntimeError::CharAtIndexNotInt`); `i` outside `[0, char_count)`
309// (`RuntimeError::CharAtOutOfBounds`, `len` = char count).
310const CHAR_AT: u8 = 0xDD;
311
312// NS-A1 Option[T] + the ruled stdlib flips (`docs/stdlib-spec.md`
313// §1.1/§1.4, §§3-5; `docs/stdlib-sequencing.md` §2 Wave A1) — this PR's own
314// reservation, same "assigned here" precedent as the record/conversion/
315// function-value/projection/char_at blocks above. `PUSH_NONE`/`MAKE_SOME`
316// take the two bytes remaining before the string-eval block (0xE0/0xE1);
317// the verb flips continue contiguously after it at 0xE2.
318//
319// Option construction:
320//   0xDE PushNone   `[]` → `none`  (`Value::OptionVal(None)`)
321//   0xDF MakeSome   `[x]` → `some(x)` — total over every value
322//
323// The verb flips, all brink-dialect intrinsics returning `Option` (absence
324// = `none`, never a fault; malformed *questions* — wrong container type,
325// unorderable elements — stay turn-terminating faults, the ruled
326// fault-vs-absence doctrine):
327//   0xE2 StrFind         `[s, sub]` → `Option[int]` (USV index, not bytes)
328//   0xE3 SeqIndexOf      `[a, x]` → `Option[int]` (structural equality)
329//   0xE4 SeqMin          `[a]` → `Option[T]` (empty → none)
330//   0xE5 SeqMax          `[a]` → `Option[T]`
331//   0xE6 SeqFirst        `[a]` → `Option[T]`
332//   0xE7 SeqLast         `[a]` → `Option[T]`
333//   0xE8 SeqPop          `[a]` → pushes `Option[T]` (popped element or
334//                        none), then the shrunk array on top — codegen
335//                        brackets it Take*/SeqPop/Set* so the array writes
336//                        back to its root cell and the Option remains as
337//                        the expression value
338//   0xE9 MapGetOpt       `[m, k]` → `Option[V]` (missing key → none; a
339//                        non-scalar key is a fault — malformed question)
340//   0xEA MapContainsValue `[m, v]` → `Bool` (content-equality scan, O(n))
341//   0xEB MapClear        `[m]` → empty map (statement-only mutator;
342//                        in-place-ness comes from the RMW write-back)
343//
344// `SeqMin`/`SeqMax` order int/float (numeric promotion), bool, string for
345// now, with float NaN placed by the ruled PROD pinned order (§4b: NaN
346// greater than everything, NaN-vs-NaN ties, -0 == +0). The dev-mode
347// NaN-fault and the full orderable roster (arrays-lexicographic, compare
348// protocol) land with wave A4 — the rows are mode-independent either way.
349const PUSH_NONE: u8 = 0xDE;
350const MAKE_SOME: u8 = 0xDF;
351const STR_FIND: u8 = 0xE2;
352const SEQ_INDEX_OF: u8 = 0xE3;
353const SEQ_MIN: u8 = 0xE4;
354const SEQ_MAX: u8 = 0xE5;
355const SEQ_FIRST: u8 = 0xE6;
356const SEQ_LAST: u8 = 0xE7;
357const SEQ_POP: u8 = 0xE8;
358const MAP_GET_OPT: u8 = 0xE9;
359const MAP_CONTAINS_VALUE: u8 = 0xEA;
360const MAP_CLEAR: u8 = 0xEB;
361
362// NS-A6 rand verbs (`docs/stdlib-spec.md` §7; this PR's own reservation,
363// same "assigned here" precedent as the NS-A1 block above): the four draw
364// ops fill the remaining bytes before the lifecycle block (0xF0+),
365// contiguously after NS-A1's 0xEB. `seed(n)` reuses the frozen
366// `SEED_RANDOM` byte (0x85) — one cell, two surfaces, no drift.
367const RAND_FLOAT: u8 = 0xEC;
368const RAND_CHANCE: u8 = 0xED;
369const RAND_PICK: u8 = 0xEE;
370const RAND_SHUFFLE: u8 = 0xEF;
371
372// NS-A5 range ops (`docs/stdlib-spec.md` §7, F7 ruled 2026-07-19; this
373// PR's own reservation). The 0xEC-0xEF block is full and 0xF0-0xF3 are
374// lifecycle, so these take the next free bytes after the lifecycle block's
375// tail. Two construction ops rather than one flag-operand op keeps the
376// whole rand/range family operand-free (disasm and roundtrip stay
377// table-driven). `rand::int` deliberately has NO byte here — it rides the
378// existing `CONVERT_INT` (0xE2): `int(x)` is ONE value-directed verb whose
379// range leg is the draw (see `brink-runtime::vm`'s `ConvertInt` dispatch).
380const RANGE_MAKE_EXCL: u8 = 0xF4;
381const RANGE_MAKE_INCL: u8 = 0xF5;
382const RANGE_NON_EMPTY: u8 = 0xF6;
383
384// NS-A8 numeric tower (`docs/tower-mini-spec.md`, issue #1114; this PR's own
385// reservation, same "assigned here" precedent as the NS-A1/NS-A6/NS-A5
386// blocks above). ONE discriminant byte with a `u8` kind immediate —
387// deliberate opcode-space economy: after NS-A5 the free one-byte space is
388// down to 0xF7-0xFD + 0xFF (0xF0-0xF3 are lifecycle, 0xF4-0xF6 the range
389// ops, 0xFE debug), so the tower's thirteen operations share a single
390// `Tower(TowerOp)` opcode instead of eating most of what remains. Wire:
391// `0xF7`, then the [`TowerOp`] kind byte (a `SequenceKind`-style closed
392// sub-enum; unknown kinds are a decode error, the same reserved-tag
393// discipline as everywhere else).
394const TOWER: u8 = 0xF7;
395
396// NS-A4 ordering verbs (`docs/stdlib-spec.md` §4b, issue #1110; this PR's
397// own reservation, same "assigned here" precedent as the NS-A1/A5/A6/A8
398// blocks above). Two ops serve all four source verbs: `sort(a)` /
399// `sorted(a)` share `SeqSorted` (in-place-ness comes from the RMW
400// write-back, the `shuffle`/`shuffled` precedent), and `sort_by(a, cmp)` /
401// `sorted_by(a, cmp)` share `SeqSortedBy`. NaN placement is
402// mode-dependent (§4b: dev fault / prod pinned order) — the *mode* lives
403// in the runtime (host knob), never in the bytecode, so one compiled
404// story serves both modes.
405const SEQ_SORTED: u8 = 0xF8;
406const SEQ_SORTED_BY: u8 = 0xF9;
407
408// NS-A7 collections+ (`docs/stdlib-spec.md` §8, issue #1113; this PR's own
409// reservation, same "assigned here" precedent as the NS-A1/A4/A5/A6/A8
410// blocks above). ONE discriminant byte with a `u8` kind immediate — the
411// NS-A8 `Tower` economy applied again: after NS-A4 the free one-byte space
412// is down to 0xFA-0xFD + 0xFF, so the five `Weighted`/heap operations share
413// a single `Collect(CollectOp)` opcode instead of eating all of it. Wire:
414// `0xFA`, then the [`CollectOp`] kind byte (a `TowerOp`-style closed
415// sub-enum; unknown kinds are a decode error, the same reserved-tag
416// discipline as everywhere else). All five kinds are operand-free:
417// `weighted_new` takes its flattened pair row as ONE array value built by
418// the preceding `ArrayNew` (a transient codegen artifact), so disasm and
419// roundtrip stay table-driven — the NS-A5 "keep the family operand-free"
420// discipline.
421const COLLECT: u8 = 0xFA;
422
423// B1 `or`-coalescing (`docs/stdlib-spec.md` §1.6a, `docs/decision-log.md`
424// "Option[T] ruled" 2026-07-18; issue #1460 — this PR's own reservation,
425// same "assigned here" precedent as the blocks above). Four free bytes
426// remained after NS-A7 (`0xFB`-`0xFD` + `0xFF`); this claims the first.
427// Native-surface-only: the surface spelling reuses the literal `or`
428// keyword, but `InfixOp::Or` (ink's boolean `||`, oracle-frozen) is left
429// untouched — `InfixOp::Coalesce` is a distinct HIR op that native
430// lowering alone produces, so this opcode is unreachable from the
431// oracle-covered brink/ink dialects.
432//
433// **RETIRED (issue #1471): the binary `Coalesce` opcode this byte
434// originally held is gone.** It evaluated both operands unconditionally
435// before combining them — an unruled implementation decision (PR
436// #1469/#1460) the maintainer then ruled must short-circuit instead: `x or
437// expensive()` may only run `expensive()` when `x` is `none`. A binary
438// opcode cannot do that (both operands are already on the stack by the time
439// it runs), so `InfixOp::Coalesce` lowers to a real branch
440// (`lir::Expr::Coalesce`) instead of the generic `Infix` → binary-opcode
441// path every other operator uses. This byte is reused for the branch
442// primitive that replaces it:
443//
444//   0xFB CoalesceSome(rel)  pops `lhs` (must be `OptionVal`); `some(v)`
445//                  pushes the unwrapped `v` and jumps `rel` bytes forward
446//                  (same relative-offset convention as `Jump`/
447//                  `JumpIfFalse`); `none` pushes nothing and falls through
448//                  to the very next instruction, which evaluates `rhs` —
449//                  the short-circuit itself: `rhs`'s bytecode is simply
450//                  never reached when `lhs` is `some`. Codegen emits a
451//                  `MakeSome` at the jump target when — and only when — the
452//                  analyzer recorded `CoalesceShape::PreserveOption` for
453//                  that step, re-wrapping the unwrapped value so both
454//                  branches agree on shape at the join.
455const COALESCE_SOME: u8 = 0xFB;
456
457// B1b the `as` binding (`docs/decision-log.md` 2026-07-26 "The `as`
458// binding: one construct, both condition positions, `{if}` spelling";
459// issue #1475 — this PR's own reservation, same "assigned here" precedent
460// as the blocks above). Three free bytes remained after B1's `COALESCE_SOME`
461// (`0xFC`-`0xFD` + `0xFF`); this claims the first. Native-surface-only:
462// nothing in the ink/brink dialects has an `as` binding to lower.
463//
464//   0xFC OptionBind(slot)  `[opt]` → `[bool]`: `opt` must be an
465//                          `OptionVal`. `some(v)` writes `v` — the
466//                          UNWRAPPED payload, typed `T` — into temp
467//                          `slot` and pushes `true`; `none` leaves the
468//                          slot untouched and pushes `false`. A
469//                          non-`OptionVal` operand is a runtime fault
470//                          (`RuntimeError::AsBindingNotOption`), the
471//                          gradual-mode residual of the strict-mode `E147`
472//                          the checker raises for the same shape.
473//
474// Test-and-bind is deliberately ONE op rather than a dup/compare/unwrap
475// sequence: it keeps the whole binding inside condition evaluation, which
476// is what makes `while EXPR as n { … }` rebind per iteration for free and
477// lets an inline `{if EXPR as n: …}` bind without hoisting a statement out
478// of its content line. The write is a plain frame-local store with no
479// `ref`/pointer write-through (unlike `SetTemp`): an `as` binding always
480// declares a FRESH slot, so it can never alias a `ref` parameter's cell.
481const OPTION_BIND: u8 = 0xFC;
482
483// Seq `remove_at` (issue #1484, decision log "Quick-docket closures"
484// 2026-07-26; `docs/stdlib-spec.md` §4/§10 — this PR's own reservation,
485// same "assigned here" precedent as the blocks above). `OptionBind` claimed
486// the first of the three bytes `Coalesce`'s comment noted free
487// (`0xFC`-`0xFD` + `0xFF` remained after `0xFB`); this claims the next
488// (`0xFD`). `MapRemove` (`0xC5`) is restricted to maps as of this PR — the
489// array-index leg it used to generalize over moves here under its own name,
490// joining the `_at` faulting-index family with `CharAt`.
491//
492//   0xFD SeqRemoveAt  `[a, i]` → updated array with the element at `i`
493//                     removed (shifts later elements left). Turn-terminating
494//                     fault on `i` out of `[0, len)` (`IndexOutOfBounds`,
495//                     matching `IndexGet`/`IndexSet`) or a non-array `a`
496//                     (`NotIndexable`).
497const SEQ_REMOVE_AT: u8 = 0xFD;
498
499// List ops
500const LIST_CONTAINS: u8 = 0xB0;
501const LIST_NOT_CONTAINS: u8 = 0xB1;
502const LIST_INTERSECT: u8 = 0xB2;
503const LIST_ALL: u8 = 0xB5;
504const LIST_INVERT: u8 = 0xB6;
505const LIST_COUNT: u8 = 0xB7;
506const LIST_MIN: u8 = 0xB8;
507const LIST_MAX: u8 = 0xB9;
508const LIST_VALUE: u8 = 0xBA;
509const LIST_RANGE: u8 = 0xBB;
510const LIST_FROM_INT: u8 = 0xBC;
511const LIST_RANDOM: u8 = 0xBD;
512
513// Lifecycle
514const DONE: u8 = 0xF0;
515const YIELD: u8 = 0xF3;
516const END: u8 = 0xF1;
517const NOP: u8 = 0xF2;
518
519// String eval
520const BEGIN_STRING_EVAL: u8 = 0xE0;
521const END_STRING_EVAL: u8 = 0xE1;
522
523// Debug
524const SOURCE_LOCATION: u8 = 0xFE;
525
526// ── Types ───────────────────────────────────────────────────────────────────
527
528/// The tower operation selected by an [`Opcode::Tower`] instruction's kind
529/// byte (NS-A8, `docs/tower-mini-spec.md`; `docs/stdlib-spec.md` §2b).
530///
531/// Constructors pop their lanes/columns and push the built value; verbs pop
532/// their operands and push the result. All semantics are glam's (T3:
533/// conventions per glam, wholesale); all operations are pure. The
534/// `+`/`-`/`*` operator family does NOT live here — it rides the existing
535/// `Add`/`Subtract`/`Multiply`/`Negate` opcodes via `value_ops::binary_op`'s
536/// tower arms.
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
538pub enum TowerOp {
539    /// `[x, y]` → `vec2`. Numeric lanes (ints promote to f32).
540    MakeVec2,
541    /// `[x, y, z]` → `vec3`.
542    MakeVec3,
543    /// `[x, y, z, w]` → `vec4`.
544    MakeVec4,
545    /// `[x, y, z, w]` → `quat` — raw lanes, glam `Quat::from_xyzw`.
546    MakeQuat,
547    /// `[col0, col1]` → `mat2` from `vec2` columns (column-major, T3).
548    MakeMat2,
549    /// `[col0, col1, col2]` → `mat3` from `vec3` columns.
550    MakeMat3,
551    /// `[col0, col1, col2, col3]` → `mat4` from `vec4` columns.
552    MakeMat4,
553    /// `[a, b]` → `float` — dot product of two same-size vectors.
554    Dot,
555    /// `[a, b]` → `vec3` — cross product, `vec3` only.
556    Cross,
557    /// `[a, b]` → componentwise minimum of two same-kind vectors.
558    Min,
559    /// `[a, b]` → componentwise maximum of two same-kind vectors.
560    Max,
561    /// `[x, lo, hi]` → componentwise clamp of three same-kind vectors.
562    Clamp,
563    /// `[a, b, t]` → linear interpolation with scalar `t`: vectors
564    /// componentwise, quats via glam's normalizing `Quat::lerp`.
565    Lerp,
566}
567
568impl TowerOp {
569    /// Every tower op, in kind-byte order (tests + tooling).
570    pub const ALL: [TowerOp; 13] = [
571        Self::MakeVec2,
572        Self::MakeVec3,
573        Self::MakeVec4,
574        Self::MakeQuat,
575        Self::MakeMat2,
576        Self::MakeMat3,
577        Self::MakeMat4,
578        Self::Dot,
579        Self::Cross,
580        Self::Min,
581        Self::Max,
582        Self::Clamp,
583        Self::Lerp,
584    ];
585
586    fn to_byte(self) -> u8 {
587        match self {
588            Self::MakeVec2 => 0,
589            Self::MakeVec3 => 1,
590            Self::MakeVec4 => 2,
591            Self::MakeQuat => 3,
592            Self::MakeMat2 => 4,
593            Self::MakeMat3 => 5,
594            Self::MakeMat4 => 6,
595            Self::Dot => 7,
596            Self::Cross => 8,
597            Self::Min => 9,
598            Self::Max => 10,
599            Self::Clamp => 11,
600            Self::Lerp => 12,
601        }
602    }
603
604    fn from_byte(b: u8) -> Result<Self, DecodeError> {
605        match b {
606            0 => Ok(Self::MakeVec2),
607            1 => Ok(Self::MakeVec3),
608            2 => Ok(Self::MakeVec4),
609            3 => Ok(Self::MakeQuat),
610            4 => Ok(Self::MakeMat2),
611            5 => Ok(Self::MakeMat3),
612            6 => Ok(Self::MakeMat4),
613            7 => Ok(Self::Dot),
614            8 => Ok(Self::Cross),
615            9 => Ok(Self::Min),
616            10 => Ok(Self::Max),
617            11 => Ok(Self::Clamp),
618            12 => Ok(Self::Lerp),
619            _ => Err(DecodeError::InvalidTowerOp(b)),
620        }
621    }
622
623    /// The `.inkt` mnemonic for this kind (also the `program_model`
624    /// disassembly text). Stable, boring, `snake_case`.
625    #[must_use]
626    pub fn mnemonic(self) -> &'static str {
627        match self {
628            Self::MakeVec2 => "make_vec2",
629            Self::MakeVec3 => "make_vec3",
630            Self::MakeVec4 => "make_vec4",
631            Self::MakeQuat => "make_quat",
632            Self::MakeMat2 => "make_mat2",
633            Self::MakeMat3 => "make_mat3",
634            Self::MakeMat4 => "make_mat4",
635            Self::Dot => "dot",
636            Self::Cross => "cross",
637            Self::Min => "tower_min",
638            Self::Max => "tower_max",
639            Self::Clamp => "tower_clamp",
640            Self::Lerp => "tower_lerp",
641        }
642    }
643
644    /// Inverse of [`mnemonic`](Self::mnemonic) for the `.inkt` reader.
645    #[must_use]
646    pub fn from_mnemonic(s: &str) -> Option<Self> {
647        Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
648    }
649}
650
651/// The collections+ operation selected by an [`Opcode::Collect`]
652/// instruction's kind byte (NS-A7, `docs/stdlib-spec.md` §8, issue #1113).
653///
654/// `Weighted[T]` construction plus `rand::roll`, and the humble heap —
655/// verbs over ordinary arrays, min-heap, ordering per the ruled §4b
656/// doctrine (`total_order_cmp`, the one comparison core). `RandRoll` is
657/// the only draw (one RNG-cell write); everything else is placement or
658/// pure reads.
659#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
660pub enum CollectOp {
661    /// `[pairs]` → `Weighted[T]` — pops ONE array of flattened
662    /// `weight, value, weight, value, …` entries (built by the preceding
663    /// `ArrayNew`; a transient codegen artifact, never observable).
664    /// Evidence-by-construction (§8): faults on a malformed pair row, a
665    /// non-int weight, or a non-positive weight — so a `Weighted` that
666    /// exists is always rollable.
667    WeightedNew,
668    /// `[w]` → `T` — one weighted draw from a `Weighted[T]` table. Total
669    /// over any table that exists (construction is the validator); writes
670    /// the RNG cell like every draw.
671    RandRoll,
672    /// `[a, x]` → `[a']` — push `x` into the min-heap maintained over the
673    /// array (sift-up; in-place-ness comes from the RMW write-back, the
674    /// `SeqSorted` precedent). §4b entry-check: DEV faults on a NaN in
675    /// the entering element; PROD places it by the pinned total order.
676    HeapPush,
677    /// `[a]` → pushes `Option[T]` (the extracted minimum, `none` on
678    /// empty), then the shrunk re-heapified array on top of it — the
679    /// `SeqPop` stack contract, so the codegen take/store bracket writes
680    /// the array back and leaves the Option as the expression value.
681    HeapPop,
682    /// `[a]` → `Option[T]` — the minimum without extraction (`none` on
683    /// empty). Pure read.
684    HeapPeek,
685}
686
687impl CollectOp {
688    /// Every collections+ op, in kind-byte order (tests + tooling).
689    pub const ALL: [CollectOp; 5] = [
690        Self::WeightedNew,
691        Self::RandRoll,
692        Self::HeapPush,
693        Self::HeapPop,
694        Self::HeapPeek,
695    ];
696
697    fn to_byte(self) -> u8 {
698        match self {
699            Self::WeightedNew => 0,
700            Self::RandRoll => 1,
701            Self::HeapPush => 2,
702            Self::HeapPop => 3,
703            Self::HeapPeek => 4,
704        }
705    }
706
707    fn from_byte(b: u8) -> Result<Self, DecodeError> {
708        match b {
709            0 => Ok(Self::WeightedNew),
710            1 => Ok(Self::RandRoll),
711            2 => Ok(Self::HeapPush),
712            3 => Ok(Self::HeapPop),
713            4 => Ok(Self::HeapPeek),
714            _ => Err(DecodeError::InvalidCollectOp(b)),
715        }
716    }
717
718    /// The `.inkt` mnemonic for this kind (also the `program_model`
719    /// disassembly text). Stable, boring, `snake_case`.
720    #[must_use]
721    pub fn mnemonic(self) -> &'static str {
722        match self {
723            Self::WeightedNew => "weighted_new",
724            Self::RandRoll => "rand_roll",
725            Self::HeapPush => "heap_push",
726            Self::HeapPop => "heap_pop",
727            Self::HeapPeek => "heap_peek",
728        }
729    }
730
731    /// Inverse of [`mnemonic`](Self::mnemonic) for the `.inkt` reader.
732    #[must_use]
733    pub fn from_mnemonic(s: &str) -> Option<Self> {
734        Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
735    }
736}
737
738/// The fn-value verb selected by an [`Opcode::SeqVerb`] instruction's kind
739/// byte (`docs/stdlib-spec.md` §4, issue #1679).
740///
741/// The **pure quartet**: `map`, `filter`, `fold`, `filter_map`. Callbacks are
742/// pure·silent by the 2026-07-18 ruling, which is what dissolves the
743/// eager/lazy question — "one logical pass, order unobservable; the
744/// implementation may fuse freely." Every kind evaluates its callback
745/// re-entrantly with output isolated, the `SeqSortedBy` shape; a callback
746/// that yields, presents a choice, calls a host external, or diverges is a
747/// turn-terminating fault.
748///
749/// The ruled **effectful spellings**: `each`, `map_each`. Slice 2 of the
750/// same issue — deliberately the opposite runtime contract: output reaches
751/// the transcript instead of being captured, and the dev-mode world-write
752/// guard is disarmed for their callback (`docs/stdlib-spec.md` §4: "the weird
753/// thing gets the ugly method" — friction lives in the name, not in an
754/// enforcement gate). Sequential in iteration order, never fused. They are
755/// deliberately NOT gated by E119 (`brink_analyzer::comparator_contract`) —
756/// their whole purpose is to be the legal home for the effects the pure
757/// quartet's callbacks may not perform.
758#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
759pub enum SeqVerbOp {
760    /// `[a, f]` → `[a']` — the array of `f(x)` for each element, in
761    /// iteration order. `f: fn(T): U`.
762    Map,
763    /// `[a, pred]` → `[a']` — the elements for which `pred(x)` is `true`,
764    /// in iteration order. `pred: fn(T): bool`; a non-bool return is a
765    /// turn-terminating fault.
766    Filter,
767    /// `[a, init, f]` → `[acc]` — left fold: `acc` starts at `init` and
768    /// becomes `f(acc, x)` for each element in iteration order.
769    /// `f: fn(U, T): U`.
770    Fold,
771    /// `[a, f]` → `[a']` — the Option-mapper: `f(x)` for each element, kept
772    /// unwrapped when `some(v)`, dropped when `none`, in iteration order.
773    /// `f: fn(T): Option[U]`; a non-Option return is a turn-terminating
774    /// fault. Still pure·silent-required — the natural companion of `map`
775    /// under the §1.4 Option ruling, not a relaxation.
776    FilterMap,
777    /// `[a, f]` → `[null]` — the effectful "do something per element, no
778    /// result" spelling: `f(x)` runs for each element, in iteration order,
779    /// for its side effects; the return value is discarded. `f: fn(T)`.
780    /// Effectful: writes and emitted output are legal.
781    Each,
782    /// `[a, f]` → `[a']` — the effectful transform: the array of `f(x)` for
783    /// each element, in iteration order, sequential and never fused; `f`
784    /// may write/emit. `f: fn(T): U`. `map`'s ugly, honest twin.
785    MapEach,
786}
787
788impl SeqVerbOp {
789    /// Every fn-value verb, in kind-byte order (tests + tooling).
790    pub const ALL: [SeqVerbOp; 6] = [
791        Self::Map,
792        Self::Filter,
793        Self::Fold,
794        Self::FilterMap,
795        Self::Each,
796        Self::MapEach,
797    ];
798
799    fn to_byte(self) -> u8 {
800        match self {
801            Self::Map => 0,
802            Self::Filter => 1,
803            Self::Fold => 2,
804            Self::FilterMap => 3,
805            Self::Each => 4,
806            Self::MapEach => 5,
807        }
808    }
809
810    fn from_byte(b: u8) -> Result<Self, DecodeError> {
811        match b {
812            0 => Ok(Self::Map),
813            1 => Ok(Self::Filter),
814            2 => Ok(Self::Fold),
815            3 => Ok(Self::FilterMap),
816            4 => Ok(Self::Each),
817            5 => Ok(Self::MapEach),
818            _ => Err(DecodeError::InvalidSeqVerbOp(b)),
819        }
820    }
821
822    /// The source spelling of this verb — also the `.inkt` mnemonic and the
823    /// `program_model` disassembly text, and the verb name runtime faults
824    /// report. Stable, boring, `snake_case`.
825    #[must_use]
826    pub fn mnemonic(self) -> &'static str {
827        match self {
828            Self::Map => "map",
829            Self::Filter => "filter",
830            Self::Fold => "fold",
831            Self::FilterMap => "filter_map",
832            Self::Each => "each",
833            Self::MapEach => "map_each",
834        }
835    }
836
837    /// Whether this verb's callback runs under the pure·silent contract
838    /// (E119-gated, output captured, dev-mode world-write guard armed) or
839    /// the effectful contract (`each`/`map_each`: output reaches the
840    /// transcript, writes are legal). The VM's `seq_map`/`seq_filter`/
841    /// `seq_fold`/`seq_filter_map`/`seq_each`/`seq_map_each` each read this
842    /// directly when entering their callback scope
843    /// (`enter_callback_scope(flow, VERB, op.is_effectful())`), so it
844    /// single-sources the classification `guard_comparator_write`'s posture
845    /// ultimately keys off — there is exactly one place a new `SeqVerbOp`
846    /// variant's pure/effectful contract can be gotten wrong.
847    #[must_use]
848    pub fn is_effectful(self) -> bool {
849        matches!(self, Self::Each | Self::MapEach)
850    }
851
852    /// Inverse of [`mnemonic`](Self::mnemonic) for the `.inkt` reader.
853    #[must_use]
854    pub fn from_mnemonic(s: &str) -> Option<Self> {
855        Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
856    }
857}
858
859/// The kind of sequence/shuffle container.
860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
861pub enum SequenceKind {
862    Cycle,
863    Stopping,
864    OnceOnly,
865    Shuffle,
866}
867
868impl SequenceKind {
869    fn to_byte(self) -> u8 {
870        match self {
871            Self::Cycle => 0,
872            Self::Stopping => 1,
873            Self::OnceOnly => 2,
874            Self::Shuffle => 3,
875        }
876    }
877
878    fn from_byte(b: u8) -> Result<Self, DecodeError> {
879        match b {
880            0 => Ok(Self::Cycle),
881            1 => Ok(Self::Stopping),
882            2 => Ok(Self::OnceOnly),
883            3 => Ok(Self::Shuffle),
884            _ => Err(DecodeError::InvalidSequenceKind(b)),
885        }
886    }
887}
888
889/// Flags packed into a `BeginChoice` instruction.
890///
891/// Under the single-pop protocol, `BeginChoice` pops at most **one** display
892/// string from the stack when `has_start_content || has_choice_only_content`.
893/// The two content flags are metadata indicating which parts of the original
894/// ink choice contributed to that string — the runtime does not pop them
895/// separately.
896#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
897#[expect(clippy::struct_excessive_bools)]
898pub struct ChoiceFlags {
899    pub has_condition: bool,
900    /// Original choice had `start` content (text before `[`).
901    pub has_start_content: bool,
902    /// Original choice had `choice_only` content (text inside `[]`).
903    /// Under the single-pop protocol this is metadata only — no extra stack pop.
904    pub has_choice_only_content: bool,
905    pub once_only: bool,
906    pub is_invisible_default: bool,
907}
908
909impl ChoiceFlags {
910    fn to_byte(self) -> u8 {
911        let mut b = 0u8;
912        if self.has_condition {
913            b |= 0x01;
914        }
915        if self.has_start_content {
916            b |= 0x02;
917        }
918        if self.has_choice_only_content {
919            b |= 0x04;
920        }
921        if self.once_only {
922            b |= 0x08;
923        }
924        if self.is_invisible_default {
925            b |= 0x10;
926        }
927        b
928    }
929
930    fn from_byte(b: u8) -> Self {
931        Self {
932            has_condition: b & 0x01 != 0,
933            has_start_content: b & 0x02 != 0,
934            has_choice_only_content: b & 0x04 != 0,
935            once_only: b & 0x08 != 0,
936            is_invisible_default: b & 0x10 != 0,
937        }
938    }
939}
940
941/// Errors that can occur when decoding from bytes.
942#[derive(Debug, Clone, PartialEq, Eq)]
943pub enum DecodeError {
944    /// Not enough bytes remaining for the expected operand.
945    UnexpectedEof,
946    /// Unknown opcode discriminant byte.
947    UnknownOpcode(u8),
948    /// Invalid definition id (bad tag byte).
949    InvalidDefinitionId(u64),
950    /// Invalid sequence kind byte.
951    InvalidSequenceKind(u8),
952    /// Invalid tower op kind byte (NS-A8 `Tower` opcode immediate).
953    InvalidTowerOp(u8),
954    /// Invalid collections+ op kind byte (NS-A7 `Collect` opcode immediate).
955    InvalidCollectOp(u8),
956    /// Invalid fn-value verb kind byte (`SeqVerb` opcode immediate,
957    /// issue #1679).
958    InvalidSeqVerbOp(u8),
959    /// .inkb magic bytes are not `INKB`.
960    BadMagic([u8; 4]),
961    /// .inkb version is not supported.
962    UnsupportedVersion(u16),
963    /// A string field contained invalid UTF-8.
964    InvalidUtf8,
965    /// Unknown value type discriminant.
966    InvalidValueType(u8),
967    /// Unknown select key discriminant.
968    InvalidSelectKey(u8),
969    /// Unknown line part discriminant.
970    InvalidLinePart(u8),
971    /// Unknown line content discriminant.
972    InvalidLineContent(u8),
973    /// Unknown plural category discriminant.
974    InvalidPluralCategory(u8),
975    /// Unknown section kind tag in .inkb offset table.
976    InvalidSectionKind(u8),
977    /// Required section kind missing from .inkb offset table.
978    MissingSectionKind(u8),
979    /// File size field doesn't match actual buffer length.
980    FileSizeMismatch { expected: u32, actual: usize },
981    /// CRC-32 checksum of section data doesn't match header.
982    ChecksumMismatch { expected: u32, actual: u32 },
983    /// Section offset table is structurally invalid (out of bounds or not monotonic).
984    InvalidSectionOffset { kind: u8, offset: u32 },
985    /// `.inkl` magic bytes are not `INKL`.
986    BadInklMagic([u8; 4]),
987    /// `.inkl` version is not supported.
988    UnsupportedInklVersion(u8),
989    /// `VAL_ARRAY`/`VAL_MAP` nesting exceeded the decoder's recursion-depth
990    /// cap (see `MAX_DECODE_DEPTH`). Guards against crafted files of deeply
991    /// nested single-element collections stack-overflowing the reader.
992    MaxDepthExceeded(usize),
993    /// A section-locally-versioned section (e.g. `AliasTable`,
994    /// `docs/modules-spec.md` §5) carried a version byte this reader doesn't
995    /// know how to decode.
996    UnsupportedSectionVersion { section: u8, version: u8 },
997    /// A `VAL_PROJECTION` segment carried an unknown kind byte — either
998    /// malformed bytecode or the RESERVED range-segment kind (`2`,
999    /// `docs/format-v4-rfc.md` §1), which nothing emits in T1e and the
1000    /// reader therefore rejects (`docs/t1e-spec.md` §3).
1001    InvalidProjSegmentKind(u8),
1002    /// An `EffectRows` call atom carried an unknown capability-parameter tag
1003    /// (T2-3, `docs/effects-spec.md` §11). Only `Any` (`0`) is legal in this
1004    /// section version; path-granular tags are reserved (#826).
1005    InvalidEffectCapParam(u8),
1006    /// An `EffectRows` call atom carried a non-`None` handle-parameter slot
1007    /// (T2-3, `docs/effects-spec.md` §11, `docs/t1d-spec.md` §7). The slot is
1008    /// reserved — nothing emits a bound handle in this section version.
1009    InvalidEffectHandleParam(u8),
1010    /// A `DirectEffects` extension-flags byte (NS-A2, `EffectRows` section
1011    /// version 3) carried a set bit outside the known
1012    /// emits/tags/faults mask — the reserved bits (3–7) are rejected until
1013    /// a section version graduates them.
1014    InvalidEffectDimensions(u8),
1015    /// A `ContainerDef`'s declared `param_count` disagreed with the number
1016    /// of per-param name/mode metadata entries that followed it (#954,
1017    /// sibling of the `.inkt` reader's same guard, #745). `ContainerDef`'s
1018    /// documented invariant is that `params.len()` always equals
1019    /// `param_count` whenever per-param metadata is present at all (empty
1020    /// `params` is the separate, legitimate "count only, no metadata" case).
1021    /// A mutated/corrupt `.inkb` asserting otherwise is malformed input.
1022    ParamCountMismatch { declared: u8, actual: usize },
1023    /// A `VAL_MAP` entry list carried the same key twice. A legitimate
1024    /// encoder never emits this — `OrderedMap::insert` de-duplicates on the
1025    /// write side — so a repeated key is a corrupt or crafted `.inkb`; the
1026    /// content-based `OrderedMap` `Eq` (issue #909) assumes each key appears
1027    /// once, so this is rejected rather than silently keeping the last
1028    /// occurrence (issue #985).
1029    DuplicateMapKey,
1030    /// An unknown discriminant tag in the conventions-projection wire codec
1031    /// (issue #2111 finding 2, `crate::conventions`) — a mode byte,
1032    /// attach-presence/resolution byte, or `SchemaTypeDef` tag outside the
1033    /// known range. One shared variant for all three: each is "an unknown
1034    /// byte in this one codec's own tag space", the same causal category, so
1035    /// this mirrors `InvalidSectionKind`'s single-variant-per-codec posture
1036    /// rather than minting three near-duplicate variants.
1037    InvalidConventionsProjectionTag(u8),
1038}
1039
1040impl fmt::Display for DecodeError {
1041    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1042        match self {
1043            Self::UnexpectedEof => write!(f, "unexpected end of bytecode"),
1044            Self::UnknownOpcode(b) => write!(f, "unknown opcode: {b:#04x}"),
1045            Self::InvalidDefinitionId(raw) => {
1046                write!(f, "invalid definition id: {raw:#018x}")
1047            }
1048            Self::InvalidSequenceKind(b) => write!(f, "invalid sequence kind: {b}"),
1049            Self::InvalidTowerOp(b) => write!(f, "invalid tower op kind: {b:#04x}"),
1050            Self::InvalidCollectOp(b) => write!(f, "invalid collections+ op kind: {b:#04x}"),
1051            Self::InvalidSeqVerbOp(b) => write!(f, "invalid fn-value verb kind: {b:#04x}"),
1052            Self::BadMagic(m) => write!(f, "bad magic: {m:02x?}"),
1053            Self::UnsupportedVersion(v) => write!(f, "unsupported .inkb version: {v}"),
1054            Self::InvalidUtf8 => write!(f, "invalid UTF-8 in string field"),
1055            Self::InvalidValueType(b) => write!(f, "invalid value type: {b:#04x}"),
1056            Self::InvalidSelectKey(b) => write!(f, "invalid select key: {b:#04x}"),
1057            Self::InvalidLinePart(b) => write!(f, "invalid line part: {b:#04x}"),
1058            Self::InvalidLineContent(b) => write!(f, "invalid line content: {b:#04x}"),
1059            Self::InvalidPluralCategory(b) => write!(f, "invalid plural category: {b:#04x}"),
1060            Self::InvalidSectionKind(b) => write!(f, "invalid section kind: {b:#04x}"),
1061            Self::MissingSectionKind(b) => write!(f, "missing required section kind: {b:#04x}"),
1062            Self::FileSizeMismatch { expected, actual } => {
1063                write!(
1064                    f,
1065                    "file size mismatch: header says {expected}, actual {actual}"
1066                )
1067            }
1068            Self::ChecksumMismatch { expected, actual } => {
1069                write!(
1070                    f,
1071                    "checksum mismatch: header {expected:#010x}, computed {actual:#010x}"
1072                )
1073            }
1074            Self::InvalidSectionOffset { kind, offset } => {
1075                write!(
1076                    f,
1077                    "invalid section offset: kind {kind:#04x} at offset {offset}"
1078                )
1079            }
1080            Self::BadInklMagic(m) => write!(f, "bad .inkl magic: {m:02x?}"),
1081            Self::UnsupportedInklVersion(v) => write!(f, "unsupported .inkl version: {v}"),
1082            Self::MaxDepthExceeded(limit) => {
1083                write!(f, "value nesting exceeded max decode depth ({limit})")
1084            }
1085            Self::UnsupportedSectionVersion { section, version } => {
1086                write!(
1087                    f,
1088                    "unsupported section-local version {version} for section {section:#04x}"
1089                )
1090            }
1091            Self::InvalidProjSegmentKind(b) => {
1092                write!(f, "invalid projection segment kind: {b:#04x}")
1093            }
1094            Self::InvalidEffectCapParam(b) => {
1095                write!(f, "invalid effect capability-parameter tag: {b:#04x}")
1096            }
1097            Self::InvalidEffectHandleParam(b) => {
1098                write!(f, "reserved effect handle-parameter slot set: {b:#04x}")
1099            }
1100            Self::InvalidEffectDimensions(b) => {
1101                write!(f, "reserved effect-dimension flag bits set: {b:#04x}")
1102            }
1103            Self::ParamCountMismatch { declared, actual } => {
1104                write!(
1105                    f,
1106                    "container params metadata count ({actual}) does not match declared param_count ({declared})"
1107                )
1108            }
1109            Self::DuplicateMapKey => write!(f, "duplicate key in map value"),
1110            Self::InvalidConventionsProjectionTag(b) => {
1111                write!(f, "invalid conventions-projection wire tag: {b:#04x}")
1112            }
1113        }
1114    }
1115}
1116
1117impl core::error::Error for DecodeError {}
1118
1119/// A single VM instruction with its operands.
1120#[derive(Debug, Clone, PartialEq)]
1121pub enum Opcode {
1122    // ── Stack & literals ────────────────────────────────────────────────
1123    PushInt(i32),
1124    PushFloat(f32),
1125    PushBool(bool),
1126    PushString(u16),
1127    PushList(u16),
1128    PushDivertTarget(DefinitionId),
1129    PushNull,
1130    Pop,
1131    Duplicate,
1132
1133    // ── Arithmetic ──────────────────────────────────────────────────────
1134    Add,
1135    Subtract,
1136    Multiply,
1137    Divide,
1138    Modulo,
1139    Negate,
1140
1141    // ── Comparison ──────────────────────────────────────────────────────
1142    Equal,
1143    NotEqual,
1144    Greater,
1145    GreaterOrEqual,
1146    Less,
1147    LessOrEqual,
1148
1149    // ── Logic ───────────────────────────────────────────────────────────
1150    Not,
1151    And,
1152    Or,
1153
1154    // ── Global vars ─────────────────────────────────────────────────────
1155    GetGlobal(DefinitionId),
1156    SetGlobal(DefinitionId),
1157
1158    // ── Temp vars ───────────────────────────────────────────────────────
1159    DeclareTemp(u16),
1160    GetTemp(u16),
1161    SetTemp(u16),
1162    /// Get a temp's raw value without auto-dereference (for passing a ref onward).
1163    GetTempRaw(u16),
1164
1165    // ── Variable pointers ──────────────────────────────────────────────
1166    /// Push a pointer to a global variable onto the eval stack.
1167    PushVarPointer(DefinitionId),
1168    /// Push a pointer to a temp variable onto the eval stack.
1169    PushTempPointer(u16),
1170
1171    // ── Control flow ────────────────────────────────────────────────────
1172    Jump(i32),
1173    JumpIfFalse(i32),
1174    Goto(DefinitionId),
1175    GotoIf(DefinitionId),
1176    GotoVariable,
1177
1178    // ── Container flow ──────────────────────────────────────────────────
1179    EnterContainer(DefinitionId),
1180    ExitContainer,
1181
1182    // ── Functions / tunnels ─────────────────────────────────────────────
1183    Call(DefinitionId),
1184    Return,
1185    TunnelCall(DefinitionId),
1186    TunnelReturn,
1187    TunnelCallVariable,
1188    /// Call through a variable holding either a divert target (classic ink
1189    /// function-via-variable) or a function value (T1c-2 direct-call form
1190    /// `f(args…)`) — both share this dispatch site. `argc` is the exact
1191    /// number of args codegen pushed before the callee at this call site
1192    /// (never derived from the resolved target's arity at runtime — issue
1193    /// #721: doing so made a gradual-mode direct-call arity mismatch leave
1194    /// a stray value on the stack instead of faulting). The divert-target
1195    /// arm ignores `argc` (unchanged oracle-verified behavior); the
1196    /// function-value arm pops exactly `argc` supplied args.
1197    CallVariable(u8),
1198
1199    // ── Threads ─────────────────────────────────────────────────────────
1200    ThreadCall(DefinitionId),
1201    ThreadStart,
1202    ThreadDone,
1203
1204    // ── Output ──────────────────────────────────────────────────────────
1205    EmitLine(u16, u8),
1206    EmitValue,
1207    EmitNewline,
1208    /// Word break — renders as a single space between content parts.
1209    Spring,
1210    Glue,
1211    BeginTag,
1212    EndTag,
1213    EvalLine(u16, u8),
1214    /// Begin capturing output into a fragment (structural preservation).
1215    BeginFragment,
1216    /// End fragment capture — store parts and push `Value::FragmentRef`.
1217    EndFragment,
1218    /// An `attach = StructName` convention handler's claimed line (issue
1219    /// #2108) — see `brink_ir::hir::Stmt::AttachElement`'s doc. Pops the
1220    /// call's result off the value stack; when it is a `Value::Record`
1221    /// matching a known `StructShapes` entry, merges its fields (converted
1222    /// via the same `stringify` display path as `string()`/interpolation)
1223    /// into the VM's per-block attachment state — no output, no `Step::Line`.
1224    AttachElement,
1225    /// Closes the run an `AttachElement` opened — see
1226    /// `brink_ir::hir::Stmt::EndElementRun`'s doc. Clears the VM's
1227    /// accumulated attachment data and starts a fresh block.
1228    EndElementRun,
1229
1230    // ── Choices ─────────────────────────────────────────────────────────
1231    BeginChoice(ChoiceFlags, DefinitionId),
1232    EndChoice,
1233
1234    // ── Sequences ───────────────────────────────────────────────────────
1235    Sequence(SequenceKind, u8),
1236    SequenceBranch(i32),
1237
1238    // ── Intrinsics ──────────────────────────────────────────────────────
1239    /// Pop a `DivertTarget` from the stack, push its visit count.
1240    VisitCount,
1241    /// Push the visit count of the *current* container (no stack input).
1242    CurrentVisitCount,
1243    TurnsSince,
1244    TurnIndex,
1245    ChoiceCount,
1246    Random,
1247    SeedRandom,
1248
1249    // ── Casts / math ────────────────────────────────────────────────────
1250    CastToInt,
1251    CastToFloat,
1252    Floor,
1253    Ceiling,
1254    Pow,
1255    Min,
1256    Max,
1257
1258    // ── External fns ────────────────────────────────────────────────────
1259    CallExternal(DefinitionId, u8),
1260
1261    // ── List ops ────────────────────────────────────────────────────────
1262    ListContains,
1263    ListNotContains,
1264    ListIntersect,
1265    ListAll,
1266    ListInvert,
1267    ListCount,
1268    ListMin,
1269    ListMax,
1270    ListValue,
1271    ListRange,
1272    ListFromInt,
1273    ListRandom,
1274
1275    // ── Collections (T1b, `docs/format-v4-rfc.md` §3 "Collections (T1a)") ─
1276    /// `[elem_0, …, elem_{n-1}]` → `Array([elem_0, …, elem_{n-1}])`.
1277    ArrayNew(u32),
1278    /// `[k_0, v_0, …, k_{n-1}, v_{n-1}]` → `Map({k_0: v_0, …})` (insertion
1279    /// order = argument order; a repeated key keeps its first position and
1280    /// takes the last value, matching `OrderedMap::insert`).
1281    MapNew(u32),
1282    /// `[container, index]` → element/value. Turn-terminating fault on
1283    /// out-of-bounds array index or missing map key (value-model-spec §6).
1284    IndexGet,
1285    /// `[container, index, value]` → updated container (take → `make_mut` →
1286    /// write-back). Turn-terminating fault on out-of-bounds array index or
1287    /// missing map key — no silent growth on write-past-end (spec §6).
1288    IndexSet,
1289    /// `[container]` → `Int(len)`. Array or map.
1290    CollectionLen,
1291    /// `[map, key]` → value. Turn-terminating fault on missing key.
1292    MapGet,
1293    /// `[map, key, value]` → updated map (insert-or-overwrite; unlike
1294    /// `IndexSet`, a missing key is not a fault — this is the stdlib
1295    /// `insert()` mutator's primitive).
1296    MapInsert,
1297    /// `[map, key]` → updated map with `key` removed (no-op if absent — the
1298    /// stdlib `remove()` mutator's primitive). Map-only as of issue #1484:
1299    /// a non-map container is a turn-terminating fault (`NotIndexable`).
1300    /// The array-index leg this op used to generalize over is
1301    /// [`SeqRemoveAt`](Self::SeqRemoveAt).
1302    MapRemove,
1303    /// `[map, key]` → `Bool`.
1304    MapContains,
1305    /// `[map]` → `Array` of keys in insertion order.
1306    CollectionKeys,
1307    /// `[map]` → `Array` of values in insertion order.
1308    CollectionValues,
1309    /// `LiteralPool[idx]` → cloned value (an `Arc` bump for collections).
1310    PushLiteral(u32),
1311
1312    // ── Sharing discipline (T1b-4, `docs/format-v4-rfc.md` §3) ──────────
1313    /// Move a global's current value out, leaving `Value::Null` behind —
1314    /// the take-half of the take → `make_mut` → write-back RMW discipline
1315    /// (value-model-spec §5). No stack input; pushes the taken value.
1316    /// Unlike `GetGlobal`, never auto-dereferences (globals can't hold
1317    /// `ref`-param pointers — those live in temps).
1318    TakeGlobal(DefinitionId),
1319    /// Move a temp's current value out, leaving `Value::Null` behind —
1320    /// mirrors `TakeGlobal` for temp slots. Auto-dereferences like
1321    /// `GetTemp`: if the temp holds a `VariablePointer`/`TempPointer`, the
1322    /// *pointed-to* location is taken (and left `Null`), not the pointer
1323    /// value itself, which stays in this slot untouched.
1324    TakeTemp(u16),
1325
1326    // ── Records (TM-4, `docs/typed-mode-spec.md` §6) ─────────────────────
1327    /// `[field_0, …, field_{n-1}]` → `Record` (n = the shape's declared
1328    /// field count, looked up from `StructShapes`; fields popped/assigned in
1329    /// shape declaration order). The `u32` operand is the `ShapeId`.
1330    RecordNew(u32),
1331    /// `[record]` → field value, looked up by name (`NameId` operand) in the
1332    /// record's own shape. Turn-terminating fault if the shape has no field
1333    /// by that name (value-model-spec §11c).
1334    RecordGetDyn(u16),
1335    /// `[record, value]` → updated record (take → `make_mut` → write-back),
1336    /// field selected by name (`NameId` operand). Turn-terminating fault if
1337    /// the shape has no field by that name.
1338    RecordSetDyn(u16),
1339    /// `[record]` → field value, looked up by flat offset into the record's
1340    /// own field vector (TM-4c, `docs/typed-mode-spec.md` §6 static-offset
1341    /// payoff). Emitted only when the record's shape is compile-time known
1342    /// (`types = strict`); turn-terminating fault if the offset is out of
1343    /// range for the popped record's field count — no shape re-check.
1344    RecordGet(u16),
1345    /// `[record, value]` → updated record (take → `make_mut` → write-back),
1346    /// field selected by flat offset (TM-4c). Turn-terminating fault if the
1347    /// offset is out of range.
1348    RecordSet(u16),
1349
1350    // ── Conversion intrinsics (TM-3 completion, `docs/typed-mode-spec.md`
1351    // §4, maintainer ruling 2026-07-13, issue #659) ──────────────────────
1352    /// `[x]` → `Int`. The `int(x)` pure conversion intrinsic: `Int`
1353    /// (identity), `Float` (truncate toward zero, matching vanilla ink's
1354    /// `INT()`), `Bool` (`true` → 1, `false` → 0), `String` (parse).
1355    /// Turn-terminating fault on a string that fails to parse, or on any
1356    /// value outside this permissive numeric+bool domain (divert targets,
1357    /// LIST values, arrays, maps, records) — value-model-spec §11c.
1358    ConvertInt,
1359    /// `[x]` → `Float`. The `float(x)` pure conversion intrinsic: `Float`
1360    /// (identity), `Int` (widen), `Bool` (`true` → 1.0, `false` → 0.0),
1361    /// `String` (parse). Same fault domain as `ConvertInt`.
1362    ConvertFloat,
1363    /// `[x]` → `String`. The `string(x)` pure conversion intrinsic: display
1364    /// form, identical to interpolation (`{x}`) — total over every `Value`,
1365    /// never faults (typed-mode-spec §4: "display is universal, not a
1366    /// coercion").
1367    ConvertString,
1368
1369    // ── Function values (T1c, `docs/t1c-spec.md` §3/§6) ──────────────────
1370    /// `[]` → `FnRef`. Push a zero-bound function value for the target
1371    /// `DefinitionId` (`#fn(name)` where the target has no `ref` params).
1372    PushFnRef(DefinitionId),
1373    /// `[bound_0, …, bound_{n-1}]` → `Closure`. Pop the `n` = `bound_count`
1374    /// bound args (in declared order) and pair each with its param name/mode
1375    /// read from the target container's own [`ContainerDef::params`] table
1376    /// (the bound prefix `params[0..n]`) to build a `Closure`.
1377    /// A `ref` bound arg is a `VariablePointer` (a captured durable cell); a
1378    /// `val` bound arg is a snapshot. The names/modes are read from the
1379    /// signature (not baked into the opcode) so there is one source of truth
1380    /// the rehydration check compares against.
1381    MakeClosure {
1382        target: DefinitionId,
1383        bound_count: u8,
1384    },
1385    /// `[arg_0, …, arg_{argc-1}, callee]` → return value. Pop the callee
1386    /// function value then the `argc` supplied (val-only) args, splice the
1387    /// closure's bound prefix ahead of them, and enter the target. Faults
1388    /// (turn-terminating, `docs/t1c-spec.md` §3): callee is not a function
1389    /// value; `bound + argc` ≠ the target's declared arity; a rehydrated env
1390    /// entry's name/mode no longer matches the current signature; the callee
1391    /// `ref`-binds a `#@local` and is invoked from a non-creating flow.
1392    CallValue(u8),
1393    /// `[arg_0, …, arg_{argc-1}, callee]` → new function value. The
1394    /// `bind(f, args…)` stdlib intrinsic (T1c-3, `docs/t1c-spec.md` §3):
1395    /// pop the callee function value then the `argc` supplied (val-only)
1396    /// args, append them to the callee's bound-arg row (val-only currying,
1397    /// consuming the head of the remaining param row), and push the new
1398    /// function value. The appended entries take their param name/mode from
1399    /// the target's signature (always `val`). Faults (turn-terminating):
1400    /// callee is not a function value; `bound + argc` exceeds the target's
1401    /// declared arity.
1402    BindValue(u8),
1403
1404    // ── Path projections (T1e, `docs/t1e-spec.md` §3) ─────────────────────
1405    /// `[seg_0, …, seg_{n-1}]` → `Projection` (`n` = `segment_count`, pushed
1406    /// by codegen in source order; the VM's LIFO pop-then-reverse restores
1407    /// it). Each popped value is classified `Int` → `ProjSegment::Index`, else →
1408    /// `ProjSegment::Key` and paired with the static `root` cell to build a
1409    /// `Value::Projection` (`docs/format-v4-rfc.md` §1). Emitted at every
1410    /// real path-projection `ref`-argument creation site (`ref
1411    /// npc.inventory[3]`) — the T1e-1 `E099` lowering fence this replaces.
1412    MakeProjection {
1413        root: DefinitionId,
1414        segment_count: u8,
1415    },
1416    /// `[projection]` → value. Root-cell RMW read: take the root cell's
1417    /// *current* value, walk the segment chain, push the result. Faults
1418    /// `ProjectionInvalidated` (turn-terminating) if the path no longer
1419    /// resolves (spec §1(2)).
1420    ProjRead,
1421    /// `[projection, value]` → (assigns, pushes nothing). Root-cell RMW
1422    /// write: take root → walk → `make_mut` spine → write the final segment
1423    /// → store back (spec §3). Faults `ProjectionInvalidated` on an
1424    /// unresolved path, same domain as `ProjRead`.
1425    ProjWrite,
1426
1427    // ── Stdlib slice 1 completion (`docs/t1b-surface-spec.md` §5, issue
1428    // #857) ───────────────────────────────────────────────────────────────
1429    /// `[s, i]` → single-character `String`. The `char_at(s, i)` stdlib pure
1430    /// function: `i` indexes Unicode scalar values ("chars"), not UTF-8
1431    /// bytes. Turn-terminating fault (value-model-spec §11c) on a non-`Int`
1432    /// `i`, a non-`String` `s`, or `i` outside `[0, char_count)`.
1433    CharAt,
1434
1435    // ── NS-A1: Option[T] + the ruled stdlib flips (`docs/stdlib-spec.md`
1436    // §1.1/§1.4, §§3-5) ──────────────────────────────────────────────────
1437    /// `[]` → `none`. Push the `Option[T]` absence value.
1438    PushNone,
1439    /// `[x]` → `some(x)`. Wrap the top of stack — total over every value.
1440    MakeSome,
1441    /// `[s, sub]` → `Option[int]`: index of `sub`'s first occurrence in
1442    /// `s`, counted in Unicode scalar values (chars, not bytes — the §3
1443    /// indexing unit `char_at` already uses); absent → `none`.
1444    /// Turn-terminating fault on non-string arguments.
1445    StrFind,
1446    /// `[a, x]` → `Option[int]`: index of the first element structurally
1447    /// equal to `x`; absent → `none`. Fault on a non-array container.
1448    SeqIndexOf,
1449    /// `[a]` → `Option[T]`: least element (empty → `none`). Orders
1450    /// int/float (numeric promotion, NaN per the §4b pinned prod order),
1451    /// bool, string; anything else faults (unorderable — wave A4 grows the
1452    /// roster). Ties keep the first occurrence.
1453    SeqMin,
1454    /// `[a]` → `Option[T]`: greatest element — see [`SeqMin`](Self::SeqMin).
1455    SeqMax,
1456    /// `[a]` → `Option[T]`: first element (empty → `none`).
1457    SeqFirst,
1458    /// `[a]` → `Option[T]`: last element (empty → `none`).
1459    SeqLast,
1460    /// `[a]` → pushes `Option[T]` (the removed last element, or `none` on
1461    /// empty), then the shrunk array on top of it. Codegen brackets this
1462    /// `TakeGlobal`/`TakeTemp` … `SetGlobal`/`SetTemp` so the array writes
1463    /// back to its root cell and the Option remains as the expression's
1464    /// value. Fault on a non-array.
1465    SeqPop,
1466    /// `[m, k]` → `Option[V]`: the non-faulting map read (`get(m, k)`,
1467    /// §5 — martyr #3 redeemed). Missing key → `none`; a key outside the
1468    /// int/string/bool key domain is a turn-terminating fault (malformed
1469    /// question), as is a non-map container. The faulting `m[k]`
1470    /// ([`MapGet`](Self::MapGet)) stays the "I expect it there" read.
1471    MapGetOpt,
1472    /// `[m, v]` → `Bool`: content-equality scan over the map's values
1473    /// (§5 — honest O(n)). Fault on a non-map.
1474    MapContainsValue,
1475    /// `[m]` → empty map. The `clear(m)` statement-only mutator's
1476    /// primitive; in-place-ness comes from the RMW write-back, exactly
1477    /// like [`MapInsert`](Self::MapInsert)/[`MapRemove`](Self::MapRemove).
1478    /// Fault on a non-map.
1479    MapClear,
1480
1481    // ── B1: `or`-coalescing (`docs/stdlib-spec.md` §1.6a, issue #1460),
1482    // short-circuited per issue #1471's ruling ──────────────────────────
1483    /// Pops `lhs` (must be an `OptionVal`). `some(v)` pushes the unwrapped
1484    /// `v` and jumps `rel` bytes forward (the same relative-offset
1485    /// convention as [`Jump`](Self::Jump)/[`JumpIfFalse`](Self::JumpIfFalse));
1486    /// `none` pushes nothing and falls through to the next instruction,
1487    /// which evaluates `rhs`. That fall-through *is* the short-circuit:
1488    /// `rhs`'s bytecode is only ever reached when `lhs` is `none` — `x or
1489    /// expensive()` runs `expensive()` exactly once, and only on `none`
1490    /// (RULED, issue #1471, flipping the eager evaluation PR #1469/#1460
1491    /// landed and flagged as unruled). Native-surface only: reachable
1492    /// exclusively through `InfixOp::Coalesce`, which the native lowering
1493    /// path alone produces (`InfixOp::Or`, ink's boolean `||`, is untouched
1494    /// and oracle-frozen).
1495    ///
1496    /// The jump target is where the two branches join: the `some(v)` branch
1497    /// has already unwrapped to `v`, and the `none` branch pushed `rhs`
1498    /// as-is. Codegen emits a [`MakeSome`](Self::MakeSome) right at that
1499    /// target exactly when the step's recorded typing says `rhs` is itself
1500    /// `Option[U]` (the two-Option form, `(Option[T],Option[T]) ->
1501    /// Option[U]`, preserving optionality for chaining), so both branches
1502    /// agree on shape at the join; for the collapse form
1503    /// (`(Option[T],T)->T`) no `MakeSome` is emitted and `v` stands
1504    /// unwrapped. The retired binary opcode decided that from `rhs`'s
1505    /// *runtime* value; short-circuiting rules that out (`rhs` may never
1506    /// run by the time the answer is needed), so the decision is made at
1507    /// lowering time from the analyzer's recorded types — see below.
1508    ///
1509    /// ## Where the collapse-vs-preserve answer comes from
1510    ///
1511    /// RULED (maintainer, 2026-07-26, issue #1492 — `docs/decision-log.md`
1512    /// "Lowering consumes analyzer types"): typing verdicts belong to
1513    /// `brink-analyzer`, which records each `or` step's operand/result
1514    /// types for LIR lowering (`brink_analyzer::coalesce_types`, threaded
1515    /// to lowering as `brink_ir::lir::CoalesceLookup`). Lowering *consumes*
1516    /// that verdict; it never re-derives it from syntax. Under `types =
1517    /// strict` an ill-typed chain never reaches codegen at all — `E066`
1518    /// rejects it during analysis — so this op only ever executes a chain
1519    /// analysis either accepted or could not statically pin.
1520    ///
1521    /// ## The runtime check *is* the semantics for an unpinned `lhs`
1522    ///
1523    /// That second case is the gradual-mode posture, and it is deliberate:
1524    /// when the left-hand type is unknown (brink dialect, `types =
1525    /// gradual` — the un-overridden native default), **the check this op
1526    /// performs is the operator's semantics**, not a fallback for a missing
1527    /// one. An `OptionVal` coalesces; a plain value raises the `TypeError`
1528    /// fault (`brink_runtime::value_ops::coalesce_unwrap_some`) — the same
1529    /// class as every other gradual runtime check. Strict/native never
1530    /// reaches this path with an unpinned `lhs`, and the analyzer records
1531    /// exactly this case as `CoalesceShape::RuntimeCheck`, on which codegen
1532    /// emits no `MakeSome`: with `rhs` possibly never evaluated there is no
1533    /// value to read a shape off, so the unwrapped collapse form is the one
1534    /// shape that stays sound for the `(Option[T],T)->T` reading the check
1535    /// admits.
1536    CoalesceSome(i32),
1537
1538    // ── B1b: the `as` binding (`docs/decision-log.md` 2026-07-26; issue
1539    //    #1475) ──────────────────────────────────────────────────────────
1540    /// `[opt]` → `[bool]` — the `as` binding's fused test-and-bind
1541    /// (`if EXPR as name { … }`, `while EXPR as name { … }`,
1542    /// `{if EXPR as name: … else: …}`). `opt` must be an `OptionVal`:
1543    /// `some(v)` stores the **unwrapped** `v` in temp `slot` and pushes
1544    /// `true`; `none` leaves `slot` untouched and pushes `false`. A
1545    /// non-`OptionVal` operand faults
1546    /// ([`RuntimeError::AsBindingNotOption`](crate::opcode) — the
1547    /// gradual-mode residual of the checker's `E147`).
1548    ///
1549    /// The slot is always freshly allocated by the binding itself, so —
1550    /// unlike [`SetTemp`](Self::SetTemp) — the write needs no
1551    /// pointer/projection write-through: an `as` binding can never land on
1552    /// a `ref` parameter's cell. Native-surface only.
1553    OptionBind(u16),
1554
1555    // ── Seq `remove_at` (issue #1484, `docs/stdlib-spec.md` §4/§10) ───────
1556    /// `[a, i]` → updated array with the element at `i` removed (shifts
1557    /// later elements left) — the stdlib `remove_at()` mutator's primitive,
1558    /// the array-index leg [`MapRemove`](Self::MapRemove) generalized over
1559    /// before this PR. Array-only: a non-array `a` is a turn-terminating
1560    /// fault (`NotIndexable`). `i` must be an `Int` in `[0, len)` — strictly
1561    /// less than `len`, matching `IndexGet`/`IndexSet` (there is no element
1562    /// to remove at `len`, unlike `MapInsert`'s append-friendly `<=`).
1563    SeqRemoveAt,
1564
1565    // ── NS-A6: the `std::rand` draw verbs (`docs/stdlib-spec.md` §7,
1566    // ruled 2026-07-18; `docs/stdlib-sequencing.md` §2 Wave A6). Every op
1567    // below draws through the ONE RNG state cell (`rng_seed` +
1568    // `previous_random` — the same cell ink's `RANDOM`/`SEED_RANDOM` have
1569    // always used; one cell, two surfaces, no drift) and is an ordinary
1570    // *write* to that cell in the effect row
1571    // (`DefinitionId::RNG_CELL`). `seed(n)` needs no new op — it lowers to
1572    // the frozen [`SeedRandom`](Self::SeedRandom). ────────────────────────
1573    /// `[]` → `Float` uniform in `[0,1)`. One draw. The value is built from
1574    /// the draw's top 24 bits (`draw >> 7`) divided by 2²⁴, so every result
1575    /// is exactly representable in the f32 payload and 1.0 is unreachable —
1576    /// part of the pinned-algorithm stability contract (see
1577    /// `brink-runtime::rand_ops`).
1578    RandFloat,
1579    /// `[p]` → `Bool`: one uniform `[0,1)` draw `u`, result `u < p` with
1580    /// `p` clamped to `[0,1]` and NaN → `false` (F3, ruled 2026-07-19:
1581    /// interpretation, not fabrication — total over the numeric domain).
1582    /// Always consumes exactly one draw, NaN included. Fault on a
1583    /// non-numeric `p` (malformed question).
1584    RandChance,
1585    /// `[coll]` → `Option[T]`: uniform draw of one element from an array
1586    /// (→ `some(elem)`) or a flags subset (→ `some(single-item list)`,
1587    /// mirroring the frozen `ListRandom` selection). Empty → `none`
1588    /// *without* consuming a draw. Fault on any other collection type.
1589    RandPick,
1590    /// `[a]` → `[a']`: Fisher-Yates shuffle of an array, `len-1` draws
1591    /// (none for `len < 2`), each advancing the RNG cell. One op serves
1592    /// both surfaces: `shuffle(a)` (statement-only, RMW write-back) and
1593    /// `shuffled(a)` (functional). Fault on a non-array.
1594    RandShuffle,
1595    /// `[start, end]` → `Range` (NS-A5, F7): construct an exclusive
1596    /// (`start..end`) range value from two int bounds. Fault on non-int
1597    /// bounds (malformed question — the T1b stdlib doctrine; no numeric
1598    /// coercion, range bounds are ints by ruling).
1599    RangeMakeExcl,
1600    /// `[start, end]` → `Range` (NS-A5, F7): construct an inclusive
1601    /// (`start..=end`) range value from two int bounds. Same fault
1602    /// contract as [`RangeMakeExcl`](Self::RangeMakeExcl).
1603    RangeMakeIncl,
1604    /// `[r]` → `Option[Range]` (NS-A5, the `non_empty(r)` validator —
1605    /// S2 ruled 2026-07-19): `some(r)` when the range denotes at least one
1606    /// element, `none` when it is empty. The Option tax sits once at the
1607    /// boundary where dynamic bounds enter; the checker types the `some`
1608    /// payload as the inhabited-range refinement. Pure — no draw, no
1609    /// write. Fault on a non-range operand.
1610    RangeNonEmpty,
1611
1612    // ── NS-A4: the ordering verbs (`docs/stdlib-spec.md` §4b, issue
1613    // #1110) ────────────────────────────────────────────────────────────
1614    /// `[a]` → `[a']`: the array sorted ascending by the §4b ordering
1615    /// doctrine — int/float (numeric promotion), bool (`false < true`),
1616    /// string (USV-lexicographic), arrays lexicographic element-wise
1617    /// (recursively). Stable (equal elements keep their input order).
1618    /// Float NaN is mode-dependent: DEV mode faults on any NaN comparand
1619    /// (`UnorderedComparand` — the upstream bug surfaces at its first
1620    /// ordering consumption); PROD mode places it by the pinned
1621    /// non-fabricating total order (`-0 == +0` ties, NaN greatest,
1622    /// NaN-vs-NaN ties). One op serves both surfaces: `sort(a)`
1623    /// (statement-only, RMW write-back) and `sorted(a)` (functional) —
1624    /// the `RandShuffle` precedent. Fault on a non-array or unorderable
1625    /// elements (structs/enums without a registered `compare`, maps,
1626    /// flags subsets, divert targets — malformed question, all modes).
1627    SeqSorted,
1628    /// `[a, cmp]` → `[a']`: the array sorted ascending by a user
1629    /// comparator — `cmp` is a function value (`FnRef`/`Closure`) of
1630    /// shape `fn(T, T): int` (negative = less, zero = tie, positive =
1631    /// greater; F0 ruled 2026-07-19). Stable. The comparator runs under
1632    /// the pure·silent contract (checker-enforced where provable); the
1633    /// VM evaluates it re-entrantly with output isolated and faults if it
1634    /// yields, presents choices, calls an external, or returns a
1635    /// non-int. No NaN check here — F14: `sort_by` does not inherit
1636    /// `F:float`; the comparator owns the element semantics. One op
1637    /// serves `sort_by(a, cmp)` (statement-only, RMW write-back) and
1638    /// `sorted_by(a, cmp)` (functional). Fault on a non-array or
1639    /// non-function comparator.
1640    SeqSortedBy,
1641
1642    // ── NS-A8: the numeric tower (`docs/tower-mini-spec.md`, issue
1643    // #1114) ────────────────────────────────────────────────────────────
1644    /// One opcode, thirteen operations: the [`TowerOp`] immediate selects
1645    /// the constructor or verb (see its per-kind docs for stack shapes).
1646    /// All pure; wrong-operand-type is a turn-terminating fault (a
1647    /// malformed question, per the ruled fault-vs-absence doctrine). The
1648    /// tower's operator family (`+`/`-`/`*`, `mat*vec`, `quat*quat`,
1649    /// `quat*vec`) rides the frozen arithmetic opcodes instead — see
1650    /// `value_ops::binary_op`.
1651    Tower(TowerOp),
1652
1653    // ── NS-A7: collections+ (`docs/stdlib-spec.md` §8, issue #1113) ────
1654    /// One opcode, five operations: the [`CollectOp`] immediate selects
1655    /// `Weighted[T]` construction, the `rand::roll` draw, or one of the
1656    /// heap verbs (see its per-kind docs for stack shapes). `RandRoll`
1657    /// writes the RNG cell; `HeapPush` carries the §4b dev/prod NaN
1658    /// entry-check; everything else is pure over its operands.
1659    Collect(CollectOp),
1660
1661    // ── The fn-value verb layer (`docs/stdlib-spec.md` §4, issue #1679) ──
1662    /// One opcode, one operation per [`SeqVerbOp`] kind: the pure trio
1663    /// `map`/`filter`/`fold`. Every kind pops a callback function value
1664    /// (`FnRef`/`Closure`) and evaluates it re-entrantly per element with
1665    /// output isolated — the `SeqSortedBy` machinery, one callback contract.
1666    /// See the per-kind docs for stack shapes.
1667    SeqVerb(SeqVerbOp),
1668
1669    // ── Lifecycle ───────────────────────────────────────────────────────
1670    Done,
1671    /// Pause for choice presentation. Like `Done` but does NOT set
1672    /// `did_safe_exit` — if no choices are pending, the story ran
1673    /// out of content rather than reaching an explicit `-> DONE`.
1674    Yield,
1675    End,
1676    Nop,
1677
1678    // ── String eval ─────────────────────────────────────────────────────
1679    BeginStringEval,
1680    EndStringEval,
1681
1682    // ── Debug ───────────────────────────────────────────────────────────
1683    SourceLocation(u32, u32),
1684}
1685
1686// ── Opcode encode / decode ──────────────────────────────────────────────────
1687
1688impl Opcode {
1689    /// Encode this instruction into the byte buffer.
1690    #[expect(clippy::too_many_lines)]
1691    pub fn encode(&self, buf: &mut Vec<u8>) {
1692        match *self {
1693            // Stack & literals
1694            Self::PushInt(v) => {
1695                write_u8(buf, PUSH_INT);
1696                write_i32(buf, v);
1697            }
1698            Self::PushFloat(v) => {
1699                write_u8(buf, PUSH_FLOAT);
1700                write_f32(buf, v);
1701            }
1702            Self::PushBool(v) => {
1703                write_u8(buf, PUSH_BOOL);
1704                write_u8(buf, u8::from(v));
1705            }
1706            Self::PushString(idx) => {
1707                write_u8(buf, PUSH_STRING);
1708                write_u16(buf, idx);
1709            }
1710            Self::PushList(idx) => {
1711                write_u8(buf, PUSH_LIST);
1712                write_u16(buf, idx);
1713            }
1714            Self::PushDivertTarget(id) => {
1715                write_u8(buf, PUSH_DIVERT_TARGET);
1716                write_def_id(buf, id);
1717            }
1718            Self::PushNull => write_u8(buf, PUSH_NULL),
1719            Self::Pop => write_u8(buf, POP),
1720            Self::Duplicate => write_u8(buf, DUPLICATE),
1721
1722            // Arithmetic
1723            Self::Add => write_u8(buf, ADD),
1724            Self::Subtract => write_u8(buf, SUBTRACT),
1725            Self::Multiply => write_u8(buf, MULTIPLY),
1726            Self::Divide => write_u8(buf, DIVIDE),
1727            Self::Modulo => write_u8(buf, MODULO),
1728            Self::Negate => write_u8(buf, NEGATE),
1729
1730            // Comparison
1731            Self::Equal => write_u8(buf, EQUAL),
1732            Self::NotEqual => write_u8(buf, NOT_EQUAL),
1733            Self::Greater => write_u8(buf, GREATER),
1734            Self::GreaterOrEqual => write_u8(buf, GREATER_OR_EQUAL),
1735            Self::Less => write_u8(buf, LESS),
1736            Self::LessOrEqual => write_u8(buf, LESS_OR_EQUAL),
1737
1738            // Logic
1739            Self::Not => write_u8(buf, NOT),
1740            Self::And => write_u8(buf, AND),
1741            Self::Or => write_u8(buf, OR),
1742
1743            // Global vars
1744            Self::GetGlobal(id) => {
1745                write_u8(buf, GET_GLOBAL);
1746                write_def_id(buf, id);
1747            }
1748            Self::SetGlobal(id) => {
1749                write_u8(buf, SET_GLOBAL);
1750                write_def_id(buf, id);
1751            }
1752
1753            // Temp vars
1754            Self::DeclareTemp(idx) => {
1755                write_u8(buf, DECLARE_TEMP);
1756                write_u16(buf, idx);
1757            }
1758            Self::GetTemp(idx) => {
1759                write_u8(buf, GET_TEMP);
1760                write_u16(buf, idx);
1761            }
1762            Self::SetTemp(idx) => {
1763                write_u8(buf, SET_TEMP);
1764                write_u16(buf, idx);
1765            }
1766            Self::GetTempRaw(idx) => {
1767                write_u8(buf, GET_TEMP_RAW);
1768                write_u16(buf, idx);
1769            }
1770
1771            // Variable pointers
1772            Self::PushVarPointer(id) => {
1773                write_u8(buf, PUSH_VAR_POINTER);
1774                write_def_id(buf, id);
1775            }
1776            Self::PushTempPointer(slot) => {
1777                write_u8(buf, PUSH_TEMP_POINTER);
1778                write_u16(buf, slot);
1779            }
1780
1781            // Control flow
1782            Self::Jump(offset) => {
1783                write_u8(buf, JUMP);
1784                write_i32(buf, offset);
1785            }
1786            Self::JumpIfFalse(offset) => {
1787                write_u8(buf, JUMP_IF_FALSE);
1788                write_i32(buf, offset);
1789            }
1790            Self::Goto(id) => {
1791                write_u8(buf, GOTO);
1792                write_def_id(buf, id);
1793            }
1794            Self::GotoIf(id) => {
1795                write_u8(buf, GOTO_IF);
1796                write_def_id(buf, id);
1797            }
1798            Self::GotoVariable => write_u8(buf, GOTO_VARIABLE),
1799
1800            // Container flow
1801            Self::EnterContainer(id) => {
1802                write_u8(buf, ENTER_CONTAINER);
1803                write_def_id(buf, id);
1804            }
1805            Self::ExitContainer => write_u8(buf, EXIT_CONTAINER),
1806
1807            // Functions / tunnels
1808            Self::Call(id) => {
1809                write_u8(buf, CALL);
1810                write_def_id(buf, id);
1811            }
1812            Self::Return => write_u8(buf, RETURN),
1813            Self::TunnelCall(id) => {
1814                write_u8(buf, TUNNEL_CALL);
1815                write_def_id(buf, id);
1816            }
1817            Self::TunnelReturn => write_u8(buf, TUNNEL_RETURN),
1818            Self::TunnelCallVariable => write_u8(buf, TUNNEL_CALL_VARIABLE),
1819            Self::CallVariable(argc) => {
1820                write_u8(buf, CALL_VARIABLE);
1821                write_u8(buf, argc);
1822            }
1823
1824            // Threads
1825            Self::ThreadCall(id) => {
1826                write_u8(buf, THREAD_CALL);
1827                write_def_id(buf, id);
1828            }
1829            Self::ThreadStart => write_u8(buf, THREAD_START),
1830            Self::ThreadDone => write_u8(buf, THREAD_DONE),
1831
1832            // Output
1833            Self::EmitLine(idx, slot_count) => {
1834                write_u8(buf, EMIT_LINE);
1835                write_u16(buf, idx);
1836                write_u8(buf, slot_count);
1837            }
1838            Self::EmitValue => write_u8(buf, EMIT_VALUE),
1839            Self::EmitNewline => write_u8(buf, EMIT_NEWLINE),
1840            Self::Spring => write_u8(buf, SPRING),
1841            Self::Glue => write_u8(buf, GLUE),
1842            Self::BeginTag => write_u8(buf, BEGIN_TAG),
1843            Self::EndTag => write_u8(buf, END_TAG),
1844            Self::EvalLine(idx, slot_count) => {
1845                write_u8(buf, EVAL_LINE);
1846                write_u16(buf, idx);
1847                write_u8(buf, slot_count);
1848            }
1849            Self::BeginFragment => write_u8(buf, BEGIN_FRAGMENT),
1850            Self::EndFragment => write_u8(buf, END_FRAGMENT),
1851            Self::AttachElement => write_u8(buf, ATTACH_ELEMENT),
1852            Self::EndElementRun => write_u8(buf, END_ELEMENT_RUN),
1853
1854            // Choices
1855            Self::BeginChoice(flags, target) => {
1856                write_u8(buf, BEGIN_CHOICE);
1857                write_u8(buf, flags.to_byte());
1858                write_def_id(buf, target);
1859            }
1860            Self::EndChoice => write_u8(buf, END_CHOICE),
1861
1862            // Sequences
1863            Self::Sequence(kind, count) => {
1864                write_u8(buf, SEQUENCE);
1865                write_u8(buf, kind.to_byte());
1866                write_u8(buf, count);
1867            }
1868            Self::SequenceBranch(offset) => {
1869                write_u8(buf, SEQUENCE_BRANCH);
1870                write_i32(buf, offset);
1871            }
1872
1873            // Intrinsics
1874            Self::VisitCount => write_u8(buf, VISIT_COUNT),
1875            Self::CurrentVisitCount => write_u8(buf, CURRENT_VISIT_COUNT),
1876            Self::TurnsSince => write_u8(buf, TURNS_SINCE),
1877            Self::TurnIndex => write_u8(buf, TURN_INDEX),
1878            Self::ChoiceCount => write_u8(buf, CHOICE_COUNT),
1879            Self::Random => write_u8(buf, RANDOM),
1880            Self::SeedRandom => write_u8(buf, SEED_RANDOM),
1881
1882            // Casts / math
1883            Self::CastToInt => write_u8(buf, CAST_TO_INT),
1884            Self::CastToFloat => write_u8(buf, CAST_TO_FLOAT),
1885            Self::Floor => write_u8(buf, FLOOR),
1886            Self::Ceiling => write_u8(buf, CEILING),
1887            Self::Pow => write_u8(buf, POW),
1888            Self::Min => write_u8(buf, MIN),
1889            Self::Max => write_u8(buf, MAX),
1890
1891            // External fns
1892            Self::CallExternal(id, argc) => {
1893                write_u8(buf, CALL_EXTERNAL);
1894                write_def_id(buf, id);
1895                write_u8(buf, argc);
1896            }
1897
1898            // List ops
1899            Self::ListContains => write_u8(buf, LIST_CONTAINS),
1900            Self::ListNotContains => write_u8(buf, LIST_NOT_CONTAINS),
1901            Self::ListIntersect => write_u8(buf, LIST_INTERSECT),
1902            Self::ListAll => write_u8(buf, LIST_ALL),
1903            Self::ListInvert => write_u8(buf, LIST_INVERT),
1904            Self::ListCount => write_u8(buf, LIST_COUNT),
1905            Self::ListMin => write_u8(buf, LIST_MIN),
1906            Self::ListMax => write_u8(buf, LIST_MAX),
1907            Self::ListValue => write_u8(buf, LIST_VALUE),
1908            Self::ListRange => write_u8(buf, LIST_RANGE),
1909            Self::ListFromInt => write_u8(buf, LIST_FROM_INT),
1910            Self::ListRandom => write_u8(buf, LIST_RANDOM),
1911
1912            // Collections
1913            Self::ArrayNew(n) => {
1914                write_u8(buf, ARRAY_NEW);
1915                write_u32(buf, n);
1916            }
1917            Self::MapNew(n) => {
1918                write_u8(buf, MAP_NEW);
1919                write_u32(buf, n);
1920            }
1921            Self::IndexGet => write_u8(buf, INDEX_GET),
1922            Self::IndexSet => write_u8(buf, INDEX_SET),
1923            Self::CollectionLen => write_u8(buf, COLLECTION_LEN),
1924            Self::MapGet => write_u8(buf, MAP_GET),
1925            Self::MapInsert => write_u8(buf, MAP_INSERT),
1926            Self::MapRemove => write_u8(buf, MAP_REMOVE),
1927            Self::MapContains => write_u8(buf, MAP_CONTAINS),
1928            Self::CollectionKeys => write_u8(buf, COLLECTION_KEYS),
1929            Self::CollectionValues => write_u8(buf, COLLECTION_VALUES),
1930            Self::PushLiteral(idx) => {
1931                write_u8(buf, PUSH_LITERAL);
1932                write_u32(buf, idx);
1933            }
1934
1935            // Sharing discipline
1936            Self::TakeGlobal(id) => {
1937                write_u8(buf, TAKE_GLOBAL);
1938                write_def_id(buf, id);
1939            }
1940            Self::TakeTemp(idx) => {
1941                write_u8(buf, TAKE_TEMP);
1942                write_u16(buf, idx);
1943            }
1944
1945            // Records
1946            Self::RecordNew(shape_id) => {
1947                write_u8(buf, RECORD_NEW);
1948                write_u32(buf, shape_id);
1949            }
1950            Self::RecordGetDyn(name_id) => {
1951                write_u8(buf, RECORD_GET_DYN);
1952                write_u16(buf, name_id);
1953            }
1954            Self::RecordSetDyn(name_id) => {
1955                write_u8(buf, RECORD_SET_DYN);
1956                write_u16(buf, name_id);
1957            }
1958            Self::RecordGet(offset) => {
1959                write_u8(buf, RECORD_GET);
1960                write_u16(buf, offset);
1961            }
1962            Self::RecordSet(offset) => {
1963                write_u8(buf, RECORD_SET);
1964                write_u16(buf, offset);
1965            }
1966
1967            // Conversion intrinsics (TM-3 completion, #659)
1968            Self::ConvertInt => write_u8(buf, CONVERT_INT),
1969            Self::ConvertFloat => write_u8(buf, CONVERT_FLOAT),
1970            Self::ConvertString => write_u8(buf, CONVERT_STRING),
1971
1972            // Function values (T1c, #700)
1973            Self::PushFnRef(id) => {
1974                write_u8(buf, PUSH_FN_REF);
1975                write_def_id(buf, id);
1976            }
1977            Self::MakeClosure {
1978                target,
1979                bound_count,
1980            } => {
1981                write_u8(buf, MAKE_CLOSURE);
1982                write_def_id(buf, target);
1983                write_u8(buf, bound_count);
1984            }
1985            Self::CallValue(argc) => {
1986                write_u8(buf, CALL_VALUE);
1987                write_u8(buf, argc);
1988            }
1989            Self::BindValue(argc) => {
1990                write_u8(buf, BIND_VALUE);
1991                write_u8(buf, argc);
1992            }
1993
1994            // Path projections (T1e)
1995            Self::MakeProjection {
1996                root,
1997                segment_count,
1998            } => {
1999                write_u8(buf, MAKE_PROJECTION);
2000                write_def_id(buf, root);
2001                write_u8(buf, segment_count);
2002            }
2003            Self::ProjRead => write_u8(buf, PROJ_READ),
2004            Self::ProjWrite => write_u8(buf, PROJ_WRITE),
2005
2006            // Stdlib slice 1 completion (#857)
2007            Self::CharAt => write_u8(buf, CHAR_AT),
2008
2009            // NS-A1 Option + stdlib flips
2010            Self::PushNone => write_u8(buf, PUSH_NONE),
2011            Self::MakeSome => write_u8(buf, MAKE_SOME),
2012            Self::StrFind => write_u8(buf, STR_FIND),
2013            Self::SeqIndexOf => write_u8(buf, SEQ_INDEX_OF),
2014            Self::SeqMin => write_u8(buf, SEQ_MIN),
2015            Self::SeqMax => write_u8(buf, SEQ_MAX),
2016            Self::SeqFirst => write_u8(buf, SEQ_FIRST),
2017            Self::SeqLast => write_u8(buf, SEQ_LAST),
2018            Self::SeqPop => write_u8(buf, SEQ_POP),
2019            Self::MapGetOpt => write_u8(buf, MAP_GET_OPT),
2020            Self::MapContainsValue => write_u8(buf, MAP_CONTAINS_VALUE),
2021            Self::MapClear => write_u8(buf, MAP_CLEAR),
2022
2023            // B1 `or`-coalescing, short-circuited (#1471)
2024            Self::CoalesceSome(offset) => {
2025                write_u8(buf, COALESCE_SOME);
2026                write_i32(buf, offset);
2027            }
2028            Self::OptionBind(slot) => {
2029                write_u8(buf, OPTION_BIND);
2030                write_u16(buf, slot);
2031            }
2032            Self::SeqRemoveAt => write_u8(buf, SEQ_REMOVE_AT),
2033            Self::RandFloat => write_u8(buf, RAND_FLOAT),
2034            Self::RandChance => write_u8(buf, RAND_CHANCE),
2035            Self::RandPick => write_u8(buf, RAND_PICK),
2036            Self::RandShuffle => write_u8(buf, RAND_SHUFFLE),
2037            Self::RangeMakeExcl => write_u8(buf, RANGE_MAKE_EXCL),
2038            Self::RangeMakeIncl => write_u8(buf, RANGE_MAKE_INCL),
2039            Self::RangeNonEmpty => write_u8(buf, RANGE_NON_EMPTY),
2040
2041            // NS-A4 ordering verbs
2042            Self::SeqSorted => write_u8(buf, SEQ_SORTED),
2043            Self::SeqSortedBy => write_u8(buf, SEQ_SORTED_BY),
2044
2045            // NS-A7 collections+: discriminant + CollectOp kind byte.
2046            Self::Collect(op) => {
2047                write_u8(buf, COLLECT);
2048                write_u8(buf, op.to_byte());
2049            }
2050
2051            // The fn-value verbs: discriminant + SeqVerbOp kind byte.
2052            Self::SeqVerb(op) => {
2053                write_u8(buf, SEQ_VERB);
2054                write_u8(buf, op.to_byte());
2055            }
2056
2057            // NS-A8 numeric tower: discriminant + TowerOp kind byte.
2058            Self::Tower(op) => {
2059                write_u8(buf, TOWER);
2060                write_u8(buf, op.to_byte());
2061            }
2062
2063            // Lifecycle
2064            Self::Done => write_u8(buf, DONE),
2065            Self::Yield => write_u8(buf, YIELD),
2066            Self::End => write_u8(buf, END),
2067            Self::Nop => write_u8(buf, NOP),
2068
2069            // String eval
2070            Self::BeginStringEval => write_u8(buf, BEGIN_STRING_EVAL),
2071            Self::EndStringEval => write_u8(buf, END_STRING_EVAL),
2072
2073            // Debug
2074            Self::SourceLocation(line, col) => {
2075                write_u8(buf, SOURCE_LOCATION);
2076                write_u32(buf, line);
2077                write_u32(buf, col);
2078            }
2079        }
2080    }
2081
2082    /// Decode a single instruction from `buf` starting at `*offset`.
2083    ///
2084    /// On success, `*offset` is advanced past the consumed bytes.
2085    #[expect(clippy::too_many_lines)]
2086    pub fn decode(buf: &[u8], offset: &mut usize) -> Result<Self, DecodeError> {
2087        let disc = read_u8(buf, offset)?;
2088
2089        let op = match disc {
2090            // Stack & literals
2091            PUSH_INT => Self::PushInt(read_i32(buf, offset)?),
2092            PUSH_FLOAT => Self::PushFloat(read_f32(buf, offset)?),
2093            PUSH_BOOL => Self::PushBool(read_u8(buf, offset)? != 0),
2094            PUSH_STRING => Self::PushString(read_u16(buf, offset)?),
2095            PUSH_LIST => Self::PushList(read_u16(buf, offset)?),
2096            PUSH_DIVERT_TARGET => Self::PushDivertTarget(read_def_id(buf, offset)?),
2097            PUSH_NULL => Self::PushNull,
2098            POP => Self::Pop,
2099            DUPLICATE => Self::Duplicate,
2100
2101            // Arithmetic
2102            ADD => Self::Add,
2103            SUBTRACT => Self::Subtract,
2104            MULTIPLY => Self::Multiply,
2105            DIVIDE => Self::Divide,
2106            MODULO => Self::Modulo,
2107            NEGATE => Self::Negate,
2108
2109            // Comparison
2110            EQUAL => Self::Equal,
2111            NOT_EQUAL => Self::NotEqual,
2112            GREATER => Self::Greater,
2113            GREATER_OR_EQUAL => Self::GreaterOrEqual,
2114            LESS => Self::Less,
2115            LESS_OR_EQUAL => Self::LessOrEqual,
2116
2117            // Logic
2118            NOT => Self::Not,
2119            AND => Self::And,
2120            OR => Self::Or,
2121
2122            // Global vars
2123            GET_GLOBAL => Self::GetGlobal(read_def_id(buf, offset)?),
2124            SET_GLOBAL => Self::SetGlobal(read_def_id(buf, offset)?),
2125
2126            // Temp vars
2127            DECLARE_TEMP => Self::DeclareTemp(read_u16(buf, offset)?),
2128            GET_TEMP => Self::GetTemp(read_u16(buf, offset)?),
2129            SET_TEMP => Self::SetTemp(read_u16(buf, offset)?),
2130            GET_TEMP_RAW => Self::GetTempRaw(read_u16(buf, offset)?),
2131
2132            // Variable pointers
2133            PUSH_VAR_POINTER => Self::PushVarPointer(read_def_id(buf, offset)?),
2134            PUSH_TEMP_POINTER => Self::PushTempPointer(read_u16(buf, offset)?),
2135
2136            // Control flow
2137            JUMP => Self::Jump(read_i32(buf, offset)?),
2138            JUMP_IF_FALSE => Self::JumpIfFalse(read_i32(buf, offset)?),
2139            GOTO => Self::Goto(read_def_id(buf, offset)?),
2140            GOTO_IF => Self::GotoIf(read_def_id(buf, offset)?),
2141            GOTO_VARIABLE => Self::GotoVariable,
2142
2143            // Container flow
2144            ENTER_CONTAINER => Self::EnterContainer(read_def_id(buf, offset)?),
2145            EXIT_CONTAINER => Self::ExitContainer,
2146
2147            // Functions / tunnels
2148            CALL => Self::Call(read_def_id(buf, offset)?),
2149            RETURN => Self::Return,
2150            TUNNEL_CALL => Self::TunnelCall(read_def_id(buf, offset)?),
2151            TUNNEL_RETURN => Self::TunnelReturn,
2152            TUNNEL_CALL_VARIABLE => Self::TunnelCallVariable,
2153            CALL_VARIABLE => Self::CallVariable(read_u8(buf, offset)?),
2154
2155            // Threads
2156            THREAD_CALL => Self::ThreadCall(read_def_id(buf, offset)?),
2157            THREAD_START => Self::ThreadStart,
2158            THREAD_DONE => Self::ThreadDone,
2159
2160            // Output
2161            EMIT_LINE => {
2162                let idx = read_u16(buf, offset)?;
2163                let slot_count = read_u8(buf, offset)?;
2164                Self::EmitLine(idx, slot_count)
2165            }
2166            EMIT_VALUE => Self::EmitValue,
2167            EMIT_NEWLINE => Self::EmitNewline,
2168            SPRING => Self::Spring,
2169            GLUE => Self::Glue,
2170            BEGIN_TAG => Self::BeginTag,
2171            END_TAG => Self::EndTag,
2172            EVAL_LINE => {
2173                let idx = read_u16(buf, offset)?;
2174                let slot_count = read_u8(buf, offset)?;
2175                Self::EvalLine(idx, slot_count)
2176            }
2177            BEGIN_FRAGMENT => Self::BeginFragment,
2178            END_FRAGMENT => Self::EndFragment,
2179            ATTACH_ELEMENT => Self::AttachElement,
2180            END_ELEMENT_RUN => Self::EndElementRun,
2181
2182            // Choices
2183            BEGIN_CHOICE => {
2184                let flags = ChoiceFlags::from_byte(read_u8(buf, offset)?);
2185                let target = read_def_id(buf, offset)?;
2186                Self::BeginChoice(flags, target)
2187            }
2188            END_CHOICE => Self::EndChoice,
2189
2190            // Sequences
2191            SEQUENCE => {
2192                let kind = SequenceKind::from_byte(read_u8(buf, offset)?)?;
2193                let count = read_u8(buf, offset)?;
2194                Self::Sequence(kind, count)
2195            }
2196            SEQUENCE_BRANCH => Self::SequenceBranch(read_i32(buf, offset)?),
2197
2198            // Intrinsics
2199            VISIT_COUNT => Self::VisitCount,
2200            CURRENT_VISIT_COUNT => Self::CurrentVisitCount,
2201            TURNS_SINCE => Self::TurnsSince,
2202            TURN_INDEX => Self::TurnIndex,
2203            CHOICE_COUNT => Self::ChoiceCount,
2204            RANDOM => Self::Random,
2205            SEED_RANDOM => Self::SeedRandom,
2206
2207            // Casts / math
2208            CAST_TO_INT => Self::CastToInt,
2209            CAST_TO_FLOAT => Self::CastToFloat,
2210            FLOOR => Self::Floor,
2211            CEILING => Self::Ceiling,
2212            POW => Self::Pow,
2213            MIN => Self::Min,
2214            MAX => Self::Max,
2215
2216            // External fns
2217            CALL_EXTERNAL => {
2218                let id = read_def_id(buf, offset)?;
2219                let argc = read_u8(buf, offset)?;
2220                Self::CallExternal(id, argc)
2221            }
2222
2223            // List ops
2224            LIST_CONTAINS => Self::ListContains,
2225            LIST_NOT_CONTAINS => Self::ListNotContains,
2226            LIST_INTERSECT => Self::ListIntersect,
2227            LIST_ALL => Self::ListAll,
2228            LIST_INVERT => Self::ListInvert,
2229            LIST_COUNT => Self::ListCount,
2230            LIST_MIN => Self::ListMin,
2231            LIST_MAX => Self::ListMax,
2232            LIST_VALUE => Self::ListValue,
2233            LIST_RANGE => Self::ListRange,
2234            LIST_FROM_INT => Self::ListFromInt,
2235            LIST_RANDOM => Self::ListRandom,
2236
2237            // Collections
2238            ARRAY_NEW => Self::ArrayNew(read_u32(buf, offset)?),
2239            MAP_NEW => Self::MapNew(read_u32(buf, offset)?),
2240            INDEX_GET => Self::IndexGet,
2241            INDEX_SET => Self::IndexSet,
2242            COLLECTION_LEN => Self::CollectionLen,
2243            MAP_GET => Self::MapGet,
2244            MAP_INSERT => Self::MapInsert,
2245            MAP_REMOVE => Self::MapRemove,
2246            MAP_CONTAINS => Self::MapContains,
2247            COLLECTION_KEYS => Self::CollectionKeys,
2248            COLLECTION_VALUES => Self::CollectionValues,
2249            PUSH_LITERAL => Self::PushLiteral(read_u32(buf, offset)?),
2250
2251            // Sharing discipline
2252            TAKE_GLOBAL => Self::TakeGlobal(read_def_id(buf, offset)?),
2253            TAKE_TEMP => Self::TakeTemp(read_u16(buf, offset)?),
2254
2255            // Records
2256            RECORD_NEW => Self::RecordNew(read_u32(buf, offset)?),
2257            RECORD_GET_DYN => Self::RecordGetDyn(read_u16(buf, offset)?),
2258            RECORD_SET_DYN => Self::RecordSetDyn(read_u16(buf, offset)?),
2259            RECORD_GET => Self::RecordGet(read_u16(buf, offset)?),
2260            RECORD_SET => Self::RecordSet(read_u16(buf, offset)?),
2261
2262            // Conversion intrinsics (TM-3 completion, #659)
2263            CONVERT_INT => Self::ConvertInt,
2264            CONVERT_FLOAT => Self::ConvertFloat,
2265            CONVERT_STRING => Self::ConvertString,
2266
2267            // Function values (T1c, #700)
2268            PUSH_FN_REF => Self::PushFnRef(read_def_id(buf, offset)?),
2269            MAKE_CLOSURE => Self::MakeClosure {
2270                target: read_def_id(buf, offset)?,
2271                bound_count: read_u8(buf, offset)?,
2272            },
2273            CALL_VALUE => Self::CallValue(read_u8(buf, offset)?),
2274            BIND_VALUE => Self::BindValue(read_u8(buf, offset)?),
2275
2276            // Path projections (T1e)
2277            MAKE_PROJECTION => Self::MakeProjection {
2278                root: read_def_id(buf, offset)?,
2279                segment_count: read_u8(buf, offset)?,
2280            },
2281            PROJ_READ => Self::ProjRead,
2282            PROJ_WRITE => Self::ProjWrite,
2283
2284            // Stdlib slice 1 completion (#857)
2285            CHAR_AT => Self::CharAt,
2286
2287            // NS-A1 Option + stdlib flips
2288            PUSH_NONE => Self::PushNone,
2289            MAKE_SOME => Self::MakeSome,
2290            STR_FIND => Self::StrFind,
2291            SEQ_INDEX_OF => Self::SeqIndexOf,
2292            SEQ_MIN => Self::SeqMin,
2293            SEQ_MAX => Self::SeqMax,
2294            SEQ_FIRST => Self::SeqFirst,
2295            SEQ_LAST => Self::SeqLast,
2296            SEQ_POP => Self::SeqPop,
2297            MAP_GET_OPT => Self::MapGetOpt,
2298            MAP_CONTAINS_VALUE => Self::MapContainsValue,
2299            MAP_CLEAR => Self::MapClear,
2300
2301            // B1 `or`-coalescing, short-circuited (#1471)
2302            COALESCE_SOME => Self::CoalesceSome(read_i32(buf, offset)?),
2303            OPTION_BIND => Self::OptionBind(read_u16(buf, offset)?),
2304            SEQ_REMOVE_AT => Self::SeqRemoveAt,
2305            RAND_FLOAT => Self::RandFloat,
2306            RAND_CHANCE => Self::RandChance,
2307            RAND_PICK => Self::RandPick,
2308            RAND_SHUFFLE => Self::RandShuffle,
2309            RANGE_MAKE_EXCL => Self::RangeMakeExcl,
2310            RANGE_MAKE_INCL => Self::RangeMakeIncl,
2311            RANGE_NON_EMPTY => Self::RangeNonEmpty,
2312
2313            // NS-A4 ordering verbs
2314            SEQ_SORTED => Self::SeqSorted,
2315            SEQ_SORTED_BY => Self::SeqSortedBy,
2316
2317            // NS-A8 numeric tower: TowerOp kind byte follows; unknown
2318            // kinds are a decode error (reserved-tag discipline).
2319            TOWER => Self::Tower(TowerOp::from_byte(read_u8(buf, offset)?)?),
2320
2321            // NS-A7 collections+: CollectOp kind byte follows; unknown
2322            // kinds are a decode error (reserved-tag discipline).
2323            COLLECT => Self::Collect(CollectOp::from_byte(read_u8(buf, offset)?)?),
2324
2325            // The fn-value verbs: SeqVerbOp kind byte follows; unknown
2326            // kinds are a decode error (reserved-tag discipline).
2327            SEQ_VERB => Self::SeqVerb(SeqVerbOp::from_byte(read_u8(buf, offset)?)?),
2328
2329            // Lifecycle
2330            DONE => Self::Done,
2331            YIELD => Self::Yield,
2332            END => Self::End,
2333            NOP => Self::Nop,
2334
2335            // String eval
2336            BEGIN_STRING_EVAL => Self::BeginStringEval,
2337            END_STRING_EVAL => Self::EndStringEval,
2338
2339            // Debug
2340            SOURCE_LOCATION => {
2341                let line = read_u32(buf, offset)?;
2342                let col = read_u32(buf, offset)?;
2343                Self::SourceLocation(line, col)
2344            }
2345
2346            _ => return Err(DecodeError::UnknownOpcode(disc)),
2347        };
2348
2349        Ok(op)
2350    }
2351}
2352
2353#[cfg(test)]
2354mod tests {
2355    use super::*;
2356    use crate::id::DefinitionTag;
2357
2358    fn roundtrip(op: &Opcode) {
2359        let mut buf = Vec::new();
2360        op.encode(&mut buf);
2361        let mut offset = 0;
2362        let decoded = Opcode::decode(&buf, &mut offset).unwrap();
2363        assert_eq!(*op, decoded, "roundtrip failed for {op:?}");
2364        assert_eq!(offset, buf.len(), "not all bytes consumed for {op:?}");
2365    }
2366
2367    fn test_id() -> DefinitionId {
2368        DefinitionId::new(DefinitionTag::Address, 0xBEEF)
2369    }
2370
2371    fn global_id() -> DefinitionId {
2372        DefinitionId::new(DefinitionTag::GlobalVar, 42)
2373    }
2374
2375    fn ext_id() -> DefinitionId {
2376        DefinitionId::new(DefinitionTag::ExternalFn, 0xCAFE)
2377    }
2378
2379    #[test]
2380    fn roundtrip_stack_literals() {
2381        roundtrip(&Opcode::PushInt(0));
2382        roundtrip(&Opcode::PushInt(-1));
2383        roundtrip(&Opcode::PushInt(i32::MAX));
2384        roundtrip(&Opcode::PushInt(i32::MIN));
2385        roundtrip(&Opcode::PushFloat(0.0));
2386        roundtrip(&Opcode::PushFloat(3.125));
2387        roundtrip(&Opcode::PushFloat(f32::NEG_INFINITY));
2388        roundtrip(&Opcode::PushBool(true));
2389        roundtrip(&Opcode::PushBool(false));
2390        roundtrip(&Opcode::PushString(0));
2391        roundtrip(&Opcode::PushString(u16::MAX));
2392        roundtrip(&Opcode::PushList(7));
2393        roundtrip(&Opcode::PushDivertTarget(test_id()));
2394        roundtrip(&Opcode::PushNull);
2395        roundtrip(&Opcode::Pop);
2396        roundtrip(&Opcode::Duplicate);
2397    }
2398
2399    #[test]
2400    fn roundtrip_arithmetic() {
2401        for op in [
2402            Opcode::Add,
2403            Opcode::Subtract,
2404            Opcode::Multiply,
2405            Opcode::Divide,
2406            Opcode::Modulo,
2407            Opcode::Negate,
2408        ] {
2409            roundtrip(&op);
2410        }
2411    }
2412
2413    #[test]
2414    fn roundtrip_comparison() {
2415        for op in [
2416            Opcode::Equal,
2417            Opcode::NotEqual,
2418            Opcode::Greater,
2419            Opcode::GreaterOrEqual,
2420            Opcode::Less,
2421            Opcode::LessOrEqual,
2422        ] {
2423            roundtrip(&op);
2424        }
2425    }
2426
2427    #[test]
2428    fn roundtrip_logic() {
2429        for op in [Opcode::Not, Opcode::And, Opcode::Or] {
2430            roundtrip(&op);
2431        }
2432    }
2433
2434    #[test]
2435    fn roundtrip_globals() {
2436        roundtrip(&Opcode::GetGlobal(global_id()));
2437        roundtrip(&Opcode::SetGlobal(global_id()));
2438    }
2439
2440    #[test]
2441    fn roundtrip_temps() {
2442        roundtrip(&Opcode::DeclareTemp(0));
2443        roundtrip(&Opcode::GetTemp(5));
2444        roundtrip(&Opcode::SetTemp(u16::MAX));
2445        roundtrip(&Opcode::GetTempRaw(3));
2446    }
2447
2448    #[test]
2449    fn roundtrip_var_pointer() {
2450        roundtrip(&Opcode::PushVarPointer(global_id()));
2451        roundtrip(&Opcode::PushTempPointer(0));
2452        roundtrip(&Opcode::PushTempPointer(u16::MAX));
2453    }
2454
2455    #[test]
2456    fn roundtrip_control_flow() {
2457        roundtrip(&Opcode::Jump(0));
2458        roundtrip(&Opcode::Jump(-42));
2459        roundtrip(&Opcode::JumpIfFalse(100));
2460        roundtrip(&Opcode::Goto(test_id()));
2461        roundtrip(&Opcode::GotoIf(test_id()));
2462        roundtrip(&Opcode::GotoVariable);
2463    }
2464
2465    #[test]
2466    fn roundtrip_container_flow() {
2467        roundtrip(&Opcode::EnterContainer(test_id()));
2468        roundtrip(&Opcode::ExitContainer);
2469    }
2470
2471    #[test]
2472    fn roundtrip_functions_tunnels() {
2473        roundtrip(&Opcode::Call(test_id()));
2474        roundtrip(&Opcode::Return);
2475        roundtrip(&Opcode::TunnelCall(test_id()));
2476        roundtrip(&Opcode::TunnelReturn);
2477        roundtrip(&Opcode::TunnelCallVariable);
2478        roundtrip(&Opcode::CallVariable(0));
2479        roundtrip(&Opcode::CallVariable(3));
2480    }
2481
2482    #[test]
2483    fn roundtrip_threads() {
2484        roundtrip(&Opcode::ThreadCall(test_id()));
2485        roundtrip(&Opcode::ThreadStart);
2486        roundtrip(&Opcode::ThreadDone);
2487    }
2488
2489    #[test]
2490    fn roundtrip_output() {
2491        roundtrip(&Opcode::EmitLine(0, 0));
2492        roundtrip(&Opcode::EmitLine(999, 3));
2493        roundtrip(&Opcode::EmitValue);
2494        roundtrip(&Opcode::EmitNewline);
2495        roundtrip(&Opcode::Spring);
2496        roundtrip(&Opcode::Glue);
2497        roundtrip(&Opcode::BeginTag);
2498        roundtrip(&Opcode::EndTag);
2499        roundtrip(&Opcode::EvalLine(0, 0));
2500        roundtrip(&Opcode::EvalLine(42, 2));
2501        roundtrip(&Opcode::AttachElement);
2502        roundtrip(&Opcode::EndElementRun);
2503    }
2504
2505    #[test]
2506    fn roundtrip_choices() {
2507        roundtrip(&Opcode::BeginChoice(
2508            ChoiceFlags {
2509                has_condition: true,
2510                has_start_content: false,
2511                has_choice_only_content: true,
2512                once_only: false,
2513                is_invisible_default: true,
2514            },
2515            test_id(),
2516        ));
2517        roundtrip(&Opcode::BeginChoice(
2518            ChoiceFlags {
2519                has_condition: false,
2520                has_start_content: true,
2521                has_choice_only_content: false,
2522                once_only: true,
2523                is_invisible_default: false,
2524            },
2525            test_id(),
2526        ));
2527        roundtrip(&Opcode::EndChoice);
2528    }
2529
2530    #[test]
2531    fn roundtrip_sequences() {
2532        for kind in [
2533            SequenceKind::Cycle,
2534            SequenceKind::Stopping,
2535            SequenceKind::OnceOnly,
2536            SequenceKind::Shuffle,
2537        ] {
2538            roundtrip(&Opcode::Sequence(kind, 5));
2539        }
2540        roundtrip(&Opcode::SequenceBranch(-10));
2541        roundtrip(&Opcode::SequenceBranch(0));
2542    }
2543
2544    #[test]
2545    fn roundtrip_intrinsics() {
2546        for op in [
2547            Opcode::VisitCount,
2548            Opcode::CurrentVisitCount,
2549            Opcode::TurnsSince,
2550            Opcode::TurnIndex,
2551            Opcode::ChoiceCount,
2552            Opcode::Random,
2553            Opcode::SeedRandom,
2554        ] {
2555            roundtrip(&op);
2556        }
2557    }
2558
2559    #[test]
2560    fn roundtrip_casts_math() {
2561        for op in [
2562            Opcode::CastToInt,
2563            Opcode::CastToFloat,
2564            Opcode::Floor,
2565            Opcode::Ceiling,
2566            Opcode::Pow,
2567            Opcode::Min,
2568            Opcode::Max,
2569        ] {
2570            roundtrip(&op);
2571        }
2572    }
2573
2574    #[test]
2575    fn roundtrip_call_external() {
2576        roundtrip(&Opcode::CallExternal(ext_id(), 3));
2577        roundtrip(&Opcode::CallExternal(ext_id(), 0));
2578    }
2579
2580    #[test]
2581    fn roundtrip_list_ops() {
2582        for op in [
2583            Opcode::ListContains,
2584            Opcode::ListNotContains,
2585            Opcode::ListIntersect,
2586            Opcode::ListAll,
2587            Opcode::ListInvert,
2588            Opcode::ListCount,
2589            Opcode::ListMin,
2590            Opcode::ListMax,
2591            Opcode::ListValue,
2592            Opcode::ListRange,
2593            Opcode::ListFromInt,
2594            Opcode::ListRandom,
2595        ] {
2596            roundtrip(&op);
2597        }
2598    }
2599
2600    #[test]
2601    fn roundtrip_collections() {
2602        for op in [
2603            Opcode::ArrayNew(0),
2604            Opcode::ArrayNew(1),
2605            Opcode::ArrayNew(u32::MAX),
2606            Opcode::MapNew(0),
2607            Opcode::MapNew(3),
2608            Opcode::IndexGet,
2609            Opcode::IndexSet,
2610            Opcode::CollectionLen,
2611            Opcode::MapGet,
2612            Opcode::MapInsert,
2613            Opcode::MapRemove,
2614            Opcode::MapContains,
2615            Opcode::CollectionKeys,
2616            Opcode::CollectionValues,
2617            Opcode::PushLiteral(0),
2618            Opcode::PushLiteral(u32::MAX),
2619        ] {
2620            roundtrip(&op);
2621        }
2622    }
2623
2624    /// The collection opcode block is contiguous (`docs/format-v4-rfc.md`
2625    /// §3): `0xBE`-`0xC9` inclusive, no gaps, no overlap with the adjacent
2626    /// list-ops block (`0xB0`-`0xBD`) or the lifecycle block (`0xF0`+).
2627    #[test]
2628    fn collection_opcode_block_is_contiguous_and_matches_rfc_layout() {
2629        let expected: [(u8, Opcode); 12] = [
2630            (0xBE, Opcode::ArrayNew(0)),
2631            (0xBF, Opcode::MapNew(0)),
2632            (0xC0, Opcode::IndexGet),
2633            (0xC1, Opcode::IndexSet),
2634            (0xC2, Opcode::CollectionLen),
2635            (0xC3, Opcode::MapGet),
2636            (0xC4, Opcode::MapInsert),
2637            (0xC5, Opcode::MapRemove),
2638            (0xC6, Opcode::MapContains),
2639            (0xC7, Opcode::CollectionKeys),
2640            (0xC8, Opcode::CollectionValues),
2641            (0xC9, Opcode::PushLiteral(0)),
2642        ];
2643        for (byte, op) in expected {
2644            let mut buf = Vec::new();
2645            op.encode(&mut buf);
2646            assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
2647        }
2648    }
2649
2650    #[test]
2651    fn roundtrip_ns_a1_option_and_stdlib_flips() {
2652        for op in [
2653            Opcode::PushNone,
2654            Opcode::MakeSome,
2655            Opcode::StrFind,
2656            Opcode::SeqIndexOf,
2657            Opcode::SeqMin,
2658            Opcode::SeqMax,
2659            Opcode::SeqFirst,
2660            Opcode::SeqLast,
2661            Opcode::SeqPop,
2662            Opcode::MapGetOpt,
2663            Opcode::MapContainsValue,
2664            Opcode::MapClear,
2665        ] {
2666            roundtrip(&op);
2667        }
2668    }
2669
2670    /// The NS-A1 block layout: `PushNone`/`MakeSome` fill the two bytes
2671    /// before the string-eval block (0xE0/0xE1), the verb flips continue
2672    /// contiguously after it (0xE2-0xEB).
2673    #[test]
2674    fn ns_a1_opcode_block_layout() {
2675        let expected: [(u8, Opcode); 12] = [
2676            (0xDE, Opcode::PushNone),
2677            (0xDF, Opcode::MakeSome),
2678            (0xE2, Opcode::StrFind),
2679            (0xE3, Opcode::SeqIndexOf),
2680            (0xE4, Opcode::SeqMin),
2681            (0xE5, Opcode::SeqMax),
2682            (0xE6, Opcode::SeqFirst),
2683            (0xE7, Opcode::SeqLast),
2684            (0xE8, Opcode::SeqPop),
2685            (0xE9, Opcode::MapGetOpt),
2686            (0xEA, Opcode::MapContainsValue),
2687            (0xEB, Opcode::MapClear),
2688        ];
2689        for (byte, op) in expected {
2690            let mut buf = Vec::new();
2691            op.encode(&mut buf);
2692            assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
2693        }
2694    }
2695
2696    /// B1 `or`-coalescing (issue #1460): one opcode, one byte, no operand.
2697    #[test]
2698    fn roundtrip_b1_coalesce_some() {
2699        roundtrip(&Opcode::CoalesceSome(0));
2700        roundtrip(&Opcode::CoalesceSome(-42));
2701        roundtrip(&Opcode::CoalesceSome(100));
2702    }
2703
2704    /// `CoalesceSome` claims the first of the four bytes free after NS-A7's
2705    /// `Collect` (`0xFA`) — `docs/format-v4-rfc.md` §5 explicitly does NOT
2706    /// freeze numeric opcode assignments (only the name/encoding
2707    /// *inventory*); the reservation comment above `COALESCE_SOME` is the
2708    /// actual (implementation-level) source of truth this test pins. Same
2709    /// byte the retired binary `Coalesce` opcode held (#1460) — reused, not
2710    /// freed, since #1471 replaced that opcode's job rather than adding a
2711    /// new one alongside it.
2712    #[test]
2713    fn coalesce_some_opcode_byte_is_0xfb() {
2714        let mut buf = Vec::new();
2715        Opcode::CoalesceSome(0).encode(&mut buf);
2716        assert_eq!(
2717            buf[0], 0xFB,
2718            "CoalesceSome encoded to unexpected discriminant"
2719        );
2720    }
2721
2722    /// B1b the `as` binding (issue #1475): one opcode with a `u16` slot
2723    /// immediate, encoded like `SetTemp`/`GetTemp`.
2724    #[test]
2725    fn roundtrip_b1b_option_bind() {
2726        roundtrip(&Opcode::OptionBind(0));
2727        roundtrip(&Opcode::OptionBind(7));
2728        roundtrip(&Opcode::OptionBind(u16::MAX));
2729    }
2730
2731    /// `OptionBind` claims the first byte free after B1's `Coalesce`
2732    /// (`0xFB`) — same "the reservation comment is the source of truth"
2733    /// posture as `coalesce_opcode_byte_is_0xfb` above.
2734    #[test]
2735    fn option_bind_opcode_byte_is_0xfc() {
2736        let mut buf = Vec::new();
2737        Opcode::OptionBind(1).encode(&mut buf);
2738        assert_eq!(
2739            buf[0], 0xFC,
2740            "OptionBind encoded to unexpected discriminant"
2741        );
2742    }
2743
2744    /// Seq `remove_at` (issue #1484): one opcode, one byte, no operand —
2745    /// same shape as `Coalesce` above.
2746    #[test]
2747    fn roundtrip_seq_remove_at() {
2748        roundtrip(&Opcode::SeqRemoveAt);
2749    }
2750
2751    /// `SeqRemoveAt` claims the byte free after `OptionBind`
2752    /// (`0xFC`-`0xFD` + `0xFF` remained after `0xFB`; `OptionBind` claimed
2753    /// `0xFC`) — the reservation comment above `SEQ_REMOVE_AT` is the actual
2754    /// (implementation-level) source of truth this test pins.
2755    #[test]
2756    fn seq_remove_at_opcode_byte_is_0xfd() {
2757        let mut buf = Vec::new();
2758        Opcode::SeqRemoveAt.encode(&mut buf);
2759        assert_eq!(
2760            buf[0], 0xFD,
2761            "SeqRemoveAt encoded to unexpected discriminant"
2762        );
2763    }
2764
2765    #[test]
2766    fn roundtrip_ns_a6_rand_verbs() {
2767        for op in [
2768            Opcode::RandFloat,
2769            Opcode::RandChance,
2770            Opcode::RandPick,
2771            Opcode::RandShuffle,
2772        ] {
2773            roundtrip(&op);
2774        }
2775    }
2776
2777    #[test]
2778    fn roundtrip_ns_a5_range_ops() {
2779        for op in [
2780            Opcode::RangeMakeExcl,
2781            Opcode::RangeMakeIncl,
2782            Opcode::RangeNonEmpty,
2783        ] {
2784            roundtrip(&op);
2785        }
2786    }
2787
2788    /// The NS-A5 block layout: the three range ops take the first free
2789    /// bytes after the lifecycle block (0xF0-0xF3). `rand::int` has NO
2790    /// byte — it rides the existing `ConvertInt` (0xE2), a value-directed
2791    /// dispatch in the VM.
2792    #[test]
2793    fn ns_a5_opcode_block_layout() {
2794        let expected: [(u8, Opcode); 3] = [
2795            (0xF4, Opcode::RangeMakeExcl),
2796            (0xF5, Opcode::RangeMakeIncl),
2797            (0xF6, Opcode::RangeNonEmpty),
2798        ];
2799        for (byte, op) in expected {
2800            let mut buf = Vec::new();
2801            op.encode(&mut buf);
2802            assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
2803        }
2804    }
2805
2806    /// The NS-A6 block layout: the four rand draw ops fill 0xEC-0xEF,
2807    /// contiguously after NS-A1's 0xEB, up against the lifecycle block
2808    /// (0xF0+). `seed(n)` deliberately has no byte here — it reuses the
2809    /// frozen `SeedRandom` (0x85): one RNG cell, two surfaces, no drift.
2810    #[test]
2811    fn ns_a6_opcode_block_layout() {
2812        let expected: [(u8, Opcode); 4] = [
2813            (0xEC, Opcode::RandFloat),
2814            (0xED, Opcode::RandChance),
2815            (0xEE, Opcode::RandPick),
2816            (0xEF, Opcode::RandShuffle),
2817        ];
2818        for (byte, op) in expected {
2819            let mut buf = Vec::new();
2820            op.encode(&mut buf);
2821            assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
2822        }
2823    }
2824
2825    /// The NS-A4 block layout: the two ordering ops take the next free
2826    /// bytes after NS-A8's `Tower` discriminant (0xF7). Two ops serve
2827    /// four source verbs — `sort`/`sorted` share `SeqSorted`,
2828    /// `sort_by`/`sorted_by` share `SeqSortedBy` (in-place-ness comes
2829    /// from the RMW write-back, the `shuffle`/`shuffled` precedent).
2830    #[test]
2831    fn ns_a4_opcode_block_layout() {
2832        let expected: [(u8, Opcode); 2] = [(0xF8, Opcode::SeqSorted), (0xF9, Opcode::SeqSortedBy)];
2833        for (byte, op) in expected {
2834            let mut buf = Vec::new();
2835            op.encode(&mut buf);
2836            assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
2837            roundtrip(&op);
2838        }
2839    }
2840
2841    #[test]
2842    fn roundtrip_ns_a8_tower_ops() {
2843        for kind in TowerOp::ALL {
2844            roundtrip(&Opcode::Tower(kind));
2845        }
2846    }
2847
2848    /// The NS-A8 layout: ONE discriminant byte (0xF7 — the first free byte
2849    /// after the NS-A5 range ops at 0xF4-0xF6) with the `TowerOp` kind as a
2850    /// u8 immediate in kind-byte order 0..=12. Deliberate opcode-space
2851    /// economy — see the `TOWER` const's comment.
2852    #[test]
2853    fn ns_a8_tower_opcode_layout() {
2854        for (i, kind) in TowerOp::ALL.into_iter().enumerate() {
2855            let mut buf = Vec::new();
2856            Opcode::Tower(kind).encode(&mut buf);
2857            #[expect(clippy::cast_possible_truncation, reason = "13 kinds")]
2858            let expected_kind = i as u8;
2859            assert_eq!(buf, [0xF7, expected_kind], "{kind:?} layout drifted");
2860        }
2861    }
2862
2863    /// An unknown tower kind byte is a decode error, not a silent skip —
2864    /// the same reserved-tag discipline as every other closed sub-enum.
2865    #[test]
2866    fn decode_unknown_tower_kind_rejected() {
2867        let buf = [0xF7, 13];
2868        let mut offset = 0;
2869        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
2870        assert_eq!(err, DecodeError::InvalidTowerOp(13));
2871    }
2872
2873    #[test]
2874    fn roundtrip_ns_a7_collect_ops() {
2875        for kind in CollectOp::ALL {
2876            roundtrip(&Opcode::Collect(kind));
2877        }
2878    }
2879
2880    /// The NS-A7 layout: ONE discriminant byte (0xFA — the first free byte
2881    /// after the NS-A4 ordering verbs at 0xF8/0xF9) with the `CollectOp`
2882    /// kind as a u8 immediate in kind-byte order 0..=4. The NS-A8 Tower
2883    /// opcode-space economy applied again — see the `COLLECT` const's
2884    /// comment.
2885    #[test]
2886    fn ns_a7_collect_opcode_layout() {
2887        for (i, kind) in CollectOp::ALL.into_iter().enumerate() {
2888            let mut buf = Vec::new();
2889            Opcode::Collect(kind).encode(&mut buf);
2890            #[expect(clippy::cast_possible_truncation, reason = "5 kinds")]
2891            let expected_kind = i as u8;
2892            assert_eq!(buf, [0xFA, expected_kind], "{kind:?} layout drifted");
2893        }
2894    }
2895
2896    /// An unknown collections+ kind byte is a decode error, not a silent
2897    /// skip — same reserved-tag discipline as `TowerOp`.
2898    #[test]
2899    fn decode_unknown_collect_kind_rejected() {
2900        let buf = [0xFA, 5];
2901        let mut offset = 0;
2902        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
2903        assert_eq!(err, DecodeError::InvalidCollectOp(5));
2904    }
2905
2906    #[test]
2907    fn roundtrip_seq_verb_ops() {
2908        for kind in SeqVerbOp::ALL {
2909            roundtrip(&Opcode::SeqVerb(kind));
2910        }
2911    }
2912
2913    /// The fn-value verb layout (issue #1679): ONE discriminant byte
2914    /// (`0xA1` — the first byte of the `0xA1`-`0xAF` run after
2915    /// `CallExternal`, chosen over the high tail's last free byte `0xFF`,
2916    /// which stays unclaimed as a future extended-opcode prefix) with the
2917    /// `SeqVerbOp` kind as a u8 immediate in kind-byte order 0..=5. See the
2918    /// `SEQ_VERB` const's comment.
2919    #[test]
2920    fn seq_verb_opcode_layout() {
2921        for (i, kind) in SeqVerbOp::ALL.into_iter().enumerate() {
2922            let mut buf = Vec::new();
2923            Opcode::SeqVerb(kind).encode(&mut buf);
2924            #[expect(clippy::cast_possible_truncation, reason = "6 kinds")]
2925            let expected_kind = i as u8;
2926            assert_eq!(buf, [0xA1, expected_kind], "{kind:?} layout drifted");
2927        }
2928    }
2929
2930    /// An unknown fn-value verb kind byte is a decode error, not a silent
2931    /// skip — same reserved-tag discipline as `TowerOp`/`CollectOp`.
2932    #[test]
2933    fn decode_unknown_seq_verb_kind_rejected() {
2934        let buf = [0xA1, 6];
2935        let mut offset = 0;
2936        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
2937        assert_eq!(err, DecodeError::InvalidSeqVerbOp(6));
2938    }
2939
2940    /// Mnemonics round-trip through the `.inkt` reader's inverse, and are
2941    /// exactly the source spellings the verbs ship under.
2942    #[test]
2943    fn seq_verb_mnemonics_round_trip() {
2944        for kind in SeqVerbOp::ALL {
2945            assert_eq!(SeqVerbOp::from_mnemonic(kind.mnemonic()), Some(kind));
2946        }
2947        assert_eq!(SeqVerbOp::Map.mnemonic(), "map");
2948        assert_eq!(SeqVerbOp::Filter.mnemonic(), "filter");
2949        assert_eq!(SeqVerbOp::Fold.mnemonic(), "fold");
2950        assert_eq!(SeqVerbOp::FilterMap.mnemonic(), "filter_map");
2951        assert_eq!(SeqVerbOp::Each.mnemonic(), "each");
2952        assert_eq!(SeqVerbOp::MapEach.mnemonic(), "map_each");
2953        assert_eq!(
2954            SeqVerbOp::from_mnemonic("map_each"),
2955            Some(SeqVerbOp::MapEach)
2956        );
2957        assert_eq!(SeqVerbOp::from_mnemonic("not_a_verb"), None);
2958    }
2959
2960    /// [`SeqVerbOp::is_effectful`] is the VM dispatch/`guard_comparator_write`
2961    /// posture switch (issue #1679 slice 2): the pure quartet is `false`,
2962    /// the effectful pair is `true`.
2963    #[test]
2964    fn seq_verb_effectful_split() {
2965        for kind in [
2966            SeqVerbOp::Map,
2967            SeqVerbOp::Filter,
2968            SeqVerbOp::Fold,
2969            SeqVerbOp::FilterMap,
2970        ] {
2971            assert!(!kind.is_effectful(), "{kind:?} must be pure");
2972        }
2973        for kind in [SeqVerbOp::Each, SeqVerbOp::MapEach] {
2974            assert!(kind.is_effectful(), "{kind:?} must be effectful");
2975        }
2976    }
2977
2978    #[test]
2979    fn roundtrip_lifecycle() {
2980        for op in [Opcode::Done, Opcode::Yield, Opcode::End, Opcode::Nop] {
2981            roundtrip(&op);
2982        }
2983    }
2984
2985    #[test]
2986    fn roundtrip_string_eval() {
2987        roundtrip(&Opcode::BeginStringEval);
2988        roundtrip(&Opcode::EndStringEval);
2989    }
2990
2991    #[test]
2992    fn roundtrip_debug() {
2993        roundtrip(&Opcode::SourceLocation(1, 0));
2994        roundtrip(&Opcode::SourceLocation(u32::MAX, u32::MAX));
2995    }
2996
2997    #[test]
2998    fn decode_unknown_opcode() {
2999        let buf = [0xFF];
3000        let mut offset = 0;
3001        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
3002        assert_eq!(err, DecodeError::UnknownOpcode(0xFF));
3003    }
3004
3005    /// The v4 collection opcode block (`0xBE`-`0xC9`, `docs/format-v4-rfc.md`
3006    /// §3 "Collections (T1a)") went live in T1b-2 (#570) — every byte in the
3007    /// block now decodes to a real `Opcode` variant (superseding the T1a-era
3008    /// "still rejected" test this replaces). `ArrayNew`/`MapNew`/
3009    /// `PushLiteral` carry a `u32` operand so a bare 1-byte buffer isn't
3010    /// enough for those three; this asserts every discriminant byte decodes
3011    /// to *some* `Opcode` (not `UnknownOpcode`), operand length aside.
3012    #[test]
3013    fn collection_opcode_block_no_longer_rejected() {
3014        for disc in 0xBEu8..=0xC9u8 {
3015            // Pad with zero bytes so the 4-byte `u32` operand opcodes
3016            // (`ArrayNew`/`MapNew`/`PushLiteral`) have enough to decode too.
3017            let buf = [disc, 0, 0, 0, 0];
3018            let mut offset = 0;
3019            let result = Opcode::decode(&buf, &mut offset);
3020            assert!(
3021                !matches!(result, Err(DecodeError::UnknownOpcode(_))),
3022                "0x{disc:02x} should decode to a real Opcode, got {result:?}"
3023            );
3024        }
3025    }
3026
3027    /// `StoreVarIfNew`/`EqVars` (`0xCB`-`0xCC`, `docs/format-v4-rfc.md` §3
3028    /// "Sharing discipline (T1a)") stay numbered but deliberately not wired
3029    /// into `Opcode` — the strict reader must keep rejecting both bytes
3030    /// until their own milestone lands (spec §6's optional ref-collapsing
3031    /// sites, not part of T1b-4/#576).
3032    #[test]
3033    fn decode_reserved_sharing_discipline_opcodes_still_rejected() {
3034        for disc in 0xCBu8..=0xCCu8 {
3035            let buf = [disc];
3036            let mut offset = 0;
3037            let err = Opcode::decode(&buf, &mut offset).unwrap_err();
3038            assert_eq!(err, DecodeError::UnknownOpcode(disc));
3039        }
3040    }
3041
3042    /// `TakeGlobal`/`TakeTemp` (`0xCA`, `0xCD` — T1b-4/#576) round-trip
3043    /// through encode/decode.
3044    #[test]
3045    fn roundtrip_take_opcodes() {
3046        roundtrip(&Opcode::TakeGlobal(global_id()));
3047        roundtrip(&Opcode::TakeTemp(0));
3048        roundtrip(&Opcode::TakeTemp(u16::MAX));
3049    }
3050
3051    /// `TakeGlobal`/`TakeTemp` land at the exact bytes the RFC comment in
3052    /// `opcode.rs` documents — `0xCA` (splitting the RFC's generic
3053    /// `TakeVar(slot)`) and `0xCD` (freshly claimed, adjacent to the
3054    /// reserved block, leaving `0xCB`/`0xCC` untouched for
3055    /// `StoreVarIfNew`/`EqVars`).
3056    #[test]
3057    fn take_opcodes_land_at_documented_bytes() {
3058        let mut buf = Vec::new();
3059        Opcode::TakeGlobal(global_id()).encode(&mut buf);
3060        assert_eq!(buf[0], 0xCA);
3061
3062        let mut buf = Vec::new();
3063        Opcode::TakeTemp(0).encode(&mut buf);
3064        assert_eq!(buf[0], 0xCD);
3065    }
3066
3067    /// All five record opcodes (`0xCE`-`0xD2` — TM-4/TM-4c) round-trip
3068    /// through encode/decode at their documented bytes.
3069    #[test]
3070    fn roundtrip_record_opcodes() {
3071        roundtrip(&Opcode::RecordNew(0));
3072        roundtrip(&Opcode::RecordNew(u32::MAX));
3073        roundtrip(&Opcode::RecordGetDyn(0));
3074        roundtrip(&Opcode::RecordGetDyn(u16::MAX));
3075        roundtrip(&Opcode::RecordSetDyn(0));
3076        roundtrip(&Opcode::RecordSetDyn(u16::MAX));
3077        roundtrip(&Opcode::RecordGet(0));
3078        roundtrip(&Opcode::RecordGet(u16::MAX));
3079        roundtrip(&Opcode::RecordSet(0));
3080        roundtrip(&Opcode::RecordSet(u16::MAX));
3081
3082        let mut buf = Vec::new();
3083        Opcode::RecordNew(1).encode(&mut buf);
3084        assert_eq!(buf[0], 0xCE);
3085
3086        let mut buf = Vec::new();
3087        Opcode::RecordGetDyn(1).encode(&mut buf);
3088        assert_eq!(buf[0], 0xCF);
3089
3090        let mut buf = Vec::new();
3091        Opcode::RecordSetDyn(1).encode(&mut buf);
3092        assert_eq!(buf[0], 0xD0);
3093
3094        let mut buf = Vec::new();
3095        Opcode::RecordGet(1).encode(&mut buf);
3096        assert_eq!(buf[0], 0xD1);
3097
3098        let mut buf = Vec::new();
3099        Opcode::RecordSet(1).encode(&mut buf);
3100        assert_eq!(buf[0], 0xD2);
3101    }
3102
3103    /// The three TM-3-completion conversion-intrinsic opcodes (`0xD3`-`0xD5`
3104    /// — issue #659) round-trip and sit contiguous and adjacent to the
3105    /// record block, matching the reservation comment above `CONVERT_INT`.
3106    #[test]
3107    fn roundtrip_conversion_opcodes() {
3108        for op in [
3109            Opcode::ConvertInt,
3110            Opcode::ConvertFloat,
3111            Opcode::ConvertString,
3112        ] {
3113            roundtrip(&op);
3114        }
3115
3116        let mut buf = Vec::new();
3117        Opcode::ConvertInt.encode(&mut buf);
3118        assert_eq!(buf[0], 0xD3);
3119
3120        let mut buf = Vec::new();
3121        Opcode::ConvertFloat.encode(&mut buf);
3122        assert_eq!(buf[0], 0xD4);
3123
3124        let mut buf = Vec::new();
3125        Opcode::ConvertString.encode(&mut buf);
3126        assert_eq!(buf[0], 0xD5);
3127    }
3128
3129    /// The `char_at(s, i)` stdlib-slice-1-completion opcode (`0xDD` — issue
3130    /// #857) round-trips and sits contiguous and adjacent to the projection
3131    /// block, matching the reservation comment above `CHAR_AT`.
3132    #[test]
3133    fn roundtrip_char_at_opcode() {
3134        roundtrip(&Opcode::CharAt);
3135
3136        let mut buf = Vec::new();
3137        Opcode::CharAt.encode(&mut buf);
3138        assert_eq!(buf[0], 0xDD);
3139    }
3140
3141    #[test]
3142    fn decode_unexpected_eof() {
3143        // PushInt needs 4 more bytes after the discriminant.
3144        let buf = [PUSH_INT, 0x00];
3145        let mut offset = 0;
3146        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
3147        assert_eq!(err, DecodeError::UnexpectedEof);
3148    }
3149
3150    #[test]
3151    fn decode_multiple_instructions() {
3152        let ops = vec![
3153            Opcode::PushInt(42),
3154            Opcode::PushBool(true),
3155            Opcode::Add,
3156            Opcode::Done,
3157        ];
3158        let mut buf = Vec::new();
3159        for op in &ops {
3160            op.encode(&mut buf);
3161        }
3162        let mut offset = 0;
3163        for expected in &ops {
3164            let decoded = Opcode::decode(&buf, &mut offset).unwrap();
3165            assert_eq!(*expected, decoded);
3166        }
3167        assert_eq!(offset, buf.len());
3168    }
3169
3170    #[test]
3171    fn choice_flags_roundtrip() {
3172        for bits in 0..32u8 {
3173            let flags = ChoiceFlags::from_byte(bits);
3174            assert_eq!(flags.to_byte(), bits);
3175        }
3176    }
3177}