use core::{ fmt::{self, Display}, mem::transmute, }; use alloc::{borrow::ToOwned, string::String, vec::Vec}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] pub enum Opcode { // Push literal values onto the stack. Nil, False, True, Number, // (float: f32) Rgba, // (r: u8, g: u8, b: u8, a: u8) // Duplicate existing values. /// Push a value relative to the bottom of the current stack window. Local, // (index: u8) /// Set the value of a value relative to the bottom of the current stack window. SetLocal, // (index: u8) /// Push a captured value. Capture, // (index: u8) /// Get the value of a definition. Def, // (index: u16) /// Set the value of a definition. SetDef, // (index: u16) // Create lists. List, // (len: u16) // Create literal functions. Function, // (params: u8, then: u16), at `then`: (local_count: u8, capture_count: u8, captures: [(source: u8, index: u8); capture_count]) // Control flow. Jump, // (offset: u16) JumpIfNot, // (offset: u16) // Function calls. Call, // (argc: u8) /// This is a fast path for system calls, which are quite common (e.g. basic arithmetic.) System, // (index: u8, argc: u8) Return, // NOTE: There must be no more opcodes after this. // They will get treated as invalid. } // Constants used by the Function opcode to indicate capture sources. pub const CAPTURE_LOCAL: u8 = 0; pub const CAPTURE_CAPTURE: u8 = 1; #[derive(Debug, Clone)] pub struct Chunk { pub bytecode: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Offset(u16); impl Chunk { pub fn new(capacity: usize) -> Result { if capacity <= (1 << 16) { Ok(Chunk { bytecode: Vec::with_capacity(capacity), }) } else { Err(ChunkSizeError) } } pub fn offset(&self) -> Offset { Offset(self.bytecode.len() as u16) } pub fn emit_bytes(&mut self, bytes: &[u8]) -> Result { if self.bytecode.len() + bytes.len() > self.bytecode.capacity() { return Err(EmitError); } let offset = Offset(self.bytecode.len() as u16); self.bytecode.extend_from_slice(bytes); Ok(offset) } pub fn emit_opcode(&mut self, opcode: Opcode) -> Result { self.emit_bytes(&[opcode as u8]) } pub fn emit_u8(&mut self, x: u8) -> Result { self.emit_bytes(&[x]) } pub fn emit_u16(&mut self, x: u16) -> Result { self.emit_bytes(&x.to_le_bytes()) } pub fn emit_u32(&mut self, x: u32) -> Result { self.emit_bytes(&x.to_le_bytes()) } pub fn emit_f32(&mut self, x: f32) -> Result { self.emit_bytes(&x.to_le_bytes()) } pub fn patch_u8(&mut self, offset: Offset, x: u8) { self.bytecode[offset.0 as usize] = x; } pub fn patch_u16(&mut self, offset: Offset, x: u16) { let b = x.to_le_bytes(); let i = offset.0 as usize; self.bytecode[i] = b[0]; self.bytecode[i + 1] = b[1]; } pub fn patch_offset(&mut self, offset: Offset, x: Offset) { self.patch_u16(offset, x.0); } // NOTE: I'm aware these aren't the fastest implementations since they validate quite a lot // during runtime, but this is just an MVP. It doesn't have to be blazingly fast. pub fn read_u8(&self, pc: &mut usize) -> Result { let x = self.bytecode.get(*pc).copied(); *pc += 1; x.ok_or(ReadError) } pub fn read_u16(&self, pc: &mut usize) -> Result { let xs = &self.bytecode[*pc..*pc + 2]; *pc += 2; Ok(u16::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) } pub fn read_u32(&self, pc: &mut usize) -> Result { let xs = &self.bytecode[*pc..*pc + 4]; *pc += 4; Ok(u32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) } pub fn read_f32(&self, pc: &mut usize) -> Result { let xs = &self.bytecode[*pc..*pc + 4]; *pc += 4; Ok(f32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) } pub fn read_opcode(&self, pc: &mut usize) -> Result { let x = self.read_u8(pc)?; if x <= Opcode::Return as u8 { Ok(unsafe { transmute::(x) }) } else { Err(ReadError) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChunkSizeError; impl Display for ChunkSizeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "chunk size must be less than 64 KiB") } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EmitError; impl Display for EmitError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "out of space in chunk") } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ReadError; impl Display for ReadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "invalid bytecode: out of bounds read or invalid opcode") } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct DefId(u16); impl DefId { pub fn to_u16(self) -> u16 { self.0 } } #[derive(Debug, Clone)] pub struct Defs { defs: Vec, } #[derive(Debug, Clone, Copy)] pub struct DefsImage { defs: usize, } impl Defs { pub fn new(capacity: usize) -> Self { assert!(capacity < u16::MAX as usize + 1); Self { defs: Vec::with_capacity(capacity), } } pub fn len(&self) -> u16 { self.defs.len() as u16 } pub fn is_empty(&self) -> bool { self.len() != 0 } pub fn get(&mut self, name: &str) -> Option { self.defs .iter() .position(|n| *n == name) .map(|index| DefId(index as u16)) } pub fn add(&mut self, name: &str) -> Result { if self.defs.iter().any(|n| n == name) { Err(DefError::Exists) } else { if self.defs.len() >= self.defs.capacity() { return Err(DefError::OutOfSpace); } let id = DefId(self.defs.len() as u16); self.defs.push(name.to_owned()); Ok(id) } } pub fn image(&self) -> DefsImage { DefsImage { defs: self.defs.len(), } } pub fn restore_image(&mut self, image: &DefsImage) { self.defs.resize_with(image.defs, || { panic!("image must be a subset of the current defs") }); } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DefError { Exists, OutOfSpace, } impl Display for DefError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { DefError::Exists => "definition already exists", DefError::OutOfSpace => "too many definitions", }) } }