pub const STDLIB_FN_BASE: u16 = 40_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Opcode {
Const = 0,
Pop = 1,
Dup = 2,
GetLocal = 3,
SetLocal = 4,
GetGlobal = 5,
SetGlobal = 6,
Add = 7,
Sub = 8,
Mul = 9,
Div = 10,
Mod = 11,
Neg = 12,
FAdd = 13,
FSub = 14,
FMul = 15,
FDiv = 16,
FNeg = 17,
Eq = 18,
Ne = 19,
Lt = 20,
Le = 21,
Gt = 22,
Ge = 23,
FEq = 24,
FNe = 25,
FLt = 26,
FLe = 27,
FGt = 28,
FGe = 29,
Not = 30,
Jump = 31,
JumpIfFalse = 32,
JumpIfTrue = 33,
Call = 34,
Return = 35,
MakeArray = 36,
MakeTuple = 37,
MakeStruct = 38,
MakeEnumVariant = 39,
Index = 40,
Field = 41,
Len = 42,
ToStr = 43,
ConcatN = 44,
MatchVariant = 45,
Halt = 0xFF,
}
impl Opcode {
pub fn from_u8(b: u8) -> Option<Opcode> {
match b {
0 => Some(Opcode::Const),
1 => Some(Opcode::Pop),
2 => Some(Opcode::Dup),
3 => Some(Opcode::GetLocal),
4 => Some(Opcode::SetLocal),
5 => Some(Opcode::GetGlobal),
6 => Some(Opcode::SetGlobal),
7 => Some(Opcode::Add),
8 => Some(Opcode::Sub),
9 => Some(Opcode::Mul),
10 => Some(Opcode::Div),
11 => Some(Opcode::Mod),
12 => Some(Opcode::Neg),
13 => Some(Opcode::FAdd),
14 => Some(Opcode::FSub),
15 => Some(Opcode::FMul),
16 => Some(Opcode::FDiv),
17 => Some(Opcode::FNeg),
18 => Some(Opcode::Eq),
19 => Some(Opcode::Ne),
20 => Some(Opcode::Lt),
21 => Some(Opcode::Le),
22 => Some(Opcode::Gt),
23 => Some(Opcode::Ge),
24 => Some(Opcode::FEq),
25 => Some(Opcode::FNe),
26 => Some(Opcode::FLt),
27 => Some(Opcode::FLe),
28 => Some(Opcode::FGt),
29 => Some(Opcode::FGe),
30 => Some(Opcode::Not),
31 => Some(Opcode::Jump),
32 => Some(Opcode::JumpIfFalse),
33 => Some(Opcode::JumpIfTrue),
34 => Some(Opcode::Call),
35 => Some(Opcode::Return),
36 => Some(Opcode::MakeArray),
37 => Some(Opcode::MakeTuple),
38 => Some(Opcode::MakeStruct),
39 => Some(Opcode::MakeEnumVariant),
40 => Some(Opcode::Index),
41 => Some(Opcode::Field),
42 => Some(Opcode::Len),
43 => Some(Opcode::ToStr),
44 => Some(Opcode::ConcatN),
45 => Some(Opcode::MatchVariant),
0xFF => Some(Opcode::Halt),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Opcode::Const => "CONST",
Opcode::Pop => "POP",
Opcode::Dup => "DUP",
Opcode::GetLocal => "GET_LOCAL",
Opcode::SetLocal => "SET_LOCAL",
Opcode::GetGlobal => "GET_GLOBAL",
Opcode::SetGlobal => "SET_GLOBAL",
Opcode::Add => "ADD",
Opcode::Sub => "SUB",
Opcode::Mul => "MUL",
Opcode::Div => "DIV",
Opcode::Mod => "MOD",
Opcode::Neg => "NEG",
Opcode::FAdd => "F_ADD",
Opcode::FSub => "F_SUB",
Opcode::FMul => "F_MUL",
Opcode::FDiv => "F_DIV",
Opcode::FNeg => "F_NEG",
Opcode::Eq => "EQ",
Opcode::Ne => "NE",
Opcode::Lt => "LT",
Opcode::Le => "LE",
Opcode::Gt => "GT",
Opcode::Ge => "GE",
Opcode::FEq => "F_EQ",
Opcode::FNe => "F_NE",
Opcode::FLt => "F_LT",
Opcode::FLe => "F_LE",
Opcode::FGt => "F_GT",
Opcode::FGe => "F_GE",
Opcode::Not => "NOT",
Opcode::Jump => "JUMP",
Opcode::JumpIfFalse => "JUMP_IF_FALSE",
Opcode::JumpIfTrue => "JUMP_IF_TRUE",
Opcode::Call => "CALL",
Opcode::Return => "RETURN",
Opcode::MakeArray => "MAKE_ARRAY",
Opcode::MakeTuple => "MAKE_TUPLE",
Opcode::MakeStruct => "MAKE_STRUCT",
Opcode::MakeEnumVariant => "MAKE_ENUM_VARIANT",
Opcode::Index => "INDEX",
Opcode::Field => "FIELD",
Opcode::Len => "LEN",
Opcode::ToStr => "TO_STR",
Opcode::ConcatN => "CONCAT_N",
Opcode::MatchVariant => "MATCH_VARIANT",
Opcode::Halt => "HALT",
}
}
pub fn operand_bytes(self) -> u8 {
match self {
Opcode::Const
| Opcode::GetLocal
| Opcode::SetLocal
| Opcode::GetGlobal
| Opcode::SetGlobal
| Opcode::Jump
| Opcode::JumpIfFalse
| Opcode::JumpIfTrue
| Opcode::MakeArray
| Opcode::MakeTuple
| Opcode::MakeStruct
| Opcode::Field
| Opcode::ConcatN => 2,
Opcode::Call | Opcode::MakeEnumVariant => 3,
Opcode::MatchVariant => 4,
Opcode::Pop
| Opcode::Dup
| Opcode::Add
| Opcode::Sub
| Opcode::Mul
| Opcode::Div
| Opcode::Mod
| Opcode::Neg
| Opcode::FAdd
| Opcode::FSub
| Opcode::FMul
| Opcode::FDiv
| Opcode::FNeg
| Opcode::Eq
| Opcode::Ne
| Opcode::Lt
| Opcode::Le
| Opcode::Gt
| Opcode::Ge
| Opcode::FEq
| Opcode::FNe
| Opcode::FLt
| Opcode::FLe
| Opcode::FGt
| Opcode::FGe
| Opcode::Not
| Opcode::Return
| Opcode::Index
| Opcode::Len
| Opcode::ToStr
| Opcode::Halt => 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
const ALL: &[Opcode] = &[
Opcode::Const,
Opcode::Pop,
Opcode::Dup,
Opcode::GetLocal,
Opcode::SetLocal,
Opcode::GetGlobal,
Opcode::SetGlobal,
Opcode::Add,
Opcode::Sub,
Opcode::Mul,
Opcode::Div,
Opcode::Mod,
Opcode::Neg,
Opcode::FAdd,
Opcode::FSub,
Opcode::FMul,
Opcode::FDiv,
Opcode::FNeg,
Opcode::Eq,
Opcode::Ne,
Opcode::Lt,
Opcode::Le,
Opcode::Gt,
Opcode::Ge,
Opcode::FEq,
Opcode::FNe,
Opcode::FLt,
Opcode::FLe,
Opcode::FGt,
Opcode::FGe,
Opcode::Not,
Opcode::Jump,
Opcode::JumpIfFalse,
Opcode::JumpIfTrue,
Opcode::Call,
Opcode::Return,
Opcode::MakeArray,
Opcode::MakeTuple,
Opcode::MakeStruct,
Opcode::MakeEnumVariant,
Opcode::Index,
Opcode::Field,
Opcode::Len,
Opcode::ToStr,
Opcode::ConcatN,
Opcode::MatchVariant,
Opcode::Halt,
];
const ZERO_OPERAND: &[Opcode] = &[
Opcode::Pop,
Opcode::Dup,
Opcode::Add,
Opcode::Sub,
Opcode::Mul,
Opcode::Div,
Opcode::Mod,
Opcode::Neg,
Opcode::FAdd,
Opcode::FSub,
Opcode::FMul,
Opcode::FDiv,
Opcode::FNeg,
Opcode::Eq,
Opcode::Ne,
Opcode::Lt,
Opcode::Le,
Opcode::Gt,
Opcode::Ge,
Opcode::FEq,
Opcode::FNe,
Opcode::FLt,
Opcode::FLe,
Opcode::FGt,
Opcode::FGe,
Opcode::Not,
Opcode::Return,
Opcode::Index,
Opcode::Len,
Opcode::ToStr,
Opcode::Halt,
];
const TWO_OPERAND: &[Opcode] = &[
Opcode::Const,
Opcode::GetLocal,
Opcode::SetLocal,
Opcode::GetGlobal,
Opcode::SetGlobal,
Opcode::Jump,
Opcode::JumpIfFalse,
Opcode::JumpIfTrue,
Opcode::MakeArray,
Opcode::MakeTuple,
Opcode::MakeStruct,
Opcode::Field,
Opcode::ConcatN,
];
const THREE_OPERAND: &[Opcode] = &[Opcode::Call, Opcode::MakeEnumVariant];
const FOUR_OPERAND: &[Opcode] = &[Opcode::MatchVariant];
#[test]
fn opening_discriminants_are_dense_from_zero() {
assert_eq!(Opcode::Const as u8, 0);
assert_eq!(Opcode::Pop as u8, 1);
assert_eq!(Opcode::Dup as u8, 2);
}
#[test]
fn halt_uses_the_sentinel_discriminant() {
assert_eq!(Opcode::Halt as u8, 0xFF);
}
#[test]
fn from_u8_round_trips_every_variant() {
assert_eq!(ALL.len(), 47, "ALL must list all 47 opcodes");
for op in ALL {
assert_eq!(
Opcode::from_u8(*op as u8),
Some(*op),
"round-trip failed for {op:?}",
);
}
}
#[test]
fn from_u8_returns_none_for_every_undefined_discriminant() {
for b in 0u8..=255 {
match Opcode::from_u8(b) {
Some(op) => assert_eq!(
op as u8, b,
"from_u8({b}) returned the wrong variant {op:?}",
),
None => {
let defined = ALL.iter().any(|op| *op as u8 == b);
assert!(!defined, "byte {b} is defined but from_u8 returned None");
}
}
}
assert_eq!(Opcode::from_u8(46), None);
assert_eq!(Opcode::from_u8(100), None);
assert_eq!(Opcode::from_u8(200), None);
assert_eq!(Opcode::from_u8(254), None);
}
#[test]
fn name_returns_the_locked_uppercase_string_per_variant() {
let cases: &[(Opcode, &str)] = &[
(Opcode::Const, "CONST"),
(Opcode::Pop, "POP"),
(Opcode::Dup, "DUP"),
(Opcode::GetLocal, "GET_LOCAL"),
(Opcode::SetLocal, "SET_LOCAL"),
(Opcode::GetGlobal, "GET_GLOBAL"),
(Opcode::SetGlobal, "SET_GLOBAL"),
(Opcode::Add, "ADD"),
(Opcode::Sub, "SUB"),
(Opcode::Mul, "MUL"),
(Opcode::Div, "DIV"),
(Opcode::Mod, "MOD"),
(Opcode::Neg, "NEG"),
(Opcode::FAdd, "F_ADD"),
(Opcode::FSub, "F_SUB"),
(Opcode::FMul, "F_MUL"),
(Opcode::FDiv, "F_DIV"),
(Opcode::FNeg, "F_NEG"),
(Opcode::Eq, "EQ"),
(Opcode::Ne, "NE"),
(Opcode::Lt, "LT"),
(Opcode::Le, "LE"),
(Opcode::Gt, "GT"),
(Opcode::Ge, "GE"),
(Opcode::FEq, "F_EQ"),
(Opcode::FNe, "F_NE"),
(Opcode::FLt, "F_LT"),
(Opcode::FLe, "F_LE"),
(Opcode::FGt, "F_GT"),
(Opcode::FGe, "F_GE"),
(Opcode::Not, "NOT"),
(Opcode::Jump, "JUMP"),
(Opcode::JumpIfFalse, "JUMP_IF_FALSE"),
(Opcode::JumpIfTrue, "JUMP_IF_TRUE"),
(Opcode::Call, "CALL"),
(Opcode::Return, "RETURN"),
(Opcode::MakeArray, "MAKE_ARRAY"),
(Opcode::MakeTuple, "MAKE_TUPLE"),
(Opcode::MakeStruct, "MAKE_STRUCT"),
(Opcode::MakeEnumVariant, "MAKE_ENUM_VARIANT"),
(Opcode::Index, "INDEX"),
(Opcode::Field, "FIELD"),
(Opcode::Len, "LEN"),
(Opcode::ToStr, "TO_STR"),
(Opcode::ConcatN, "CONCAT_N"),
(Opcode::MatchVariant, "MATCH_VARIANT"),
(Opcode::Halt, "HALT"),
];
assert_eq!(cases.len(), ALL.len(), "name table missing a variant");
for (op, expected) in cases {
assert_eq!(op.name(), *expected, "wrong name for {op:?}");
assert!(!op.name().is_empty(), "name() returned empty for {op:?}");
}
}
#[test]
fn name_is_unique_per_variant() {
let names: BTreeSet<&'static str> = ALL.iter().map(|op| op.name()).collect();
assert_eq!(
names.len(),
ALL.len(),
"duplicate names found: {names:?} (expected {} unique)",
ALL.len(),
);
}
#[test]
fn operand_bytes_matches_the_locked_table_per_variant() {
let cases: &[(Opcode, u8)] = &[
(Opcode::Const, 2),
(Opcode::Pop, 0),
(Opcode::Dup, 0),
(Opcode::GetLocal, 2),
(Opcode::SetLocal, 2),
(Opcode::GetGlobal, 2),
(Opcode::SetGlobal, 2),
(Opcode::Add, 0),
(Opcode::Sub, 0),
(Opcode::Mul, 0),
(Opcode::Div, 0),
(Opcode::Mod, 0),
(Opcode::Neg, 0),
(Opcode::FAdd, 0),
(Opcode::FSub, 0),
(Opcode::FMul, 0),
(Opcode::FDiv, 0),
(Opcode::FNeg, 0),
(Opcode::Eq, 0),
(Opcode::Ne, 0),
(Opcode::Lt, 0),
(Opcode::Le, 0),
(Opcode::Gt, 0),
(Opcode::Ge, 0),
(Opcode::FEq, 0),
(Opcode::FNe, 0),
(Opcode::FLt, 0),
(Opcode::FLe, 0),
(Opcode::FGt, 0),
(Opcode::FGe, 0),
(Opcode::Not, 0),
(Opcode::Jump, 2),
(Opcode::JumpIfFalse, 2),
(Opcode::JumpIfTrue, 2),
(Opcode::Call, 3),
(Opcode::Return, 0),
(Opcode::MakeArray, 2),
(Opcode::MakeTuple, 2),
(Opcode::MakeStruct, 2),
(Opcode::MakeEnumVariant, 3),
(Opcode::Index, 0),
(Opcode::Field, 2),
(Opcode::Len, 0),
(Opcode::ToStr, 0),
(Opcode::ConcatN, 2),
(Opcode::MatchVariant, 4),
(Opcode::Halt, 0),
];
assert_eq!(
cases.len(),
ALL.len(),
"operand_bytes table missing a variant"
);
for (op, expected) in cases {
assert_eq!(
op.operand_bytes(),
*expected,
"wrong operand_bytes for {op:?}",
);
}
}
#[test]
fn operand_bytes_is_bounded_by_four_across_every_variant() {
for op in ALL {
assert!(
op.operand_bytes() <= 4,
"{op:?} reports operand_bytes={} > 4",
op.operand_bytes(),
);
}
}
#[test]
fn operand_width_groups_partition_the_variant_set() {
for op in ZERO_OPERAND {
assert_eq!(
op.operand_bytes(),
0,
"{op:?} listed as zero-operand but reports {}",
op.operand_bytes(),
);
}
for op in TWO_OPERAND {
assert_eq!(
op.operand_bytes(),
2,
"{op:?} listed as two-operand but reports {}",
op.operand_bytes(),
);
}
for op in THREE_OPERAND {
assert_eq!(
op.operand_bytes(),
3,
"{op:?} listed as three-operand but reports {}",
op.operand_bytes(),
);
}
for op in FOUR_OPERAND {
assert_eq!(
op.operand_bytes(),
4,
"{op:?} listed as four-operand but reports {}",
op.operand_bytes(),
);
}
let total =
ZERO_OPERAND.len() + TWO_OPERAND.len() + THREE_OPERAND.len() + FOUR_OPERAND.len();
assert_eq!(
total,
ALL.len(),
"operand-width groups do not cover every variant",
);
let mut seen: BTreeSet<u8> = BTreeSet::new();
for op in ZERO_OPERAND
.iter()
.chain(TWO_OPERAND)
.chain(THREE_OPERAND)
.chain(FOUR_OPERAND)
{
assert!(
seen.insert(*op as u8),
"{op:?} appears in more than one operand-width group",
);
}
assert_eq!(seen.len(), ALL.len());
}
}