//! Bytecode instruction set for the Engram virtual machine. //! //! The VM is a simple stack machine. Every instruction pops its operands //! from the stack and pushes its result. Control flow uses relative signed //! offsets from the instruction *after* the jump. use serde::{Deserialize, Serialize}; /// A runtime value on the VM stack. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Value { Int(i64), Float(f64), Str(String), Bool(bool), Nil, /// A list of values (used for `activate` results and array literals). List(Vec), /// A struct instance: type name + ordered field name-value pairs. Struct { type_name: String, fields: Vec<(String, Value)> }, } impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Value::Int(n) => write!(f, "{n}"), Value::Float(n) => write!(f, "{n}"), Value::Str(s) => write!(f, "{s}"), Value::Bool(b) => write!(f, "{b}"), Value::Nil => write!(f, "nil"), Value::List(vs) => { let items: Vec<_> = vs.iter().map(|v| v.to_string()).collect(); write!(f, "[{}]", items.join(", ")) } Value::Struct { type_name, fields } => { let fs: Vec<_> = fields.iter().map(|(k, v)| format!("{k}: {v}")).collect(); write!(f, "{type_name} {{ {} }}", fs.join(", ")) } } } } /// A single VM instruction. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Bytecode { // ── Stack ───────────────────────────────────────────────────────────────── /// Push a constant value onto the stack. Push(Value), /// Discard the top of stack. Pop, /// Duplicate the top of stack. Dup, // ── Arithmetic ──────────────────────────────────────────────────────────── Add, Sub, Mul, Div, // ── Comparison ──────────────────────────────────────────────────────────── Eq, NotEq, Lt, Gt, LtEq, GtEq, // ── Logical ─────────────────────────────────────────────────────────────── And, Or, Not, // ── Locals ─────────────────────────────────────────────────────────────── /// Load a local variable by name. LoadLocal(String), /// Store the top of stack into a local variable. StoreLocal(String), // ── Functions ───────────────────────────────────────────────────────────── /// Call a function by name with `arity` arguments. Call { name: String, arity: u32 }, /// Return from the current function (leaves return value on stack). Return, // ── Control flow ────────────────────────────────────────────────────────── /// Unconditional jump: `ip += offset` (offset is from the *next* instruction). Jump(i32), /// Jump if the top of stack is truthy; pops the value. JumpIf(i32), /// Jump if the top of stack is falsy; pops the value. JumpIfNot(i32), // ── Fields & Indexing ───────────────────────────────────────────────────── /// Load a named field from the struct on top of stack. GetField(String), /// Index into an array: pops index then array. GetIndex, // ── Special ─────────────────────────────────────────────────────────────── /// `activate TypeName "query"` — emit a semantic query stub. /// In a full implementation this would call into the Engram runtime. Activate { type_name: String, query: String }, /// Mark the start of a sealed section (the runtime enforces protection). SealedBegin, /// Mark the end of a sealed section. SealedEnd, /// Build a struct value: pops `fields.len()` values from the stack (in /// order), combines them with the field names, and pushes a `Struct` value. BuildStruct { type_name: String, fields: Vec }, /// No-op — used as a placeholder for forward jumps. Nop, /// Halt the VM. Halt, } impl std::fmt::Display for Bytecode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Bytecode::Push(v) => write!(f, "PUSH {v}"), Bytecode::Pop => write!(f, "POP"), Bytecode::Dup => write!(f, "DUP"), Bytecode::Add => write!(f, "ADD"), Bytecode::Sub => write!(f, "SUB"), Bytecode::Mul => write!(f, "MUL"), Bytecode::Div => write!(f, "DIV"), Bytecode::Eq => write!(f, "EQ"), Bytecode::NotEq => write!(f, "NEQ"), Bytecode::Lt => write!(f, "LT"), Bytecode::Gt => write!(f, "GT"), Bytecode::LtEq => write!(f, "LTE"), Bytecode::GtEq => write!(f, "GTE"), Bytecode::And => write!(f, "AND"), Bytecode::Or => write!(f, "OR"), Bytecode::Not => write!(f, "NOT"), Bytecode::LoadLocal(n) => write!(f, "LOAD {n}"), Bytecode::StoreLocal(n) => write!(f, "STORE {n}"), Bytecode::Call { name, arity } => write!(f, "CALL {name}/{arity}"), Bytecode::Return => write!(f, "RETURN"), Bytecode::Jump(off) => write!(f, "JUMP {off:+}"), Bytecode::JumpIf(off) => write!(f, "JUMPIF {off:+}"), Bytecode::JumpIfNot(off) => write!(f, "JUMPIFNOT {off:+}"), Bytecode::GetField(n) => write!(f, "GETFIELD {n}"), Bytecode::GetIndex => write!(f, "GETINDEX"), Bytecode::Activate { type_name, query } => { write!(f, "ACTIVATE {type_name} \"{query}\"") } Bytecode::SealedBegin => write!(f, "SEALED_BEGIN"), Bytecode::SealedEnd => write!(f, "SEALED_END"), Bytecode::BuildStruct { type_name, fields } => { write!(f, "BUILD_STRUCT {type_name}({})", fields.join(", ")) } Bytecode::Nop => write!(f, "NOP"), Bytecode::Halt => write!(f, "HALT"), } } } /// Serialize bytecode instructions to bytes for storage/sealing. pub fn serialize_bytecode(instructions: &[Bytecode]) -> Result, String> { serde_json::to_vec(instructions).map_err(|e| e.to_string()) } /// Deserialize bytecode instructions from bytes. pub fn deserialize_bytecode(bytes: &[u8]) -> Result, String> { serde_json::from_slice(bytes).map_err(|e| e.to_string()) } impl Bytecode { /// Deserialize a bytecode slice from JSON bytes (convenience wrapper). pub fn deserialize_all(bytes: &[u8]) -> Result, String> { deserialize_bytecode(bytes) } }