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