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}