use core::fmt;
use alloc::vec::Vec;
use crate::codec::{
read_def_id, read_f32, read_i32, read_u8, read_u16, read_u32, write_def_id, write_f32,
write_i32, write_u8, write_u16, write_u32,
};
use crate::id::DefinitionId;
const PUSH_INT: u8 = 0x01;
const PUSH_FLOAT: u8 = 0x02;
const PUSH_BOOL: u8 = 0x03;
const PUSH_STRING: u8 = 0x04;
const PUSH_LIST: u8 = 0x05;
const PUSH_DIVERT_TARGET: u8 = 0x06;
const PUSH_NULL: u8 = 0x07;
const POP: u8 = 0x08;
const DUPLICATE: u8 = 0x09;
const ADD: u8 = 0x10;
const SUBTRACT: u8 = 0x11;
const MULTIPLY: u8 = 0x12;
const DIVIDE: u8 = 0x13;
const MODULO: u8 = 0x14;
const NEGATE: u8 = 0x15;
const EQUAL: u8 = 0x20;
const NOT_EQUAL: u8 = 0x21;
const GREATER: u8 = 0x22;
const GREATER_OR_EQUAL: u8 = 0x23;
const LESS: u8 = 0x24;
const LESS_OR_EQUAL: u8 = 0x25;
const NOT: u8 = 0x28;
const AND: u8 = 0x29;
const OR: u8 = 0x2A;
const GET_GLOBAL: u8 = 0x30;
const SET_GLOBAL: u8 = 0x31;
const DECLARE_TEMP: u8 = 0x34;
const GET_TEMP: u8 = 0x35;
const SET_TEMP: u8 = 0x36;
const GET_TEMP_RAW: u8 = 0x37;
const PUSH_VAR_POINTER: u8 = 0x38;
const PUSH_TEMP_POINTER: u8 = 0x39;
const JUMP: u8 = 0x40;
const JUMP_IF_FALSE: u8 = 0x41;
const GOTO: u8 = 0x42;
const GOTO_IF: u8 = 0x43;
const GOTO_VARIABLE: u8 = 0x44;
const ENTER_CONTAINER: u8 = 0x48;
const EXIT_CONTAINER: u8 = 0x49;
const CALL: u8 = 0x50;
const RETURN: u8 = 0x51;
const TUNNEL_CALL: u8 = 0x52;
const TUNNEL_RETURN: u8 = 0x53;
const TUNNEL_CALL_VARIABLE: u8 = 0x54;
const CALL_VARIABLE: u8 = 0x55;
const THREAD_CALL: u8 = 0x57;
const THREAD_START: u8 = 0x58;
const THREAD_DONE: u8 = 0x59;
const EMIT_LINE: u8 = 0x60;
const EMIT_VALUE: u8 = 0x61;
const EMIT_NEWLINE: u8 = 0x62;
const SPRING: u8 = 0x67;
const GLUE: u8 = 0x63;
const BEGIN_TAG: u8 = 0x64;
const END_TAG: u8 = 0x65;
const EVAL_LINE: u8 = 0x66;
const BEGIN_FRAGMENT: u8 = 0x68;
const END_FRAGMENT: u8 = 0x69;
const ATTACH_ELEMENT: u8 = 0x6A;
const END_ELEMENT_RUN: u8 = 0x6B;
const EMIT_LINE_NL: u8 = 0x6C;
const BINARY_IMM: u8 = 0x6D;
const BINARY_JUMP_IF_FALSE: u8 = 0x6E;
const BINARY_IMM_JUMP_IF_FALSE: u8 = 0x6F;
const GET_TEMP_BINARY_IMM: u8 = 0x70;
const GET_TEMP_BINARY_IMM_JUMP_IF_FALSE: u8 = 0x71;
const DUPLICATE_BINARY_IMM_JUMP_IF_FALSE: u8 = 0x74;
const BEGIN_CHOICE: u8 = 0x72;
const END_CHOICE: u8 = 0x73;
const SEQUENCE: u8 = 0x78;
const SEQUENCE_BRANCH: u8 = 0x79;
const VISIT_COUNT: u8 = 0x80;
const TURNS_SINCE: u8 = 0x81;
const TURN_INDEX: u8 = 0x82;
const CHOICE_COUNT: u8 = 0x83;
const RANDOM: u8 = 0x84;
const SEED_RANDOM: u8 = 0x85;
const CURRENT_VISIT_COUNT: u8 = 0x86;
const TOUCH_VISIT: u8 = 0x87;
const SHUFFLE_INDEX_OF: u8 = 0x88;
const CAST_TO_INT: u8 = 0x90;
const CAST_TO_FLOAT: u8 = 0x91;
const FLOOR: u8 = 0x92;
const CEILING: u8 = 0x93;
const POW: u8 = 0x94;
const MIN: u8 = 0x95;
const MAX: u8 = 0x96;
const CALL_EXTERNAL: u8 = 0xA0;
const SEQ_VERB: u8 = 0xA1;
const ARRAY_NEW: u8 = 0xBE;
const MAP_NEW: u8 = 0xBF;
const INDEX_GET: u8 = 0xC0;
const INDEX_SET: u8 = 0xC1;
const COLLECTION_LEN: u8 = 0xC2;
const MAP_GET: u8 = 0xC3;
const MAP_INSERT: u8 = 0xC4;
const MAP_REMOVE: u8 = 0xC5;
const MAP_CONTAINS: u8 = 0xC6;
const COLLECTION_KEYS: u8 = 0xC7;
const COLLECTION_VALUES: u8 = 0xC8;
const PUSH_LITERAL: u8 = 0xC9;
const TAKE_GLOBAL: u8 = 0xCA;
const TAKE_TEMP: u8 = 0xCD;
const RECORD_NEW: u8 = 0xCE;
const RECORD_GET_DYN: u8 = 0xCF;
const RECORD_SET_DYN: u8 = 0xD0;
const RECORD_GET: u8 = 0xD1;
const RECORD_SET: u8 = 0xD2;
const CONVERT_INT: u8 = 0xD3;
const CONVERT_FLOAT: u8 = 0xD4;
const CONVERT_STRING: u8 = 0xD5;
const PUSH_FN_REF: u8 = 0xD6;
const MAKE_CLOSURE: u8 = 0xD7;
const CALL_VALUE: u8 = 0xD8;
const BIND_VALUE: u8 = 0xD9;
const MAKE_PROJECTION: u8 = 0xDA;
const PROJ_READ: u8 = 0xDB;
const PROJ_WRITE: u8 = 0xDC;
const CHAR_AT: u8 = 0xDD;
const PUSH_NONE: u8 = 0xDE;
const MAKE_SOME: u8 = 0xDF;
const STR_FIND: u8 = 0xE2;
const SEQ_INDEX_OF: u8 = 0xE3;
const SEQ_MIN: u8 = 0xE4;
const SEQ_MAX: u8 = 0xE5;
const SEQ_FIRST: u8 = 0xE6;
const SEQ_LAST: u8 = 0xE7;
const SEQ_POP: u8 = 0xE8;
const MAP_GET_OPT: u8 = 0xE9;
const MAP_CONTAINS_VALUE: u8 = 0xEA;
const MAP_CLEAR: u8 = 0xEB;
const RAND_FLOAT: u8 = 0xEC;
const RAND_CHANCE: u8 = 0xED;
const RAND_PICK: u8 = 0xEE;
const RAND_SHUFFLE: u8 = 0xEF;
const RANGE_MAKE_EXCL: u8 = 0xF4;
const RANGE_MAKE_INCL: u8 = 0xF5;
const RANGE_NON_EMPTY: u8 = 0xF6;
const TOWER: u8 = 0xF7;
const SEQ_SORTED: u8 = 0xF8;
const SEQ_SORTED_BY: u8 = 0xF9;
const COLLECT: u8 = 0xFA;
const COALESCE_SOME: u8 = 0xFB;
const OPTION_BIND: u8 = 0xFC;
const SEQ_REMOVE_AT: u8 = 0xFD;
const LIST_CONTAINS: u8 = 0xB0;
const LIST_NOT_CONTAINS: u8 = 0xB1;
const LIST_INTERSECT: u8 = 0xB2;
const LIST_ALL: u8 = 0xB5;
const LIST_INVERT: u8 = 0xB6;
const LIST_COUNT: u8 = 0xB7;
const LIST_MIN: u8 = 0xB8;
const LIST_MAX: u8 = 0xB9;
const LIST_VALUE: u8 = 0xBA;
const LIST_RANGE: u8 = 0xBB;
const LIST_FROM_INT: u8 = 0xBC;
const LIST_RANDOM: u8 = 0xBD;
const DONE: u8 = 0xF0;
const YIELD: u8 = 0xF3;
const END: u8 = 0xF1;
const NOP: u8 = 0xF2;
const BEGIN_STRING_EVAL: u8 = 0xE0;
const END_STRING_EVAL: u8 = 0xE1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TowerOp {
MakeVec2,
MakeVec3,
MakeVec4,
MakeQuat,
MakeMat2,
MakeMat3,
MakeMat4,
Dot,
Cross,
Min,
Max,
Clamp,
Lerp,
}
impl TowerOp {
pub const ALL: [TowerOp; 13] = [
Self::MakeVec2,
Self::MakeVec3,
Self::MakeVec4,
Self::MakeQuat,
Self::MakeMat2,
Self::MakeMat3,
Self::MakeMat4,
Self::Dot,
Self::Cross,
Self::Min,
Self::Max,
Self::Clamp,
Self::Lerp,
];
fn to_byte(self) -> u8 {
match self {
Self::MakeVec2 => 0,
Self::MakeVec3 => 1,
Self::MakeVec4 => 2,
Self::MakeQuat => 3,
Self::MakeMat2 => 4,
Self::MakeMat3 => 5,
Self::MakeMat4 => 6,
Self::Dot => 7,
Self::Cross => 8,
Self::Min => 9,
Self::Max => 10,
Self::Clamp => 11,
Self::Lerp => 12,
}
}
fn from_byte(b: u8) -> Result<Self, DecodeError> {
match b {
0 => Ok(Self::MakeVec2),
1 => Ok(Self::MakeVec3),
2 => Ok(Self::MakeVec4),
3 => Ok(Self::MakeQuat),
4 => Ok(Self::MakeMat2),
5 => Ok(Self::MakeMat3),
6 => Ok(Self::MakeMat4),
7 => Ok(Self::Dot),
8 => Ok(Self::Cross),
9 => Ok(Self::Min),
10 => Ok(Self::Max),
11 => Ok(Self::Clamp),
12 => Ok(Self::Lerp),
_ => Err(DecodeError::InvalidTowerOp(b)),
}
}
#[must_use]
pub fn mnemonic(self) -> &'static str {
match self {
Self::MakeVec2 => "make_vec2",
Self::MakeVec3 => "make_vec3",
Self::MakeVec4 => "make_vec4",
Self::MakeQuat => "make_quat",
Self::MakeMat2 => "make_mat2",
Self::MakeMat3 => "make_mat3",
Self::MakeMat4 => "make_mat4",
Self::Dot => "dot",
Self::Cross => "cross",
Self::Min => "tower_min",
Self::Max => "tower_max",
Self::Clamp => "tower_clamp",
Self::Lerp => "tower_lerp",
}
}
#[must_use]
pub fn from_mnemonic(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CollectOp {
WeightedNew,
RandRoll,
HeapPush,
HeapPop,
HeapPeek,
}
impl CollectOp {
pub const ALL: [CollectOp; 5] = [
Self::WeightedNew,
Self::RandRoll,
Self::HeapPush,
Self::HeapPop,
Self::HeapPeek,
];
fn to_byte(self) -> u8 {
match self {
Self::WeightedNew => 0,
Self::RandRoll => 1,
Self::HeapPush => 2,
Self::HeapPop => 3,
Self::HeapPeek => 4,
}
}
fn from_byte(b: u8) -> Result<Self, DecodeError> {
match b {
0 => Ok(Self::WeightedNew),
1 => Ok(Self::RandRoll),
2 => Ok(Self::HeapPush),
3 => Ok(Self::HeapPop),
4 => Ok(Self::HeapPeek),
_ => Err(DecodeError::InvalidCollectOp(b)),
}
}
#[must_use]
pub fn mnemonic(self) -> &'static str {
match self {
Self::WeightedNew => "weighted_new",
Self::RandRoll => "rand_roll",
Self::HeapPush => "heap_push",
Self::HeapPop => "heap_pop",
Self::HeapPeek => "heap_peek",
}
}
#[must_use]
pub fn from_mnemonic(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SeqVerbOp {
Map,
Filter,
Fold,
FilterMap,
Each,
MapEach,
}
impl SeqVerbOp {
pub const ALL: [SeqVerbOp; 6] = [
Self::Map,
Self::Filter,
Self::Fold,
Self::FilterMap,
Self::Each,
Self::MapEach,
];
fn to_byte(self) -> u8 {
match self {
Self::Map => 0,
Self::Filter => 1,
Self::Fold => 2,
Self::FilterMap => 3,
Self::Each => 4,
Self::MapEach => 5,
}
}
fn from_byte(b: u8) -> Result<Self, DecodeError> {
match b {
0 => Ok(Self::Map),
1 => Ok(Self::Filter),
2 => Ok(Self::Fold),
3 => Ok(Self::FilterMap),
4 => Ok(Self::Each),
5 => Ok(Self::MapEach),
_ => Err(DecodeError::InvalidSeqVerbOp(b)),
}
}
#[must_use]
pub fn mnemonic(self) -> &'static str {
match self {
Self::Map => "map",
Self::Filter => "filter",
Self::Fold => "fold",
Self::FilterMap => "filter_map",
Self::Each => "each",
Self::MapEach => "map_each",
}
}
#[must_use]
pub fn is_effectful(self) -> bool {
matches!(self, Self::Each | Self::MapEach)
}
#[must_use]
pub fn from_mnemonic(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|op| op.mnemonic() == s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SequenceKind {
Cycle,
Stopping,
OnceOnly,
Shuffle,
}
impl SequenceKind {
fn to_byte(self) -> u8 {
match self {
Self::Cycle => 0,
Self::Stopping => 1,
Self::OnceOnly => 2,
Self::Shuffle => 3,
}
}
fn from_byte(b: u8) -> Result<Self, DecodeError> {
match b {
0 => Ok(Self::Cycle),
1 => Ok(Self::Stopping),
2 => Ok(Self::OnceOnly),
3 => Ok(Self::Shuffle),
_ => Err(DecodeError::InvalidSequenceKind(b)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BinaryKind {
Add,
Subtract,
Multiply,
Divide,
Modulo,
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
}
impl BinaryKind {
pub const ALL: [Self; 11] = [
Self::Add,
Self::Subtract,
Self::Multiply,
Self::Divide,
Self::Modulo,
Self::Equal,
Self::NotEqual,
Self::Greater,
Self::GreaterOrEqual,
Self::Less,
Self::LessOrEqual,
];
fn to_byte(self) -> u8 {
match self {
Self::Add => 0,
Self::Subtract => 1,
Self::Multiply => 2,
Self::Divide => 3,
Self::Modulo => 4,
Self::Equal => 5,
Self::NotEqual => 6,
Self::Greater => 7,
Self::GreaterOrEqual => 8,
Self::Less => 9,
Self::LessOrEqual => 10,
}
}
fn from_byte(b: u8) -> Result<Self, DecodeError> {
Self::ALL
.get(b as usize)
.copied()
.ok_or(DecodeError::InvalidBinaryKind(b))
}
#[must_use]
pub fn mnemonic(self) -> &'static str {
match self {
Self::Add => "add",
Self::Subtract => "sub",
Self::Multiply => "mul",
Self::Divide => "div",
Self::Modulo => "mod",
Self::Equal => "eq",
Self::NotEqual => "ne",
Self::Greater => "gt",
Self::GreaterOrEqual => "ge",
Self::Less => "lt",
Self::LessOrEqual => "le",
}
}
#[must_use]
pub fn from_mnemonic(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|k| k.mnemonic() == s)
}
#[must_use]
pub fn of_opcode(op: &Opcode) -> Option<Self> {
Some(match op {
Opcode::Add => Self::Add,
Opcode::Subtract => Self::Subtract,
Opcode::Multiply => Self::Multiply,
Opcode::Divide => Self::Divide,
Opcode::Modulo => Self::Modulo,
Opcode::Equal => Self::Equal,
Opcode::NotEqual => Self::NotEqual,
Opcode::Greater => Self::Greater,
Opcode::GreaterOrEqual => Self::GreaterOrEqual,
Opcode::Less => Self::Less,
Opcode::LessOrEqual => Self::LessOrEqual,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[expect(clippy::struct_excessive_bools)]
pub struct ChoiceFlags {
pub has_condition: bool,
pub has_start_content: bool,
pub has_choice_only_content: bool,
pub once_only: bool,
pub is_invisible_default: bool,
}
impl ChoiceFlags {
fn to_byte(self) -> u8 {
let mut b = 0u8;
if self.has_condition {
b |= 0x01;
}
if self.has_start_content {
b |= 0x02;
}
if self.has_choice_only_content {
b |= 0x04;
}
if self.once_only {
b |= 0x08;
}
if self.is_invisible_default {
b |= 0x10;
}
b
}
fn from_byte(b: u8) -> Self {
Self {
has_condition: b & 0x01 != 0,
has_start_content: b & 0x02 != 0,
has_choice_only_content: b & 0x04 != 0,
once_only: b & 0x08 != 0,
is_invisible_default: b & 0x10 != 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
UnexpectedEof,
UnknownOpcode(u8),
InvalidDefinitionId(u64),
InvalidSequenceKind(u8),
InvalidBinaryKind(u8),
InvalidTowerOp(u8),
InvalidCollectOp(u8),
InvalidSeqVerbOp(u8),
BadMagic([u8; 4]),
UnsupportedVersion(u16),
InvalidUtf8,
InvalidValueType(u8),
InvalidSelectKey(u8),
InvalidLinePart(u8),
InvalidLineContent(u8),
InvalidPluralCategory(u8),
InvalidSectionKind(u8),
MissingSectionKind(u8),
FileSizeMismatch { expected: u32, actual: usize },
ChecksumMismatch { expected: u32, actual: u32 },
InvalidSectionOffset { kind: u8, offset: u32 },
BadInklMagic([u8; 4]),
UnsupportedInklVersion(u8),
MaxDepthExceeded(usize),
UnsupportedSectionVersion { section: u8, version: u8 },
InvalidProjSegmentKind(u8),
InvalidEffectCapParam(u8),
InvalidEffectHandleParam(u8),
InvalidEffectDimensions(u8),
InvalidDebugLocalFlags(u8),
ParamCountMismatch { declared: u8, actual: usize },
DuplicateMapKey,
InvalidConventionsProjectionTag(u8),
InvalidFileSurface(u8),
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEof => write!(f, "unexpected end of bytecode"),
Self::UnknownOpcode(b) => write!(f, "unknown opcode: {b:#04x}"),
Self::InvalidDefinitionId(raw) => {
write!(f, "invalid definition id: {raw:#018x}")
}
Self::InvalidSequenceKind(b) => write!(f, "invalid sequence kind: {b}"),
Self::InvalidBinaryKind(b) => write!(f, "invalid binary kind: {b}"),
Self::InvalidTowerOp(b) => write!(f, "invalid tower op kind: {b:#04x}"),
Self::InvalidCollectOp(b) => write!(f, "invalid collections+ op kind: {b:#04x}"),
Self::InvalidSeqVerbOp(b) => write!(f, "invalid fn-value verb kind: {b:#04x}"),
Self::BadMagic(m) => write!(f, "bad magic: {m:02x?}"),
Self::UnsupportedVersion(v) => write!(f, "unsupported .inkb version: {v}"),
Self::InvalidUtf8 => write!(f, "invalid UTF-8 in string field"),
Self::InvalidValueType(b) => write!(f, "invalid value type: {b:#04x}"),
Self::InvalidSelectKey(b) => write!(f, "invalid select key: {b:#04x}"),
Self::InvalidLinePart(b) => write!(f, "invalid line part: {b:#04x}"),
Self::InvalidLineContent(b) => write!(f, "invalid line content: {b:#04x}"),
Self::InvalidPluralCategory(b) => write!(f, "invalid plural category: {b:#04x}"),
Self::InvalidSectionKind(b) => write!(f, "invalid section kind: {b:#04x}"),
Self::MissingSectionKind(b) => write!(f, "missing required section kind: {b:#04x}"),
Self::FileSizeMismatch { expected, actual } => {
write!(
f,
"file size mismatch: header says {expected}, actual {actual}"
)
}
Self::ChecksumMismatch { expected, actual } => {
write!(
f,
"checksum mismatch: header {expected:#010x}, computed {actual:#010x}"
)
}
Self::InvalidSectionOffset { kind, offset } => {
write!(
f,
"invalid section offset: kind {kind:#04x} at offset {offset}"
)
}
Self::BadInklMagic(m) => write!(f, "bad .inkl magic: {m:02x?}"),
Self::UnsupportedInklVersion(v) => write!(f, "unsupported .inkl version: {v}"),
Self::MaxDepthExceeded(limit) => {
write!(f, "value nesting exceeded max decode depth ({limit})")
}
Self::UnsupportedSectionVersion { section, version } => {
write!(
f,
"unsupported section-local version {version} for section {section:#04x}"
)
}
Self::InvalidProjSegmentKind(b) => {
write!(f, "invalid projection segment kind: {b:#04x}")
}
Self::InvalidEffectCapParam(b) => {
write!(f, "invalid effect capability-parameter tag: {b:#04x}")
}
Self::InvalidEffectHandleParam(b) => {
write!(f, "reserved effect handle-parameter slot set: {b:#04x}")
}
Self::InvalidEffectDimensions(b) => {
write!(f, "reserved effect-dimension flag bits set: {b:#04x}")
}
Self::InvalidDebugLocalFlags(b) => {
write!(f, "reserved debug-local flag bits set: {b:#04x}")
}
Self::ParamCountMismatch { declared, actual } => {
write!(
f,
"container params metadata count ({actual}) does not match declared param_count ({declared})"
)
}
Self::DuplicateMapKey => write!(f, "duplicate key in map value"),
Self::InvalidConventionsProjectionTag(b) => {
write!(f, "invalid conventions-projection wire tag: {b:#04x}")
}
Self::InvalidFileSurface(b) => {
write!(f, "invalid DebugInfo file-table surface tag: {b:#04x}")
}
}
}
}
impl core::error::Error for DecodeError {}
#[derive(Debug, Clone, PartialEq)]
pub enum Opcode {
PushInt(i32),
PushFloat(f32),
PushBool(bool),
PushString(u16),
PushList(u16),
PushDivertTarget(DefinitionId),
PushNull,
Pop,
Duplicate,
Add,
Subtract,
Multiply,
Divide,
Modulo,
Negate,
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
Not,
And,
Or,
GetGlobal(DefinitionId),
SetGlobal(DefinitionId),
DeclareTemp(u16),
GetTemp(u16),
SetTemp(u16),
GetTempRaw(u16),
PushVarPointer(DefinitionId),
PushTempPointer(u16),
Jump(i32),
JumpIfFalse(i32),
Goto(DefinitionId),
GotoIf(DefinitionId),
GotoVariable,
EnterContainer(DefinitionId),
ExitContainer,
Call(DefinitionId),
Return,
TunnelCall(DefinitionId),
TunnelReturn,
TunnelCallVariable,
CallVariable(u8),
ThreadCall(DefinitionId),
ThreadStart,
ThreadDone,
EmitLine(u16, u8),
EmitValue,
EmitNewline,
EmitLineNl(u16, u8),
BinaryImm(BinaryKind, i32),
BinaryJumpIfFalse(BinaryKind, i32),
BinaryImmJumpIfFalse(BinaryKind, i32, i32),
GetTempBinaryImm(u16, BinaryKind, i32),
GetTempBinaryImmJumpIfFalse(u16, BinaryKind, i32, i32),
DuplicateBinaryImmJumpIfFalse(BinaryKind, i32, i32),
Spring,
Glue,
BeginTag,
EndTag,
EvalLine(u16, u8),
BeginFragment,
EndFragment,
AttachElement,
EndElementRun,
BeginChoice(ChoiceFlags, DefinitionId),
EndChoice,
Sequence(SequenceKind, u8),
SequenceBranch(i32),
VisitCount,
CurrentVisitCount,
TouchVisit,
ShuffleIndexOf,
TurnsSince,
TurnIndex,
ChoiceCount,
Random,
SeedRandom,
CastToInt,
CastToFloat,
Floor,
Ceiling,
Pow,
Min,
Max,
CallExternal(DefinitionId, u8),
ListContains,
ListNotContains,
ListIntersect,
ListAll,
ListInvert,
ListCount,
ListMin,
ListMax,
ListValue,
ListRange,
ListFromInt,
ListRandom,
ArrayNew(u32),
MapNew(u32),
IndexGet,
IndexSet,
CollectionLen,
MapGet,
MapInsert,
MapRemove,
MapContains,
CollectionKeys,
CollectionValues,
PushLiteral(u32),
TakeGlobal(DefinitionId),
TakeTemp(u16),
RecordNew(u32),
RecordGetDyn(u16),
RecordSetDyn(u16),
RecordGet(u16),
RecordSet(u16),
ConvertInt,
ConvertFloat,
ConvertString,
PushFnRef(DefinitionId),
MakeClosure {
target: DefinitionId,
bound_count: u8,
},
CallValue(u8),
BindValue(u8),
MakeProjection {
root: DefinitionId,
segment_count: u8,
},
ProjRead,
ProjWrite,
CharAt,
PushNone,
MakeSome,
StrFind,
SeqIndexOf,
SeqMin,
SeqMax,
SeqFirst,
SeqLast,
SeqPop,
MapGetOpt,
MapContainsValue,
MapClear,
CoalesceSome(i32),
OptionBind(u16),
SeqRemoveAt,
RandFloat,
RandChance,
RandPick,
RandShuffle,
RangeMakeExcl,
RangeMakeIncl,
RangeNonEmpty,
SeqSorted,
SeqSortedBy,
Tower(TowerOp),
Collect(CollectOp),
SeqVerb(SeqVerbOp),
Done,
Yield,
End,
Nop,
BeginStringEval,
EndStringEval,
}
const CLASS_GOTO: u8 = 1;
const CLASS_GOTO_IF: u8 = 2;
const CLASS_ENTER_CONTAINER: u8 = 3;
const CLASS_CALL: u8 = 4;
const CLASS_TUNNEL_CALL: u8 = 5;
const CLASS_THREAD_CALL: u8 = 6;
const CLASS_BEGIN_CHOICE: u8 = 7;
const CLASS_GET_GLOBAL: u8 = 8;
const CLASS_SET_GLOBAL: u8 = 9;
const CLASS_TAKE_GLOBAL: u8 = 10;
const STATIC_CLASS: [u8; 256] = {
let mut table = [0u8; 256];
table[GOTO as usize] = CLASS_GOTO;
table[GOTO_IF as usize] = CLASS_GOTO_IF;
table[ENTER_CONTAINER as usize] = CLASS_ENTER_CONTAINER;
table[CALL as usize] = CLASS_CALL;
table[TUNNEL_CALL as usize] = CLASS_TUNNEL_CALL;
table[THREAD_CALL as usize] = CLASS_THREAD_CALL;
table[BEGIN_CHOICE as usize] = CLASS_BEGIN_CHOICE;
table[GET_GLOBAL as usize] = CLASS_GET_GLOBAL;
table[SET_GLOBAL as usize] = CLASS_SET_GLOBAL;
table[TAKE_GLOBAL as usize] = CLASS_TAKE_GLOBAL;
table
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
Goto,
GotoIf,
EnterContainer,
Call,
TunnelCall,
ThreadCall,
BeginChoice(ChoiceFlags),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TargetSite {
pub kind: TargetKind,
pub operand: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GlobalKind {
Get,
Set,
Take,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticKind {
Target(TargetKind),
Global(GlobalKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticSite {
pub kind: StaticKind,
pub operand: usize,
pub end: usize,
}
impl Opcode {
#[expect(clippy::too_many_lines)]
pub fn encode(&self, buf: &mut Vec<u8>) {
match *self {
Self::PushInt(v) => {
write_u8(buf, PUSH_INT);
write_i32(buf, v);
}
Self::PushFloat(v) => {
write_u8(buf, PUSH_FLOAT);
write_f32(buf, v);
}
Self::PushBool(v) => {
write_u8(buf, PUSH_BOOL);
write_u8(buf, u8::from(v));
}
Self::PushString(idx) => {
write_u8(buf, PUSH_STRING);
write_u16(buf, idx);
}
Self::PushList(idx) => {
write_u8(buf, PUSH_LIST);
write_u16(buf, idx);
}
Self::PushDivertTarget(id) => {
write_u8(buf, PUSH_DIVERT_TARGET);
write_def_id(buf, id);
}
Self::PushNull => write_u8(buf, PUSH_NULL),
Self::Pop => write_u8(buf, POP),
Self::Duplicate => write_u8(buf, DUPLICATE),
Self::Add => write_u8(buf, ADD),
Self::Subtract => write_u8(buf, SUBTRACT),
Self::Multiply => write_u8(buf, MULTIPLY),
Self::Divide => write_u8(buf, DIVIDE),
Self::Modulo => write_u8(buf, MODULO),
Self::Negate => write_u8(buf, NEGATE),
Self::Equal => write_u8(buf, EQUAL),
Self::NotEqual => write_u8(buf, NOT_EQUAL),
Self::Greater => write_u8(buf, GREATER),
Self::GreaterOrEqual => write_u8(buf, GREATER_OR_EQUAL),
Self::Less => write_u8(buf, LESS),
Self::LessOrEqual => write_u8(buf, LESS_OR_EQUAL),
Self::Not => write_u8(buf, NOT),
Self::And => write_u8(buf, AND),
Self::Or => write_u8(buf, OR),
Self::GetGlobal(id) => {
write_u8(buf, GET_GLOBAL);
write_def_id(buf, id);
}
Self::SetGlobal(id) => {
write_u8(buf, SET_GLOBAL);
write_def_id(buf, id);
}
Self::DeclareTemp(idx) => {
write_u8(buf, DECLARE_TEMP);
write_u16(buf, idx);
}
Self::GetTemp(idx) => {
write_u8(buf, GET_TEMP);
write_u16(buf, idx);
}
Self::SetTemp(idx) => {
write_u8(buf, SET_TEMP);
write_u16(buf, idx);
}
Self::GetTempRaw(idx) => {
write_u8(buf, GET_TEMP_RAW);
write_u16(buf, idx);
}
Self::PushVarPointer(id) => {
write_u8(buf, PUSH_VAR_POINTER);
write_def_id(buf, id);
}
Self::PushTempPointer(slot) => {
write_u8(buf, PUSH_TEMP_POINTER);
write_u16(buf, slot);
}
Self::Jump(offset) => {
write_u8(buf, JUMP);
write_i32(buf, offset);
}
Self::JumpIfFalse(offset) => {
write_u8(buf, JUMP_IF_FALSE);
write_i32(buf, offset);
}
Self::Goto(id) => {
write_u8(buf, GOTO);
write_def_id(buf, id);
}
Self::GotoIf(id) => {
write_u8(buf, GOTO_IF);
write_def_id(buf, id);
}
Self::GotoVariable => write_u8(buf, GOTO_VARIABLE),
Self::EnterContainer(id) => {
write_u8(buf, ENTER_CONTAINER);
write_def_id(buf, id);
}
Self::ExitContainer => write_u8(buf, EXIT_CONTAINER),
Self::Call(id) => {
write_u8(buf, CALL);
write_def_id(buf, id);
}
Self::Return => write_u8(buf, RETURN),
Self::TunnelCall(id) => {
write_u8(buf, TUNNEL_CALL);
write_def_id(buf, id);
}
Self::TunnelReturn => write_u8(buf, TUNNEL_RETURN),
Self::TunnelCallVariable => write_u8(buf, TUNNEL_CALL_VARIABLE),
Self::CallVariable(argc) => {
write_u8(buf, CALL_VARIABLE);
write_u8(buf, argc);
}
Self::ThreadCall(id) => {
write_u8(buf, THREAD_CALL);
write_def_id(buf, id);
}
Self::ThreadStart => write_u8(buf, THREAD_START),
Self::ThreadDone => write_u8(buf, THREAD_DONE),
Self::EmitLine(idx, slot_count) => {
write_u8(buf, EMIT_LINE);
write_u16(buf, idx);
write_u8(buf, slot_count);
}
Self::EmitValue => write_u8(buf, EMIT_VALUE),
Self::EmitNewline => write_u8(buf, EMIT_NEWLINE),
Self::EmitLineNl(idx, slot_count) => {
write_u8(buf, EMIT_LINE_NL);
write_u16(buf, idx);
write_u8(buf, slot_count);
}
Self::BinaryImm(kind, imm) => {
write_u8(buf, BINARY_IMM);
write_u8(buf, kind.to_byte());
write_i32(buf, imm);
}
Self::BinaryJumpIfFalse(kind, rel) => {
write_u8(buf, BINARY_JUMP_IF_FALSE);
write_u8(buf, kind.to_byte());
write_i32(buf, rel);
}
Self::BinaryImmJumpIfFalse(kind, imm, rel) => {
write_u8(buf, BINARY_IMM_JUMP_IF_FALSE);
write_u8(buf, kind.to_byte());
write_i32(buf, imm);
write_i32(buf, rel);
}
Self::GetTempBinaryImm(slot, kind, imm) => {
write_u8(buf, GET_TEMP_BINARY_IMM);
write_u16(buf, slot);
write_u8(buf, kind.to_byte());
write_i32(buf, imm);
}
Self::GetTempBinaryImmJumpIfFalse(slot, kind, imm, rel) => {
write_u8(buf, GET_TEMP_BINARY_IMM_JUMP_IF_FALSE);
write_u16(buf, slot);
write_u8(buf, kind.to_byte());
write_i32(buf, imm);
write_i32(buf, rel);
}
Self::DuplicateBinaryImmJumpIfFalse(kind, imm, rel) => {
write_u8(buf, DUPLICATE_BINARY_IMM_JUMP_IF_FALSE);
write_u8(buf, kind.to_byte());
write_i32(buf, imm);
write_i32(buf, rel);
}
Self::Spring => write_u8(buf, SPRING),
Self::Glue => write_u8(buf, GLUE),
Self::BeginTag => write_u8(buf, BEGIN_TAG),
Self::EndTag => write_u8(buf, END_TAG),
Self::EvalLine(idx, slot_count) => {
write_u8(buf, EVAL_LINE);
write_u16(buf, idx);
write_u8(buf, slot_count);
}
Self::BeginFragment => write_u8(buf, BEGIN_FRAGMENT),
Self::EndFragment => write_u8(buf, END_FRAGMENT),
Self::AttachElement => write_u8(buf, ATTACH_ELEMENT),
Self::EndElementRun => write_u8(buf, END_ELEMENT_RUN),
Self::BeginChoice(flags, target) => {
write_u8(buf, BEGIN_CHOICE);
write_u8(buf, flags.to_byte());
write_def_id(buf, target);
}
Self::EndChoice => write_u8(buf, END_CHOICE),
Self::Sequence(kind, count) => {
write_u8(buf, SEQUENCE);
write_u8(buf, kind.to_byte());
write_u8(buf, count);
}
Self::SequenceBranch(offset) => {
write_u8(buf, SEQUENCE_BRANCH);
write_i32(buf, offset);
}
Self::VisitCount => write_u8(buf, VISIT_COUNT),
Self::CurrentVisitCount => write_u8(buf, CURRENT_VISIT_COUNT),
Self::TouchVisit => write_u8(buf, TOUCH_VISIT),
Self::ShuffleIndexOf => write_u8(buf, SHUFFLE_INDEX_OF),
Self::TurnsSince => write_u8(buf, TURNS_SINCE),
Self::TurnIndex => write_u8(buf, TURN_INDEX),
Self::ChoiceCount => write_u8(buf, CHOICE_COUNT),
Self::Random => write_u8(buf, RANDOM),
Self::SeedRandom => write_u8(buf, SEED_RANDOM),
Self::CastToInt => write_u8(buf, CAST_TO_INT),
Self::CastToFloat => write_u8(buf, CAST_TO_FLOAT),
Self::Floor => write_u8(buf, FLOOR),
Self::Ceiling => write_u8(buf, CEILING),
Self::Pow => write_u8(buf, POW),
Self::Min => write_u8(buf, MIN),
Self::Max => write_u8(buf, MAX),
Self::CallExternal(id, argc) => {
write_u8(buf, CALL_EXTERNAL);
write_def_id(buf, id);
write_u8(buf, argc);
}
Self::ListContains => write_u8(buf, LIST_CONTAINS),
Self::ListNotContains => write_u8(buf, LIST_NOT_CONTAINS),
Self::ListIntersect => write_u8(buf, LIST_INTERSECT),
Self::ListAll => write_u8(buf, LIST_ALL),
Self::ListInvert => write_u8(buf, LIST_INVERT),
Self::ListCount => write_u8(buf, LIST_COUNT),
Self::ListMin => write_u8(buf, LIST_MIN),
Self::ListMax => write_u8(buf, LIST_MAX),
Self::ListValue => write_u8(buf, LIST_VALUE),
Self::ListRange => write_u8(buf, LIST_RANGE),
Self::ListFromInt => write_u8(buf, LIST_FROM_INT),
Self::ListRandom => write_u8(buf, LIST_RANDOM),
Self::ArrayNew(n) => {
write_u8(buf, ARRAY_NEW);
write_u32(buf, n);
}
Self::MapNew(n) => {
write_u8(buf, MAP_NEW);
write_u32(buf, n);
}
Self::IndexGet => write_u8(buf, INDEX_GET),
Self::IndexSet => write_u8(buf, INDEX_SET),
Self::CollectionLen => write_u8(buf, COLLECTION_LEN),
Self::MapGet => write_u8(buf, MAP_GET),
Self::MapInsert => write_u8(buf, MAP_INSERT),
Self::MapRemove => write_u8(buf, MAP_REMOVE),
Self::MapContains => write_u8(buf, MAP_CONTAINS),
Self::CollectionKeys => write_u8(buf, COLLECTION_KEYS),
Self::CollectionValues => write_u8(buf, COLLECTION_VALUES),
Self::PushLiteral(idx) => {
write_u8(buf, PUSH_LITERAL);
write_u32(buf, idx);
}
Self::TakeGlobal(id) => {
write_u8(buf, TAKE_GLOBAL);
write_def_id(buf, id);
}
Self::TakeTemp(idx) => {
write_u8(buf, TAKE_TEMP);
write_u16(buf, idx);
}
Self::RecordNew(shape_id) => {
write_u8(buf, RECORD_NEW);
write_u32(buf, shape_id);
}
Self::RecordGetDyn(name_id) => {
write_u8(buf, RECORD_GET_DYN);
write_u16(buf, name_id);
}
Self::RecordSetDyn(name_id) => {
write_u8(buf, RECORD_SET_DYN);
write_u16(buf, name_id);
}
Self::RecordGet(offset) => {
write_u8(buf, RECORD_GET);
write_u16(buf, offset);
}
Self::RecordSet(offset) => {
write_u8(buf, RECORD_SET);
write_u16(buf, offset);
}
Self::ConvertInt => write_u8(buf, CONVERT_INT),
Self::ConvertFloat => write_u8(buf, CONVERT_FLOAT),
Self::ConvertString => write_u8(buf, CONVERT_STRING),
Self::PushFnRef(id) => {
write_u8(buf, PUSH_FN_REF);
write_def_id(buf, id);
}
Self::MakeClosure {
target,
bound_count,
} => {
write_u8(buf, MAKE_CLOSURE);
write_def_id(buf, target);
write_u8(buf, bound_count);
}
Self::CallValue(argc) => {
write_u8(buf, CALL_VALUE);
write_u8(buf, argc);
}
Self::BindValue(argc) => {
write_u8(buf, BIND_VALUE);
write_u8(buf, argc);
}
Self::MakeProjection {
root,
segment_count,
} => {
write_u8(buf, MAKE_PROJECTION);
write_def_id(buf, root);
write_u8(buf, segment_count);
}
Self::ProjRead => write_u8(buf, PROJ_READ),
Self::ProjWrite => write_u8(buf, PROJ_WRITE),
Self::CharAt => write_u8(buf, CHAR_AT),
Self::PushNone => write_u8(buf, PUSH_NONE),
Self::MakeSome => write_u8(buf, MAKE_SOME),
Self::StrFind => write_u8(buf, STR_FIND),
Self::SeqIndexOf => write_u8(buf, SEQ_INDEX_OF),
Self::SeqMin => write_u8(buf, SEQ_MIN),
Self::SeqMax => write_u8(buf, SEQ_MAX),
Self::SeqFirst => write_u8(buf, SEQ_FIRST),
Self::SeqLast => write_u8(buf, SEQ_LAST),
Self::SeqPop => write_u8(buf, SEQ_POP),
Self::MapGetOpt => write_u8(buf, MAP_GET_OPT),
Self::MapContainsValue => write_u8(buf, MAP_CONTAINS_VALUE),
Self::MapClear => write_u8(buf, MAP_CLEAR),
Self::CoalesceSome(offset) => {
write_u8(buf, COALESCE_SOME);
write_i32(buf, offset);
}
Self::OptionBind(slot) => {
write_u8(buf, OPTION_BIND);
write_u16(buf, slot);
}
Self::SeqRemoveAt => write_u8(buf, SEQ_REMOVE_AT),
Self::RandFloat => write_u8(buf, RAND_FLOAT),
Self::RandChance => write_u8(buf, RAND_CHANCE),
Self::RandPick => write_u8(buf, RAND_PICK),
Self::RandShuffle => write_u8(buf, RAND_SHUFFLE),
Self::RangeMakeExcl => write_u8(buf, RANGE_MAKE_EXCL),
Self::RangeMakeIncl => write_u8(buf, RANGE_MAKE_INCL),
Self::RangeNonEmpty => write_u8(buf, RANGE_NON_EMPTY),
Self::SeqSorted => write_u8(buf, SEQ_SORTED),
Self::SeqSortedBy => write_u8(buf, SEQ_SORTED_BY),
Self::Collect(op) => {
write_u8(buf, COLLECT);
write_u8(buf, op.to_byte());
}
Self::SeqVerb(op) => {
write_u8(buf, SEQ_VERB);
write_u8(buf, op.to_byte());
}
Self::Tower(op) => {
write_u8(buf, TOWER);
write_u8(buf, op.to_byte());
}
Self::Done => write_u8(buf, DONE),
Self::Yield => write_u8(buf, YIELD),
Self::End => write_u8(buf, END),
Self::Nop => write_u8(buf, NOP),
Self::BeginStringEval => write_u8(buf, BEGIN_STRING_EVAL),
Self::EndStringEval => write_u8(buf, END_STRING_EVAL),
}
}
pub const TARGET_OPERAND_LEN: usize = 8;
#[must_use]
#[inline]
pub fn peek_static(buf: &[u8], offset: usize) -> Option<StaticSite> {
let class = STATIC_CLASS[*buf.get(offset)? as usize];
if class == 0 {
return None;
}
let (kind, operand) = match class {
CLASS_GOTO => (StaticKind::Target(TargetKind::Goto), offset + 1),
CLASS_GOTO_IF => (StaticKind::Target(TargetKind::GotoIf), offset + 1),
CLASS_ENTER_CONTAINER => (StaticKind::Target(TargetKind::EnterContainer), offset + 1),
CLASS_CALL => (StaticKind::Target(TargetKind::Call), offset + 1),
CLASS_TUNNEL_CALL => (StaticKind::Target(TargetKind::TunnelCall), offset + 1),
CLASS_THREAD_CALL => (StaticKind::Target(TargetKind::ThreadCall), offset + 1),
CLASS_BEGIN_CHOICE => {
let flags = ChoiceFlags::from_byte(*buf.get(offset + 1)?);
(
StaticKind::Target(TargetKind::BeginChoice(flags)),
offset + 2,
)
}
CLASS_GET_GLOBAL => (StaticKind::Global(GlobalKind::Get), offset + 1),
CLASS_SET_GLOBAL => (StaticKind::Global(GlobalKind::Set), offset + 1),
CLASS_TAKE_GLOBAL => (StaticKind::Global(GlobalKind::Take), offset + 1),
_ => return None,
};
let end = operand + Self::TARGET_OPERAND_LEN;
(end <= buf.len()).then_some(StaticSite { kind, operand, end })
}
#[must_use]
pub fn peek_target(buf: &[u8], offset: usize) -> Option<TargetSite> {
let site = Self::peek_static(buf, offset)?;
match site.kind {
StaticKind::Target(kind) => Some(TargetSite {
kind,
operand: site.operand,
end: site.end,
}),
StaticKind::Global(_) => None,
}
}
#[expect(clippy::too_many_lines)]
pub fn decode(buf: &[u8], offset: &mut usize) -> Result<Self, DecodeError> {
let disc = read_u8(buf, offset)?;
let op = match disc {
PUSH_INT => Self::PushInt(read_i32(buf, offset)?),
PUSH_FLOAT => Self::PushFloat(read_f32(buf, offset)?),
PUSH_BOOL => Self::PushBool(read_u8(buf, offset)? != 0),
PUSH_STRING => Self::PushString(read_u16(buf, offset)?),
PUSH_LIST => Self::PushList(read_u16(buf, offset)?),
PUSH_DIVERT_TARGET => Self::PushDivertTarget(read_def_id(buf, offset)?),
PUSH_NULL => Self::PushNull,
POP => Self::Pop,
DUPLICATE => Self::Duplicate,
ADD => Self::Add,
SUBTRACT => Self::Subtract,
MULTIPLY => Self::Multiply,
DIVIDE => Self::Divide,
MODULO => Self::Modulo,
NEGATE => Self::Negate,
EQUAL => Self::Equal,
NOT_EQUAL => Self::NotEqual,
GREATER => Self::Greater,
GREATER_OR_EQUAL => Self::GreaterOrEqual,
LESS => Self::Less,
LESS_OR_EQUAL => Self::LessOrEqual,
NOT => Self::Not,
AND => Self::And,
OR => Self::Or,
GET_GLOBAL => Self::GetGlobal(read_def_id(buf, offset)?),
SET_GLOBAL => Self::SetGlobal(read_def_id(buf, offset)?),
DECLARE_TEMP => Self::DeclareTemp(read_u16(buf, offset)?),
GET_TEMP => Self::GetTemp(read_u16(buf, offset)?),
SET_TEMP => Self::SetTemp(read_u16(buf, offset)?),
GET_TEMP_RAW => Self::GetTempRaw(read_u16(buf, offset)?),
PUSH_VAR_POINTER => Self::PushVarPointer(read_def_id(buf, offset)?),
PUSH_TEMP_POINTER => Self::PushTempPointer(read_u16(buf, offset)?),
JUMP => Self::Jump(read_i32(buf, offset)?),
JUMP_IF_FALSE => Self::JumpIfFalse(read_i32(buf, offset)?),
GOTO => Self::Goto(read_def_id(buf, offset)?),
GOTO_IF => Self::GotoIf(read_def_id(buf, offset)?),
GOTO_VARIABLE => Self::GotoVariable,
ENTER_CONTAINER => Self::EnterContainer(read_def_id(buf, offset)?),
EXIT_CONTAINER => Self::ExitContainer,
CALL => Self::Call(read_def_id(buf, offset)?),
RETURN => Self::Return,
TUNNEL_CALL => Self::TunnelCall(read_def_id(buf, offset)?),
TUNNEL_RETURN => Self::TunnelReturn,
TUNNEL_CALL_VARIABLE => Self::TunnelCallVariable,
CALL_VARIABLE => Self::CallVariable(read_u8(buf, offset)?),
THREAD_CALL => Self::ThreadCall(read_def_id(buf, offset)?),
THREAD_START => Self::ThreadStart,
THREAD_DONE => Self::ThreadDone,
EMIT_LINE => {
let idx = read_u16(buf, offset)?;
let slot_count = read_u8(buf, offset)?;
Self::EmitLine(idx, slot_count)
}
EMIT_VALUE => Self::EmitValue,
EMIT_NEWLINE => Self::EmitNewline,
EMIT_LINE_NL => {
let idx = read_u16(buf, offset)?;
let slot_count = read_u8(buf, offset)?;
Self::EmitLineNl(idx, slot_count)
}
BINARY_IMM => {
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
Self::BinaryImm(kind, read_i32(buf, offset)?)
}
BINARY_JUMP_IF_FALSE => {
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
Self::BinaryJumpIfFalse(kind, read_i32(buf, offset)?)
}
BINARY_IMM_JUMP_IF_FALSE => {
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
let imm = read_i32(buf, offset)?;
Self::BinaryImmJumpIfFalse(kind, imm, read_i32(buf, offset)?)
}
GET_TEMP_BINARY_IMM => {
let slot = read_u16(buf, offset)?;
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
Self::GetTempBinaryImm(slot, kind, read_i32(buf, offset)?)
}
GET_TEMP_BINARY_IMM_JUMP_IF_FALSE => {
let slot = read_u16(buf, offset)?;
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
let imm = read_i32(buf, offset)?;
Self::GetTempBinaryImmJumpIfFalse(slot, kind, imm, read_i32(buf, offset)?)
}
DUPLICATE_BINARY_IMM_JUMP_IF_FALSE => {
let kind = BinaryKind::from_byte(read_u8(buf, offset)?)?;
let imm = read_i32(buf, offset)?;
Self::DuplicateBinaryImmJumpIfFalse(kind, imm, read_i32(buf, offset)?)
}
SPRING => Self::Spring,
GLUE => Self::Glue,
BEGIN_TAG => Self::BeginTag,
END_TAG => Self::EndTag,
EVAL_LINE => {
let idx = read_u16(buf, offset)?;
let slot_count = read_u8(buf, offset)?;
Self::EvalLine(idx, slot_count)
}
BEGIN_FRAGMENT => Self::BeginFragment,
END_FRAGMENT => Self::EndFragment,
ATTACH_ELEMENT => Self::AttachElement,
END_ELEMENT_RUN => Self::EndElementRun,
BEGIN_CHOICE => {
let flags = ChoiceFlags::from_byte(read_u8(buf, offset)?);
let target = read_def_id(buf, offset)?;
Self::BeginChoice(flags, target)
}
END_CHOICE => Self::EndChoice,
SEQUENCE => {
let kind = SequenceKind::from_byte(read_u8(buf, offset)?)?;
let count = read_u8(buf, offset)?;
Self::Sequence(kind, count)
}
SEQUENCE_BRANCH => Self::SequenceBranch(read_i32(buf, offset)?),
VISIT_COUNT => Self::VisitCount,
CURRENT_VISIT_COUNT => Self::CurrentVisitCount,
TOUCH_VISIT => Self::TouchVisit,
SHUFFLE_INDEX_OF => Self::ShuffleIndexOf,
TURNS_SINCE => Self::TurnsSince,
TURN_INDEX => Self::TurnIndex,
CHOICE_COUNT => Self::ChoiceCount,
RANDOM => Self::Random,
SEED_RANDOM => Self::SeedRandom,
CAST_TO_INT => Self::CastToInt,
CAST_TO_FLOAT => Self::CastToFloat,
FLOOR => Self::Floor,
CEILING => Self::Ceiling,
POW => Self::Pow,
MIN => Self::Min,
MAX => Self::Max,
CALL_EXTERNAL => {
let id = read_def_id(buf, offset)?;
let argc = read_u8(buf, offset)?;
Self::CallExternal(id, argc)
}
LIST_CONTAINS => Self::ListContains,
LIST_NOT_CONTAINS => Self::ListNotContains,
LIST_INTERSECT => Self::ListIntersect,
LIST_ALL => Self::ListAll,
LIST_INVERT => Self::ListInvert,
LIST_COUNT => Self::ListCount,
LIST_MIN => Self::ListMin,
LIST_MAX => Self::ListMax,
LIST_VALUE => Self::ListValue,
LIST_RANGE => Self::ListRange,
LIST_FROM_INT => Self::ListFromInt,
LIST_RANDOM => Self::ListRandom,
ARRAY_NEW => Self::ArrayNew(read_u32(buf, offset)?),
MAP_NEW => Self::MapNew(read_u32(buf, offset)?),
INDEX_GET => Self::IndexGet,
INDEX_SET => Self::IndexSet,
COLLECTION_LEN => Self::CollectionLen,
MAP_GET => Self::MapGet,
MAP_INSERT => Self::MapInsert,
MAP_REMOVE => Self::MapRemove,
MAP_CONTAINS => Self::MapContains,
COLLECTION_KEYS => Self::CollectionKeys,
COLLECTION_VALUES => Self::CollectionValues,
PUSH_LITERAL => Self::PushLiteral(read_u32(buf, offset)?),
TAKE_GLOBAL => Self::TakeGlobal(read_def_id(buf, offset)?),
TAKE_TEMP => Self::TakeTemp(read_u16(buf, offset)?),
RECORD_NEW => Self::RecordNew(read_u32(buf, offset)?),
RECORD_GET_DYN => Self::RecordGetDyn(read_u16(buf, offset)?),
RECORD_SET_DYN => Self::RecordSetDyn(read_u16(buf, offset)?),
RECORD_GET => Self::RecordGet(read_u16(buf, offset)?),
RECORD_SET => Self::RecordSet(read_u16(buf, offset)?),
CONVERT_INT => Self::ConvertInt,
CONVERT_FLOAT => Self::ConvertFloat,
CONVERT_STRING => Self::ConvertString,
PUSH_FN_REF => Self::PushFnRef(read_def_id(buf, offset)?),
MAKE_CLOSURE => Self::MakeClosure {
target: read_def_id(buf, offset)?,
bound_count: read_u8(buf, offset)?,
},
CALL_VALUE => Self::CallValue(read_u8(buf, offset)?),
BIND_VALUE => Self::BindValue(read_u8(buf, offset)?),
MAKE_PROJECTION => Self::MakeProjection {
root: read_def_id(buf, offset)?,
segment_count: read_u8(buf, offset)?,
},
PROJ_READ => Self::ProjRead,
PROJ_WRITE => Self::ProjWrite,
CHAR_AT => Self::CharAt,
PUSH_NONE => Self::PushNone,
MAKE_SOME => Self::MakeSome,
STR_FIND => Self::StrFind,
SEQ_INDEX_OF => Self::SeqIndexOf,
SEQ_MIN => Self::SeqMin,
SEQ_MAX => Self::SeqMax,
SEQ_FIRST => Self::SeqFirst,
SEQ_LAST => Self::SeqLast,
SEQ_POP => Self::SeqPop,
MAP_GET_OPT => Self::MapGetOpt,
MAP_CONTAINS_VALUE => Self::MapContainsValue,
MAP_CLEAR => Self::MapClear,
COALESCE_SOME => Self::CoalesceSome(read_i32(buf, offset)?),
OPTION_BIND => Self::OptionBind(read_u16(buf, offset)?),
SEQ_REMOVE_AT => Self::SeqRemoveAt,
RAND_FLOAT => Self::RandFloat,
RAND_CHANCE => Self::RandChance,
RAND_PICK => Self::RandPick,
RAND_SHUFFLE => Self::RandShuffle,
RANGE_MAKE_EXCL => Self::RangeMakeExcl,
RANGE_MAKE_INCL => Self::RangeMakeIncl,
RANGE_NON_EMPTY => Self::RangeNonEmpty,
SEQ_SORTED => Self::SeqSorted,
SEQ_SORTED_BY => Self::SeqSortedBy,
TOWER => Self::Tower(TowerOp::from_byte(read_u8(buf, offset)?)?),
COLLECT => Self::Collect(CollectOp::from_byte(read_u8(buf, offset)?)?),
SEQ_VERB => Self::SeqVerb(SeqVerbOp::from_byte(read_u8(buf, offset)?)?),
DONE => Self::Done,
YIELD => Self::Yield,
END => Self::End,
NOP => Self::Nop,
BEGIN_STRING_EVAL => Self::BeginStringEval,
END_STRING_EVAL => Self::EndStringEval,
_ => return Err(DecodeError::UnknownOpcode(disc)),
};
Ok(op)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id::DefinitionTag;
fn roundtrip(op: &Opcode) {
let mut buf = Vec::new();
op.encode(&mut buf);
let mut offset = 0;
let decoded = Opcode::decode(&buf, &mut offset).unwrap();
assert_eq!(*op, decoded, "roundtrip failed for {op:?}");
assert_eq!(offset, buf.len(), "not all bytes consumed for {op:?}");
}
fn test_id() -> DefinitionId {
DefinitionId::new(DefinitionTag::Address, 0xBEEF)
}
#[test]
fn peek_target_locates_static_target_operands() {
let flags = ChoiceFlags {
has_condition: true,
has_start_content: false,
has_choice_only_content: true,
once_only: false,
is_invisible_default: false,
};
let ops = [
Opcode::PushInt(7),
Opcode::Goto(test_id()),
Opcode::BeginChoice(flags, test_id()),
Opcode::Call(test_id()),
Opcode::Nop,
];
let mut buf = Vec::new();
let mut starts = Vec::new();
for op in &ops {
starts.push(buf.len());
op.encode(&mut buf);
}
let ends: Vec<usize> = starts.iter().skip(1).copied().chain([buf.len()]).collect();
assert_eq!(
Opcode::peek_target(&buf, starts[0]),
None,
"PushInt is not a target op"
);
assert_eq!(
Opcode::peek_target(&buf, starts[1]),
Some(TargetSite {
kind: TargetKind::Goto,
operand: starts[1] + 1,
end: ends[1],
})
);
assert_eq!(
Opcode::peek_target(&buf, starts[2]),
Some(TargetSite {
kind: TargetKind::BeginChoice(flags),
operand: starts[2] + 2,
end: ends[2],
}),
"the choice flags byte precedes the operand"
);
assert_eq!(
Opcode::peek_target(&buf, starts[3]).map(|s| s.kind),
Some(TargetKind::Call)
);
assert_eq!(
Opcode::peek_target(&buf, starts[4]),
None,
"Nop is not a target op"
);
let mut gbuf = Vec::new();
Opcode::GetGlobal(test_id()).encode(&mut gbuf);
let set_at = gbuf.len();
Opcode::SetGlobal(test_id()).encode(&mut gbuf);
let take_at = gbuf.len();
Opcode::TakeGlobal(test_id()).encode(&mut gbuf);
let ptr_at = gbuf.len();
Opcode::PushVarPointer(test_id()).encode(&mut gbuf);
assert_eq!(
Opcode::peek_static(&gbuf, 0).map(|s| (s.kind, s.operand, s.end)),
Some((StaticKind::Global(GlobalKind::Get), 1, set_at))
);
assert_eq!(
Opcode::peek_static(&gbuf, set_at).map(|s| s.kind),
Some(StaticKind::Global(GlobalKind::Set))
);
assert_eq!(
Opcode::peek_static(&gbuf, take_at).map(|s| s.kind),
Some(StaticKind::Global(GlobalKind::Take))
);
assert_eq!(
Opcode::peek_target(&gbuf, 0),
None,
"a global is not a target"
);
assert_eq!(
Opcode::peek_static(&gbuf, ptr_at),
None,
"PushVarPointer stays symbolic"
);
assert_eq!(Opcode::peek_target(&buf, buf.len()), None, "past the end");
assert_eq!(
Opcode::peek_target(&buf[..ends[1] - 1], starts[1]),
None,
"a buffer that ends inside the operand is not a site"
);
let site = Opcode::peek_target(&buf, starts[1]).expect("site");
let raw = u64::from_le_bytes(buf[site.operand..site.end].try_into().expect("8 bytes"));
assert_eq!(DefinitionId::from_raw(raw), Some(test_id()));
}
fn global_id() -> DefinitionId {
DefinitionId::new(DefinitionTag::GlobalVar, 42)
}
fn ext_id() -> DefinitionId {
DefinitionId::new(DefinitionTag::ExternalFn, 0xCAFE)
}
#[test]
fn roundtrip_stack_literals() {
roundtrip(&Opcode::PushInt(0));
roundtrip(&Opcode::PushInt(-1));
roundtrip(&Opcode::PushInt(i32::MAX));
roundtrip(&Opcode::PushInt(i32::MIN));
roundtrip(&Opcode::PushFloat(0.0));
roundtrip(&Opcode::PushFloat(3.125));
roundtrip(&Opcode::PushFloat(f32::NEG_INFINITY));
roundtrip(&Opcode::PushBool(true));
roundtrip(&Opcode::PushBool(false));
roundtrip(&Opcode::PushString(0));
roundtrip(&Opcode::PushString(u16::MAX));
roundtrip(&Opcode::PushList(7));
roundtrip(&Opcode::PushDivertTarget(test_id()));
roundtrip(&Opcode::PushNull);
roundtrip(&Opcode::Pop);
roundtrip(&Opcode::Duplicate);
}
#[test]
fn roundtrip_arithmetic() {
for op in [
Opcode::Add,
Opcode::Subtract,
Opcode::Multiply,
Opcode::Divide,
Opcode::Modulo,
Opcode::Negate,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_comparison() {
for op in [
Opcode::Equal,
Opcode::NotEqual,
Opcode::Greater,
Opcode::GreaterOrEqual,
Opcode::Less,
Opcode::LessOrEqual,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_logic() {
for op in [Opcode::Not, Opcode::And, Opcode::Or] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_globals() {
roundtrip(&Opcode::GetGlobal(global_id()));
roundtrip(&Opcode::SetGlobal(global_id()));
}
#[test]
fn roundtrip_temps() {
roundtrip(&Opcode::DeclareTemp(0));
roundtrip(&Opcode::GetTemp(5));
roundtrip(&Opcode::SetTemp(u16::MAX));
roundtrip(&Opcode::GetTempRaw(3));
}
#[test]
fn roundtrip_var_pointer() {
roundtrip(&Opcode::PushVarPointer(global_id()));
roundtrip(&Opcode::PushTempPointer(0));
roundtrip(&Opcode::PushTempPointer(u16::MAX));
}
#[test]
fn roundtrip_control_flow() {
roundtrip(&Opcode::Jump(0));
roundtrip(&Opcode::Jump(-42));
roundtrip(&Opcode::JumpIfFalse(100));
roundtrip(&Opcode::Goto(test_id()));
roundtrip(&Opcode::GotoIf(test_id()));
roundtrip(&Opcode::GotoVariable);
}
#[test]
fn roundtrip_container_flow() {
roundtrip(&Opcode::EnterContainer(test_id()));
roundtrip(&Opcode::ExitContainer);
}
#[test]
fn roundtrip_functions_tunnels() {
roundtrip(&Opcode::Call(test_id()));
roundtrip(&Opcode::Return);
roundtrip(&Opcode::TunnelCall(test_id()));
roundtrip(&Opcode::TunnelReturn);
roundtrip(&Opcode::TunnelCallVariable);
roundtrip(&Opcode::CallVariable(0));
roundtrip(&Opcode::CallVariable(3));
}
#[test]
fn roundtrip_threads() {
roundtrip(&Opcode::ThreadCall(test_id()));
roundtrip(&Opcode::ThreadStart);
roundtrip(&Opcode::ThreadDone);
}
#[test]
fn roundtrip_output() {
roundtrip(&Opcode::EmitLine(0, 0));
roundtrip(&Opcode::EmitLine(999, 3));
roundtrip(&Opcode::EmitValue);
roundtrip(&Opcode::EmitNewline);
roundtrip(&Opcode::EmitLineNl(0x1234, 3));
for kind in BinaryKind::ALL {
roundtrip(&Opcode::BinaryImm(kind, -7));
roundtrip(&Opcode::BinaryJumpIfFalse(kind, 300));
roundtrip(&Opcode::BinaryImmJumpIfFalse(kind, i32::MIN, -12));
roundtrip(&Opcode::GetTempBinaryImm(0xBEEF, kind, 1));
roundtrip(&Opcode::GetTempBinaryImmJumpIfFalse(3, kind, -1, 0x7FFF));
roundtrip(&Opcode::DuplicateBinaryImmJumpIfFalse(kind, 2, -9));
assert_eq!(BinaryKind::from_mnemonic(kind.mnemonic()), Some(kind));
}
assert_eq!(
Opcode::decode(&[BINARY_IMM, 11, 0, 0, 0, 0], &mut 0),
Err(DecodeError::InvalidBinaryKind(11))
);
roundtrip(&Opcode::Spring);
roundtrip(&Opcode::Glue);
roundtrip(&Opcode::BeginTag);
roundtrip(&Opcode::EndTag);
roundtrip(&Opcode::EvalLine(0, 0));
roundtrip(&Opcode::EvalLine(42, 2));
roundtrip(&Opcode::AttachElement);
roundtrip(&Opcode::EndElementRun);
}
#[test]
fn roundtrip_choices() {
roundtrip(&Opcode::BeginChoice(
ChoiceFlags {
has_condition: true,
has_start_content: false,
has_choice_only_content: true,
once_only: false,
is_invisible_default: true,
},
test_id(),
));
roundtrip(&Opcode::BeginChoice(
ChoiceFlags {
has_condition: false,
has_start_content: true,
has_choice_only_content: false,
once_only: true,
is_invisible_default: false,
},
test_id(),
));
roundtrip(&Opcode::EndChoice);
}
#[test]
fn roundtrip_sequences() {
for kind in [
SequenceKind::Cycle,
SequenceKind::Stopping,
SequenceKind::OnceOnly,
SequenceKind::Shuffle,
] {
roundtrip(&Opcode::Sequence(kind, 5));
}
roundtrip(&Opcode::SequenceBranch(-10));
roundtrip(&Opcode::SequenceBranch(0));
}
#[test]
fn roundtrip_intrinsics() {
for op in [
Opcode::VisitCount,
Opcode::CurrentVisitCount,
Opcode::TouchVisit,
Opcode::ShuffleIndexOf,
Opcode::TurnsSince,
Opcode::TurnIndex,
Opcode::ChoiceCount,
Opcode::Random,
Opcode::SeedRandom,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_casts_math() {
for op in [
Opcode::CastToInt,
Opcode::CastToFloat,
Opcode::Floor,
Opcode::Ceiling,
Opcode::Pow,
Opcode::Min,
Opcode::Max,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_call_external() {
roundtrip(&Opcode::CallExternal(ext_id(), 3));
roundtrip(&Opcode::CallExternal(ext_id(), 0));
}
#[test]
fn roundtrip_list_ops() {
for op in [
Opcode::ListContains,
Opcode::ListNotContains,
Opcode::ListIntersect,
Opcode::ListAll,
Opcode::ListInvert,
Opcode::ListCount,
Opcode::ListMin,
Opcode::ListMax,
Opcode::ListValue,
Opcode::ListRange,
Opcode::ListFromInt,
Opcode::ListRandom,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_collections() {
for op in [
Opcode::ArrayNew(0),
Opcode::ArrayNew(1),
Opcode::ArrayNew(u32::MAX),
Opcode::MapNew(0),
Opcode::MapNew(3),
Opcode::IndexGet,
Opcode::IndexSet,
Opcode::CollectionLen,
Opcode::MapGet,
Opcode::MapInsert,
Opcode::MapRemove,
Opcode::MapContains,
Opcode::CollectionKeys,
Opcode::CollectionValues,
Opcode::PushLiteral(0),
Opcode::PushLiteral(u32::MAX),
] {
roundtrip(&op);
}
}
#[test]
fn collection_opcode_block_is_contiguous_and_matches_rfc_layout() {
let expected: [(u8, Opcode); 12] = [
(0xBE, Opcode::ArrayNew(0)),
(0xBF, Opcode::MapNew(0)),
(0xC0, Opcode::IndexGet),
(0xC1, Opcode::IndexSet),
(0xC2, Opcode::CollectionLen),
(0xC3, Opcode::MapGet),
(0xC4, Opcode::MapInsert),
(0xC5, Opcode::MapRemove),
(0xC6, Opcode::MapContains),
(0xC7, Opcode::CollectionKeys),
(0xC8, Opcode::CollectionValues),
(0xC9, Opcode::PushLiteral(0)),
];
for (byte, op) in expected {
let mut buf = Vec::new();
op.encode(&mut buf);
assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
}
}
#[test]
fn roundtrip_ns_a1_option_and_stdlib_flips() {
for op in [
Opcode::PushNone,
Opcode::MakeSome,
Opcode::StrFind,
Opcode::SeqIndexOf,
Opcode::SeqMin,
Opcode::SeqMax,
Opcode::SeqFirst,
Opcode::SeqLast,
Opcode::SeqPop,
Opcode::MapGetOpt,
Opcode::MapContainsValue,
Opcode::MapClear,
] {
roundtrip(&op);
}
}
#[test]
fn ns_a1_opcode_block_layout() {
let expected: [(u8, Opcode); 12] = [
(0xDE, Opcode::PushNone),
(0xDF, Opcode::MakeSome),
(0xE2, Opcode::StrFind),
(0xE3, Opcode::SeqIndexOf),
(0xE4, Opcode::SeqMin),
(0xE5, Opcode::SeqMax),
(0xE6, Opcode::SeqFirst),
(0xE7, Opcode::SeqLast),
(0xE8, Opcode::SeqPop),
(0xE9, Opcode::MapGetOpt),
(0xEA, Opcode::MapContainsValue),
(0xEB, Opcode::MapClear),
];
for (byte, op) in expected {
let mut buf = Vec::new();
op.encode(&mut buf);
assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
}
}
#[test]
fn roundtrip_b1_coalesce_some() {
roundtrip(&Opcode::CoalesceSome(0));
roundtrip(&Opcode::CoalesceSome(-42));
roundtrip(&Opcode::CoalesceSome(100));
}
#[test]
fn coalesce_some_opcode_byte_is_0xfb() {
let mut buf = Vec::new();
Opcode::CoalesceSome(0).encode(&mut buf);
assert_eq!(
buf[0], 0xFB,
"CoalesceSome encoded to unexpected discriminant"
);
}
#[test]
fn roundtrip_b1b_option_bind() {
roundtrip(&Opcode::OptionBind(0));
roundtrip(&Opcode::OptionBind(7));
roundtrip(&Opcode::OptionBind(u16::MAX));
}
#[test]
fn option_bind_opcode_byte_is_0xfc() {
let mut buf = Vec::new();
Opcode::OptionBind(1).encode(&mut buf);
assert_eq!(
buf[0], 0xFC,
"OptionBind encoded to unexpected discriminant"
);
}
#[test]
fn roundtrip_seq_remove_at() {
roundtrip(&Opcode::SeqRemoveAt);
}
#[test]
fn seq_remove_at_opcode_byte_is_0xfd() {
let mut buf = Vec::new();
Opcode::SeqRemoveAt.encode(&mut buf);
assert_eq!(
buf[0], 0xFD,
"SeqRemoveAt encoded to unexpected discriminant"
);
}
#[test]
fn roundtrip_ns_a6_rand_verbs() {
for op in [
Opcode::RandFloat,
Opcode::RandChance,
Opcode::RandPick,
Opcode::RandShuffle,
] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_ns_a5_range_ops() {
for op in [
Opcode::RangeMakeExcl,
Opcode::RangeMakeIncl,
Opcode::RangeNonEmpty,
] {
roundtrip(&op);
}
}
#[test]
fn ns_a5_opcode_block_layout() {
let expected: [(u8, Opcode); 3] = [
(0xF4, Opcode::RangeMakeExcl),
(0xF5, Opcode::RangeMakeIncl),
(0xF6, Opcode::RangeNonEmpty),
];
for (byte, op) in expected {
let mut buf = Vec::new();
op.encode(&mut buf);
assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
}
}
#[test]
fn ns_a6_opcode_block_layout() {
let expected: [(u8, Opcode); 4] = [
(0xEC, Opcode::RandFloat),
(0xED, Opcode::RandChance),
(0xEE, Opcode::RandPick),
(0xEF, Opcode::RandShuffle),
];
for (byte, op) in expected {
let mut buf = Vec::new();
op.encode(&mut buf);
assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
}
}
#[test]
fn ns_a4_opcode_block_layout() {
let expected: [(u8, Opcode); 2] = [(0xF8, Opcode::SeqSorted), (0xF9, Opcode::SeqSortedBy)];
for (byte, op) in expected {
let mut buf = Vec::new();
op.encode(&mut buf);
assert_eq!(buf[0], byte, "{op:?} encoded to unexpected discriminant");
roundtrip(&op);
}
}
#[test]
fn roundtrip_ns_a8_tower_ops() {
for kind in TowerOp::ALL {
roundtrip(&Opcode::Tower(kind));
}
}
#[test]
fn ns_a8_tower_opcode_layout() {
for (i, kind) in TowerOp::ALL.into_iter().enumerate() {
let mut buf = Vec::new();
Opcode::Tower(kind).encode(&mut buf);
#[expect(clippy::cast_possible_truncation, reason = "13 kinds")]
let expected_kind = i as u8;
assert_eq!(buf, [0xF7, expected_kind], "{kind:?} layout drifted");
}
}
#[test]
fn decode_unknown_tower_kind_rejected() {
let buf = [0xF7, 13];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::InvalidTowerOp(13));
}
#[test]
fn roundtrip_ns_a7_collect_ops() {
for kind in CollectOp::ALL {
roundtrip(&Opcode::Collect(kind));
}
}
#[test]
fn ns_a7_collect_opcode_layout() {
for (i, kind) in CollectOp::ALL.into_iter().enumerate() {
let mut buf = Vec::new();
Opcode::Collect(kind).encode(&mut buf);
#[expect(clippy::cast_possible_truncation, reason = "5 kinds")]
let expected_kind = i as u8;
assert_eq!(buf, [0xFA, expected_kind], "{kind:?} layout drifted");
}
}
#[test]
fn decode_unknown_collect_kind_rejected() {
let buf = [0xFA, 5];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::InvalidCollectOp(5));
}
#[test]
fn roundtrip_seq_verb_ops() {
for kind in SeqVerbOp::ALL {
roundtrip(&Opcode::SeqVerb(kind));
}
}
#[test]
fn seq_verb_opcode_layout() {
for (i, kind) in SeqVerbOp::ALL.into_iter().enumerate() {
let mut buf = Vec::new();
Opcode::SeqVerb(kind).encode(&mut buf);
#[expect(clippy::cast_possible_truncation, reason = "6 kinds")]
let expected_kind = i as u8;
assert_eq!(buf, [0xA1, expected_kind], "{kind:?} layout drifted");
}
}
#[test]
fn decode_unknown_seq_verb_kind_rejected() {
let buf = [0xA1, 6];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::InvalidSeqVerbOp(6));
}
#[test]
fn seq_verb_mnemonics_round_trip() {
for kind in SeqVerbOp::ALL {
assert_eq!(SeqVerbOp::from_mnemonic(kind.mnemonic()), Some(kind));
}
assert_eq!(SeqVerbOp::Map.mnemonic(), "map");
assert_eq!(SeqVerbOp::Filter.mnemonic(), "filter");
assert_eq!(SeqVerbOp::Fold.mnemonic(), "fold");
assert_eq!(SeqVerbOp::FilterMap.mnemonic(), "filter_map");
assert_eq!(SeqVerbOp::Each.mnemonic(), "each");
assert_eq!(SeqVerbOp::MapEach.mnemonic(), "map_each");
assert_eq!(
SeqVerbOp::from_mnemonic("map_each"),
Some(SeqVerbOp::MapEach)
);
assert_eq!(SeqVerbOp::from_mnemonic("not_a_verb"), None);
}
#[test]
fn seq_verb_effectful_split() {
for kind in [
SeqVerbOp::Map,
SeqVerbOp::Filter,
SeqVerbOp::Fold,
SeqVerbOp::FilterMap,
] {
assert!(!kind.is_effectful(), "{kind:?} must be pure");
}
for kind in [SeqVerbOp::Each, SeqVerbOp::MapEach] {
assert!(kind.is_effectful(), "{kind:?} must be effectful");
}
}
#[test]
fn roundtrip_lifecycle() {
for op in [Opcode::Done, Opcode::Yield, Opcode::End, Opcode::Nop] {
roundtrip(&op);
}
}
#[test]
fn roundtrip_string_eval() {
roundtrip(&Opcode::BeginStringEval);
roundtrip(&Opcode::EndStringEval);
}
#[test]
fn decode_retired_source_location_byte_is_unknown_opcode() {
let buf = [0xFE];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::UnknownOpcode(0xFE));
}
#[test]
fn decode_unknown_opcode() {
let buf = [0xFF];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::UnknownOpcode(0xFF));
}
#[test]
fn collection_opcode_block_no_longer_rejected() {
for disc in 0xBEu8..=0xC9u8 {
let buf = [disc, 0, 0, 0, 0];
let mut offset = 0;
let result = Opcode::decode(&buf, &mut offset);
assert!(
!matches!(result, Err(DecodeError::UnknownOpcode(_))),
"0x{disc:02x} should decode to a real Opcode, got {result:?}"
);
}
}
#[test]
fn decode_reserved_sharing_discipline_opcodes_still_rejected() {
for disc in 0xCBu8..=0xCCu8 {
let buf = [disc];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::UnknownOpcode(disc));
}
}
#[test]
fn roundtrip_take_opcodes() {
roundtrip(&Opcode::TakeGlobal(global_id()));
roundtrip(&Opcode::TakeTemp(0));
roundtrip(&Opcode::TakeTemp(u16::MAX));
}
#[test]
fn take_opcodes_land_at_documented_bytes() {
let mut buf = Vec::new();
Opcode::TakeGlobal(global_id()).encode(&mut buf);
assert_eq!(buf[0], 0xCA);
let mut buf = Vec::new();
Opcode::TakeTemp(0).encode(&mut buf);
assert_eq!(buf[0], 0xCD);
}
#[test]
fn roundtrip_record_opcodes() {
roundtrip(&Opcode::RecordNew(0));
roundtrip(&Opcode::RecordNew(u32::MAX));
roundtrip(&Opcode::RecordGetDyn(0));
roundtrip(&Opcode::RecordGetDyn(u16::MAX));
roundtrip(&Opcode::RecordSetDyn(0));
roundtrip(&Opcode::RecordSetDyn(u16::MAX));
roundtrip(&Opcode::RecordGet(0));
roundtrip(&Opcode::RecordGet(u16::MAX));
roundtrip(&Opcode::RecordSet(0));
roundtrip(&Opcode::RecordSet(u16::MAX));
let mut buf = Vec::new();
Opcode::RecordNew(1).encode(&mut buf);
assert_eq!(buf[0], 0xCE);
let mut buf = Vec::new();
Opcode::RecordGetDyn(1).encode(&mut buf);
assert_eq!(buf[0], 0xCF);
let mut buf = Vec::new();
Opcode::RecordSetDyn(1).encode(&mut buf);
assert_eq!(buf[0], 0xD0);
let mut buf = Vec::new();
Opcode::RecordGet(1).encode(&mut buf);
assert_eq!(buf[0], 0xD1);
let mut buf = Vec::new();
Opcode::RecordSet(1).encode(&mut buf);
assert_eq!(buf[0], 0xD2);
}
#[test]
fn roundtrip_conversion_opcodes() {
for op in [
Opcode::ConvertInt,
Opcode::ConvertFloat,
Opcode::ConvertString,
] {
roundtrip(&op);
}
let mut buf = Vec::new();
Opcode::ConvertInt.encode(&mut buf);
assert_eq!(buf[0], 0xD3);
let mut buf = Vec::new();
Opcode::ConvertFloat.encode(&mut buf);
assert_eq!(buf[0], 0xD4);
let mut buf = Vec::new();
Opcode::ConvertString.encode(&mut buf);
assert_eq!(buf[0], 0xD5);
}
#[test]
fn roundtrip_char_at_opcode() {
roundtrip(&Opcode::CharAt);
let mut buf = Vec::new();
Opcode::CharAt.encode(&mut buf);
assert_eq!(buf[0], 0xDD);
}
#[test]
fn decode_unexpected_eof() {
let buf = [PUSH_INT, 0x00];
let mut offset = 0;
let err = Opcode::decode(&buf, &mut offset).unwrap_err();
assert_eq!(err, DecodeError::UnexpectedEof);
}
#[test]
fn decode_multiple_instructions() {
let ops = vec![
Opcode::PushInt(42),
Opcode::PushBool(true),
Opcode::Add,
Opcode::Done,
];
let mut buf = Vec::new();
for op in &ops {
op.encode(&mut buf);
}
let mut offset = 0;
for expected in &ops {
let decoded = Opcode::decode(&buf, &mut offset).unwrap();
assert_eq!(*expected, decoded);
}
assert_eq!(offset, buf.len());
}
#[test]
fn choice_flags_roundtrip() {
for bits in 0..32u8 {
let flags = ChoiceFlags::from_byte(bits);
assert_eq!(flags.to_byte(), bits);
}
}
}