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