Skip to main content

brink_format/
opcode.rs

1use core::fmt;
2
3use crate::codec::{
4    read_def_id, read_f32, read_i32, read_u8, read_u16, read_u32, write_def_id, write_f32,
5    write_i32, write_u8, write_u16, write_u32,
6};
7use crate::id::DefinitionId;
8
9// ── Discriminant bytes ──────────────────────────────────────────────────────
10
11// Stack & literals
12const PUSH_INT: u8 = 0x01;
13const PUSH_FLOAT: u8 = 0x02;
14const PUSH_BOOL: u8 = 0x03;
15const PUSH_STRING: u8 = 0x04;
16const PUSH_LIST: u8 = 0x05;
17const PUSH_DIVERT_TARGET: u8 = 0x06;
18const PUSH_NULL: u8 = 0x07;
19const POP: u8 = 0x08;
20const DUPLICATE: u8 = 0x09;
21
22// Arithmetic
23const ADD: u8 = 0x10;
24const SUBTRACT: u8 = 0x11;
25const MULTIPLY: u8 = 0x12;
26const DIVIDE: u8 = 0x13;
27const MODULO: u8 = 0x14;
28const NEGATE: u8 = 0x15;
29
30// Comparison
31const EQUAL: u8 = 0x20;
32const NOT_EQUAL: u8 = 0x21;
33const GREATER: u8 = 0x22;
34const GREATER_OR_EQUAL: u8 = 0x23;
35const LESS: u8 = 0x24;
36const LESS_OR_EQUAL: u8 = 0x25;
37
38// Logic
39const NOT: u8 = 0x28;
40const AND: u8 = 0x29;
41const OR: u8 = 0x2A;
42
43// Global vars
44const GET_GLOBAL: u8 = 0x30;
45const SET_GLOBAL: u8 = 0x31;
46
47// Temp vars
48const DECLARE_TEMP: u8 = 0x34;
49const GET_TEMP: u8 = 0x35;
50const SET_TEMP: u8 = 0x36;
51const GET_TEMP_RAW: u8 = 0x37;
52
53// Variable pointers
54const PUSH_VAR_POINTER: u8 = 0x38;
55const PUSH_TEMP_POINTER: u8 = 0x39;
56
57// Control flow
58const JUMP: u8 = 0x40;
59const JUMP_IF_FALSE: u8 = 0x41;
60const GOTO: u8 = 0x42;
61const GOTO_IF: u8 = 0x43;
62const GOTO_VARIABLE: u8 = 0x44;
63
64// Container flow
65const ENTER_CONTAINER: u8 = 0x48;
66const EXIT_CONTAINER: u8 = 0x49;
67
68// Functions / tunnels
69const CALL: u8 = 0x50;
70const RETURN: u8 = 0x51;
71const TUNNEL_CALL: u8 = 0x52;
72const TUNNEL_RETURN: u8 = 0x53;
73const TUNNEL_CALL_VARIABLE: u8 = 0x54;
74const CALL_VARIABLE: u8 = 0x55;
75
76// Threads
77const THREAD_CALL: u8 = 0x57;
78const THREAD_START: u8 = 0x58;
79const THREAD_DONE: u8 = 0x59;
80
81// Output
82const EMIT_LINE: u8 = 0x60;
83const EMIT_VALUE: u8 = 0x61;
84const EMIT_NEWLINE: u8 = 0x62;
85const SPRING: u8 = 0x67;
86const GLUE: u8 = 0x63;
87const BEGIN_TAG: u8 = 0x64;
88const END_TAG: u8 = 0x65;
89const EVAL_LINE: u8 = 0x66;
90const BEGIN_FRAGMENT: u8 = 0x68;
91const END_FRAGMENT: u8 = 0x69;
92
93// Choices
94const BEGIN_CHOICE: u8 = 0x72;
95const END_CHOICE: u8 = 0x73;
96// Sequences
97const SEQUENCE: u8 = 0x78;
98const SEQUENCE_BRANCH: u8 = 0x79;
99
100// Intrinsics
101const VISIT_COUNT: u8 = 0x80;
102const TURNS_SINCE: u8 = 0x81;
103const TURN_INDEX: u8 = 0x82;
104const CHOICE_COUNT: u8 = 0x83;
105const RANDOM: u8 = 0x84;
106const SEED_RANDOM: u8 = 0x85;
107const CURRENT_VISIT_COUNT: u8 = 0x86;
108
109// Casts / math
110const CAST_TO_INT: u8 = 0x90;
111const CAST_TO_FLOAT: u8 = 0x91;
112const FLOOR: u8 = 0x92;
113const CEILING: u8 = 0x93;
114const POW: u8 = 0x94;
115const MIN: u8 = 0x95;
116const MAX: u8 = 0x96;
117
118// External fns
119const CALL_EXTERNAL: u8 = 0xA0;
120
121// List ops
122const LIST_CONTAINS: u8 = 0xB0;
123const LIST_NOT_CONTAINS: u8 = 0xB1;
124const LIST_INTERSECT: u8 = 0xB2;
125const LIST_ALL: u8 = 0xB5;
126const LIST_INVERT: u8 = 0xB6;
127const LIST_COUNT: u8 = 0xB7;
128const LIST_MIN: u8 = 0xB8;
129const LIST_MAX: u8 = 0xB9;
130const LIST_VALUE: u8 = 0xBA;
131const LIST_RANGE: u8 = 0xBB;
132const LIST_FROM_INT: u8 = 0xBC;
133const LIST_RANDOM: u8 = 0xBD;
134
135// Lifecycle
136const DONE: u8 = 0xF0;
137const YIELD: u8 = 0xF3;
138const END: u8 = 0xF1;
139const NOP: u8 = 0xF2;
140
141// String eval
142const BEGIN_STRING_EVAL: u8 = 0xE0;
143const END_STRING_EVAL: u8 = 0xE1;
144
145// Debug
146const SOURCE_LOCATION: u8 = 0xFE;
147
148// ── Types ───────────────────────────────────────────────────────────────────
149
150/// The kind of sequence/shuffle container.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum SequenceKind {
153    Cycle,
154    Stopping,
155    OnceOnly,
156    Shuffle,
157}
158
159impl SequenceKind {
160    fn to_byte(self) -> u8 {
161        match self {
162            Self::Cycle => 0,
163            Self::Stopping => 1,
164            Self::OnceOnly => 2,
165            Self::Shuffle => 3,
166        }
167    }
168
169    fn from_byte(b: u8) -> Result<Self, DecodeError> {
170        match b {
171            0 => Ok(Self::Cycle),
172            1 => Ok(Self::Stopping),
173            2 => Ok(Self::OnceOnly),
174            3 => Ok(Self::Shuffle),
175            _ => Err(DecodeError::InvalidSequenceKind(b)),
176        }
177    }
178}
179
180/// Flags packed into a `BeginChoice` instruction.
181///
182/// Under the single-pop protocol, `BeginChoice` pops at most **one** display
183/// string from the stack when `has_start_content || has_choice_only_content`.
184/// The two content flags are metadata indicating which parts of the original
185/// ink choice contributed to that string — the runtime does not pop them
186/// separately.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188#[expect(clippy::struct_excessive_bools)]
189pub struct ChoiceFlags {
190    pub has_condition: bool,
191    /// Original choice had `start` content (text before `[`).
192    pub has_start_content: bool,
193    /// Original choice had `choice_only` content (text inside `[]`).
194    /// Under the single-pop protocol this is metadata only — no extra stack pop.
195    pub has_choice_only_content: bool,
196    pub once_only: bool,
197    pub is_invisible_default: bool,
198}
199
200impl ChoiceFlags {
201    fn to_byte(self) -> u8 {
202        let mut b = 0u8;
203        if self.has_condition {
204            b |= 0x01;
205        }
206        if self.has_start_content {
207            b |= 0x02;
208        }
209        if self.has_choice_only_content {
210            b |= 0x04;
211        }
212        if self.once_only {
213            b |= 0x08;
214        }
215        if self.is_invisible_default {
216            b |= 0x10;
217        }
218        b
219    }
220
221    fn from_byte(b: u8) -> Self {
222        Self {
223            has_condition: b & 0x01 != 0,
224            has_start_content: b & 0x02 != 0,
225            has_choice_only_content: b & 0x04 != 0,
226            once_only: b & 0x08 != 0,
227            is_invisible_default: b & 0x10 != 0,
228        }
229    }
230}
231
232/// Errors that can occur when decoding from bytes.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum DecodeError {
235    /// Not enough bytes remaining for the expected operand.
236    UnexpectedEof,
237    /// Unknown opcode discriminant byte.
238    UnknownOpcode(u8),
239    /// Invalid definition id (bad tag byte).
240    InvalidDefinitionId(u64),
241    /// Invalid sequence kind byte.
242    InvalidSequenceKind(u8),
243    /// .inkb magic bytes are not `INKB`.
244    BadMagic([u8; 4]),
245    /// .inkb version is not supported.
246    UnsupportedVersion(u16),
247    /// A string field contained invalid UTF-8.
248    InvalidUtf8,
249    /// Unknown value type discriminant.
250    InvalidValueType(u8),
251    /// Unknown select key discriminant.
252    InvalidSelectKey(u8),
253    /// Unknown line part discriminant.
254    InvalidLinePart(u8),
255    /// Unknown line content discriminant.
256    InvalidLineContent(u8),
257    /// Unknown plural category discriminant.
258    InvalidPluralCategory(u8),
259    /// Unknown section kind tag in .inkb offset table.
260    InvalidSectionKind(u8),
261    /// Required section kind missing from .inkb offset table.
262    MissingSectionKind(u8),
263    /// File size field doesn't match actual buffer length.
264    FileSizeMismatch { expected: u32, actual: usize },
265    /// CRC-32 checksum of section data doesn't match header.
266    ChecksumMismatch { expected: u32, actual: u32 },
267    /// Section offset table is structurally invalid (out of bounds or not monotonic).
268    InvalidSectionOffset { kind: u8, offset: u32 },
269    /// `.inkl` magic bytes are not `INKL`.
270    BadInklMagic([u8; 4]),
271    /// `.inkl` version is not supported.
272    UnsupportedInklVersion(u8),
273}
274
275impl fmt::Display for DecodeError {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        match self {
278            Self::UnexpectedEof => write!(f, "unexpected end of bytecode"),
279            Self::UnknownOpcode(b) => write!(f, "unknown opcode: {b:#04x}"),
280            Self::InvalidDefinitionId(raw) => {
281                write!(f, "invalid definition id: {raw:#018x}")
282            }
283            Self::InvalidSequenceKind(b) => write!(f, "invalid sequence kind: {b}"),
284            Self::BadMagic(m) => write!(f, "bad magic: {m:02x?}"),
285            Self::UnsupportedVersion(v) => write!(f, "unsupported .inkb version: {v}"),
286            Self::InvalidUtf8 => write!(f, "invalid UTF-8 in string field"),
287            Self::InvalidValueType(b) => write!(f, "invalid value type: {b:#04x}"),
288            Self::InvalidSelectKey(b) => write!(f, "invalid select key: {b:#04x}"),
289            Self::InvalidLinePart(b) => write!(f, "invalid line part: {b:#04x}"),
290            Self::InvalidLineContent(b) => write!(f, "invalid line content: {b:#04x}"),
291            Self::InvalidPluralCategory(b) => write!(f, "invalid plural category: {b:#04x}"),
292            Self::InvalidSectionKind(b) => write!(f, "invalid section kind: {b:#04x}"),
293            Self::MissingSectionKind(b) => write!(f, "missing required section kind: {b:#04x}"),
294            Self::FileSizeMismatch { expected, actual } => {
295                write!(
296                    f,
297                    "file size mismatch: header says {expected}, actual {actual}"
298                )
299            }
300            Self::ChecksumMismatch { expected, actual } => {
301                write!(
302                    f,
303                    "checksum mismatch: header {expected:#010x}, computed {actual:#010x}"
304                )
305            }
306            Self::InvalidSectionOffset { kind, offset } => {
307                write!(
308                    f,
309                    "invalid section offset: kind {kind:#04x} at offset {offset}"
310                )
311            }
312            Self::BadInklMagic(m) => write!(f, "bad .inkl magic: {m:02x?}"),
313            Self::UnsupportedInklVersion(v) => write!(f, "unsupported .inkl version: {v}"),
314        }
315    }
316}
317
318impl std::error::Error for DecodeError {}
319
320/// A single VM instruction with its operands.
321#[derive(Debug, Clone, PartialEq)]
322pub enum Opcode {
323    // ── Stack & literals ────────────────────────────────────────────────
324    PushInt(i32),
325    PushFloat(f32),
326    PushBool(bool),
327    PushString(u16),
328    PushList(u16),
329    PushDivertTarget(DefinitionId),
330    PushNull,
331    Pop,
332    Duplicate,
333
334    // ── Arithmetic ──────────────────────────────────────────────────────
335    Add,
336    Subtract,
337    Multiply,
338    Divide,
339    Modulo,
340    Negate,
341
342    // ── Comparison ──────────────────────────────────────────────────────
343    Equal,
344    NotEqual,
345    Greater,
346    GreaterOrEqual,
347    Less,
348    LessOrEqual,
349
350    // ── Logic ───────────────────────────────────────────────────────────
351    Not,
352    And,
353    Or,
354
355    // ── Global vars ─────────────────────────────────────────────────────
356    GetGlobal(DefinitionId),
357    SetGlobal(DefinitionId),
358
359    // ── Temp vars ───────────────────────────────────────────────────────
360    DeclareTemp(u16),
361    GetTemp(u16),
362    SetTemp(u16),
363    /// Get a temp's raw value without auto-dereference (for passing a ref onward).
364    GetTempRaw(u16),
365
366    // ── Variable pointers ──────────────────────────────────────────────
367    /// Push a pointer to a global variable onto the eval stack.
368    PushVarPointer(DefinitionId),
369    /// Push a pointer to a temp variable onto the eval stack.
370    PushTempPointer(u16),
371
372    // ── Control flow ────────────────────────────────────────────────────
373    Jump(i32),
374    JumpIfFalse(i32),
375    Goto(DefinitionId),
376    GotoIf(DefinitionId),
377    GotoVariable,
378
379    // ── Container flow ──────────────────────────────────────────────────
380    EnterContainer(DefinitionId),
381    ExitContainer,
382
383    // ── Functions / tunnels ─────────────────────────────────────────────
384    Call(DefinitionId),
385    Return,
386    TunnelCall(DefinitionId),
387    TunnelReturn,
388    TunnelCallVariable,
389    CallVariable,
390
391    // ── Threads ─────────────────────────────────────────────────────────
392    ThreadCall(DefinitionId),
393    ThreadStart,
394    ThreadDone,
395
396    // ── Output ──────────────────────────────────────────────────────────
397    EmitLine(u16, u8),
398    EmitValue,
399    EmitNewline,
400    /// Word break — renders as a single space between content parts.
401    Spring,
402    Glue,
403    BeginTag,
404    EndTag,
405    EvalLine(u16, u8),
406    /// Begin capturing output into a fragment (structural preservation).
407    BeginFragment,
408    /// End fragment capture — store parts and push `Value::FragmentRef`.
409    EndFragment,
410
411    // ── Choices ─────────────────────────────────────────────────────────
412    BeginChoice(ChoiceFlags, DefinitionId),
413    EndChoice,
414
415    // ── Sequences ───────────────────────────────────────────────────────
416    Sequence(SequenceKind, u8),
417    SequenceBranch(i32),
418
419    // ── Intrinsics ──────────────────────────────────────────────────────
420    /// Pop a `DivertTarget` from the stack, push its visit count.
421    VisitCount,
422    /// Push the visit count of the *current* container (no stack input).
423    CurrentVisitCount,
424    TurnsSince,
425    TurnIndex,
426    ChoiceCount,
427    Random,
428    SeedRandom,
429
430    // ── Casts / math ────────────────────────────────────────────────────
431    CastToInt,
432    CastToFloat,
433    Floor,
434    Ceiling,
435    Pow,
436    Min,
437    Max,
438
439    // ── External fns ────────────────────────────────────────────────────
440    CallExternal(DefinitionId, u8),
441
442    // ── List ops ────────────────────────────────────────────────────────
443    ListContains,
444    ListNotContains,
445    ListIntersect,
446    ListAll,
447    ListInvert,
448    ListCount,
449    ListMin,
450    ListMax,
451    ListValue,
452    ListRange,
453    ListFromInt,
454    ListRandom,
455
456    // ── Lifecycle ───────────────────────────────────────────────────────
457    Done,
458    /// Pause for choice presentation. Like `Done` but does NOT set
459    /// `did_safe_exit` — if no choices are pending, the story ran
460    /// out of content rather than reaching an explicit `-> DONE`.
461    Yield,
462    End,
463    Nop,
464
465    // ── String eval ─────────────────────────────────────────────────────
466    BeginStringEval,
467    EndStringEval,
468
469    // ── Debug ───────────────────────────────────────────────────────────
470    SourceLocation(u32, u32),
471}
472
473// ── Opcode encode / decode ──────────────────────────────────────────────────
474
475impl Opcode {
476    /// Encode this instruction into the byte buffer.
477    #[expect(clippy::too_many_lines)]
478    pub fn encode(&self, buf: &mut Vec<u8>) {
479        match *self {
480            // Stack & literals
481            Self::PushInt(v) => {
482                write_u8(buf, PUSH_INT);
483                write_i32(buf, v);
484            }
485            Self::PushFloat(v) => {
486                write_u8(buf, PUSH_FLOAT);
487                write_f32(buf, v);
488            }
489            Self::PushBool(v) => {
490                write_u8(buf, PUSH_BOOL);
491                write_u8(buf, u8::from(v));
492            }
493            Self::PushString(idx) => {
494                write_u8(buf, PUSH_STRING);
495                write_u16(buf, idx);
496            }
497            Self::PushList(idx) => {
498                write_u8(buf, PUSH_LIST);
499                write_u16(buf, idx);
500            }
501            Self::PushDivertTarget(id) => {
502                write_u8(buf, PUSH_DIVERT_TARGET);
503                write_def_id(buf, id);
504            }
505            Self::PushNull => write_u8(buf, PUSH_NULL),
506            Self::Pop => write_u8(buf, POP),
507            Self::Duplicate => write_u8(buf, DUPLICATE),
508
509            // Arithmetic
510            Self::Add => write_u8(buf, ADD),
511            Self::Subtract => write_u8(buf, SUBTRACT),
512            Self::Multiply => write_u8(buf, MULTIPLY),
513            Self::Divide => write_u8(buf, DIVIDE),
514            Self::Modulo => write_u8(buf, MODULO),
515            Self::Negate => write_u8(buf, NEGATE),
516
517            // Comparison
518            Self::Equal => write_u8(buf, EQUAL),
519            Self::NotEqual => write_u8(buf, NOT_EQUAL),
520            Self::Greater => write_u8(buf, GREATER),
521            Self::GreaterOrEqual => write_u8(buf, GREATER_OR_EQUAL),
522            Self::Less => write_u8(buf, LESS),
523            Self::LessOrEqual => write_u8(buf, LESS_OR_EQUAL),
524
525            // Logic
526            Self::Not => write_u8(buf, NOT),
527            Self::And => write_u8(buf, AND),
528            Self::Or => write_u8(buf, OR),
529
530            // Global vars
531            Self::GetGlobal(id) => {
532                write_u8(buf, GET_GLOBAL);
533                write_def_id(buf, id);
534            }
535            Self::SetGlobal(id) => {
536                write_u8(buf, SET_GLOBAL);
537                write_def_id(buf, id);
538            }
539
540            // Temp vars
541            Self::DeclareTemp(idx) => {
542                write_u8(buf, DECLARE_TEMP);
543                write_u16(buf, idx);
544            }
545            Self::GetTemp(idx) => {
546                write_u8(buf, GET_TEMP);
547                write_u16(buf, idx);
548            }
549            Self::SetTemp(idx) => {
550                write_u8(buf, SET_TEMP);
551                write_u16(buf, idx);
552            }
553            Self::GetTempRaw(idx) => {
554                write_u8(buf, GET_TEMP_RAW);
555                write_u16(buf, idx);
556            }
557
558            // Variable pointers
559            Self::PushVarPointer(id) => {
560                write_u8(buf, PUSH_VAR_POINTER);
561                write_def_id(buf, id);
562            }
563            Self::PushTempPointer(slot) => {
564                write_u8(buf, PUSH_TEMP_POINTER);
565                write_u16(buf, slot);
566            }
567
568            // Control flow
569            Self::Jump(offset) => {
570                write_u8(buf, JUMP);
571                write_i32(buf, offset);
572            }
573            Self::JumpIfFalse(offset) => {
574                write_u8(buf, JUMP_IF_FALSE);
575                write_i32(buf, offset);
576            }
577            Self::Goto(id) => {
578                write_u8(buf, GOTO);
579                write_def_id(buf, id);
580            }
581            Self::GotoIf(id) => {
582                write_u8(buf, GOTO_IF);
583                write_def_id(buf, id);
584            }
585            Self::GotoVariable => write_u8(buf, GOTO_VARIABLE),
586
587            // Container flow
588            Self::EnterContainer(id) => {
589                write_u8(buf, ENTER_CONTAINER);
590                write_def_id(buf, id);
591            }
592            Self::ExitContainer => write_u8(buf, EXIT_CONTAINER),
593
594            // Functions / tunnels
595            Self::Call(id) => {
596                write_u8(buf, CALL);
597                write_def_id(buf, id);
598            }
599            Self::Return => write_u8(buf, RETURN),
600            Self::TunnelCall(id) => {
601                write_u8(buf, TUNNEL_CALL);
602                write_def_id(buf, id);
603            }
604            Self::TunnelReturn => write_u8(buf, TUNNEL_RETURN),
605            Self::TunnelCallVariable => write_u8(buf, TUNNEL_CALL_VARIABLE),
606            Self::CallVariable => write_u8(buf, CALL_VARIABLE),
607
608            // Threads
609            Self::ThreadCall(id) => {
610                write_u8(buf, THREAD_CALL);
611                write_def_id(buf, id);
612            }
613            Self::ThreadStart => write_u8(buf, THREAD_START),
614            Self::ThreadDone => write_u8(buf, THREAD_DONE),
615
616            // Output
617            Self::EmitLine(idx, slot_count) => {
618                write_u8(buf, EMIT_LINE);
619                write_u16(buf, idx);
620                write_u8(buf, slot_count);
621            }
622            Self::EmitValue => write_u8(buf, EMIT_VALUE),
623            Self::EmitNewline => write_u8(buf, EMIT_NEWLINE),
624            Self::Spring => write_u8(buf, SPRING),
625            Self::Glue => write_u8(buf, GLUE),
626            Self::BeginTag => write_u8(buf, BEGIN_TAG),
627            Self::EndTag => write_u8(buf, END_TAG),
628            Self::EvalLine(idx, slot_count) => {
629                write_u8(buf, EVAL_LINE);
630                write_u16(buf, idx);
631                write_u8(buf, slot_count);
632            }
633            Self::BeginFragment => write_u8(buf, BEGIN_FRAGMENT),
634            Self::EndFragment => write_u8(buf, END_FRAGMENT),
635
636            // Choices
637            Self::BeginChoice(flags, target) => {
638                write_u8(buf, BEGIN_CHOICE);
639                write_u8(buf, flags.to_byte());
640                write_def_id(buf, target);
641            }
642            Self::EndChoice => write_u8(buf, END_CHOICE),
643
644            // Sequences
645            Self::Sequence(kind, count) => {
646                write_u8(buf, SEQUENCE);
647                write_u8(buf, kind.to_byte());
648                write_u8(buf, count);
649            }
650            Self::SequenceBranch(offset) => {
651                write_u8(buf, SEQUENCE_BRANCH);
652                write_i32(buf, offset);
653            }
654
655            // Intrinsics
656            Self::VisitCount => write_u8(buf, VISIT_COUNT),
657            Self::CurrentVisitCount => write_u8(buf, CURRENT_VISIT_COUNT),
658            Self::TurnsSince => write_u8(buf, TURNS_SINCE),
659            Self::TurnIndex => write_u8(buf, TURN_INDEX),
660            Self::ChoiceCount => write_u8(buf, CHOICE_COUNT),
661            Self::Random => write_u8(buf, RANDOM),
662            Self::SeedRandom => write_u8(buf, SEED_RANDOM),
663
664            // Casts / math
665            Self::CastToInt => write_u8(buf, CAST_TO_INT),
666            Self::CastToFloat => write_u8(buf, CAST_TO_FLOAT),
667            Self::Floor => write_u8(buf, FLOOR),
668            Self::Ceiling => write_u8(buf, CEILING),
669            Self::Pow => write_u8(buf, POW),
670            Self::Min => write_u8(buf, MIN),
671            Self::Max => write_u8(buf, MAX),
672
673            // External fns
674            Self::CallExternal(id, argc) => {
675                write_u8(buf, CALL_EXTERNAL);
676                write_def_id(buf, id);
677                write_u8(buf, argc);
678            }
679
680            // List ops
681            Self::ListContains => write_u8(buf, LIST_CONTAINS),
682            Self::ListNotContains => write_u8(buf, LIST_NOT_CONTAINS),
683            Self::ListIntersect => write_u8(buf, LIST_INTERSECT),
684            Self::ListAll => write_u8(buf, LIST_ALL),
685            Self::ListInvert => write_u8(buf, LIST_INVERT),
686            Self::ListCount => write_u8(buf, LIST_COUNT),
687            Self::ListMin => write_u8(buf, LIST_MIN),
688            Self::ListMax => write_u8(buf, LIST_MAX),
689            Self::ListValue => write_u8(buf, LIST_VALUE),
690            Self::ListRange => write_u8(buf, LIST_RANGE),
691            Self::ListFromInt => write_u8(buf, LIST_FROM_INT),
692            Self::ListRandom => write_u8(buf, LIST_RANDOM),
693
694            // Lifecycle
695            Self::Done => write_u8(buf, DONE),
696            Self::Yield => write_u8(buf, YIELD),
697            Self::End => write_u8(buf, END),
698            Self::Nop => write_u8(buf, NOP),
699
700            // String eval
701            Self::BeginStringEval => write_u8(buf, BEGIN_STRING_EVAL),
702            Self::EndStringEval => write_u8(buf, END_STRING_EVAL),
703
704            // Debug
705            Self::SourceLocation(line, col) => {
706                write_u8(buf, SOURCE_LOCATION);
707                write_u32(buf, line);
708                write_u32(buf, col);
709            }
710        }
711    }
712
713    /// Decode a single instruction from `buf` starting at `*offset`.
714    ///
715    /// On success, `*offset` is advanced past the consumed bytes.
716    #[expect(clippy::too_many_lines)]
717    pub fn decode(buf: &[u8], offset: &mut usize) -> Result<Self, DecodeError> {
718        let disc = read_u8(buf, offset)?;
719
720        let op = match disc {
721            // Stack & literals
722            PUSH_INT => Self::PushInt(read_i32(buf, offset)?),
723            PUSH_FLOAT => Self::PushFloat(read_f32(buf, offset)?),
724            PUSH_BOOL => Self::PushBool(read_u8(buf, offset)? != 0),
725            PUSH_STRING => Self::PushString(read_u16(buf, offset)?),
726            PUSH_LIST => Self::PushList(read_u16(buf, offset)?),
727            PUSH_DIVERT_TARGET => Self::PushDivertTarget(read_def_id(buf, offset)?),
728            PUSH_NULL => Self::PushNull,
729            POP => Self::Pop,
730            DUPLICATE => Self::Duplicate,
731
732            // Arithmetic
733            ADD => Self::Add,
734            SUBTRACT => Self::Subtract,
735            MULTIPLY => Self::Multiply,
736            DIVIDE => Self::Divide,
737            MODULO => Self::Modulo,
738            NEGATE => Self::Negate,
739
740            // Comparison
741            EQUAL => Self::Equal,
742            NOT_EQUAL => Self::NotEqual,
743            GREATER => Self::Greater,
744            GREATER_OR_EQUAL => Self::GreaterOrEqual,
745            LESS => Self::Less,
746            LESS_OR_EQUAL => Self::LessOrEqual,
747
748            // Logic
749            NOT => Self::Not,
750            AND => Self::And,
751            OR => Self::Or,
752
753            // Global vars
754            GET_GLOBAL => Self::GetGlobal(read_def_id(buf, offset)?),
755            SET_GLOBAL => Self::SetGlobal(read_def_id(buf, offset)?),
756
757            // Temp vars
758            DECLARE_TEMP => Self::DeclareTemp(read_u16(buf, offset)?),
759            GET_TEMP => Self::GetTemp(read_u16(buf, offset)?),
760            SET_TEMP => Self::SetTemp(read_u16(buf, offset)?),
761            GET_TEMP_RAW => Self::GetTempRaw(read_u16(buf, offset)?),
762
763            // Variable pointers
764            PUSH_VAR_POINTER => Self::PushVarPointer(read_def_id(buf, offset)?),
765            PUSH_TEMP_POINTER => Self::PushTempPointer(read_u16(buf, offset)?),
766
767            // Control flow
768            JUMP => Self::Jump(read_i32(buf, offset)?),
769            JUMP_IF_FALSE => Self::JumpIfFalse(read_i32(buf, offset)?),
770            GOTO => Self::Goto(read_def_id(buf, offset)?),
771            GOTO_IF => Self::GotoIf(read_def_id(buf, offset)?),
772            GOTO_VARIABLE => Self::GotoVariable,
773
774            // Container flow
775            ENTER_CONTAINER => Self::EnterContainer(read_def_id(buf, offset)?),
776            EXIT_CONTAINER => Self::ExitContainer,
777
778            // Functions / tunnels
779            CALL => Self::Call(read_def_id(buf, offset)?),
780            RETURN => Self::Return,
781            TUNNEL_CALL => Self::TunnelCall(read_def_id(buf, offset)?),
782            TUNNEL_RETURN => Self::TunnelReturn,
783            TUNNEL_CALL_VARIABLE => Self::TunnelCallVariable,
784            CALL_VARIABLE => Self::CallVariable,
785
786            // Threads
787            THREAD_CALL => Self::ThreadCall(read_def_id(buf, offset)?),
788            THREAD_START => Self::ThreadStart,
789            THREAD_DONE => Self::ThreadDone,
790
791            // Output
792            EMIT_LINE => {
793                let idx = read_u16(buf, offset)?;
794                let slot_count = read_u8(buf, offset)?;
795                Self::EmitLine(idx, slot_count)
796            }
797            EMIT_VALUE => Self::EmitValue,
798            EMIT_NEWLINE => Self::EmitNewline,
799            SPRING => Self::Spring,
800            GLUE => Self::Glue,
801            BEGIN_TAG => Self::BeginTag,
802            END_TAG => Self::EndTag,
803            EVAL_LINE => {
804                let idx = read_u16(buf, offset)?;
805                let slot_count = read_u8(buf, offset)?;
806                Self::EvalLine(idx, slot_count)
807            }
808            BEGIN_FRAGMENT => Self::BeginFragment,
809            END_FRAGMENT => Self::EndFragment,
810
811            // Choices
812            BEGIN_CHOICE => {
813                let flags = ChoiceFlags::from_byte(read_u8(buf, offset)?);
814                let target = read_def_id(buf, offset)?;
815                Self::BeginChoice(flags, target)
816            }
817            END_CHOICE => Self::EndChoice,
818
819            // Sequences
820            SEQUENCE => {
821                let kind = SequenceKind::from_byte(read_u8(buf, offset)?)?;
822                let count = read_u8(buf, offset)?;
823                Self::Sequence(kind, count)
824            }
825            SEQUENCE_BRANCH => Self::SequenceBranch(read_i32(buf, offset)?),
826
827            // Intrinsics
828            VISIT_COUNT => Self::VisitCount,
829            CURRENT_VISIT_COUNT => Self::CurrentVisitCount,
830            TURNS_SINCE => Self::TurnsSince,
831            TURN_INDEX => Self::TurnIndex,
832            CHOICE_COUNT => Self::ChoiceCount,
833            RANDOM => Self::Random,
834            SEED_RANDOM => Self::SeedRandom,
835
836            // Casts / math
837            CAST_TO_INT => Self::CastToInt,
838            CAST_TO_FLOAT => Self::CastToFloat,
839            FLOOR => Self::Floor,
840            CEILING => Self::Ceiling,
841            POW => Self::Pow,
842            MIN => Self::Min,
843            MAX => Self::Max,
844
845            // External fns
846            CALL_EXTERNAL => {
847                let id = read_def_id(buf, offset)?;
848                let argc = read_u8(buf, offset)?;
849                Self::CallExternal(id, argc)
850            }
851
852            // List ops
853            LIST_CONTAINS => Self::ListContains,
854            LIST_NOT_CONTAINS => Self::ListNotContains,
855            LIST_INTERSECT => Self::ListIntersect,
856            LIST_ALL => Self::ListAll,
857            LIST_INVERT => Self::ListInvert,
858            LIST_COUNT => Self::ListCount,
859            LIST_MIN => Self::ListMin,
860            LIST_MAX => Self::ListMax,
861            LIST_VALUE => Self::ListValue,
862            LIST_RANGE => Self::ListRange,
863            LIST_FROM_INT => Self::ListFromInt,
864            LIST_RANDOM => Self::ListRandom,
865
866            // Lifecycle
867            DONE => Self::Done,
868            YIELD => Self::Yield,
869            END => Self::End,
870            NOP => Self::Nop,
871
872            // String eval
873            BEGIN_STRING_EVAL => Self::BeginStringEval,
874            END_STRING_EVAL => Self::EndStringEval,
875
876            // Debug
877            SOURCE_LOCATION => {
878                let line = read_u32(buf, offset)?;
879                let col = read_u32(buf, offset)?;
880                Self::SourceLocation(line, col)
881            }
882
883            _ => return Err(DecodeError::UnknownOpcode(disc)),
884        };
885
886        Ok(op)
887    }
888}
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893    use crate::id::DefinitionTag;
894
895    fn roundtrip(op: &Opcode) {
896        let mut buf = Vec::new();
897        op.encode(&mut buf);
898        let mut offset = 0;
899        let decoded = Opcode::decode(&buf, &mut offset).unwrap();
900        assert_eq!(*op, decoded, "roundtrip failed for {op:?}");
901        assert_eq!(offset, buf.len(), "not all bytes consumed for {op:?}");
902    }
903
904    fn test_id() -> DefinitionId {
905        DefinitionId::new(DefinitionTag::Address, 0xBEEF)
906    }
907
908    fn global_id() -> DefinitionId {
909        DefinitionId::new(DefinitionTag::GlobalVar, 42)
910    }
911
912    fn ext_id() -> DefinitionId {
913        DefinitionId::new(DefinitionTag::ExternalFn, 0xCAFE)
914    }
915
916    #[test]
917    fn roundtrip_stack_literals() {
918        roundtrip(&Opcode::PushInt(0));
919        roundtrip(&Opcode::PushInt(-1));
920        roundtrip(&Opcode::PushInt(i32::MAX));
921        roundtrip(&Opcode::PushInt(i32::MIN));
922        roundtrip(&Opcode::PushFloat(0.0));
923        roundtrip(&Opcode::PushFloat(3.125));
924        roundtrip(&Opcode::PushFloat(f32::NEG_INFINITY));
925        roundtrip(&Opcode::PushBool(true));
926        roundtrip(&Opcode::PushBool(false));
927        roundtrip(&Opcode::PushString(0));
928        roundtrip(&Opcode::PushString(u16::MAX));
929        roundtrip(&Opcode::PushList(7));
930        roundtrip(&Opcode::PushDivertTarget(test_id()));
931        roundtrip(&Opcode::PushNull);
932        roundtrip(&Opcode::Pop);
933        roundtrip(&Opcode::Duplicate);
934    }
935
936    #[test]
937    fn roundtrip_arithmetic() {
938        for op in [
939            Opcode::Add,
940            Opcode::Subtract,
941            Opcode::Multiply,
942            Opcode::Divide,
943            Opcode::Modulo,
944            Opcode::Negate,
945        ] {
946            roundtrip(&op);
947        }
948    }
949
950    #[test]
951    fn roundtrip_comparison() {
952        for op in [
953            Opcode::Equal,
954            Opcode::NotEqual,
955            Opcode::Greater,
956            Opcode::GreaterOrEqual,
957            Opcode::Less,
958            Opcode::LessOrEqual,
959        ] {
960            roundtrip(&op);
961        }
962    }
963
964    #[test]
965    fn roundtrip_logic() {
966        for op in [Opcode::Not, Opcode::And, Opcode::Or] {
967            roundtrip(&op);
968        }
969    }
970
971    #[test]
972    fn roundtrip_globals() {
973        roundtrip(&Opcode::GetGlobal(global_id()));
974        roundtrip(&Opcode::SetGlobal(global_id()));
975    }
976
977    #[test]
978    fn roundtrip_temps() {
979        roundtrip(&Opcode::DeclareTemp(0));
980        roundtrip(&Opcode::GetTemp(5));
981        roundtrip(&Opcode::SetTemp(u16::MAX));
982        roundtrip(&Opcode::GetTempRaw(3));
983    }
984
985    #[test]
986    fn roundtrip_var_pointer() {
987        roundtrip(&Opcode::PushVarPointer(global_id()));
988        roundtrip(&Opcode::PushTempPointer(0));
989        roundtrip(&Opcode::PushTempPointer(u16::MAX));
990    }
991
992    #[test]
993    fn roundtrip_control_flow() {
994        roundtrip(&Opcode::Jump(0));
995        roundtrip(&Opcode::Jump(-42));
996        roundtrip(&Opcode::JumpIfFalse(100));
997        roundtrip(&Opcode::Goto(test_id()));
998        roundtrip(&Opcode::GotoIf(test_id()));
999        roundtrip(&Opcode::GotoVariable);
1000    }
1001
1002    #[test]
1003    fn roundtrip_container_flow() {
1004        roundtrip(&Opcode::EnterContainer(test_id()));
1005        roundtrip(&Opcode::ExitContainer);
1006    }
1007
1008    #[test]
1009    fn roundtrip_functions_tunnels() {
1010        roundtrip(&Opcode::Call(test_id()));
1011        roundtrip(&Opcode::Return);
1012        roundtrip(&Opcode::TunnelCall(test_id()));
1013        roundtrip(&Opcode::TunnelReturn);
1014        roundtrip(&Opcode::TunnelCallVariable);
1015        roundtrip(&Opcode::CallVariable);
1016    }
1017
1018    #[test]
1019    fn roundtrip_threads() {
1020        roundtrip(&Opcode::ThreadCall(test_id()));
1021        roundtrip(&Opcode::ThreadStart);
1022        roundtrip(&Opcode::ThreadDone);
1023    }
1024
1025    #[test]
1026    fn roundtrip_output() {
1027        roundtrip(&Opcode::EmitLine(0, 0));
1028        roundtrip(&Opcode::EmitLine(999, 3));
1029        roundtrip(&Opcode::EmitValue);
1030        roundtrip(&Opcode::EmitNewline);
1031        roundtrip(&Opcode::Spring);
1032        roundtrip(&Opcode::Glue);
1033        roundtrip(&Opcode::BeginTag);
1034        roundtrip(&Opcode::EndTag);
1035        roundtrip(&Opcode::EvalLine(0, 0));
1036        roundtrip(&Opcode::EvalLine(42, 2));
1037    }
1038
1039    #[test]
1040    fn roundtrip_choices() {
1041        roundtrip(&Opcode::BeginChoice(
1042            ChoiceFlags {
1043                has_condition: true,
1044                has_start_content: false,
1045                has_choice_only_content: true,
1046                once_only: false,
1047                is_invisible_default: true,
1048            },
1049            test_id(),
1050        ));
1051        roundtrip(&Opcode::BeginChoice(
1052            ChoiceFlags {
1053                has_condition: false,
1054                has_start_content: true,
1055                has_choice_only_content: false,
1056                once_only: true,
1057                is_invisible_default: false,
1058            },
1059            test_id(),
1060        ));
1061        roundtrip(&Opcode::EndChoice);
1062    }
1063
1064    #[test]
1065    fn roundtrip_sequences() {
1066        for kind in [
1067            SequenceKind::Cycle,
1068            SequenceKind::Stopping,
1069            SequenceKind::OnceOnly,
1070            SequenceKind::Shuffle,
1071        ] {
1072            roundtrip(&Opcode::Sequence(kind, 5));
1073        }
1074        roundtrip(&Opcode::SequenceBranch(-10));
1075        roundtrip(&Opcode::SequenceBranch(0));
1076    }
1077
1078    #[test]
1079    fn roundtrip_intrinsics() {
1080        for op in [
1081            Opcode::VisitCount,
1082            Opcode::CurrentVisitCount,
1083            Opcode::TurnsSince,
1084            Opcode::TurnIndex,
1085            Opcode::ChoiceCount,
1086            Opcode::Random,
1087            Opcode::SeedRandom,
1088        ] {
1089            roundtrip(&op);
1090        }
1091    }
1092
1093    #[test]
1094    fn roundtrip_casts_math() {
1095        for op in [
1096            Opcode::CastToInt,
1097            Opcode::CastToFloat,
1098            Opcode::Floor,
1099            Opcode::Ceiling,
1100            Opcode::Pow,
1101            Opcode::Min,
1102            Opcode::Max,
1103        ] {
1104            roundtrip(&op);
1105        }
1106    }
1107
1108    #[test]
1109    fn roundtrip_call_external() {
1110        roundtrip(&Opcode::CallExternal(ext_id(), 3));
1111        roundtrip(&Opcode::CallExternal(ext_id(), 0));
1112    }
1113
1114    #[test]
1115    fn roundtrip_list_ops() {
1116        for op in [
1117            Opcode::ListContains,
1118            Opcode::ListNotContains,
1119            Opcode::ListIntersect,
1120            Opcode::ListAll,
1121            Opcode::ListInvert,
1122            Opcode::ListCount,
1123            Opcode::ListMin,
1124            Opcode::ListMax,
1125            Opcode::ListValue,
1126            Opcode::ListRange,
1127            Opcode::ListFromInt,
1128            Opcode::ListRandom,
1129        ] {
1130            roundtrip(&op);
1131        }
1132    }
1133
1134    #[test]
1135    fn roundtrip_lifecycle() {
1136        for op in [Opcode::Done, Opcode::Yield, Opcode::End, Opcode::Nop] {
1137            roundtrip(&op);
1138        }
1139    }
1140
1141    #[test]
1142    fn roundtrip_string_eval() {
1143        roundtrip(&Opcode::BeginStringEval);
1144        roundtrip(&Opcode::EndStringEval);
1145    }
1146
1147    #[test]
1148    fn roundtrip_debug() {
1149        roundtrip(&Opcode::SourceLocation(1, 0));
1150        roundtrip(&Opcode::SourceLocation(u32::MAX, u32::MAX));
1151    }
1152
1153    #[test]
1154    fn decode_unknown_opcode() {
1155        let buf = [0xFF];
1156        let mut offset = 0;
1157        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
1158        assert_eq!(err, DecodeError::UnknownOpcode(0xFF));
1159    }
1160
1161    #[test]
1162    fn decode_unexpected_eof() {
1163        // PushInt needs 4 more bytes after the discriminant.
1164        let buf = [PUSH_INT, 0x00];
1165        let mut offset = 0;
1166        let err = Opcode::decode(&buf, &mut offset).unwrap_err();
1167        assert_eq!(err, DecodeError::UnexpectedEof);
1168    }
1169
1170    #[test]
1171    fn decode_multiple_instructions() {
1172        let ops = vec![
1173            Opcode::PushInt(42),
1174            Opcode::PushBool(true),
1175            Opcode::Add,
1176            Opcode::Done,
1177        ];
1178        let mut buf = Vec::new();
1179        for op in &ops {
1180            op.encode(&mut buf);
1181        }
1182        let mut offset = 0;
1183        for expected in &ops {
1184            let decoded = Opcode::decode(&buf, &mut offset).unwrap();
1185            assert_eq!(*expected, decoded);
1186        }
1187        assert_eq!(offset, buf.len());
1188    }
1189
1190    #[test]
1191    fn choice_flags_roundtrip() {
1192        for bits in 0..32u8 {
1193            let flags = ChoiceFlags::from_byte(bits);
1194            assert_eq!(flags.to_byte(), bits);
1195        }
1196    }
1197}