use crate::chunk::Chunk;
use crate::chunk::Program;
use crate::errors::QalaError;
use crate::opcode::{Opcode, STDLIB_FN_BASE};
use crate::span::LineIndex;
use crate::span::Span;
use crate::value::ConstValue;
use crate::value::Value;
const MAX_FRAMES: usize = 1024;
const MAX_STACK: usize = 65536;
const MAX_HEAP: usize = 1_000_000;
const MAX_DISPLAY_DEPTH: u32 = 64;
#[derive(Clone, PartialEq)]
pub enum HeapObject {
Int(i64),
Array(Vec<Value>),
Tuple(Vec<Value>),
Str(String),
Struct {
type_name: String,
fields: Vec<Value>,
},
EnumVariant {
type_name: String,
variant: String,
payload: Vec<Value>,
},
FileHandle {
path: String,
content: String,
closed: bool,
},
}
#[derive(Clone)]
struct HeapSlot {
object: HeapObject,
refcount: u32,
}
#[derive(Clone, Default)]
pub struct Heap {
objects: Vec<HeapSlot>,
free: Vec<u32>,
}
impl Heap {
pub fn new() -> Self {
Self::default()
}
pub fn alloc(&mut self, obj: HeapObject) -> Option<u32> {
if let Some(slot) = self.free.pop() {
let idx = slot as usize;
if let Some(s) = self.objects.get_mut(idx) {
s.object = obj;
s.refcount = 1;
return Some(slot);
}
debug_assert!(
false,
"heap free-list contained out-of-range index {slot}; slab len={}",
self.objects.len()
);
self.free.push(slot);
}
if self.objects.len() >= MAX_HEAP {
return None;
}
let idx = self.objects.len() as u32;
self.objects.push(HeapSlot {
object: obj,
refcount: 1,
});
Some(idx)
}
pub fn get(&self, slot: u32) -> Option<&HeapObject> {
self.objects
.get(slot as usize)
.filter(|s| s.refcount > 0)
.map(|s| &s.object)
}
pub fn get_mut(&mut self, slot: u32) -> Option<&mut HeapObject> {
self.objects
.get_mut(slot as usize)
.filter(|s| s.refcount > 0)
.map(|s| &mut s.object)
}
#[allow(dead_code)]
pub fn inc(&mut self, slot: u32) {
if let Some(s) = self
.objects
.get_mut(slot as usize)
.filter(|s| s.refcount > 0)
{
s.refcount = s.refcount.saturating_add(1);
}
}
pub fn dec(&mut self, slot: u32) -> Option<HeapObject> {
let s = self.objects.get_mut(slot as usize)?;
if s.refcount == 0 {
return None;
}
s.refcount -= 1;
if s.refcount == 0 {
let freed = std::mem::replace(&mut s.object, HeapObject::Int(0));
self.free.push(slot);
Some(freed)
} else {
None
}
}
}
#[derive(Clone)]
pub struct CallFrame {
pub chunk_idx: usize,
pub ip: usize,
pub base: usize,
pub locals: Vec<Value>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StateValue {
pub rendered: String,
pub type_name: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct NamedValue {
pub name: String,
pub value: StateValue,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct VmState {
pub chunk_index: usize,
pub ip: usize,
pub current_line: usize,
pub stack: Vec<StateValue>,
pub variables: Vec<NamedValue>,
pub console: Vec<String>,
pub leak_log: Vec<String>,
}
pub struct Vm {
program: Program,
stack: Vec<Value>,
frames: Vec<CallFrame>,
pub(crate) heap: Heap,
pub(crate) console: Vec<String>,
pub(crate) leak_log: Vec<String>,
globals: Vec<Value>,
src: String,
repl_history: Vec<String>,
}
impl Vm {
pub fn new(program: Program, src: String) -> Vm {
let main_index = program.main_index;
let frames = vec![CallFrame {
chunk_idx: main_index,
ip: 0,
base: 0,
locals: Vec::new(),
}];
Vm {
program,
stack: Vec::new(),
frames,
heap: Heap::new(),
console: Vec::new(),
leak_log: Vec::new(),
globals: Vec::new(),
src,
repl_history: Vec::new(),
}
}
pub fn new_repl() -> Vm {
Vm {
program: Program::new(),
stack: Vec::new(),
frames: Vec::new(),
heap: Heap::new(),
console: Vec::new(),
leak_log: Vec::new(),
globals: Vec::new(),
src: String::new(),
repl_history: Vec::new(),
}
}
fn frame(&self) -> Result<&CallFrame, QalaError> {
self.frames.last().ok_or_else(|| QalaError::Runtime {
span: Span::new(0, 0),
message: "no active call frame".to_string(),
})
}
fn frame_mut(&mut self) -> Result<&mut CallFrame, QalaError> {
self.frames.last_mut().ok_or_else(|| QalaError::Runtime {
span: Span::new(0, 0),
message: "no active call frame".to_string(),
})
}
fn chunk(&self) -> Result<&Chunk, QalaError> {
let idx = self.frame()?.chunk_idx;
self.program
.chunks
.get(idx)
.ok_or_else(|| QalaError::Runtime {
span: Span::new(0, 0),
message: format!("call frame references missing chunk {idx}"),
})
}
fn push(&mut self, v: Value) -> Result<(), QalaError> {
if self.stack.len() >= MAX_STACK {
return Err(self.runtime_err("value stack overflow"));
}
self.stack.push(v);
Ok(())
}
fn pop(&mut self) -> Result<Value, QalaError> {
self.stack
.pop()
.ok_or_else(|| self.runtime_err("stack underflow"))
}
pub(crate) fn runtime_err(&self, message: &str) -> QalaError {
let span = self.error_span();
QalaError::Runtime {
span,
message: message.to_string(),
}
}
fn error_span(&self) -> Span {
let line = self
.frame()
.ok()
.and_then(|f| {
let ip = f.ip;
self.chunk()
.ok()
.and_then(|c| c.source_lines.get(ip).copied())
})
.unwrap_or(0);
if line == 0 {
return Span::new(0, 0);
}
let index = LineIndex::new(&self.src);
line_span(&index, &self.src, line)
}
fn dispatch_one(&mut self) -> Result<StepOutcome, QalaError> {
let ip = self.frame()?.ip;
let code_len = self.chunk()?.code.len();
if ip >= code_len {
return Err(self.runtime_err("instruction pointer past end of chunk"));
}
let byte = self.chunk()?.code[ip];
let op = Opcode::from_u8(byte)
.ok_or_else(|| self.runtime_err(&format!("bad opcode byte {byte:#x}")))?;
let operand_len = op.operand_bytes() as usize;
if ip + 1 + operand_len > code_len {
return Err(self.runtime_err("truncated operand"));
}
let next = ip + 1 + operand_len;
self.frame_mut()?.ip = next;
self.run_opcode(op, ip, next)
}
fn run_opcode(
&mut self,
op: Opcode,
opcode_pos: usize,
next: usize,
) -> Result<StepOutcome, QalaError> {
match op {
Opcode::Const => {
let idx = self.read_operand_u16(opcode_pos)?;
self.op_const(idx)?;
}
Opcode::Pop => {
self.pop()?;
}
Opcode::Dup => {
let top = *self
.stack
.last()
.ok_or_else(|| self.runtime_err("stack underflow"))?;
self.push(top)?;
}
Opcode::GetLocal => {
let slot = self.read_operand_u16(opcode_pos)? as usize;
let v = *self
.frame()?
.locals
.get(slot)
.ok_or_else(|| self.runtime_err(&format!("bad local slot {slot}")))?;
self.push(v)?;
}
Opcode::SetLocal => {
let slot = self.read_operand_u16(opcode_pos)? as usize;
let v = self.pop()?;
let locals = &mut self.frame_mut()?.locals;
if slot >= locals.len() {
locals.resize(slot + 1, Value::void());
}
locals[slot] = v;
}
Opcode::GetGlobal => {
let idx = self.read_operand_u16(opcode_pos)? as usize;
let v = *self
.globals
.get(idx)
.ok_or_else(|| self.runtime_err(&format!("bad global slot {idx}")))?;
self.push(v)?;
}
Opcode::SetGlobal => {
let idx = self.read_operand_u16(opcode_pos)? as usize;
let v = self.pop()?;
if idx >= self.globals.len() {
self.globals.resize(idx + 1, Value::void());
}
self.globals[idx] = v;
}
Opcode::Add => self.op_arith_i64(IntOp::Add)?,
Opcode::Sub => self.op_arith_i64(IntOp::Sub)?,
Opcode::Mul => self.op_arith_i64(IntOp::Mul)?,
Opcode::Div => self.op_arith_i64(IntOp::Div)?,
Opcode::Mod => self.op_arith_i64(IntOp::Mod)?,
Opcode::Neg => {
let n = self.pop_i64()?;
let r = n
.checked_neg()
.ok_or_else(|| self.runtime_err("integer overflow"))?;
self.push_i64(r)?;
}
Opcode::FAdd => self.op_arith_f64(FloatOp::Add)?,
Opcode::FSub => self.op_arith_f64(FloatOp::Sub)?,
Opcode::FMul => self.op_arith_f64(FloatOp::Mul)?,
Opcode::FDiv => self.op_arith_f64(FloatOp::Div)?,
Opcode::FNeg => {
let x = self.pop_f64()?;
self.push(Value::from_f64(-x))?;
}
Opcode::Eq => self.op_compare(CmpOp::Eq)?,
Opcode::Ne => self.op_compare(CmpOp::Ne)?,
Opcode::Lt => self.op_compare(CmpOp::Lt)?,
Opcode::Le => self.op_compare(CmpOp::Le)?,
Opcode::Gt => self.op_compare(CmpOp::Gt)?,
Opcode::Ge => self.op_compare(CmpOp::Ge)?,
Opcode::FEq => self.op_compare_f64(CmpOp::Eq)?,
Opcode::FNe => self.op_compare_f64(CmpOp::Ne)?,
Opcode::FLt => self.op_compare_f64(CmpOp::Lt)?,
Opcode::FLe => self.op_compare_f64(CmpOp::Le)?,
Opcode::FGt => self.op_compare_f64(CmpOp::Gt)?,
Opcode::FGe => self.op_compare_f64(CmpOp::Ge)?,
Opcode::Not => {
let b = self.pop_bool()?;
self.push(Value::bool(!b))?;
}
Opcode::Jump => {
let offset = self.read_operand_i16(opcode_pos)?;
self.do_jump(next, offset)?;
}
Opcode::JumpIfFalse => {
let offset = self.read_operand_i16(opcode_pos)?;
if !self.pop_bool()? {
self.do_jump(next, offset)?;
}
}
Opcode::JumpIfTrue => {
let offset = self.read_operand_i16(opcode_pos)?;
if self.pop_bool()? {
self.do_jump(next, offset)?;
}
}
Opcode::Call => {
let fn_id = self.read_operand_u16(opcode_pos)?;
let argc = self.read_operand_u8(opcode_pos)?;
self.op_call(fn_id, argc)?;
}
Opcode::Return => {
if self.op_return()? {
return Ok(StepOutcome::Halted);
}
}
Opcode::MakeArray => {
let count = self.read_operand_u16(opcode_pos)?;
self.op_make_collection(count, false)?;
}
Opcode::MakeTuple => {
let count = self.read_operand_u16(opcode_pos)?;
self.op_make_collection(count, true)?;
}
Opcode::MakeStruct => {
let struct_id = self.read_operand_u16(opcode_pos)?;
self.op_make_struct(struct_id)?;
}
Opcode::MakeEnumVariant => {
let variant_id = self.read_operand_u16(opcode_pos)?;
let payload_count = self.read_operand_u8(opcode_pos)?;
self.op_make_enum_variant(variant_id, payload_count)?;
}
Opcode::Index => self.op_index()?,
Opcode::Field => {
let field_index = self.read_operand_u16(opcode_pos)?;
self.op_field(field_index)?;
}
Opcode::Len => self.op_len()?,
Opcode::ToStr => self.op_to_str()?,
Opcode::ConcatN => {
let count = self.read_operand_u16(opcode_pos)?;
self.op_concat_n(count)?;
}
Opcode::MatchVariant => {
let variant_id = self.read_operand_u16(opcode_pos)?;
let offset = self.read_match_variant_offset(opcode_pos)?;
self.op_match_variant(variant_id, offset, next)?;
}
Opcode::Halt => return Ok(StepOutcome::Halted),
}
let _ = next;
Ok(StepOutcome::Ran)
}
fn read_operand_u16(&self, opcode_pos: usize) -> Result<u16, QalaError> {
let chunk = self.chunk()?;
let off = opcode_pos + 1;
if off + 2 > chunk.code.len() {
return Err(self.runtime_err("truncated operand"));
}
Ok(chunk.read_u16(off))
}
fn read_operand_u8(&self, opcode_pos: usize) -> Result<u8, QalaError> {
let chunk = self.chunk()?;
let off = opcode_pos + 3;
chunk
.code
.get(off)
.copied()
.ok_or_else(|| self.runtime_err("truncated operand"))
}
fn read_match_variant_offset(&self, opcode_pos: usize) -> Result<i16, QalaError> {
let chunk = self.chunk()?;
let off = opcode_pos + 3;
if off + 2 > chunk.code.len() {
return Err(self.runtime_err("truncated operand"));
}
Ok(chunk.read_i16(off))
}
fn op_const(&mut self, idx: u16) -> Result<(), QalaError> {
let constant = self
.chunk()?
.constants
.get(idx as usize)
.cloned()
.ok_or_else(|| self.runtime_err(&format!("bad constant index {idx}")))?;
let value = match constant {
ConstValue::I64(n) => {
let slot = self
.heap
.alloc(HeapObject::Int(n))
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
Value::pointer(slot)
}
ConstValue::F64(x) => Value::from_f64(x),
ConstValue::Bool(b) => Value::bool(b),
ConstValue::Byte(b) => Value::byte(b),
ConstValue::Void => Value::void(),
ConstValue::Str(s) => {
let slot = self
.heap
.alloc(HeapObject::Str(s))
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
Value::pointer(slot)
}
ConstValue::Function(id) => Value::function(id),
};
self.push(value)
}
fn pop_i64(&mut self) -> Result<i64, QalaError> {
let v = self.pop()?;
let slot = v
.as_pointer()
.ok_or_else(|| self.runtime_err("expected an integer"))?;
match self.heap.get(slot) {
Some(HeapObject::Int(n)) => Ok(*n),
_ => Err(self.runtime_err("expected an integer")),
}
}
fn pop_f64(&mut self) -> Result<f64, QalaError> {
let v = self.pop()?;
v.as_f64()
.ok_or_else(|| self.runtime_err("expected a float"))
}
fn pop_bool(&mut self) -> Result<bool, QalaError> {
let v = self.pop()?;
v.as_bool()
.ok_or_else(|| self.runtime_err("expected a boolean"))
}
#[allow(dead_code)]
fn pop_str(&mut self) -> Result<String, QalaError> {
let v = self.pop()?;
let slot = v
.as_pointer()
.ok_or_else(|| self.runtime_err("expected a string"))?;
match self.heap.get(slot) {
Some(HeapObject::Str(s)) => Ok(s.clone()),
_ => Err(self.runtime_err("expected a string")),
}
}
fn push_i64(&mut self, n: i64) -> Result<(), QalaError> {
let slot = self
.heap
.alloc(HeapObject::Int(n))
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn read_operand_i16(&self, opcode_pos: usize) -> Result<i16, QalaError> {
let chunk = self.chunk()?;
let off = opcode_pos + 1;
if off + 2 > chunk.code.len() {
return Err(self.runtime_err("truncated operand"));
}
Ok(chunk.read_i16(off))
}
fn op_arith_i64(&mut self, op: IntOp) -> Result<(), QalaError> {
let rhs = self.pop_i64()?;
let lhs = self.pop_i64()?;
let result = match op {
IntOp::Add => lhs
.checked_add(rhs)
.ok_or_else(|| self.runtime_err("integer overflow"))?,
IntOp::Sub => lhs
.checked_sub(rhs)
.ok_or_else(|| self.runtime_err("integer overflow"))?,
IntOp::Mul => lhs
.checked_mul(rhs)
.ok_or_else(|| self.runtime_err("integer overflow"))?,
IntOp::Div => lhs
.checked_div(rhs)
.ok_or_else(|| self.runtime_err("division by zero"))?,
IntOp::Mod => lhs
.checked_rem(rhs)
.ok_or_else(|| self.runtime_err("modulo by zero"))?,
};
self.push_i64(result)
}
fn op_arith_f64(&mut self, op: FloatOp) -> Result<(), QalaError> {
let rhs = self.pop_f64()?;
let lhs = self.pop_f64()?;
let result = match op {
FloatOp::Add => lhs + rhs,
FloatOp::Sub => lhs - rhs,
FloatOp::Mul => lhs * rhs,
FloatOp::Div => lhs / rhs,
};
self.push(Value::from_f64(result))
}
fn op_compare(&mut self, op: CmpOp) -> Result<(), QalaError> {
let rhs = self.pop()?;
let lhs = self.pop()?;
let ordering = self.compare_values(lhs, rhs)?;
self.push(Value::bool(op.holds(ordering)))
}
fn compare_values(&self, lhs: Value, rhs: Value) -> Result<std::cmp::Ordering, QalaError> {
if let (Some(a), Some(b)) = (lhs.as_bool(), rhs.as_bool()) {
return Ok(a.cmp(&b));
}
match (lhs.as_pointer(), rhs.as_pointer()) {
(Some(a), Some(b)) => match (self.heap.get(a), self.heap.get(b)) {
(Some(HeapObject::Int(x)), Some(HeapObject::Int(y))) => Ok(x.cmp(y)),
(Some(HeapObject::Str(x)), Some(HeapObject::Str(y))) => Ok(x.cmp(y)),
_ => Err(self.runtime_err("cannot compare values of different types")),
},
_ => Err(self.runtime_err("cannot compare values of different types")),
}
}
fn op_compare_f64(&mut self, op: CmpOp) -> Result<(), QalaError> {
let rhs = self.pop_f64()?;
let lhs = self.pop_f64()?;
let result = match op {
CmpOp::Eq => lhs == rhs,
CmpOp::Ne => lhs != rhs,
CmpOp::Lt => lhs < rhs,
CmpOp::Le => lhs <= rhs,
CmpOp::Gt => lhs > rhs,
CmpOp::Ge => lhs >= rhs,
};
self.push(Value::bool(result))
}
fn do_jump(&mut self, fall_through: usize, offset: i16) -> Result<(), QalaError> {
let code_len = self.chunk()?.code.len();
let target = fall_through as isize + offset as isize;
if target < 0 || target as usize > code_len {
return Err(self.runtime_err("jump target out of range"));
}
self.frame_mut()?.ip = target as usize;
Ok(())
}
fn op_call(&mut self, fn_id: u16, argc: u8) -> Result<(), QalaError> {
if fn_id >= STDLIB_FN_BASE {
return self.call_stdlib(fn_id, argc);
}
if self.frames.len() >= MAX_FRAMES {
return Err(self.runtime_err("stack overflow"));
}
let chunk_idx = fn_id as usize;
if self.program.chunks.get(chunk_idx).is_none() {
return Err(self.runtime_err(&format!("call to missing function {fn_id}")));
}
let argc = argc as usize;
if self.stack.len() < argc {
return Err(self.runtime_err("stack underflow building a call frame"));
}
let base = self.stack.len() - argc;
let locals = self.stack.split_off(base);
self.frames.push(CallFrame {
chunk_idx,
ip: 0,
base,
locals,
});
Ok(())
}
fn call_stdlib(&mut self, fn_id: u16, argc: u8) -> Result<(), QalaError> {
let argc = argc as usize;
if self.stack.len() < argc {
return Err(self.runtime_err("stack underflow building a stdlib call"));
}
let at = self.stack.len() - argc;
let args = self.stack.split_off(at);
let result = crate::stdlib::dispatch(self, fn_id, &args)?;
self.push(result)
}
fn op_return(&mut self) -> Result<bool, QalaError> {
let frame = self
.frames
.pop()
.ok_or_else(|| self.runtime_err("return with no active call frame"))?;
let result = if self.stack.len() > frame.base {
self.stack.pop().unwrap_or_else(Value::void)
} else {
Value::void()
};
self.check_frame_handle_leaks(&frame.locals, result);
if frame.base <= self.stack.len() {
self.stack.truncate(frame.base);
}
if self.frames.is_empty() {
self.stack.push(result);
return Ok(true);
}
self.push(result)?;
Ok(false)
}
fn check_frame_handle_leaks(&mut self, locals: &[Value], returned: Value) {
for &local in locals {
let Some(slot) = local.as_pointer() else {
continue;
};
if local == returned {
continue;
}
let is_open_handle = matches!(
self.heap.get(slot),
Some(HeapObject::FileHandle { closed: false, .. })
);
if !is_open_handle {
continue;
}
if let Some(HeapObject::FileHandle {
path,
closed: false,
..
}) = self.heap.dec(slot)
{
self.leak_log
.push(format!("file handle for {path} dropped without close"));
}
}
}
pub(crate) fn call_function_value(
&mut self,
callable: Value,
args: &[Value],
) -> Result<Value, QalaError> {
let fn_id = callable
.as_function()
.ok_or_else(|| self.runtime_err("value is not callable"))?;
if fn_id >= STDLIB_FN_BASE {
return Err(self.runtime_err("a stdlib function cannot be used as a callback in v1"));
}
let chunk_idx = fn_id as usize;
if self.program.chunks.get(chunk_idx).is_none() {
return Err(self.runtime_err(&format!("call to missing function {fn_id}")));
}
if self.frames.len() >= MAX_FRAMES {
return Err(self.runtime_err("stack overflow"));
}
let depth = self.frames.len();
for arg in args {
self.push(*arg)?;
}
let base = self.stack.len() - args.len();
let locals = self.stack.split_off(base);
self.frames.push(CallFrame {
chunk_idx,
ip: 0,
base,
locals,
});
loop {
if self.frames.len() == depth {
break;
}
match self.dispatch_one()? {
StepOutcome::Ran => {}
StepOutcome::Halted => {
if self.frames.len() == depth {
break;
}
return Err(self.runtime_err("callback halted without returning"));
}
}
}
self.pop()
}
fn pop_n(&mut self, n: usize) -> Result<Vec<Value>, QalaError> {
if self.stack.len() < n {
return Err(self.runtime_err("stack underflow building a heap object"));
}
let at = self.stack.len() - n;
Ok(self.stack.split_off(at))
}
fn op_make_collection(&mut self, count: u16, tuple: bool) -> Result<(), QalaError> {
let elements = self.pop_n(count as usize)?;
let object = if tuple {
HeapObject::Tuple(elements)
} else {
HeapObject::Array(elements)
};
let slot = self
.heap
.alloc(object)
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn op_make_struct(&mut self, struct_id: u16) -> Result<(), QalaError> {
let info = self
.program
.structs
.get(struct_id as usize)
.ok_or_else(|| self.runtime_err(&format!("bad struct id {struct_id}")))?;
let type_name = info.name.clone();
let field_count = info.field_count as usize;
let fields = self.pop_n(field_count)?;
let slot = self
.heap
.alloc(HeapObject::Struct { type_name, fields })
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn op_make_enum_variant(
&mut self,
variant_id: u16,
payload_count: u8,
) -> Result<(), QalaError> {
let (enum_name, variant_name) = self
.program
.enum_variant_names
.get(variant_id as usize)
.ok_or_else(|| self.runtime_err(&format!("bad variant id {variant_id}")))?;
let type_name = enum_name.clone();
let variant = variant_name.clone();
let payload = self.pop_n(payload_count as usize)?;
let slot = self
.heap
.alloc(HeapObject::EnumVariant {
type_name,
variant,
payload,
})
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn op_index(&mut self) -> Result<(), QalaError> {
let index = self.pop_i64()?;
let collection = self.pop()?;
let slot = collection
.as_pointer()
.ok_or_else(|| self.runtime_err("expected an array"))?;
let element = match self.heap.get(slot) {
Some(HeapObject::Array(items)) | Some(HeapObject::Tuple(items)) => {
let len = items.len();
if index < 0 || index as usize >= len {
return Err(self.runtime_err(&format!(
"array index {index} out of bounds for length {len}"
)));
}
items[index as usize]
}
_ => return Err(self.runtime_err("expected an array")),
};
self.push(element)
}
fn op_field(&mut self, field_index: u16) -> Result<(), QalaError> {
let target = self.pop()?;
let slot = target
.as_pointer()
.ok_or_else(|| self.runtime_err("expected a struct"))?;
let field = match self.heap.get(slot) {
Some(HeapObject::Struct { fields, .. }) => fields
.get(field_index as usize)
.copied()
.ok_or_else(|| self.runtime_err(&format!("bad field index {field_index}")))?,
_ => return Err(self.runtime_err("expected a struct")),
};
self.push(field)
}
fn op_len(&mut self) -> Result<(), QalaError> {
let value = self.pop()?;
let slot = value
.as_pointer()
.ok_or_else(|| self.runtime_err("expected an array or string"))?;
let len = match self.heap.get(slot) {
Some(HeapObject::Array(items)) | Some(HeapObject::Tuple(items)) => items.len(),
Some(HeapObject::Str(s)) => s.chars().count(),
_ => return Err(self.runtime_err("expected an array or string")),
};
self.push_i64(len as i64)
}
pub(crate) fn value_to_string(&self, v: Value) -> String {
self.value_to_string_depth(v, 0)
}
fn value_to_string_depth(&self, v: Value, depth: u32) -> String {
if depth > MAX_DISPLAY_DEPTH {
return "<...>".to_string();
}
if let Some(b) = v.as_bool() {
return if b {
"true".to_string()
} else {
"false".to_string()
};
}
if let Some(b) = v.as_byte() {
return b.to_string();
}
if v.as_void() {
return "()".to_string();
}
if let Some(id) = v.as_function() {
return format!("fn#{id}");
}
if let Some(slot) = v.as_pointer() {
return match self.heap.get(slot) {
Some(HeapObject::Int(n)) => n.to_string(),
Some(HeapObject::Str(s)) => s.clone(),
Some(HeapObject::Array(items)) => {
let parts: Vec<String> = items
.iter()
.map(|e| self.value_to_string_depth(*e, depth + 1))
.collect();
format!("[{}]", parts.join(", "))
}
Some(HeapObject::Tuple(items)) => {
let parts: Vec<String> = items
.iter()
.map(|e| self.value_to_string_depth(*e, depth + 1))
.collect();
format!("({})", parts.join(", "))
}
Some(HeapObject::Struct { type_name, fields }) => {
let parts: Vec<String> = fields
.iter()
.map(|e| self.value_to_string_depth(*e, depth + 1))
.collect();
format!("{type_name} {{ {} }}", parts.join(", "))
}
Some(HeapObject::EnumVariant {
type_name,
variant,
payload,
}) => {
if payload.is_empty() {
format!("{type_name}::{variant}")
} else {
let parts: Vec<String> = payload
.iter()
.map(|e| self.value_to_string_depth(*e, depth + 1))
.collect();
format!("{type_name}::{variant}({})", parts.join(", "))
}
}
Some(HeapObject::FileHandle { path, .. }) => format!("<file \"{path}\">"),
None => "<dangling>".to_string(),
};
}
match v.as_f64() {
Some(x) if x.is_nan() => "NaN".to_string(),
Some(x) if x == f64::INFINITY => "inf".to_string(),
Some(x) if x == f64::NEG_INFINITY => "-inf".to_string(),
Some(x) => format!("{x}"),
None => "<unknown>".to_string(),
}
}
pub(crate) fn runtime_type_name(&self, v: Value) -> String {
self.runtime_type_name_depth(v, 0)
}
fn runtime_type_name_depth(&self, v: Value, depth: u32) -> String {
if depth > MAX_DISPLAY_DEPTH {
return "...".to_string();
}
if v.as_bool().is_some() {
return "bool".to_string();
}
if v.as_byte().is_some() {
return "byte".to_string();
}
if v.as_void() {
return "void".to_string();
}
if v.as_function().is_some() {
return "fn".to_string();
}
if let Some(slot) = v.as_pointer() {
return match self.heap.get(slot) {
Some(HeapObject::Int(_)) => "i64".to_string(),
Some(HeapObject::Str(_)) => "str".to_string(),
Some(HeapObject::Array(items)) => match items.first() {
Some(first) => {
format!("[{}]", self.runtime_type_name_depth(*first, depth + 1))
}
None => "[]".to_string(),
},
Some(HeapObject::Tuple(items)) => {
let parts: Vec<String> = items
.iter()
.map(|e| self.runtime_type_name_depth(*e, depth + 1))
.collect();
format!("({})", parts.join(", "))
}
Some(HeapObject::Struct { type_name, .. }) => type_name.clone(),
Some(HeapObject::EnumVariant {
type_name, variant, ..
}) => format!("{type_name}::{variant}"),
Some(HeapObject::FileHandle { .. }) => "FileHandle".to_string(),
None => "?".to_string(),
};
}
"f64".to_string()
}
pub fn render_value(&self, v: Value) -> (String, String) {
(self.value_to_string(v), self.runtime_type_name(v))
}
fn op_to_str(&mut self) -> Result<(), QalaError> {
let v = self.pop()?;
let s = self.value_to_string(v);
let slot = self
.heap
.alloc(HeapObject::Str(s))
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn op_concat_n(&mut self, count: u16) -> Result<(), QalaError> {
let parts = self.pop_n(count as usize)?;
let mut joined = String::new();
for part in parts {
joined.push_str(&self.value_to_string(part));
}
let slot = self
.heap
.alloc(HeapObject::Str(joined))
.ok_or_else(|| self.runtime_err("heap exhausted"))?;
self.push(Value::pointer(slot))
}
fn op_match_variant(
&mut self,
variant_id: u16,
offset: i16,
fall_through: usize,
) -> Result<(), QalaError> {
let scrutinee = *self
.stack
.last()
.ok_or_else(|| self.runtime_err("stack underflow at a match"))?;
let slot = scrutinee
.as_pointer()
.ok_or_else(|| self.runtime_err("match scrutinee is not an enum value"))?;
let (want_enum, want_variant) = self
.program
.enum_variant_names
.get(variant_id as usize)
.ok_or_else(|| self.runtime_err(&format!("bad variant id {variant_id}")))?
.clone();
let (matches, payload) = match self.heap.get(slot) {
Some(HeapObject::EnumVariant {
type_name,
variant,
payload,
}) => {
let hit = *type_name == want_enum && *variant == want_variant;
(hit, if hit { payload.clone() } else { Vec::new() })
}
_ => {
return Err(self.runtime_err("match scrutinee is not an enum value"));
}
};
if matches {
self.pop()?;
for value in payload {
self.push(value)?;
}
Ok(())
} else {
self.do_jump(fall_through, offset)
}
}
pub fn run(&mut self) -> Result<(), QalaError> {
loop {
match self.dispatch_one()? {
StepOutcome::Ran => continue,
StepOutcome::Halted => return Ok(()),
}
}
}
pub fn step(&mut self) -> Result<StepOutcome, QalaError> {
self.dispatch_one()
}
pub fn get_state(&self) -> VmState {
let stack: Vec<StateValue> = self
.stack
.iter()
.map(|v| StateValue {
rendered: self.value_to_string(*v),
type_name: self.runtime_type_name(*v),
})
.collect();
let (chunk_index, ip, variables) = match self.frames.last() {
Some(frame) => {
let names = self
.program
.chunks
.get(frame.chunk_idx)
.map(|c| c.local_names.as_slice())
.unwrap_or(&[]);
let variables: Vec<NamedValue> = frame
.locals
.iter()
.enumerate()
.map(|(i, v)| {
let name = match names.get(i) {
Some(n) if !n.is_empty() => n.clone(),
_ => format!("slot{i}"),
};
NamedValue {
name,
value: StateValue {
rendered: self.value_to_string(*v),
type_name: self.runtime_type_name(*v),
},
}
})
.collect();
(frame.chunk_idx, frame.ip, variables)
}
None => {
let last_idx = self.program.chunks.len().saturating_sub(1);
let ip = self
.program
.chunks
.get(last_idx)
.map(|c| c.code.len())
.unwrap_or(0);
(last_idx, ip, Vec::new())
}
};
let current_line = self
.program
.chunks
.get(chunk_index)
.and_then(|c| c.source_lines.get(ip).copied())
.unwrap_or(0) as usize;
VmState {
chunk_index,
ip,
current_line,
stack,
variables,
console: self.console.clone(),
leak_log: self.leak_log.clone(),
}
}
pub fn repl_eval(&mut self, source: &str) -> Result<Value, QalaError> {
let kind = classify_repl_line(source);
let combined = self.build_repl_source(source, kind);
let tokens = crate::lexer::Lexer::tokenize(&combined)?;
let ast = crate::parser::Parser::parse(&tokens)?;
let (typed, type_errors, _warnings) = crate::typechecker::check_program(&ast, &combined);
if let Some(first) = type_errors.into_iter().next() {
return Err(first);
}
let program = crate::codegen::compile_program(&typed, &combined).map_err(|errors| {
errors
.into_iter()
.next()
.unwrap_or_else(|| QalaError::Runtime {
span: Span::new(0, 0),
message: "codegen failed".to_string(),
})
})?;
let result = self.run_repl_program(program, &combined, kind)?;
self.repl_history.push(source.to_string());
Ok(result)
}
fn build_repl_source(&self, new_line: &str, new_kind: ReplLineKind) -> String {
let mut items = String::new();
let mut body = String::new();
for line in &self.repl_history {
match classify_repl_line(line) {
ReplLineKind::Item => {
items.push_str(line);
items.push('\n');
}
ReplLineKind::Expression | ReplLineKind::Statement => {
body.push_str(line);
body.push('\n');
}
}
}
match new_kind {
ReplLineKind::Item => {
items.push_str(new_line);
items.push('\n');
}
ReplLineKind::Expression => {
body.push_str("let ");
body.push_str(REPL_RESULT_NAME);
body.push_str(" = ");
body.push_str(new_line);
body.push('\n');
}
ReplLineKind::Statement => {
body.push_str(new_line);
body.push('\n');
}
}
format!("{items}fn {REPL_ENTRY_NAME}() is io {{\n{body}}}\n")
}
fn run_repl_program(
&mut self,
program: Program,
combined: &str,
kind: ReplLineKind,
) -> Result<Value, QalaError> {
let entry = program
.fn_names
.iter()
.position(|n| n == REPL_ENTRY_NAME)
.ok_or_else(|| QalaError::Runtime {
span: Span::new(0, 0),
message: "repl: the synthetic entry function is missing".to_string(),
})?;
let result_slot: Option<usize> = if kind == ReplLineKind::Expression {
program
.chunks
.get(entry)
.and_then(|c| c.local_names.iter().position(|n| n == REPL_RESULT_NAME))
} else {
None
};
self.program = program;
self.src = combined.to_string();
self.stack.clear();
self.heap = Heap::new();
self.globals.clear();
self.frames = vec![CallFrame {
chunk_idx: entry,
ip: 0,
base: 0,
locals: Vec::new(),
}];
let mut captured = Value::void();
loop {
if let Some(slot) = result_slot
&& let Some(frame) = self.frames.last()
&& frame.chunk_idx == entry
&& let Some(chunk) = self.program.chunks.get(entry)
&& chunk.code.get(frame.ip).copied() == Some(Opcode::Return as u8)
&& let Some(v) = frame.locals.get(slot)
{
captured = *v;
}
match self.dispatch_one()? {
StepOutcome::Ran => continue,
StepOutcome::Halted => break,
}
}
Ok(captured)
}
}
const REPL_ENTRY_NAME: &str = "__repl_main";
const REPL_RESULT_NAME: &str = "__repl_result";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReplLineKind {
Item,
Expression,
Statement,
}
fn classify_repl_line(line: &str) -> ReplLineKind {
let probe = format!("fn __probe() is io {{ let __t = {line}\n}}\n");
if let Ok(tokens) = crate::lexer::Lexer::tokenize(&probe)
&& crate::parser::Parser::parse(&tokens).is_ok()
{
return ReplLineKind::Expression;
}
if let Ok(tokens) = crate::lexer::Lexer::tokenize(line)
&& let Ok(ast) = crate::parser::Parser::parse(&tokens)
&& !ast.is_empty()
{
return ReplLineKind::Item;
}
ReplLineKind::Statement
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepOutcome {
Ran,
Halted,
}
#[derive(Clone, Copy)]
enum IntOp {
Add,
Sub,
Mul,
Div,
Mod,
}
#[derive(Clone, Copy)]
enum FloatOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Clone, Copy)]
enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
impl CmpOp {
fn holds(self, ordering: std::cmp::Ordering) -> bool {
use std::cmp::Ordering::{Equal, Greater, Less};
match self {
CmpOp::Eq => ordering == Equal,
CmpOp::Ne => ordering != Equal,
CmpOp::Lt => ordering == Less,
CmpOp::Le => ordering != Greater,
CmpOp::Gt => ordering == Greater,
CmpOp::Ge => ordering != Less,
}
}
}
fn line_span(index: &LineIndex, src: &str, line: u32) -> Span {
let mut starts: Vec<usize> = vec![0];
for (i, b) in src.bytes().enumerate() {
if b == b'\n' {
starts.push(i + 1);
}
}
let line_idx = (line as usize).saturating_sub(1);
let Some(&start) = starts.get(line_idx) else {
return Span::new(src.len(), 0);
};
let mut end = starts.get(line_idx + 1).copied().unwrap_or(src.len());
let bytes = src.as_bytes();
while end > start && (bytes[end - 1] == b'\n' || bytes[end - 1] == b'\r') {
end -= 1;
}
let _ = index;
Span::new(start, end - start)
}
#[cfg(test)]
mod tests {
use super::*;
fn program_with(chunk: Chunk) -> Program {
let mut p = Program::new();
p.chunks.push(chunk);
p.fn_names.push("main".to_string());
p.main_index = 0;
p
}
#[test]
fn heap_alloc_then_get_round_trips_the_object() {
let mut h = Heap::new();
let slot = h.alloc(HeapObject::Int(42)).expect("alloc");
assert!(h.get(slot) == Some(&HeapObject::Int(42)));
}
#[test]
fn heap_alloc_hands_out_distinct_slots() {
let mut h = Heap::new();
let a = h.alloc(HeapObject::Int(1)).expect("alloc a");
let b = h.alloc(HeapObject::Int(2)).expect("alloc b");
assert_ne!(a, b, "two live allocations must get distinct slots");
assert!(h.get(a) == Some(&HeapObject::Int(1)));
assert!(h.get(b) == Some(&HeapObject::Int(2)));
}
#[test]
fn heap_get_of_a_bad_slot_is_none_not_a_panic() {
let h = Heap::new();
assert!(h.get(0).is_none(), "empty heap, slot 0 is out of range");
assert!(h.get(9999).is_none());
}
#[test]
fn heap_get_mut_mutates_the_object_in_place() {
let mut h = Heap::new();
let slot = h.alloc(HeapObject::Str("a".to_string())).expect("alloc");
if let Some(HeapObject::Str(s)) = h.get_mut(slot) {
s.push('b');
}
assert!(h.get(slot) == Some(&HeapObject::Str("ab".to_string())));
}
#[test]
fn heap_inc_then_dec_keeps_the_slot_alive_until_count_reaches_zero() {
let mut h = Heap::new();
let slot = h.alloc(HeapObject::Int(7)).expect("alloc"); h.inc(slot); assert!(
h.dec(slot).is_none(),
"dec to a positive count returns None"
);
assert!(h.get(slot) == Some(&HeapObject::Int(7)), "slot still alive");
}
#[test]
fn heap_dec_to_zero_frees_the_slot_and_returns_the_freed_object() {
let mut h = Heap::new();
let slot = h.alloc(HeapObject::Int(99)).expect("alloc"); assert!(
h.dec(slot) == Some(HeapObject::Int(99)),
"dec to zero returns the freed object"
);
assert!(h.get(slot).is_none(), "a freed slot reads as None");
}
#[test]
fn heap_dec_returns_the_freed_file_handle_so_the_caller_can_leak_check() {
let mut h = Heap::new();
let handle = HeapObject::FileHandle {
path: "data.txt".to_string(),
content: String::new(),
closed: false,
};
let slot = h.alloc(handle.clone()).expect("alloc");
let freed = h.dec(slot).expect("dec to zero returns the object");
match freed {
HeapObject::FileHandle { closed, path, .. } => {
assert!(!closed, "the freed handle is still open -- a leak");
assert_eq!(path, "data.txt");
}
_ => panic!("expected a FileHandle from dec, got another variant"),
}
}
#[test]
fn heap_dec_of_a_bad_or_freed_slot_is_none_not_a_panic() {
let mut h = Heap::new();
assert!(h.dec(0).is_none());
let slot = h.alloc(HeapObject::Int(1)).expect("alloc");
assert!(h.dec(slot).is_some(), "first dec frees it");
assert!(
h.dec(slot).is_none(),
"a second dec of a freed slot is None"
);
}
#[test]
fn heap_alloc_reuses_a_freed_slot_before_growing_the_slab() {
let mut h = Heap::new();
let first = h.alloc(HeapObject::Int(1)).expect("alloc first");
h.dec(first);
let reused = h.alloc(HeapObject::Int(2)).expect("alloc reused");
assert_eq!(
reused, first,
"a freed slot index is reused by the next alloc"
);
assert!(h.get(reused) == Some(&HeapObject::Int(2)));
}
#[test]
fn heap_inc_of_a_bad_slot_is_a_silent_no_op() {
let mut h = Heap::new();
h.inc(0);
h.inc(12345);
assert!(h.get(0).is_none());
}
#[test]
fn heap_caps_are_the_documented_values() {
assert_eq!(MAX_FRAMES, 1024);
assert_eq!(MAX_STACK, 65536);
assert_eq!(MAX_HEAP, 1_000_000);
}
#[test]
fn vm_new_pushes_the_initial_main_frame() {
let mut p = Program::new();
p.chunks.push(Chunk::new());
p.chunks.push(Chunk::new());
p.fn_names.push("first".to_string());
p.fn_names.push("main".to_string());
p.main_index = 1;
let vm = Vm::new(p, String::new());
assert_eq!(vm.frames.len(), 1);
let f = vm.frame().expect("the main frame exists");
assert_eq!(f.chunk_idx, 1);
assert_eq!(f.ip, 0);
assert_eq!(f.base, 0);
}
#[test]
fn vm_push_then_pop_round_trips_a_value() {
let vm_program = program_with(Chunk::new());
let mut vm = Vm::new(vm_program, String::new());
vm.push(Value::bool(true)).expect("push");
let v = vm.pop().expect("pop");
assert_eq!(v.as_bool(), Some(true));
}
#[test]
fn vm_pop_on_an_empty_stack_is_a_runtime_underflow_not_a_panic() {
let mut vm = Vm::new(program_with(Chunk::new()), String::new());
match vm.pop() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("underflow"), "got: {message}");
}
Err(other) => panic!("expected a Runtime underflow, got {other:?}"),
Ok(_) => panic!("expected a Runtime underflow, got Ok(value)"),
}
}
#[test]
fn vm_push_past_max_stack_is_a_runtime_overflow_not_a_panic() {
let mut vm = Vm::new(program_with(Chunk::new()), String::new());
vm.stack.resize(MAX_STACK, Value::void());
match vm.push(Value::void()) {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("overflow"), "got: {message}");
}
other => panic!("expected a Runtime overflow, got {other:?}"),
}
}
#[test]
fn vm_runtime_err_carries_the_source_line_of_the_current_instruction() {
let mut chunk = Chunk::new();
chunk.code.push(0);
chunk.source_lines.push(3);
let src = "line one\nline two\nline three\nline four".to_string();
let vm = Vm::new(program_with(chunk), src.clone());
let err = vm.runtime_err("boom");
let span = err.span();
assert_eq!(span.slice(&src), "line three");
}
#[test]
fn vm_runtime_err_on_a_missing_source_line_is_a_zero_width_span_not_a_panic() {
let vm = Vm::new(program_with(Chunk::new()), "anything".to_string());
let err = vm.runtime_err("boom");
let span = err.span();
assert_eq!(span.len, 0, "an unresolved line yields a zero-width span");
}
#[test]
fn line_span_of_a_line_past_the_source_is_a_zero_width_span_not_a_panic() {
let src = "only one line";
let index = LineIndex::new(src);
let span = line_span(&index, src, 99);
assert_eq!(span.len, 0);
}
fn emit_const(chunk: &mut Chunk, v: ConstValue, line: u32) {
let idx = chunk.add_constant(v);
chunk.write_op(Opcode::Const, line);
chunk.write_u16(idx, line);
}
#[test]
fn dispatch_runs_a_const_then_pop_program_clean() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(7), 1);
chunk.write_op(Opcode::Pop, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("a CONST/POP/HALT program runs clean");
assert!(vm.stack.is_empty(), "POP must leave the stack empty");
}
#[test]
fn dispatch_const_of_an_i64_pushes_a_pointer_to_a_heap_int() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(42), 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let top = *vm.stack.last().expect("the CONST left a value");
assert_eq!(top.as_function(), None, "an i64 constant is not a function");
let slot = top
.as_pointer()
.expect("an i64 constant must push a heap pointer");
assert!(
vm.heap.get(slot) == Some(&HeapObject::Int(42)),
"the pointer must reach a heap Int(42)"
);
}
#[test]
fn dispatch_const_of_a_bool_pushes_a_tagged_scalar_not_a_pointer() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let top = *vm.stack.last().expect("value");
assert_eq!(top.as_bool(), Some(true));
assert_eq!(top.as_pointer(), None, "a bool is not a heap pointer");
}
#[test]
fn dispatch_const_of_a_function_pushes_a_function_value_carrying_the_id() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Function(13), 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let top = *vm.stack.last().expect("value");
assert_eq!(top.as_function(), Some(13), "fn-id must round-trip");
assert_eq!(top.as_pointer(), None, "a function is not a heap pointer");
}
#[test]
fn dispatch_dup_duplicates_the_top_value() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
chunk.write_op(Opcode::Dup, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
assert_eq!(vm.stack.len(), 2, "DUP pushes a copy");
assert!(vm.stack[0] == vm.stack[1], "the copy equals the original");
}
#[test]
fn dispatch_set_local_then_get_local_round_trips_a_value() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Byte(5), 1);
chunk.write_op(Opcode::SetLocal, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::GetLocal, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let top = *vm.stack.last().expect("GET_LOCAL pushed a value");
assert_eq!(top.as_byte(), Some(5), "the local round-trips");
}
#[test]
fn dispatch_get_local_of_an_unset_slot_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.write_op(Opcode::GetLocal, 1);
chunk.write_u16(4, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("bad local slot"), "got: {message}");
}
other => panic!("expected a Runtime bad-local error, got {other:?}"),
}
}
#[test]
fn dispatch_set_global_then_get_global_round_trips_a_value() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(false), 1);
chunk.write_op(Opcode::SetGlobal, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::GetGlobal, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let top = *vm.stack.last().expect("GET_GLOBAL pushed a value");
assert_eq!(top.as_bool(), Some(false));
}
#[test]
fn malformed_a_bad_opcode_byte_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.code.push(46);
chunk.source_lines.push(1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("bad opcode byte"), "got: {message}");
}
other => panic!("expected a Runtime bad-opcode error, got {other:?}"),
}
}
#[test]
fn malformed_a_truncated_operand_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.code.push(Opcode::Const as u8);
chunk.code.push(0); chunk.source_lines.push(1);
chunk.source_lines.push(1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("truncated operand"), "got: {message}");
}
other => panic!("expected a Runtime truncated-operand error, got {other:?}"),
}
}
#[test]
fn malformed_a_bad_constant_index_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.write_op(Opcode::Const, 1);
chunk.write_u16(9, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("bad constant index"), "got: {message}");
}
other => panic!("expected a Runtime bad-constant error, got {other:?}"),
}
}
#[test]
fn a_call_to_a_stdlib_fn_id_dispatches_to_the_native_function() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Str("hi".to_string()), 1);
chunk.write_op(Opcode::Call, 1);
chunk.write_u16(STDLIB_FN_BASE + 1, 1);
chunk.code.push(1); chunk.source_lines.push(1);
chunk.write_op(Opcode::Pop, 1);
chunk.write_op(Opcode::Return, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("a println CALL runs clean");
assert_eq!(
vm.console,
vec!["hi\n".to_string()],
"the native println wrote its argument to the console"
);
}
#[test]
fn dispatch_an_ip_past_the_end_of_the_chunk_is_a_runtime_error() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(
message.contains("instruction pointer past end"),
"got: {message}"
);
}
other => panic!("expected a Runtime ip-past-end error, got {other:?}"),
}
}
#[test]
fn step_advances_ip_by_one_full_instruction_each_call() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
chunk.write_op(Opcode::Pop, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
assert_eq!(vm.frame().expect("frame").ip, 0);
assert_eq!(vm.step().expect("step 1"), StepOutcome::Ran);
assert_eq!(vm.frame().expect("frame").ip, 3, "CONST advances ip by 3");
assert_eq!(vm.stack.len(), 1, "CONST pushed one value");
assert_eq!(vm.step().expect("step 2"), StepOutcome::Ran);
assert_eq!(vm.frame().expect("frame").ip, 4, "POP advances ip by 1");
assert_eq!(vm.stack.len(), 0, "POP cleared the stack");
assert_eq!(vm.step().expect("step 3"), StepOutcome::Halted);
}
#[test]
fn run_and_step_share_dispatch_one_reaching_the_same_end_state() {
let build = || {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
emit_const(&mut chunk, ConstValue::Bool(false), 1);
chunk.write_op(Opcode::Halt, 1);
program_with(chunk)
};
let mut via_run = Vm::new(build(), "x".to_string());
via_run.run().expect("run");
let mut via_step = Vm::new(build(), "x".to_string());
while via_step.step().expect("step") == StepOutcome::Ran {}
assert_eq!(via_run.stack.len(), via_step.stack.len());
assert!(
via_run.stack.len() == 2
&& via_run.stack[0] == via_step.stack[0]
&& via_run.stack[1] == via_step.stack[1],
"run and step must reach the same stack state"
);
}
fn run_chunk(chunk: Chunk, src: &str) -> Result<Vm, QalaError> {
let mut vm = Vm::new(program_with(chunk), src.to_string());
vm.run()?;
Ok(vm)
}
fn top_i64(vm: &Vm) -> i64 {
let top = *vm.stack.last().expect("a value on the stack");
let slot = top.as_pointer().expect("the result is a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Int(n)) => *n,
_ => panic!("the result pointer does not reach a heap Int"),
}
}
fn binary_i64_chunk(a: i64, b: i64, op: Opcode) -> Chunk {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(a), 1);
emit_const(&mut chunk, ConstValue::I64(b), 1);
chunk.write_op(op, 1);
chunk.write_op(Opcode::Halt, 1);
chunk
}
#[test]
fn arith_add_sub_mul_compute_correct_i64_results() {
let add = run_chunk(binary_i64_chunk(20, 22, Opcode::Add), "x").expect("add");
assert_eq!(top_i64(&add), 42);
let sub = run_chunk(binary_i64_chunk(50, 8, Opcode::Sub), "x").expect("sub");
assert_eq!(top_i64(&sub), 42);
let mul = run_chunk(binary_i64_chunk(6, 7, Opcode::Mul), "x").expect("mul");
assert_eq!(top_i64(&mul), 42);
}
#[test]
fn arith_div_and_mod_compute_correct_i64_results() {
let div = run_chunk(binary_i64_chunk(85, 2, Opcode::Div), "x").expect("div");
assert_eq!(top_i64(&div), 42);
let modr = run_chunk(binary_i64_chunk(85, 2, Opcode::Mod), "x").expect("mod");
assert_eq!(top_i64(&modr), 1);
}
#[test]
fn arith_neg_negates_an_i64() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(42), 1);
chunk.write_op(Opcode::Neg, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("neg");
assert_eq!(top_i64(&vm), -42);
}
#[test]
fn arith_add_overflow_is_a_runtime_error_not_a_wraparound() {
let chunk = binary_i64_chunk(i64::MAX, 1, Opcode::Add);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("integer overflow"), "got: {message}");
}
Err(other) => panic!("expected an overflow Runtime error, got {other:?}"),
Ok(_) => panic!("expected an overflow Runtime error, the program ran clean"),
}
}
#[test]
fn arith_neg_of_i64_min_is_a_runtime_overflow() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(i64::MIN), 1);
chunk.write_op(Opcode::Neg, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("integer overflow"), "got: {message}");
}
Err(other) => panic!("expected an overflow Runtime error, got {other:?}"),
Ok(_) => panic!("expected an overflow Runtime error, the program ran clean"),
}
}
#[test]
fn div_by_zero_is_a_runtime_error_carrying_the_div_opcode_source_line() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
emit_const(&mut chunk, ConstValue::I64(0), 1);
chunk.write_op(Opcode::Div, 4);
chunk.write_op(Opcode::Halt, 4);
let src = "one\ntwo\nthree\nfour line here\nfive";
match run_chunk(chunk, src) {
Err(QalaError::Runtime { message, span }) => {
assert!(message.contains("division by zero"), "got: {message}");
assert_eq!(
span.slice(src),
"four line here",
"the error must point at the DIV's source line"
);
}
Err(other) => panic!("expected a division-by-zero Runtime error, got {other:?}"),
Ok(_) => panic!("expected a division-by-zero error, the program ran clean"),
}
}
#[test]
fn div_by_zero_mod_form_is_a_runtime_modulo_by_zero_error() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(7), 1);
emit_const(&mut chunk, ConstValue::I64(0), 1);
chunk.write_op(Opcode::Mod, 2);
chunk.write_op(Opcode::Halt, 2);
let src = "first line\nsecond line is the mod";
match run_chunk(chunk, src) {
Err(QalaError::Runtime { message, span }) => {
assert!(message.contains("modulo by zero"), "got: {message}");
assert_eq!(span.slice(src), "second line is the mod");
}
Err(other) => panic!("expected a modulo-by-zero Runtime error, got {other:?}"),
Ok(_) => panic!("expected a modulo-by-zero error, the program ran clean"),
}
}
#[test]
fn div_of_i64_min_by_negative_one_is_caught_as_a_runtime_error() {
let chunk = binary_i64_chunk(i64::MIN, -1, Opcode::Div);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("division by zero"), "got: {message}");
}
Err(other) => panic!("expected a Runtime error for i64::MIN / -1, got {other:?}"),
Ok(_) => panic!("expected a Runtime error for i64::MIN / -1, ran clean"),
}
}
fn binary_f64_chunk(a: f64, b: f64, op: Opcode) -> Chunk {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::F64(a), 1);
emit_const(&mut chunk, ConstValue::F64(b), 1);
chunk.write_op(op, 1);
chunk.write_op(Opcode::Halt, 1);
chunk
}
#[test]
fn float_arith_add_sub_mul_neg_compute_correct_f64_results() {
let add = run_chunk(binary_f64_chunk(1.5, 2.0, Opcode::FAdd), "x").expect("fadd");
assert_eq!(add.stack.last().unwrap().as_f64(), Some(3.5));
let sub = run_chunk(binary_f64_chunk(5.0, 1.5, Opcode::FSub), "x").expect("fsub");
assert_eq!(sub.stack.last().unwrap().as_f64(), Some(3.5));
let mul = run_chunk(binary_f64_chunk(2.0, 1.75, Opcode::FMul), "x").expect("fmul");
assert_eq!(mul.stack.last().unwrap().as_f64(), Some(3.5));
let mut neg = Chunk::new();
emit_const(&mut neg, ConstValue::F64(3.5), 1);
neg.write_op(Opcode::FNeg, 1);
neg.write_op(Opcode::Halt, 1);
let negv = run_chunk(neg, "x").expect("fneg");
assert_eq!(negv.stack.last().unwrap().as_f64(), Some(-3.5));
}
#[test]
fn float_arith_div_by_zero_is_ieee754_inf_not_an_error() {
let vm = run_chunk(binary_f64_chunk(1.0, 0.0, Opcode::FDiv), "x")
.expect("float division by zero must not error");
let top = vm.stack.last().unwrap().as_f64().expect("an f64 result");
assert_eq!(top, f64::INFINITY, "1.0 / 0.0 is positive infinity");
}
#[test]
fn float_arith_zero_over_zero_is_ieee754_nan_not_an_error() {
let vm = run_chunk(binary_f64_chunk(0.0, 0.0, Opcode::FDiv), "x")
.expect("0.0 / 0.0 must not error");
let top = vm.stack.last().unwrap().as_f64().expect("an f64 result");
assert!(top.is_nan(), "0.0 / 0.0 is NaN");
}
#[test]
fn compare_i64_eq_ne_lt_le_gt_ge_produce_correct_bools() {
let cmp = |a: i64, b: i64, op: Opcode| -> bool {
let vm = run_chunk(binary_i64_chunk(a, b, op), "x").expect("compare");
vm.stack.last().unwrap().as_bool().expect("a bool result")
};
assert!(cmp(3, 3, Opcode::Eq));
assert!(!cmp(3, 4, Opcode::Eq));
assert!(cmp(3, 4, Opcode::Ne));
assert!(cmp(3, 4, Opcode::Lt));
assert!(!cmp(4, 3, Opcode::Lt));
assert!(cmp(3, 3, Opcode::Le));
assert!(cmp(5, 4, Opcode::Gt));
assert!(cmp(4, 4, Opcode::Ge));
assert!(!cmp(3, 4, Opcode::Ge));
}
#[test]
fn compare_str_eq_and_lt_compare_lexicographically() {
let cmp = |a: &str, b: &str, op: Opcode| -> bool {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Str(a.to_string()), 1);
emit_const(&mut chunk, ConstValue::Str(b.to_string()), 1);
chunk.write_op(op, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("str compare");
vm.stack.last().unwrap().as_bool().expect("a bool result")
};
assert!(cmp("apple", "apple", Opcode::Eq));
assert!(!cmp("apple", "banana", Opcode::Eq));
assert!(cmp("apple", "banana", Opcode::Lt), "apple < banana");
assert!(!cmp("banana", "apple", Opcode::Lt));
assert!(cmp("apple", "banana", Opcode::Ne));
}
#[test]
fn compare_bool_eq_and_ordering_follow_false_lt_true() {
let cmp = |a: bool, b: bool, op: Opcode| -> bool {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(a), 1);
emit_const(&mut chunk, ConstValue::Bool(b), 1);
chunk.write_op(op, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("bool compare");
vm.stack.last().unwrap().as_bool().expect("a bool result")
};
assert!(cmp(true, true, Opcode::Eq));
assert!(cmp(false, true, Opcode::Ne));
assert!(cmp(false, true, Opcode::Lt));
assert!(!cmp(true, false, Opcode::Lt));
}
#[test]
fn compare_mismatched_operand_types_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
emit_const(&mut chunk, ConstValue::Str("x".to_string()), 1);
chunk.write_op(Opcode::Eq, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("cannot compare"), "got: {message}");
}
Err(other) => panic!("expected a Runtime compare-mismatch error, got {other:?}"),
Ok(_) => panic!("expected a compare-mismatch error, the program ran clean"),
}
}
#[test]
fn compare_f64_eq_lt_ge_follow_ieee754_including_nan() {
let cmp = |a: f64, b: f64, op: Opcode| -> bool {
let vm = run_chunk(binary_f64_chunk(a, b, op), "x").expect("f64 compare");
vm.stack.last().unwrap().as_bool().expect("a bool result")
};
assert!(cmp(1.5, 1.5, Opcode::FEq));
assert!(cmp(1.0, 2.0, Opcode::FLt));
assert!(cmp(2.0, 2.0, Opcode::FGe));
assert!(!cmp(f64::NAN, f64::NAN, Opcode::FEq), "NaN == NaN is false");
assert!(cmp(f64::NAN, f64::NAN, Opcode::FNe), "NaN != NaN is true");
assert!(!cmp(f64::NAN, 1.0, Opcode::FLt), "NaN < x is false");
assert!(!cmp(f64::NAN, 1.0, Opcode::FGt), "NaN > x is false");
}
#[test]
fn logic_not_negates_a_bool() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
chunk.write_op(Opcode::Not, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("not");
assert_eq!(vm.stack.last().unwrap().as_bool(), Some(false));
}
#[test]
fn jumps_jump_moves_ip_over_a_skipped_instruction() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1); chunk.write_op(Opcode::Jump, 1); chunk.write_i16(3, 1); emit_const(&mut chunk, ConstValue::Bool(false), 1); chunk.write_op(Opcode::Halt, 1); let vm = run_chunk(chunk, "x").expect("jump");
assert_eq!(vm.stack.len(), 1, "the skipped CONST left nothing");
assert_eq!(vm.stack[0].as_bool(), Some(true));
}
#[test]
fn jumps_jump_if_false_branches_on_false_and_falls_through_on_true() {
let outcome = |cond: bool| -> usize {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(cond), 1); chunk.write_op(Opcode::JumpIfFalse, 1); chunk.write_i16(3, 1); emit_const(&mut chunk, ConstValue::I64(111), 1); chunk.write_op(Opcode::Halt, 1); let vm = run_chunk(chunk, "x").expect("jump_if_false");
vm.stack.len()
};
assert_eq!(outcome(false), 0, "false branches past the CONST");
assert_eq!(outcome(true), 1, "true falls through to the CONST");
}
#[test]
fn jumps_jump_if_true_branches_on_true_and_falls_through_on_false() {
let outcome = |cond: bool| -> usize {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(cond), 1);
chunk.write_op(Opcode::JumpIfTrue, 1);
chunk.write_i16(3, 1);
emit_const(&mut chunk, ConstValue::I64(222), 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("jump_if_true");
vm.stack.len()
};
assert_eq!(outcome(true), 0, "true branches past the CONST");
assert_eq!(outcome(false), 1, "false falls through to the CONST");
}
#[test]
fn jumps_a_backward_jump_lands_at_an_earlier_instruction() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1); chunk.write_op(Opcode::Jump, 1); chunk.write_i16(-6, 1); let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.step().expect("step the CONST");
assert_eq!(vm.frame().expect("frame").ip, 3, "ip after CONST is 3");
vm.step().expect("step the JUMP");
assert_eq!(
vm.frame().expect("frame").ip,
0,
"the backward JUMP lands at 0"
);
}
#[test]
fn jumps_a_jump_target_outside_the_chunk_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.write_op(Opcode::Jump, 1); chunk.write_i16(1000, 1); match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(
message.contains("jump target out of range"),
"got: {message}"
);
}
Err(other) => panic!("expected a Runtime jump-out-of-range error, got {other:?}"),
Ok(_) => panic!("expected a jump-out-of-range error, the program ran clean"),
}
}
#[test]
fn arith_add_of_non_integer_operands_is_a_runtime_type_error() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
emit_const(&mut chunk, ConstValue::Bool(false), 1);
chunk.write_op(Opcode::Add, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("expected an integer"), "got: {message}");
}
Err(other) => panic!("expected a Runtime type error, got {other:?}"),
Ok(_) => panic!("expected a Runtime type error, the program ran clean"),
}
}
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::typechecker::check_program;
fn compile_qala(src: &str) -> Program {
let tokens = Lexer::tokenize(src).expect("lex failed");
let ast = Parser::parse(&tokens).expect("parse failed");
let (typed, terrors, _) = check_program(&ast, src);
assert!(terrors.is_empty(), "typecheck errors: {terrors:?}");
crate::codegen::compile_program(&typed, src)
.unwrap_or_else(|e| panic!("codegen errors: {e:?}"))
}
fn program_result_i64(vm: &Vm) -> i64 {
let top = *vm.stack.last().expect("the program left a result value");
let slot = top.as_pointer().expect("the result is a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Int(n)) => *n,
_ => panic!("the result pointer does not reach a heap Int"),
}
}
#[test]
fn call_return_a_callee_returns_a_value_onto_the_callers_stack() {
let mut main = Chunk::new();
main.write_op(Opcode::Call, 1);
main.write_u16(1, 1); main.code.push(0); main.source_lines.push(1);
main.write_op(Opcode::Return, 1);
let mut callee = Chunk::new();
emit_const(&mut callee, ConstValue::I64(7), 1);
callee.write_op(Opcode::Return, 1);
let mut p = Program::new();
p.chunks.push(main);
p.chunks.push(callee);
p.fn_names.push("main".to_string());
p.fn_names.push("callee".to_string());
p.main_index = 0;
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("the call/return program runs clean");
assert_eq!(program_result_i64(&vm), 7);
}
#[test]
fn call_passes_arguments_into_the_callee_frames_local_slots() {
let mut main = Chunk::new();
emit_const(&mut main, ConstValue::I64(42), 1);
main.write_op(Opcode::Call, 1);
main.write_u16(1, 1);
main.code.push(1); main.source_lines.push(1);
main.write_op(Opcode::Return, 1);
let mut id = Chunk::new();
id.write_op(Opcode::GetLocal, 1);
id.write_u16(0, 1);
id.write_op(Opcode::Return, 1);
let mut p = Program::new();
p.chunks.push(main);
p.chunks.push(id);
p.fn_names.push("main".to_string());
p.fn_names.push("id".to_string());
p.main_index = 0;
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("run");
assert_eq!(program_result_i64(&vm), 42, "the argument reached local 0");
}
#[test]
fn call_to_a_missing_function_is_a_runtime_error_not_a_panic() {
let mut main = Chunk::new();
main.write_op(Opcode::Call, 1);
main.write_u16(9, 1);
main.code.push(0);
main.source_lines.push(1);
main.write_op(Opcode::Return, 1);
match run_chunk(main, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("missing function"), "got: {message}");
}
Err(other) => panic!("expected a Runtime missing-function error, got {other:?}"),
Ok(_) => panic!("expected a Runtime missing-function error, the program ran clean"),
}
}
#[test]
fn a_stdlib_call_with_a_wrong_argument_count_is_a_clean_runtime_error() {
let mut main = Chunk::new();
main.write_op(Opcode::Call, 1);
main.write_u16(STDLIB_FN_BASE, 1);
main.code.push(0); main.source_lines.push(1);
main.write_op(Opcode::Return, 1);
match run_chunk(main, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("expects 1 argument"), "got: {message}");
}
Err(other) => panic!("expected a Runtime arity error, got {other:?}"),
Ok(_) => panic!("a wrong-arity stdlib call must error"),
}
}
#[test]
fn fibonacci_recursion_computes_the_correct_numeric_result() {
let src = "\
fn fib(n: i64) -> i64 is pure {
if n <= 1 { return n }
return fib(n - 1) + fib(n - 2)
}
fn main() -> i64 is pure {
return fib(10)
}
";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the fibonacci program runs clean");
assert_eq!(program_result_i64(&vm), 55, "fib(10) must be 55");
}
#[test]
fn deep_recursion_is_a_clean_stack_overflow_runtime_error_with_no_host_panic() {
let src = "\
fn r(n: i64) -> i64 is pure {
return r(n + 1)
}
fn main() -> i64 is pure {
return r(0)
}
";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(
message.contains("stack overflow"),
"unbounded recursion must report a stack overflow, got: {message}"
);
}
Err(other) => panic!("expected a Runtime stack-overflow error, got {other:?}"),
Ok(_) => panic!("unbounded recursion must error, the program ran clean"),
}
}
#[test]
fn call_function_value_runs_a_user_callback_and_returns_its_result() {
let mut main = Chunk::new();
emit_const(&mut main, ConstValue::I64(0), 1);
main.write_op(Opcode::Return, 1);
let mut double = Chunk::new();
double.write_op(Opcode::GetLocal, 1);
double.write_u16(0, 1);
double.write_op(Opcode::GetLocal, 1);
double.write_u16(0, 1);
double.write_op(Opcode::Add, 1);
double.write_op(Opcode::Return, 1);
let mut p = Program::new();
p.chunks.push(main);
p.chunks.push(double);
p.fn_names.push("main".to_string());
p.fn_names.push("double".to_string());
p.main_index = 0;
let mut vm = Vm::new(p, "x".to_string());
let arg_slot = vm.heap.alloc(HeapObject::Int(21)).expect("alloc arg");
let result = vm
.call_function_value(Value::function(1), &[Value::pointer(arg_slot)])
.expect("the callback runs and returns");
let result_slot = result.as_pointer().expect("a heap-Int result");
assert!(
vm.heap.get(result_slot) == Some(&HeapObject::Int(42)),
"double(21) must be 42"
);
}
#[test]
fn call_function_value_of_a_non_function_is_a_runtime_error() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
match vm.call_function_value(Value::bool(true), &[]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("not callable"), "got: {message}");
}
Err(other) => panic!("expected a Runtime not-callable error, got {other:?}"),
Ok(_) => panic!("expected a Runtime not-callable error, got a value"),
}
}
#[test]
fn make_array_builds_a_heap_array_then_index_and_len_read_it() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(10), 1);
emit_const(&mut chunk, ConstValue::I64(20), 1);
emit_const(&mut chunk, ConstValue::I64(30), 1);
chunk.write_op(Opcode::MakeArray, 1);
chunk.write_u16(3, 1);
chunk.write_op(Opcode::Dup, 1);
emit_const(&mut chunk, ConstValue::I64(1), 1);
chunk.write_op(Opcode::Index, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("make_array");
assert_eq!(top_i64(&vm), 20, "INDEX 1 of [10,20,30] is 20");
let array_ptr = vm.stack[vm.stack.len() - 2];
let slot = array_ptr.as_pointer().expect("an array pointer");
match vm.heap.get(slot) {
Some(HeapObject::Array(items)) => assert_eq!(items.len(), 3),
_ => panic!("MAKE_ARRAY must build a heap Array"),
}
}
#[test]
fn len_of_a_built_array_pushes_the_element_count() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
emit_const(&mut chunk, ConstValue::I64(2), 1);
chunk.write_op(Opcode::MakeArray, 1);
chunk.write_u16(2, 1);
chunk.write_op(Opcode::Len, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("len");
assert_eq!(top_i64(&vm), 2, "LEN of a 2-element array is 2");
}
#[test]
fn len_of_a_string_counts_unicode_scalar_values() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Str("hello".to_string()), 1);
chunk.write_op(Opcode::Len, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("len str");
assert_eq!(top_i64(&vm), 5, "LEN of \"hello\" is 5");
}
#[test]
fn make_tuple_builds_a_distinct_heap_tuple_object() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(7), 1);
emit_const(&mut chunk, ConstValue::Bool(true), 1);
chunk.write_op(Opcode::MakeTuple, 1);
chunk.write_u16(2, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("make_tuple");
let top = *vm.stack.last().expect("a tuple pointer");
let slot = top.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Tuple(items)) => {
assert_eq!(items.len(), 2, "the tuple has two elements");
assert_eq!(items[1].as_bool(), Some(true));
}
_ => panic!("MAKE_TUPLE must build a heap Tuple, not an Array"),
}
}
#[test]
fn index_out_of_bounds_is_a_runtime_error_carrying_the_index_length_and_line() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(100), 1);
emit_const(&mut chunk, ConstValue::I64(200), 1);
chunk.write_op(Opcode::MakeArray, 1);
chunk.write_u16(2, 1);
emit_const(&mut chunk, ConstValue::I64(5), 1);
chunk.write_op(Opcode::Index, 4); chunk.write_op(Opcode::Halt, 4);
let src = "one\ntwo\nthree\nthe index line\nfive";
match run_chunk(chunk, src) {
Err(QalaError::Runtime { message, span }) => {
assert!(message.contains('5'), "the index is named: {message}");
assert!(message.contains('2'), "the length is named: {message}");
assert!(message.contains("out of bounds"), "got: {message}");
assert_eq!(
span.slice(src),
"the index line",
"the error must point at the INDEX's source line"
);
}
Err(other) => panic!("expected an out-of-bounds Runtime error, got {other:?}"),
Ok(_) => panic!("expected an out-of-bounds error, the program ran clean"),
}
}
#[test]
fn index_of_a_negative_index_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
chunk.write_op(Opcode::MakeArray, 1);
chunk.write_u16(1, 1);
emit_const(&mut chunk, ConstValue::I64(-1), 1);
chunk.write_op(Opcode::Index, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("out of bounds"), "got: {message}");
}
Err(other) => panic!("expected an out-of-bounds Runtime error, got {other:?}"),
Ok(_) => panic!("expected an out-of-bounds error, the program ran clean"),
}
}
#[test]
fn make_struct_labels_the_struct_with_its_declared_name_and_field_can_read_it() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
emit_const(&mut chunk, ConstValue::I64(2), 1);
chunk.write_op(Opcode::MakeStruct, 1);
chunk.write_u16(0, 1); chunk.write_op(Opcode::Dup, 1);
chunk.write_op(Opcode::Field, 1);
chunk.write_u16(1, 1); chunk.write_op(Opcode::Halt, 1);
let mut p = program_with(chunk);
p.structs.push(crate::chunk::StructInfo {
name: "Point".to_string(),
field_count: 2,
});
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("make_struct");
assert_eq!(top_i64(&vm), 2, "FIELD 1 of Point{{1,2}} is 2");
let struct_ptr = vm.stack[vm.stack.len() - 2];
let slot = struct_ptr.as_pointer().expect("a struct pointer");
match vm.heap.get(slot) {
Some(HeapObject::Struct { type_name, fields }) => {
assert_eq!(type_name, "Point", "the struct carries its declared name");
assert_eq!(fields.len(), 2);
}
_ => panic!("MAKE_STRUCT must build a heap Struct"),
}
}
#[test]
fn make_struct_with_a_bad_struct_id_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.write_op(Opcode::MakeStruct, 1);
chunk.write_u16(9, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("bad struct id"), "got: {message}");
}
Err(other) => panic!("expected a Runtime bad-struct-id error, got {other:?}"),
Ok(_) => panic!("expected a Runtime bad-struct-id error, the program ran clean"),
}
}
#[test]
fn make_enum_variant_builds_a_variant_with_its_enum_and_variant_names() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(5), 1);
chunk.write_op(Opcode::MakeEnumVariant, 1);
chunk.write_u16(0, 1); chunk.code.push(1); chunk.source_lines.push(1);
chunk.write_op(Opcode::Halt, 1);
let mut p = program_with(chunk);
p.enum_variant_names
.push(("Shape".to_string(), "Circle".to_string()));
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("make_enum_variant");
let top = *vm.stack.last().expect("a variant pointer");
let slot = top.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::EnumVariant {
type_name,
variant,
payload,
}) => {
assert_eq!(type_name, "Shape", "the enum name");
assert_eq!(variant, "Circle", "the variant name");
assert_eq!(payload.len(), 1, "Circle carries one payload value");
}
_ => panic!("MAKE_ENUM_VARIANT must build a heap EnumVariant"),
}
}
#[test]
fn make_enum_variant_with_a_bad_variant_id_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
chunk.write_op(Opcode::MakeEnumVariant, 1);
chunk.write_u16(9, 1);
chunk.code.push(0);
chunk.source_lines.push(1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("bad variant id"), "got: {message}");
}
Err(other) => panic!("expected a Runtime bad-variant-id error, got {other:?}"),
Ok(_) => panic!("expected a Runtime bad-variant-id error, the program ran clean"),
}
}
#[test]
fn field_of_a_non_struct_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
chunk.write_op(Opcode::MakeArray, 1);
chunk.write_u16(1, 1);
chunk.write_op(Opcode::Field, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::Halt, 1);
match run_chunk(chunk, "x") {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("expected a struct"), "got: {message}");
}
Err(other) => panic!("expected a Runtime not-a-struct error, got {other:?}"),
Ok(_) => panic!("expected a Runtime not-a-struct error, the program ran clean"),
}
}
fn top_str(vm: &Vm) -> String {
let top = *vm.stack.last().expect("a value on the stack");
let slot = top.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Str(s)) => s.clone(),
_ => panic!("the result pointer does not reach a heap Str"),
}
}
#[test]
fn to_str_of_an_i64_renders_the_decimal_form() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(-42), 1);
chunk.write_op(Opcode::ToStr, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("to_str i64");
assert_eq!(top_str(&vm), "-42");
}
#[test]
fn to_str_of_a_bool_renders_the_lowercase_keyword() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Bool(true), 1);
chunk.write_op(Opcode::ToStr, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("to_str bool");
assert_eq!(top_str(&vm), "true");
}
#[test]
fn to_str_of_a_float_hand_spells_nan_and_infinities() {
let to_str = |x: f64| -> String {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::F64(x), 1);
chunk.write_op(Opcode::ToStr, 1);
chunk.write_op(Opcode::Halt, 1);
top_str(&run_chunk(chunk, "x").expect("to_str f64"))
};
assert_eq!(to_str(f64::NAN), "NaN", "a NaN renders as NaN");
assert_eq!(to_str(f64::INFINITY), "inf");
assert_eq!(to_str(f64::NEG_INFINITY), "-inf");
assert_eq!(to_str(3.5), "3.5", "a finite float uses the default form");
}
#[test]
fn value_to_string_renders_each_runtime_value_kind_with_its_locked_spelling() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
assert_eq!(vm.value_to_string(Value::bool(false)), "false");
assert_eq!(
vm.value_to_string(Value::byte(65)),
"65",
"a byte is decimal"
);
assert_eq!(vm.value_to_string(Value::void()), "()");
assert_eq!(vm.value_to_string(Value::function(7)), "fn#7");
assert_eq!(vm.value_to_string(Value::from_f64(2.0)), "2");
let int_slot = vm.heap.alloc(HeapObject::Int(-3)).expect("alloc");
assert_eq!(vm.value_to_string(Value::pointer(int_slot)), "-3");
let str_slot = vm
.heap
.alloc(HeapObject::Str("raw text".to_string()))
.expect("alloc");
assert_eq!(
vm.value_to_string(Value::pointer(str_slot)),
"raw text",
"a string renders unquoted"
);
let arr_slot = vm
.heap
.alloc(HeapObject::Array(vec![
Value::pointer(int_slot),
Value::bool(true),
]))
.expect("alloc");
assert_eq!(vm.value_to_string(Value::pointer(arr_slot)), "[-3, true]");
let variant_slot = vm
.heap
.alloc(HeapObject::EnumVariant {
type_name: "Shape".to_string(),
variant: "Circle".to_string(),
payload: vec![Value::pointer(int_slot)],
})
.expect("alloc");
assert_eq!(
vm.value_to_string(Value::pointer(variant_slot)),
"Shape::Circle(-3)"
);
let bare_variant = vm
.heap
.alloc(HeapObject::EnumVariant {
type_name: "Color".to_string(),
variant: "Red".to_string(),
payload: Vec::new(),
})
.expect("alloc");
assert_eq!(
vm.value_to_string(Value::pointer(bare_variant)),
"Color::Red",
"a payload-less variant renders without parentheses"
);
}
#[test]
fn value_to_string_depth_limit_prevents_stack_overflow() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
let leaf = vm.heap.alloc(HeapObject::Int(1)).expect("alloc");
let mut inner = Value::pointer(leaf);
for _ in 0..(MAX_DISPLAY_DEPTH + 2) {
let slot = vm
.heap
.alloc(HeapObject::Array(vec![inner]))
.expect("alloc");
inner = Value::pointer(slot);
}
let result = vm.value_to_string(inner);
assert!(
result.contains("<...>"),
"expected depth sentinel in output, got: {result}"
);
}
#[test]
fn concat_n_joins_several_values_into_one_string() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Str("a=".to_string()), 1);
emit_const(&mut chunk, ConstValue::I64(1), 1);
chunk.write_op(Opcode::ToStr, 1);
emit_const(&mut chunk, ConstValue::Str("!".to_string()), 1);
chunk.write_op(Opcode::ConcatN, 1);
chunk.write_u16(3, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("concat_n");
assert_eq!(top_str(&vm), "a=1!", "CONCAT_N joins in source order");
}
#[test]
fn concat_n_of_an_interpolated_nan_float_renders_the_word_nan() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::Str("v=".to_string()), 1);
emit_const(&mut chunk, ConstValue::F64(f64::NAN), 1);
chunk.write_op(Opcode::ConcatN, 1);
chunk.write_u16(2, 1);
chunk.write_op(Opcode::Halt, 1);
let vm = run_chunk(chunk, "x").expect("concat_n nan");
assert_eq!(top_str(&vm), "v=NaN", "an interpolated NaN renders as NaN");
}
#[test]
fn match_variant_on_a_match_destructures_the_payload_onto_the_stack() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(99), 1);
chunk.write_op(Opcode::MakeEnumVariant, 1);
chunk.write_u16(0, 1);
chunk.code.push(1); chunk.source_lines.push(1);
chunk.write_op(Opcode::MatchVariant, 1);
chunk.write_u16(0, 1);
chunk.write_i16(0, 1);
chunk.write_op(Opcode::Halt, 1);
let mut p = program_with(chunk);
p.enum_variant_names
.push(("Shape".to_string(), "Circle".to_string()));
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("match_variant hit");
assert_eq!(vm.stack.len(), 1, "only the payload remains");
assert_eq!(top_i64(&vm), 99, "the destructured payload is 99");
}
#[test]
fn match_variant_on_a_miss_leaves_the_scrutinee_and_branches_by_the_offset() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1); chunk.write_op(Opcode::MakeEnumVariant, 1); chunk.write_u16(0, 1); chunk.code.push(1); chunk.source_lines.push(1);
chunk.write_op(Opcode::MatchVariant, 1); chunk.write_u16(1, 1); chunk.write_i16(3, 1); emit_const(&mut chunk, ConstValue::I64(777), 1); chunk.write_op(Opcode::Halt, 1); let mut p = program_with(chunk);
p.enum_variant_names
.push(("Shape".to_string(), "Circle".to_string()));
p.enum_variant_names
.push(("Shape".to_string(), "Square".to_string()));
let mut vm = Vm::new(p, "x".to_string());
vm.run().expect("match_variant miss");
assert_eq!(
vm.stack.len(),
1,
"the scrutinee stays, the CONST was skipped"
);
let top = *vm.stack.last().expect("the scrutinee");
let slot = top.as_pointer().expect("the scrutinee is an enum pointer");
assert!(
matches!(vm.heap.get(slot), Some(HeapObject::EnumVariant { .. })),
"the value left on the stack is the original scrutinee"
);
}
#[test]
fn match_variant_of_a_non_enum_scrutinee_is_a_runtime_error_not_a_panic() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(5), 1);
chunk.write_op(Opcode::MatchVariant, 1);
chunk.write_u16(0, 1);
chunk.write_i16(0, 1);
chunk.write_op(Opcode::Halt, 1);
let mut p = program_with(chunk);
p.enum_variant_names
.push(("E".to_string(), "V".to_string()));
let mut vm = Vm::new(p, "x".to_string());
match vm.run() {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("not an enum"), "got: {message}");
}
other => panic!("expected a Runtime non-enum-scrutinee error, got {other:?}"),
}
}
#[test]
fn defer_bytecode_runs_in_lifo_order_at_scope_exit() {
let src = "\
fn first() -> i64 is pure { return 1 }
fn second() -> i64 is pure { return 2 }
fn run() -> i64 is pure {
defer first()
defer second()
return 0
}
fn main() -> i64 is pure { return run() }
";
let program = compile_qala(src);
let run_idx = program
.fn_names
.iter()
.position(|n| n == "run")
.expect("the run function exists");
let code = &program.chunks[run_idx].code;
let mut call_ids: Vec<u16> = Vec::new();
let mut ip = 0;
while ip < code.len() {
let Some(op) = Opcode::from_u8(code[ip]) else {
break;
};
if op == Opcode::Call {
call_ids.push(u16::from_le_bytes([code[ip + 1], code[ip + 2]]));
}
ip += 1 + op.operand_bytes() as usize;
}
let first_id = program.fn_names.iter().position(|n| n == "first").unwrap() as u16;
let second_id = program.fn_names.iter().position(|n| n == "second").unwrap() as u16;
assert_eq!(
call_ids,
vec![second_id, first_id],
"the run chunk must CALL second's defer before first's defer -- LIFO"
);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the program with two defers runs clean");
assert_eq!(
program_result_i64(&vm),
0,
"run returns 0; the defers ran for effect"
);
}
#[test]
fn state_reflects_the_value_stack_and_ip_after_each_step() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(10), 1);
emit_const(&mut chunk, ConstValue::I64(20), 1);
chunk.write_op(Opcode::Add, 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
let s0 = vm.get_state();
assert_eq!(s0.ip, 0);
assert_eq!(s0.chunk_index, 0);
assert!(s0.stack.is_empty(), "no instruction has run yet");
assert_eq!(vm.step().expect("step 1"), StepOutcome::Ran);
let s1 = vm.get_state();
assert_eq!(s1.ip, 3, "CONST advanced ip by its 3 bytes");
assert_eq!(s1.stack.len(), 1);
assert_eq!(s1.stack[0].rendered, "10");
assert_eq!(vm.step().expect("step 2"), StepOutcome::Ran);
let s2 = vm.get_state();
assert_eq!(s2.ip, 6);
assert_eq!(s2.stack.len(), 2);
assert_eq!(s2.stack[1].rendered, "20");
assert_eq!(vm.step().expect("step 3"), StepOutcome::Ran);
let s3 = vm.get_state();
assert_eq!(s3.ip, 7);
assert_eq!(s3.stack.len(), 1, "ADD popped two, pushed one");
assert_eq!(s3.stack[0].rendered, "30", "10 + 20 == 30");
}
#[test]
fn state_current_line_tracks_the_source_map_then_is_zero_after_halt() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 7);
chunk.write_op(Opcode::Halt, 7);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
assert_eq!(vm.step().expect("step 1"), StepOutcome::Ran);
let stepped = vm.get_state();
assert_eq!(stepped.ip, 3, "CONST advanced ip past its 3 bytes");
assert_eq!(
stepped.current_line, 7,
"current_line is the source line the source map records for ip"
);
assert_eq!(vm.step().expect("step 2"), StepOutcome::Halted);
let halted = vm.get_state();
assert_eq!(
halted.current_line, 0,
"a finished program has no current line"
);
}
#[test]
fn state_value_type_name_is_correct_for_each_primitive_kind() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1);
emit_const(&mut chunk, ConstValue::F64(2.5), 1);
emit_const(&mut chunk, ConstValue::Bool(true), 1);
emit_const(&mut chunk, ConstValue::Str("hi".to_string()), 1);
chunk.write_op(Opcode::Halt, 1);
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let state = vm.get_state();
assert_eq!(state.stack.len(), 4, "four values on the stack");
assert_eq!(state.stack[0].type_name, "i64");
assert_eq!(state.stack[1].type_name, "f64");
assert_eq!(state.stack[2].type_name, "bool");
assert_eq!(state.stack[3].type_name, "str");
}
#[test]
fn state_variables_carry_the_real_source_names_from_the_chunk() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(3), 1);
chunk.write_op(Opcode::SetLocal, 1);
chunk.write_u16(0, 1);
emit_const(&mut chunk, ConstValue::I64(9), 1);
chunk.write_op(Opcode::SetLocal, 1);
chunk.write_u16(1, 1);
chunk.write_op(Opcode::Halt, 1);
chunk.local_names = vec!["count".to_string(), "total".to_string()];
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let state = vm.get_state();
assert_eq!(state.variables.len(), 2, "two locals are bound");
assert_eq!(state.variables[0].name, "count", "slot 0 is the real name");
assert_eq!(state.variables[0].value.rendered, "3");
assert_eq!(state.variables[1].name, "total");
assert_eq!(state.variables[1].value.rendered, "9");
}
#[test]
fn state_falls_back_to_slot_index_for_an_unnamed_temporary() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(7), 1);
chunk.write_op(Opcode::SetLocal, 1);
chunk.write_u16(0, 1);
chunk.write_op(Opcode::Halt, 1);
chunk.local_names = vec![String::new()];
let mut vm = Vm::new(program_with(chunk), "x".to_string());
vm.run().expect("run");
let state = vm.get_state();
assert_eq!(state.variables.len(), 1);
assert_eq!(
state.variables[0].name, "slot0",
"an unnamed slot falls back to slot{{i}}"
);
}
#[test]
fn state_on_a_finished_program_is_a_terminal_snapshot_not_a_panic() {
let src = "fn main() -> i64 is pure { return 42 }";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the program runs clean");
let state = vm.get_state();
assert!(
state.variables.is_empty(),
"a finished program has no frame"
);
assert!(
state.stack.iter().any(|s| s.rendered == "42"),
"the program result is visible in the terminal snapshot"
);
}
#[test]
fn get_state_output_is_deterministic_across_two_calls() {
let src = "\
fn main() -> i64 is pure {
let a = 1
let b = 2
return a + b
}
";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
for _ in 0..4 {
if vm.step().expect("step") == StepOutcome::Halted {
break;
}
}
let first = vm.get_state();
let second = vm.get_state();
assert_eq!(first.ip, second.ip);
assert_eq!(first.chunk_index, second.chunk_index);
assert_eq!(first.stack.len(), second.stack.len());
assert_eq!(first.variables.len(), second.variables.len());
for (a, b) in first.variables.iter().zip(second.variables.iter()) {
assert_eq!(a.name, b.name, "variable order is stable across calls");
assert_eq!(a.value.rendered, b.value.rendered);
}
}
#[test]
fn step_advances_ip_by_exactly_one_full_instruction_for_every_width() {
let mut chunk = Chunk::new();
emit_const(&mut chunk, ConstValue::I64(1), 1); chunk.write_op(Opcode::Pop, 1); emit_const(&mut chunk, ConstValue::I64(2), 1); chunk.write_op(Opcode::Halt, 1); let mut vm = Vm::new(program_with(chunk), "x".to_string());
let before1 = vm.frame().expect("frame").ip;
assert_eq!(vm.step().expect("step 1"), StepOutcome::Ran);
let after1 = vm.frame().expect("frame").ip;
assert_eq!(
after1 - before1,
1 + Opcode::Const.operand_bytes() as usize,
"CONST advances by 1 + its operand width"
);
let before2 = vm.frame().expect("frame").ip;
assert_eq!(vm.step().expect("step 2"), StepOutcome::Ran);
let after2 = vm.frame().expect("frame").ip;
assert_eq!(
after2 - before2,
1 + Opcode::Pop.operand_bytes() as usize,
"POP is a zero-operand opcode -- ip advances by exactly 1"
);
let before3 = vm.frame().expect("frame").ip;
assert_eq!(vm.step().expect("step 3"), StepOutcome::Ran);
let after3 = vm.frame().expect("frame").ip;
assert_eq!(
after3 - before3,
1 + Opcode::Const.operand_bytes() as usize,
"the second CONST advances by 1 + its operand width"
);
}
#[test]
fn vm_state_implements_serialize_for_the_wasm_bridge() {
fn assert_serialize<T: serde::Serialize>(_: &T) {}
let src = "fn main() -> i64 is pure { return 5 }";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("run");
let state = vm.get_state();
assert_serialize(&state);
}
#[test]
fn runtime_type_name_renders_compound_types_structurally() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
let int_slot = vm.heap.alloc(HeapObject::Int(1)).expect("alloc int");
let arr = vm
.heap
.alloc(HeapObject::Array(vec![Value::pointer(int_slot)]))
.expect("alloc array");
assert_eq!(vm.runtime_type_name(Value::pointer(arr)), "[i64]");
let empty = vm.heap.alloc(HeapObject::Array(Vec::new())).expect("alloc");
assert_eq!(
vm.runtime_type_name(Value::pointer(empty)),
"[]",
"an empty array has no element type to show"
);
let s = vm.heap.alloc(HeapObject::Str("k".to_string())).expect("s");
let tup = vm
.heap
.alloc(HeapObject::Tuple(vec![
Value::pointer(int_slot),
Value::pointer(s),
]))
.expect("alloc tuple");
assert_eq!(vm.runtime_type_name(Value::pointer(tup)), "(i64, str)");
}
#[test]
fn runtime_type_name_depth_limit_prevents_stack_overflow() {
let mut vm = Vm::new(program_with(Chunk::new()), "x".to_string());
let leaf = vm.heap.alloc(HeapObject::Int(1)).expect("alloc");
let mut inner = Value::pointer(leaf);
for _ in 0..(MAX_DISPLAY_DEPTH + 2) {
let slot = vm
.heap
.alloc(HeapObject::Array(vec![inner]))
.expect("alloc");
inner = Value::pointer(slot);
}
let result = vm.runtime_type_name(inner);
assert!(
result.contains("..."),
"expected depth sentinel in type name, got: {result}"
);
}
fn repl_result_i64(vm: &Vm, v: Value) -> i64 {
let slot = v.as_pointer().expect("a REPL i64 result is a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Int(n)) => *n,
_ => panic!("the REPL result pointer does not reach a heap Int"),
}
}
#[test]
fn repl_persists_a_binding_from_one_call_to_the_next() {
let mut vm = Vm::new_repl();
let first = vm
.repl_eval("let x = 5")
.expect("the let binding compiles and runs");
assert!(first.as_void(), "a let-statement REPL line yields void");
let second = vm
.repl_eval("x + 1")
.expect("the expression sees the prior binding");
assert_eq!(
repl_result_i64(&vm, second),
6,
"x (5) defined on the first call plus 1 is 6 on the second"
);
}
#[test]
fn repl_evaluates_a_standalone_expression() {
let mut vm = Vm::new_repl();
let r = vm.repl_eval("2 * 21").expect("a bare expression evaluates");
assert_eq!(repl_result_i64(&vm, r), 42);
}
#[test]
fn render_value_renders_an_i64_result_to_its_display_and_type_pair() {
let mut vm = Vm::new_repl();
vm.repl_eval("let x = 5")
.expect("the let binding compiles and runs");
let result = vm
.repl_eval("x + 1")
.expect("the expression sees the prior binding");
let (display, type_name) = vm.render_value(result);
assert_eq!(display, "6", "x (5) + 1 displays as 6");
assert_eq!(type_name, "i64", "the result is an i64");
}
#[test]
fn repl_keeps_the_console_buffer_across_calls() {
let mut vm = Vm::new_repl();
vm.repl_eval("let x = 1").expect("first call");
vm.repl_eval("println(\"output one\")")
.expect("println call");
vm.repl_eval("let y = 2").expect("a later call");
assert!(
vm.console
.iter()
.any(|line| line.trim_end_matches('\n') == "output one"),
"a repl call must not clear the console -- output accumulates"
);
}
#[test]
fn repl_a_non_compiling_line_does_not_poison_later_calls() {
let mut vm = Vm::new_repl();
vm.repl_eval("let x = 10")
.expect("the first binding is fine");
match vm.repl_eval("let bad = no_such_name") {
Err(_) => {} Ok(_) => panic!("a line with an undefined name must not compile"),
}
let r = vm
.repl_eval("x + 2")
.expect("the earlier binding is intact after the bad line");
assert_eq!(
repl_result_i64(&vm, r),
12,
"x (10) is still in scope -- the bad line did not poison the history"
);
}
#[test]
fn repl_a_parse_error_line_is_also_not_appended_to_history() {
let mut vm = Vm::new_repl();
vm.repl_eval("let a = 7").expect("a valid binding");
match vm.repl_eval("let = = =") {
Err(_) => {}
Ok(_) => panic!("a malformed line must not compile"),
}
let r = vm
.repl_eval("a")
.expect("the binding survives the parse error");
assert_eq!(repl_result_i64(&vm, r), 7);
}
#[test]
fn repl_a_function_declaration_line_is_callable_on_a_later_call() {
let mut vm = Vm::new_repl();
let decl = vm
.repl_eval("fn triple(n: i64) -> i64 is pure { return n * 3 }")
.expect("a fn declaration compiles");
assert!(decl.as_void(), "an item-declaration REPL line yields void");
let r = vm
.repl_eval("triple(14)")
.expect("the declared function is callable later");
assert_eq!(repl_result_i64(&vm, r), 42, "triple(14) is 42");
}
#[test]
fn repl_call_expression_is_classified_as_expression_not_item() {
let mut vm = Vm::new_repl();
vm.repl_eval("fn triple(n: i64) -> i64 is pure { return n * 3 }")
.expect("fn declaration compiles");
let r = vm
.repl_eval("triple(14)")
.expect("the call expression evaluates");
assert_eq!(
repl_result_i64(&vm, r),
42,
"a call expression is an Expression -- its value must be returned, not void"
);
}
#[test]
fn repl_new_repl_starts_blank() {
let vm = Vm::new_repl();
assert!(vm.program.chunks.is_empty(), "no program yet");
assert!(vm.frames.is_empty(), "no frame until the first repl call");
assert!(vm.repl_history.is_empty(), "an empty accumulated history");
assert!(vm.console.is_empty(), "an empty console");
}
#[test]
fn stdlib_call_runs_a_native_function_end_to_end() {
let src = "fn main() is io { println(\"hi from qala\") }\n";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("a program that calls println runs clean");
assert!(
vm.console
.iter()
.any(|line| line.trim_end_matches('\n') == "hi from qala"),
"println wrote its argument to the console, got: {:?}",
vm.console
);
}
#[test]
fn stdlib_len_call_returns_the_collection_length() {
let src = "fn main() -> i64 is pure { return len([10, 20, 30]) }\n";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("a program that calls len runs clean");
assert_eq!(program_result_i64(&vm), 3, "len([10,20,30]) is 3");
}
#[test]
fn leak_detected_for_a_file_handle_dropped_without_close() {
let src = "fn main() is io {\n let f = open(\"data.txt\")\n}\n";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run()
.expect("the program itself runs clean -- a leak is not an error");
assert!(
!vm.leak_log.is_empty(),
"an open handle dropped without close must be logged as a leak"
);
assert!(
vm.leak_log.iter().any(|m| m.contains("data.txt")),
"the leak message names the leaked handle's path, got: {:?}",
vm.leak_log
);
}
#[test]
fn no_leak_when_a_file_handle_is_closed_with_defer() {
let src = "fn use_handle() -> i64 is io {\n \
let f = open(\"data.txt\")\n \
defer close(f)\n \
return 0\n}\n\
fn main() is io {\n let n = use_handle()\n}\n";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the program runs clean");
assert!(
vm.leak_log.is_empty(),
"a handle closed via defer must not be logged as a leak, got: {:?}",
vm.leak_log
);
}
#[test]
fn no_leak_when_a_file_handle_is_closed_explicitly() {
let src = "fn main() is io {\n \
let f = open(\"data.txt\")\n \
close(f)\n}\n";
let program = compile_qala(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the program runs clean");
assert!(
vm.leak_log.is_empty(),
"an explicitly closed handle must not leak, got: {:?}",
vm.leak_log
);
}
}