use std::collections::HashMap;
use std::rc::Rc;
use crate::ast::*;
use crate::builtins::Builtin;
use crate::interpreter::Value;
#[derive(Debug, thiserror::Error)]
pub enum VmError {
#[error("no functions defined")]
NoFunctionsDefined,
#[error("undefined function: {name}")]
UndefinedFunction { name: String },
#[error("division by zero")]
DivisionByZero,
#[error("no field '{field}' on record")]
FieldNotFound { field: String },
#[error("unknown opcode: {op}")]
UnknownOpcode { op: u8 },
#[error("{0}")]
Type(&'static str),
}
type VmResult<T> = Result<T, VmError>;
#[derive(Debug)]
pub struct VmRuntimeError {
pub error: VmError,
pub span: Option<crate::ast::Span>,
pub call_stack: Vec<String>,
}
impl std::fmt::Display for VmRuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for VmRuntimeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error("undefined variable: {name}")]
UndefinedVariable { name: String },
#[error("undefined function: {name}")]
UndefinedFunction { name: String },
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
pub(crate) mod jit_arm64;
#[cfg(feature = "cranelift")]
pub(crate) mod jit_cranelift;
#[cfg(feature = "llvm")]
pub(crate) mod jit_llvm;
pub(crate) const OP_ADD: u8 = 0;
pub(crate) const OP_SUB: u8 = 1;
pub(crate) const OP_MUL: u8 = 2;
pub(crate) const OP_DIV: u8 = 3;
pub(crate) const OP_EQ: u8 = 4;
pub(crate) const OP_NE: u8 = 5;
pub(crate) const OP_GT: u8 = 6;
pub(crate) const OP_LT: u8 = 7;
pub(crate) const OP_GE: u8 = 8;
pub(crate) const OP_LE: u8 = 9;
pub(crate) const OP_MOVE: u8 = 10;
pub(crate) const OP_NOT: u8 = 11;
pub(crate) const OP_NEG: u8 = 12;
pub(crate) const OP_WRAPOK: u8 = 13;
pub(crate) const OP_WRAPERR: u8 = 14;
pub(crate) const OP_ISOK: u8 = 15;
pub(crate) const OP_ISERR: u8 = 16;
pub(crate) const OP_UNWRAP: u8 = 17;
pub(crate) const OP_RECFLD: u8 = 18;
pub(crate) const OP_LISTGET: u8 = 19;
pub(crate) const OP_ADD_NN: u8 = 29;
pub(crate) const OP_SUB_NN: u8 = 30;
pub(crate) const OP_MUL_NN: u8 = 31;
pub(crate) const OP_DIV_NN: u8 = 32;
pub(crate) const OP_ADDK_N: u8 = 33; pub(crate) const OP_SUBK_N: u8 = 34; pub(crate) const OP_MULK_N: u8 = 35; pub(crate) const OP_DIVK_N: u8 = 36;
pub(crate) const OP_LEN: u8 = 37; pub(crate) const OP_LISTAPPEND: u8 = 38; pub(crate) const OP_INDEX: u8 = 39; pub(crate) const OP_STR: u8 = 40; pub(crate) const OP_NUM: u8 = 41; pub(crate) const OP_ABS: u8 = 42; pub(crate) const OP_MIN: u8 = 43; pub(crate) const OP_MAX: u8 = 44; pub(crate) const OP_FLR: u8 = 45; pub(crate) const OP_CEL: u8 = 46; pub(crate) const OP_GET: u8 = 47; pub(crate) const OP_SPL: u8 = 48; pub(crate) const OP_CAT: u8 = 49; pub(crate) const OP_HAS: u8 = 50; pub(crate) const OP_HD: u8 = 51; pub(crate) const OP_TL: u8 = 52; pub(crate) const OP_REV: u8 = 53; pub(crate) const OP_SRT: u8 = 54; pub(crate) const OP_SLC: u8 = 55; pub(crate) const OP_RND0: u8 = 57; pub(crate) const OP_RND2: u8 = 58; pub(crate) const OP_NOW: u8 = 59; pub(crate) const OP_ENV: u8 = 60; pub(crate) const OP_JPTH: u8 = 61; pub(crate) const OP_JDMP: u8 = 62; pub(crate) const OP_JPAR: u8 = 63; pub(crate) const OP_RECFLD_NAME: u8 = 64; pub(crate) const OP_JMPNN: u8 = 56; pub(crate) const OP_ISNUM: u8 = 65; pub(crate) const OP_ISTEXT: u8 = 66; pub(crate) const OP_ISBOOL: u8 = 67; pub(crate) const OP_ISLIST: u8 = 68; pub(crate) const OP_MAPNEW: u8 = 69; pub(crate) const OP_MGET: u8 = 70; pub(crate) const OP_MSET: u8 = 71; pub(crate) const OP_MHAS: u8 = 72; pub(crate) const OP_MKEYS: u8 = 73; pub(crate) const OP_MVALS: u8 = 74; pub(crate) const OP_MDEL: u8 = 75; pub(crate) const OP_PRT: u8 = 76; pub(crate) const OP_RD: u8 = 77; pub(crate) const OP_RDL: u8 = 78; pub(crate) const OP_WR: u8 = 79; pub(crate) const OP_WRL: u8 = 80; pub(crate) const OP_TRM: u8 = 81; pub(crate) const OP_UNQ: u8 = 82; pub(crate) const OP_POST: u8 = 83; pub(crate) const OP_GETH: u8 = 84; pub(crate) const OP_POSTH: u8 = 85; pub(crate) const OP_MOD: u8 = 86; pub(crate) const OP_ROU: u8 = 87;
pub(crate) const OP_LOADK: u8 = 20;
pub(crate) const OP_JMP: u8 = 21;
pub(crate) const OP_JMPF: u8 = 22;
pub(crate) const OP_JMPT: u8 = 23;
pub(crate) const OP_CALL: u8 = 24;
pub(crate) const OP_RET: u8 = 25;
pub(crate) const OP_RECNEW: u8 = 26;
pub(crate) const OP_RECWITH: u8 = 27;
pub(crate) const OP_LISTNEW: u8 = 28;
#[inline(always)]
fn encode_abc(op: u8, a: u8, b: u8, c: u8) -> u32 {
(op as u32) << 24 | (a as u32) << 16 | (b as u32) << 8 | c as u32
}
#[inline(always)]
fn encode_abx(op: u8, a: u8, bx: u16) -> u32 {
(op as u32) << 24 | (a as u32) << 16 | bx as u32
}
#[derive(Debug, Clone, Default)]
pub struct Chunk {
pub code: Vec<u32>,
pub constants: Vec<Value>,
#[allow(dead_code)] pub param_count: u8,
pub reg_count: u8,
pub spans: Vec<crate::ast::Span>,
}
impl Chunk {
fn new(param_count: u8) -> Self {
Chunk { code: Vec::new(), constants: Vec::new(), param_count, reg_count: param_count, spans: Vec::new() }
}
fn add_const(&mut self, val: Value) -> u16 {
for (i, c) in self.constants.iter().enumerate() {
match (c, &val) {
(Value::Number(a), Value::Number(b)) if (a - b).abs() < f64::EPSILON => return i as u16,
(Value::Text(a), Value::Text(b)) if a == b => return i as u16,
(Value::Bool(a), Value::Bool(b)) if a == b => return i as u16,
(Value::Nil, Value::Nil) => return i as u16,
_ => {}
}
}
let idx = self.constants.len();
assert!(idx <= u16::MAX as usize, "constant pool overflow: more than 65535 constants in one function");
self.constants.push(val);
idx as u16
}
fn add_const_raw(&mut self, val: Value) -> u16 {
let idx = self.constants.len();
assert!(idx <= u16::MAX as usize, "constant pool overflow: more than 65535 constants in one function");
self.constants.push(val);
idx as u16
}
fn emit(&mut self, inst: u32, span: crate::ast::Span) -> usize {
let idx = self.code.len();
self.code.push(inst);
self.spans.push(span);
idx
}
fn patch_jump(&mut self, jump_pos: usize) {
let target = self.code.len();
let offset_i32 = target as i32 - jump_pos as i32 - 1;
assert!(
offset_i32 >= i16::MIN as i32 && offset_i32 <= i16::MAX as i32,
"jump offset {offset_i32} exceeds i16 range — function body too large (max ~32K instructions)"
);
let offset = offset_i32 as i16;
let inst = self.code[jump_pos];
self.code[jump_pos] = (inst & 0xFFFF0000) | (offset as u16 as u32);
}
}
#[derive(Debug, Clone)]
pub(crate) struct TypeInfo {
pub name: String,
pub fields: Vec<String>, pub num_fields: u64, }
#[derive(Debug, Clone, Default)]
pub(crate) struct TypeRegistry {
pub types: Vec<Rc<TypeInfo>>,
pub name_to_id: HashMap<String, u16>,
}
impl TypeRegistry {
fn register(&mut self, name: String, fields: Vec<String>, num_fields: u64) -> u16 {
if let Some(&id) = self.name_to_id.get(&name) {
return id;
}
let id = self.types.len() as u16;
self.name_to_id.insert(name.clone(), id);
self.types.push(Rc::new(TypeInfo { name, fields, num_fields }));
id
}
fn field_index(&self, type_id: u16, field: &str) -> Option<usize> {
self.types.get(type_id as usize)
.and_then(|info| info.fields.iter().position(|f| f == field))
}
}
pub struct CompiledProgram {
pub chunks: Vec<Chunk>,
pub func_names: Vec<String>,
pub(crate) nan_constants: Vec<Vec<NanVal>>,
pub(crate) type_registry: TypeRegistry,
pub is_tool: Vec<bool>,
}
impl CompiledProgram {
fn func_index(&self, name: &str) -> Option<u16> {
self.func_names.iter().position(|n| n == name).map(|i| i as u16)
}
}
impl Drop for CompiledProgram {
fn drop(&mut self) {
for chunk_consts in &self.nan_constants {
for v in chunk_consts {
v.drop_rc();
}
}
}
}
struct LoopContext {
loop_top: usize,
continue_patches: Option<Vec<usize>>,
break_patches: Vec<usize>,
result_reg: u8,
}
struct RegCompiler {
chunks: Vec<Chunk>,
func_names: Vec<String>,
current: Chunk,
locals: Vec<(String, u8)>,
next_reg: u8,
max_reg: u8,
reg_is_num: [bool; 256], reg_record_type: [u16; 256], first_error: Option<CompileError>,
current_span: crate::ast::Span,
loop_stack: Vec<LoopContext>,
type_registry: TypeRegistry,
func_return_types: Vec<Type>, }
impl RegCompiler {
fn new() -> Self {
RegCompiler {
chunks: Vec::new(),
func_names: Vec::new(),
current: Chunk::new(0),
locals: Vec::new(),
next_reg: 0,
max_reg: 0,
reg_is_num: [false; 256],
reg_record_type: [u16::MAX; 256],
first_error: None,
current_span: crate::ast::Span::UNKNOWN,
loop_stack: Vec::new(),
type_registry: TypeRegistry::default(),
func_return_types: Vec::new(),
}
}
fn alloc_reg(&mut self) -> u8 {
assert!(self.next_reg < 255, "register overflow: function uses more than 255 registers");
let r = self.next_reg;
self.next_reg += 1;
if self.next_reg > self.max_reg {
self.max_reg = self.next_reg;
}
self.reg_is_num[r as usize] = false;
self.reg_record_type[r as usize] = u16::MAX;
r
}
fn resolve_local(&self, name: &str) -> Option<u8> {
self.locals.iter().rev().find(|(n, _)| n == name).map(|(_, r)| *r)
}
fn add_local(&mut self, name: &str, reg: u8) {
self.locals.push((name.to_string(), reg));
}
fn emit_abc(&mut self, op: u8, a: u8, b: u8, c: u8) -> usize {
self.current.emit(encode_abc(op, a, b, c), self.current_span)
}
fn emit_abx(&mut self, op: u8, a: u8, bx: u16) -> usize {
self.current.emit(encode_abx(op, a, bx), self.current_span)
}
fn emit_jmpf(&mut self, reg: u8) -> usize {
self.emit_abx(OP_JMPF, reg, 0)
}
fn emit_jmpt(&mut self, reg: u8) -> usize {
self.emit_abx(OP_JMPT, reg, 0)
}
fn emit_jmp_placeholder(&mut self) -> usize {
self.emit_abx(OP_JMP, 0, 0)
}
fn emit_jump_to(&mut self, target: usize) {
let pos = self.current.code.len();
let offset_i32 = target as i32 - pos as i32 - 1;
assert!(
offset_i32 >= i16::MIN as i32 && offset_i32 <= i16::MAX as i32,
"jump offset {offset_i32} exceeds i16 range — function body too large (max ~32K instructions)"
);
self.emit_abx(OP_JMP, 0, offset_i32 as i16 as u16);
}
fn resolve_type_id(&self, ty: &Type) -> u16 {
match ty {
Type::Named(name) => self.type_registry.name_to_id.get(name).copied().unwrap_or(u16::MAX),
_ => u16::MAX,
}
}
fn search_field_index(&self, field: &str) -> Option<usize> {
let mut found_idx = None;
for info in self.type_registry.types.iter() {
if let Some(idx) = info.fields.iter().position(|f| f == field) {
match found_idx {
None => found_idx = Some(idx),
Some(prev) if prev == idx => {} Some(_) => return None, }
}
}
found_idx
}
fn compile_program(mut self, program: &Program) -> Result<CompiledProgram, CompileError> {
for decl in &program.declarations {
if let Decl::TypeDef { name, fields, .. } = decl {
let field_names: Vec<String> = fields.iter().map(|p| p.name.clone()).collect();
let mut num_fields: u64 = 0;
for (i, p) in fields.iter().enumerate() {
if p.ty == crate::ast::Type::Number && i < 64 {
num_fields |= 1 << i;
}
}
self.type_registry.register(name.clone(), field_names, num_fields);
}
}
let mut is_tool: Vec<bool> = Vec::new();
for decl in &program.declarations {
match decl {
Decl::Function { name, return_type, .. } => {
self.func_names.push(name.clone());
self.func_return_types.push(return_type.clone());
is_tool.push(false);
}
Decl::Tool { name, return_type, .. } => {
self.func_names.push(name.clone());
self.func_return_types.push(return_type.clone());
is_tool.push(true);
}
Decl::TypeDef { .. } | Decl::Alias { .. } | Decl::Use { .. } | Decl::Error { .. } => {}
}
}
for decl in &program.declarations {
if let Decl::Function { params, body, .. } = decl {
assert!(
params.len() <= 255,
"function has {} parameters; maximum is 255",
params.len()
);
self.current = Chunk::new(params.len() as u8);
self.locals.clear();
self.next_reg = params.len() as u8;
self.max_reg = self.next_reg;
self.reg_is_num = [false; 256];
self.reg_record_type = [u16::MAX; 256];
for (i, p) in params.iter().enumerate() {
self.add_local(&p.name, i as u8);
if p.ty == Type::Number {
self.reg_is_num[i] = true;
}
self.reg_record_type[i] = self.resolve_type_id(&p.ty);
}
let result = self.compile_body(body);
let ret_reg = result.unwrap_or_else(|| {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
});
let last_is_ret = self.current.code.last()
.map(|inst| (inst >> 24) as u8 == OP_RET)
.unwrap_or(false);
if !last_is_ret {
self.emit_abx(OP_RET, ret_reg, 0);
}
self.current.reg_count = self.max_reg;
self.chunks.push(std::mem::take(&mut self.current));
} else if let Decl::Tool { params, .. } = decl {
self.current = Chunk::new(params.len() as u8);
self.next_reg = params.len() as u8;
self.max_reg = self.next_reg;
let nil_reg = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, nil_reg, ki);
let ok_reg = self.alloc_reg();
self.emit_abc(OP_WRAPOK, ok_reg, nil_reg, 0);
self.emit_abx(OP_RET, ok_reg, 0);
self.current.reg_count = self.max_reg;
self.chunks.push(std::mem::take(&mut self.current));
}
}
if let Some(e) = self.first_error {
return Err(e);
}
Ok(CompiledProgram { chunks: self.chunks, func_names: self.func_names, nan_constants: Vec::new(), type_registry: self.type_registry, is_tool })
}
fn compile_body(&mut self, stmts: &[crate::ast::Spanned<Stmt>]) -> Option<u8> {
let saved_locals = self.locals.len();
let mut result = None;
for spanned in stmts {
self.current_span = spanned.span;
result = self.compile_stmt(&spanned.node);
}
self.locals.truncate(saved_locals);
result
}
fn compile_stmt(&mut self, stmt: &Stmt) -> Option<u8> {
match stmt {
Stmt::Let { name, value } => {
if let Some(existing_reg) = self.resolve_local(name) {
let reg = self.compile_expr(value);
if reg != existing_reg {
self.emit_abc(OP_MOVE, existing_reg, reg, 0);
self.reg_record_type[existing_reg as usize] = self.reg_record_type[reg as usize];
}
} else {
let reg = self.compile_expr(value);
self.add_local(name, reg);
}
None
}
Stmt::Destructure { bindings, value } => {
let record_reg = self.compile_expr(value);
let rec_type = self.reg_record_type[record_reg as usize];
for binding in bindings {
let field_idx = if rec_type != u16::MAX {
self.type_registry.field_index(rec_type, binding)
} else {
self.search_field_index(binding)
};
match field_idx {
Some(idx) => {
let c = idx as u8;
if let Some(existing_reg) = self.resolve_local(binding) {
self.emit_abc(OP_RECFLD, existing_reg, record_reg, c);
} else {
let field_reg = self.alloc_reg();
self.emit_abc(OP_RECFLD, field_reg, record_reg, c);
self.add_local(binding, field_reg);
}
}
None => {
let ki = self.current.add_const(Value::Text(binding.clone()));
assert!(ki <= 255, "constant pool overflow for dynamic destructure field");
if let Some(existing_reg) = self.resolve_local(binding) {
self.emit_abc(OP_RECFLD_NAME, existing_reg, record_reg, ki as u8);
} else {
let field_reg = self.alloc_reg();
self.emit_abc(OP_RECFLD_NAME, field_reg, record_reg, ki as u8);
self.add_local(binding, field_reg);
}
}
}
}
None
}
Stmt::Guard { condition, negated, body, else_body } => {
let saved_next = self.next_reg;
let cond_reg = self.compile_expr(condition);
let jump = if *negated {
self.emit_jmpt(cond_reg)
} else {
self.emit_jmpf(cond_reg)
};
if let Some(else_b) = else_body {
let result_reg = self.alloc_reg();
let then_result = self.compile_body(body);
let then_reg = then_result.unwrap_or_else(|| {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
});
if then_reg != result_reg {
self.emit_abc(OP_MOVE, result_reg, then_reg, 0);
}
let jump_over_else = self.emit_jmp_placeholder();
self.current.patch_jump(jump);
self.next_reg = result_reg + 1;
let else_result = self.compile_body(else_b);
let else_reg = else_result.unwrap_or_else(|| {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
});
if else_reg != result_reg {
self.emit_abc(OP_MOVE, result_reg, else_reg, 0);
}
self.current.patch_jump(jump_over_else);
self.next_reg = result_reg + 1;
Some(result_reg)
} else {
let body_result = self.compile_body(body);
let ret_reg = body_result.unwrap_or_else(|| {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
});
self.emit_abx(OP_RET, ret_reg, 0);
self.current.patch_jump(jump);
self.next_reg = saved_next;
None
}
}
Stmt::Match { subject, arms } => {
let sub_reg = match subject {
Some(e) => self.compile_expr(e),
None => {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
}
};
let result_reg = self.alloc_reg();
self.compile_match_arms(sub_reg, result_reg, arms);
Some(result_reg)
}
Stmt::ForEach { binding, collection, body } => {
let coll_reg = self.compile_expr(collection);
self.add_local("__fe_coll", coll_reg);
let idx_reg = self.alloc_reg();
let zero_ki = self.current.add_const(Value::Number(0.0));
self.emit_abx(OP_LOADK, idx_reg, zero_ki);
self.add_local("__fe_idx", idx_reg);
let last_reg = self.alloc_reg();
let nil_ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, last_reg, nil_ki);
self.add_local("__fe_last", last_reg);
let bind_reg = self.alloc_reg();
self.emit_abx(OP_LOADK, bind_reg, nil_ki);
self.add_local(binding, bind_reg);
let one_reg = self.alloc_reg();
let one_ki = self.current.add_const(Value::Number(1.0));
self.emit_abx(OP_LOADK, one_reg, one_ki);
let loop_top = self.current.code.len();
self.emit_abc(OP_LISTGET, bind_reg, coll_reg, idx_reg);
let exit_jump = self.emit_jmp_placeholder();
self.loop_stack.push(LoopContext {
loop_top,
continue_patches: Some(Vec::new()), break_patches: Vec::new(),
result_reg: last_reg,
});
let saved_locals = self.locals.len();
let body_result = self.compile_body(body);
self.locals.truncate(saved_locals);
if let Some(br) = body_result
&& br != last_reg {
self.emit_abc(OP_MOVE, last_reg, br, 0);
}
let continue_target = self.current.code.len();
if let Some(patches) = &self.loop_stack.last().unwrap().continue_patches {
let patches: Vec<usize> = patches.clone();
for patch in patches {
let offset = continue_target as isize - patch as isize - 1;
let encoded = encode_abx(OP_JMP, 0, offset as i16 as u16);
self.current.code[patch] = encoded;
}
}
self.emit_abc(OP_ADD, idx_reg, idx_reg, one_reg);
self.emit_jump_to(loop_top);
self.current.patch_jump(exit_jump);
let ctx = self.loop_stack.pop().unwrap();
for patch in ctx.break_patches {
self.current.patch_jump(patch);
}
Some(last_reg)
}
Stmt::ForRange { binding, start, end, body } => {
let start_reg = self.compile_expr(start);
let end_reg = self.compile_expr(end);
let last_reg = self.alloc_reg();
let nil_ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, last_reg, nil_ki);
self.add_local("__fr_last", last_reg);
let counter_reg = self.alloc_reg();
self.emit_abc(OP_MOVE, counter_reg, start_reg, 0);
self.add_local(binding, counter_reg);
let one_reg = self.alloc_reg();
let one_ki = self.current.add_const(Value::Number(1.0));
self.emit_abx(OP_LOADK, one_reg, one_ki);
let loop_top = self.current.code.len();
let cmp_reg = self.alloc_reg();
self.emit_abc(OP_LT, cmp_reg, counter_reg, end_reg);
let exit_jump = self.emit_jmpf(cmp_reg);
self.loop_stack.push(LoopContext {
loop_top,
continue_patches: Some(Vec::new()),
break_patches: Vec::new(),
result_reg: last_reg,
});
let saved_locals = self.locals.len();
let body_result = self.compile_body(body);
self.locals.truncate(saved_locals);
if let Some(br) = body_result
&& br != last_reg {
self.emit_abc(OP_MOVE, last_reg, br, 0);
}
let continue_target = self.current.code.len();
if let Some(patches) = &self.loop_stack.last().unwrap().continue_patches {
let patches: Vec<usize> = patches.clone();
for patch in patches {
let offset = continue_target as isize - patch as isize - 1;
let encoded = encode_abx(OP_JMP, 0, offset as i16 as u16);
self.current.code[patch] = encoded;
}
}
self.emit_abc(OP_ADD, counter_reg, counter_reg, one_reg);
self.emit_jump_to(loop_top);
self.current.patch_jump(exit_jump);
let ctx = self.loop_stack.pop().unwrap();
for patch in ctx.break_patches {
self.current.patch_jump(patch);
}
Some(last_reg)
}
Stmt::While { condition, body } => {
let last_reg = self.alloc_reg();
let nil_ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, last_reg, nil_ki);
let loop_top = self.current.code.len();
let cond_reg = self.compile_expr(condition);
let exit_jump = self.emit_jmpf(cond_reg);
self.loop_stack.push(LoopContext {
loop_top,
continue_patches: None, break_patches: Vec::new(),
result_reg: last_reg,
});
let saved_locals = self.locals.len();
let body_result = self.compile_body(body);
self.locals.truncate(saved_locals);
if let Some(br) = body_result
&& br != last_reg {
self.emit_abc(OP_MOVE, last_reg, br, 0);
}
self.emit_jump_to(loop_top);
self.current.patch_jump(exit_jump);
let ctx = self.loop_stack.pop().unwrap();
for patch in ctx.break_patches {
self.current.patch_jump(patch);
}
Some(last_reg)
}
Stmt::Return(expr) => {
let reg = self.compile_expr(expr);
self.emit_abx(OP_RET, reg, 0);
None
}
Stmt::Break(expr) => {
if let Some(ctx) = self.loop_stack.last() {
let result_reg = ctx.result_reg;
if let Some(e) = expr {
let reg = self.compile_expr(e);
if reg != result_reg {
self.emit_abc(OP_MOVE, result_reg, reg, 0);
}
}
let jmp = self.emit_jmp_placeholder();
if let Some(ctx) = self.loop_stack.last_mut() {
ctx.break_patches.push(jmp);
}
}
None
}
Stmt::Continue => {
if let Some(ctx) = self.loop_stack.last() {
if ctx.continue_patches.is_some() {
let jmp = self.emit_jmp_placeholder();
if let Some(ctx) = self.loop_stack.last_mut()
&& let Some(patches) = ctx.continue_patches.as_mut() {
patches.push(jmp);
}
} else {
let top = ctx.loop_top;
self.emit_jump_to(top);
}
}
None
}
Stmt::Expr(expr) => {
let reg = self.compile_expr(expr);
Some(reg)
}
}
}
fn compile_match_arms(&mut self, sub_reg: u8, result_reg: u8, arms: &[MatchArm]) {
let mut end_jumps = Vec::with_capacity(arms.len());
for arm in arms {
let saved_next = self.next_reg;
let saved_locals = self.locals.len();
match &arm.pattern {
Pattern::Wildcard => {
let body_result = self.compile_body(&arm.body);
if let Some(br) = body_result
&& br != result_reg {
self.emit_abc(OP_MOVE, result_reg, br, 0);
}
self.next_reg = saved_next;
self.locals.truncate(saved_locals);
for j in end_jumps {
self.current.patch_jump(j);
}
return;
}
Pattern::Ok(binding) => {
let test_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, test_reg, sub_reg, 0);
let skip = self.emit_jmpf(test_reg);
if binding != "_" {
let bind_reg = self.alloc_reg();
self.emit_abc(OP_UNWRAP, bind_reg, sub_reg, 0);
self.add_local(binding, bind_reg);
}
let body_result = self.compile_body(&arm.body);
if let Some(br) = body_result
&& br != result_reg {
self.emit_abc(OP_MOVE, result_reg, br, 0);
}
end_jumps.push(self.emit_jmp_placeholder());
self.current.patch_jump(skip);
}
Pattern::Err(binding) => {
let test_reg = self.alloc_reg();
self.emit_abc(OP_ISERR, test_reg, sub_reg, 0);
let skip = self.emit_jmpf(test_reg);
if binding != "_" {
let bind_reg = self.alloc_reg();
self.emit_abc(OP_UNWRAP, bind_reg, sub_reg, 0);
self.add_local(binding, bind_reg);
}
let body_result = self.compile_body(&arm.body);
if let Some(br) = body_result
&& br != result_reg {
self.emit_abc(OP_MOVE, result_reg, br, 0);
}
end_jumps.push(self.emit_jmp_placeholder());
self.current.patch_jump(skip);
}
Pattern::Literal(lit) => {
let val = match lit {
Literal::Number(n) => Value::Number(*n),
Literal::Text(s) => Value::Text(s.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Nil => Value::Nil,
};
let const_reg = self.alloc_reg();
let ki = self.current.add_const(val);
self.emit_abx(OP_LOADK, const_reg, ki);
let eq_reg = self.alloc_reg();
self.emit_abc(OP_EQ, eq_reg, sub_reg, const_reg);
let skip = self.emit_jmpf(eq_reg);
let body_result = self.compile_body(&arm.body);
if let Some(br) = body_result
&& br != result_reg {
self.emit_abc(OP_MOVE, result_reg, br, 0);
}
end_jumps.push(self.emit_jmp_placeholder());
self.current.patch_jump(skip);
}
Pattern::TypeIs { ty, binding } => {
let opcode = match ty {
Type::Number => OP_ISNUM,
Type::Text => OP_ISTEXT,
Type::Bool => OP_ISBOOL,
Type::List(_) => OP_ISLIST,
_ => OP_ISNUM, };
let test_reg = self.alloc_reg();
self.emit_abc(opcode, test_reg, sub_reg, 0);
let skip = self.emit_jmpf(test_reg);
if binding != "_" {
let bind_reg = self.alloc_reg();
self.emit_abc(OP_MOVE, bind_reg, sub_reg, 0);
self.locals.push((binding.clone(), bind_reg));
}
let body_result = self.compile_body(&arm.body);
if let Some(br) = body_result
&& br != result_reg {
self.emit_abc(OP_MOVE, result_reg, br, 0);
}
end_jumps.push(self.emit_jmp_placeholder());
self.current.patch_jump(skip);
}
}
self.next_reg = saved_next;
self.locals.truncate(saved_locals);
}
let nil_ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, result_reg, nil_ki);
for j in end_jumps {
self.current.patch_jump(j);
}
}
fn try_const_fold(expr: &Expr) -> Option<Value> {
match expr {
Expr::Literal(lit) => Some(match lit {
Literal::Number(n) => Value::Number(*n),
Literal::Text(s) => Value::Text(s.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Nil => Value::Nil,
}),
Expr::BinOp { op, left, right } => {
let lv = Self::try_const_fold(left)?;
let rv = Self::try_const_fold(right)?;
match (&lv, &rv) {
(Value::Number(a), Value::Number(b)) => Some(match op {
BinOp::Add => Value::Number(a + b),
BinOp::Subtract => Value::Number(a - b),
BinOp::Multiply => Value::Number(a * b),
BinOp::Divide if *b != 0.0 => Value::Number(a / b),
BinOp::Equals => Value::Bool((a - b).abs() < f64::EPSILON),
BinOp::NotEquals => Value::Bool((a - b).abs() >= f64::EPSILON),
BinOp::GreaterThan => Value::Bool(a > b),
BinOp::LessThan => Value::Bool(a < b),
BinOp::GreaterOrEqual => Value::Bool(a >= b),
BinOp::LessOrEqual => Value::Bool(a <= b),
_ => return None,
}),
(Value::Text(a), Value::Text(b)) => match op {
BinOp::Add => {
let mut out = String::with_capacity(a.len() + b.len());
out.push_str(a);
out.push_str(b);
Some(Value::Text(out))
}
_ => None,
},
(Value::Bool(a), Value::Bool(b)) => match op {
BinOp::Equals => Some(Value::Bool(a == b)),
BinOp::NotEquals => Some(Value::Bool(a != b)),
BinOp::And => Some(Value::Bool(*a && *b)),
BinOp::Or => Some(Value::Bool(*a || *b)),
_ => None,
},
_ => None,
}
}
Expr::UnaryOp { op, operand } => {
let v = Self::try_const_fold(operand)?;
match (&v, op) {
(Value::Number(n), UnaryOp::Negate) => Some(Value::Number(-n)),
(Value::Bool(b), UnaryOp::Not) => Some(Value::Bool(!b)),
_ => None,
}
}
_ => None,
}
}
fn compile_expr(&mut self, expr: &Expr) -> u8 {
if matches!(expr, Expr::BinOp { .. } | Expr::UnaryOp { .. })
&& let Some(ref val) = Self::try_const_fold(expr) {
let is_num = matches!(val, Value::Number(_));
let reg = self.alloc_reg();
let ki = self.current.add_const(val.clone());
self.emit_abx(OP_LOADK, reg, ki);
if is_num { self.reg_is_num[reg as usize] = true; }
return reg;
}
match expr {
Expr::Literal(lit) => {
let is_num = matches!(lit, Literal::Number(_));
let val = match lit {
Literal::Number(n) => Value::Number(*n),
Literal::Text(s) => Value::Text(s.clone()),
Literal::Bool(b) => Value::Bool(*b),
Literal::Nil => Value::Nil,
};
let reg = self.alloc_reg();
let ki = self.current.add_const(val);
self.emit_abx(OP_LOADK, reg, ki);
if is_num { self.reg_is_num[reg as usize] = true; }
reg
}
Expr::Ref(name) => {
if let Some(reg) = self.resolve_local(name) {
reg } else {
self.first_error.get_or_insert(CompileError::UndefinedVariable { name: name.clone() });
0 }
}
Expr::Field { object, field, safe } => {
let obj_reg = self.compile_expr(object);
let obj_type = self.reg_record_type[obj_reg as usize];
let field_idx = if obj_type != u16::MAX {
self.type_registry.field_index(obj_type, field)
} else {
self.search_field_index(field)
};
match field_idx {
Some(idx) => {
let c = idx as u8;
let field_is_num = obj_type != u16::MAX
&& idx < 64
&& (self.type_registry.types[obj_type as usize].num_fields & (1 << idx)) != 0;
if *safe {
self.emit_abx(OP_JMPNN, obj_reg, 1);
self.emit_abx(OP_JMP, 0, 1);
self.emit_abc(OP_RECFLD, obj_reg, obj_reg, c);
self.reg_record_type[obj_reg as usize] = u16::MAX;
if field_is_num { self.reg_is_num[obj_reg as usize] = true; }
obj_reg
} else {
let ra = self.alloc_reg();
self.emit_abc(OP_RECFLD, ra, obj_reg, c);
if field_is_num { self.reg_is_num[ra as usize] = true; }
ra
}
}
None => {
let ki = self.current.add_const(Value::Text(field.clone()));
assert!(ki <= 255, "constant pool overflow for dynamic field name");
if *safe {
self.emit_abx(OP_JMPNN, obj_reg, 1);
self.emit_abx(OP_JMP, 0, 1);
self.emit_abc(OP_RECFLD_NAME, obj_reg, obj_reg, ki as u8);
self.reg_record_type[obj_reg as usize] = u16::MAX;
obj_reg
} else {
let ra = self.alloc_reg();
self.emit_abc(OP_RECFLD_NAME, ra, obj_reg, ki as u8);
ra
}
}
}
}
Expr::Index { object, index, safe } => {
let obj_reg = self.compile_expr(object);
assert!(*index <= 255, "index literal {} exceeds 8-bit limit in OP_INDEX", index);
if *safe {
self.emit_abx(OP_JMPNN, obj_reg, 1);
self.emit_abx(OP_JMP, 0, 1);
self.emit_abc(OP_INDEX, obj_reg, obj_reg, *index as u8);
obj_reg
} else {
let ra = self.alloc_reg();
self.emit_abc(OP_INDEX, ra, obj_reg, *index as u8);
ra
}
}
Expr::Call { function, args, unwrap } => {
if let Some(builtin) = Builtin::from_name(function) {
let nargs = args.len();
match (builtin, nargs) {
(Builtin::Len, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_LEN, ra, rb, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Str, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_STR, ra, rb, 0);
return ra;
}
(Builtin::Num, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_NUM, ra, rb, 0);
return ra;
}
(Builtin::Abs, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_ABS, ra, rb, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Min | Builtin::Max, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
let op = if builtin == Builtin::Min { OP_MIN } else { OP_MAX };
self.emit_abc(op, ra, rb, rc);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Mod, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_MOD, ra, rb, rc);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Flr | Builtin::Cel | Builtin::Rou, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
let op = match builtin {
Builtin::Flr => OP_FLR,
Builtin::Cel => OP_CEL,
_ => OP_ROU,
};
self.emit_abc(op, ra, rb, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Spl, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_SPL, ra, rb, rc);
return ra;
}
(Builtin::Cat, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_CAT, ra, rb, rc);
return ra;
}
(Builtin::Has, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_HAS, ra, rb, rc);
return ra;
}
(Builtin::Hd, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_HD, ra, rb, 0);
return ra;
}
(Builtin::Tl, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_TL, ra, rb, 0);
return ra;
}
(Builtin::Rev, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_REV, ra, rb, 0);
return ra;
}
(Builtin::Srt, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_SRT, ra, rb, 0);
return ra;
}
(Builtin::Slc, 3) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let rd = self.compile_expr(&args[2]);
debug_assert_eq!(rd, rc + 1, "slc args should be consecutive regs");
let ra = self.alloc_reg();
self.emit_abc(OP_SLC, ra, rb, rc);
return ra;
}
(Builtin::Rnd, 0) => {
let ra = self.alloc_reg();
self.emit_abc(OP_RND0, ra, 0, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Now, 0) => {
let ra = self.alloc_reg();
self.emit_abc(OP_NOW, ra, 0, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Rnd, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_RND2, ra, rb, rc);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Env, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_ENV, ra, rb, 0);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Get, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_GET, ra, rb, 0);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Post, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_POST, ra, rb, rc);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Get, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_GETH, ra, rb, rc);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Post, 3) => {
let rb = self.compile_expr(&args[0]);
let r_body = self.compile_expr(&args[1]);
let r_hdrs = self.compile_expr(&args[2]);
let ra = self.alloc_reg();
self.emit_abc(OP_POSTH, ra, rb, r_body);
self.emit_abc(0, r_hdrs, 0, 0);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Jpth, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_JPTH, ra, rb, rc);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Jdmp, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_JDMP, ra, rb, 0);
return ra;
}
(Builtin::Trm, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_TRM, ra, rb, 0);
return ra;
}
(Builtin::Unq, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_UNQ, ra, rb, 0);
return ra;
}
(Builtin::Prnt, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_PRT, ra, rb, 0);
return ra;
}
(Builtin::Rd | Builtin::Rdl, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
let op = if builtin == Builtin::Rdl { OP_RDL } else { OP_RD };
self.emit_abc(op, ra, rb, 0);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Wr | Builtin::Wrl, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
let op = if builtin == Builtin::Wr { OP_WR } else { OP_WRL };
self.emit_abc(op, ra, rb, rc);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Jpar, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_JPAR, ra, rb, 0);
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, ra, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, ra, 0);
self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, ra, ra, 0);
self.next_reg = ra + 1;
}
return ra;
}
(Builtin::Mmap, 0) => {
let ra = self.alloc_reg();
self.emit_abc(OP_MAPNEW, ra, 0, 0);
return ra;
}
(Builtin::Mget, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_MGET, ra, rb, rc);
return ra;
}
(Builtin::Mset, 3) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let rd = self.compile_expr(&args[2]);
debug_assert_eq!(rd, rc + 1, "mset key/val args should be consecutive regs");
let ra = self.alloc_reg();
self.emit_abc(OP_MSET, ra, rb, rc);
return ra;
}
(Builtin::Mhas, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_MHAS, ra, rb, rc);
return ra;
}
(Builtin::Mkeys, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_MKEYS, ra, rb, 0);
return ra;
}
(Builtin::Mvals, 1) => {
let rb = self.compile_expr(&args[0]);
let ra = self.alloc_reg();
self.emit_abc(OP_MVALS, ra, rb, 0);
return ra;
}
(Builtin::Mdel, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
let ra = self.alloc_reg();
self.emit_abc(OP_MDEL, ra, rb, rc);
return ra;
}
_ => {}
}
}
let arg_regs: Vec<u8> = args.iter().map(|a| self.compile_expr(a)).collect();
let func_idx = self.func_names.iter().position(|n| n == function)
.unwrap_or_else(|| {
self.first_error.get_or_insert(CompileError::UndefinedFunction { name: function.clone() });
0 });
let a = self.alloc_reg(); let args_base = self.next_reg;
assert!((self.next_reg as usize) + args.len() <= 255, "register overflow: call requires too many register slots");
self.next_reg += args.len() as u8;
if self.next_reg > self.max_reg {
self.max_reg = self.next_reg;
}
for (i, &arg_reg) in arg_regs.iter().enumerate() {
let target = args_base + i as u8;
if arg_reg != target {
self.emit_abc(OP_MOVE, target, arg_reg, 0);
}
}
assert!(func_idx <= 255, "too many functions: function index {} exceeds 8-bit limit in OP_CALL", func_idx);
let bx = ((func_idx as u16) << 8) | args.len() as u16;
self.emit_abx(OP_CALL, a, bx);
if func_idx < self.func_return_types.len() {
self.reg_record_type[a as usize] = self.resolve_type_id(&self.func_return_types[func_idx]);
}
self.next_reg = a + 1;
if *unwrap {
let check_reg = self.alloc_reg();
self.emit_abc(OP_ISOK, check_reg, a, 0);
let skip_ret = self.emit_jmpt(check_reg);
self.emit_abx(OP_RET, a, 0); self.current.patch_jump(skip_ret);
self.emit_abc(OP_UNWRAP, a, a, 0); self.next_reg = a + 1; }
a
}
Expr::BinOp { op, left, right } => {
let is_arith = matches!(op, BinOp::Add | BinOp::Subtract | BinOp::Multiply | BinOp::Divide);
if is_arith {
if let Expr::Literal(Literal::Number(n)) = right.as_ref() {
let rb = self.compile_expr(left);
if self.reg_is_num[rb as usize] {
let ki = self.current.add_const(Value::Number(*n));
if ki <= 255 {
let ra = self.alloc_reg();
let opcode = match op {
BinOp::Add => OP_ADDK_N,
BinOp::Subtract => OP_SUBK_N,
BinOp::Multiply => OP_MULK_N,
_ => OP_DIVK_N, };
self.emit_abc(opcode, ra, rb, ki as u8);
self.reg_is_num[ra as usize] = true;
return ra;
}
}
}
if matches!(op, BinOp::Add | BinOp::Multiply)
&& let Expr::Literal(Literal::Number(n)) = left.as_ref() {
let rc = self.compile_expr(right);
if self.reg_is_num[rc as usize] {
let ki = self.current.add_const(Value::Number(*n));
if ki <= 255 {
let ra = self.alloc_reg();
let opcode = match op {
BinOp::Add => OP_ADDK_N,
_ => OP_MULK_N, };
self.emit_abc(opcode, ra, rc, ki as u8);
self.reg_is_num[ra as usize] = true;
return ra;
}
}
}
}
if matches!(op, BinOp::And | BinOp::Or) {
let ra = self.compile_expr(left);
let result = self.alloc_reg();
self.emit_abc(OP_MOVE, result, ra, 0);
let jump = if *op == BinOp::And {
self.emit_jmpf(ra)
} else {
self.emit_jmpt(ra)
};
let rb = self.compile_expr(right);
if rb != result {
self.emit_abc(OP_MOVE, result, rb, 0);
}
self.current.patch_jump(jump);
return result;
}
let rb = self.compile_expr(left);
let rc = self.compile_expr(right);
let both_num = self.reg_is_num[rb as usize] && self.reg_is_num[rc as usize];
let (opcode, result_is_num) = match op {
BinOp::Add if both_num => (OP_ADD_NN, true),
BinOp::Subtract if both_num => (OP_SUB_NN, true),
BinOp::Multiply if both_num => (OP_MUL_NN, true),
BinOp::Divide if both_num => (OP_DIV_NN, true),
BinOp::Add => (OP_ADD, false),
BinOp::Subtract => (OP_SUB, false),
BinOp::Multiply => (OP_MUL, false),
BinOp::Divide => (OP_DIV, false),
BinOp::Equals => (OP_EQ, false),
BinOp::NotEquals => (OP_NE, false),
BinOp::GreaterThan => (OP_GT, false),
BinOp::LessThan => (OP_LT, false),
BinOp::GreaterOrEqual => (OP_GE, false),
BinOp::LessOrEqual => (OP_LE, false),
BinOp::Append => (OP_LISTAPPEND, false),
_ => (OP_LISTAPPEND, false), };
let ra = self.alloc_reg();
self.emit_abc(opcode, ra, rb, rc);
if result_is_num { self.reg_is_num[ra as usize] = true; }
ra
}
Expr::UnaryOp { op, operand } => {
let rb = self.compile_expr(operand);
let ra = self.alloc_reg();
let opcode = match op {
UnaryOp::Not => OP_NOT,
UnaryOp::Negate => OP_NEG,
};
self.emit_abc(opcode, ra, rb, 0);
if *op == UnaryOp::Negate && self.reg_is_num[rb as usize] {
self.reg_is_num[ra as usize] = true;
}
ra
}
Expr::Ok(inner) => {
let rb = self.compile_expr(inner);
let ra = self.alloc_reg();
self.emit_abc(OP_WRAPOK, ra, rb, 0);
ra
}
Expr::Err(inner) => {
let rb = self.compile_expr(inner);
let ra = self.alloc_reg();
self.emit_abc(OP_WRAPERR, ra, rb, 0);
ra
}
Expr::List(items) => {
let item_regs: Vec<u8> = items.iter().map(|item| self.compile_expr(item)).collect();
let a = self.alloc_reg(); let items_base = self.next_reg;
assert!((self.next_reg as usize) + items.len() <= 255, "register overflow: list literal requires too many register slots");
self.next_reg += items.len() as u8;
if self.next_reg > self.max_reg {
self.max_reg = self.next_reg;
}
for (i, &item_reg) in item_regs.iter().enumerate() {
let target = items_base + i as u8;
if item_reg != target {
self.emit_abc(OP_MOVE, target, item_reg, 0);
}
}
self.emit_abx(OP_LISTNEW, a, items.len() as u16);
a
}
Expr::Record { type_name, fields } => {
let type_id = match self.type_registry.name_to_id.get(type_name) {
Some(&id) => id,
None => {
let field_names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
self.type_registry.register(type_name.clone(), field_names, 0)
}
};
let canonical_order: Vec<String> = self.type_registry.types[type_id as usize].fields.clone();
let source_fields: HashMap<&str, &Expr> = fields.iter()
.map(|(n, e)| (n.as_str(), e))
.collect();
let ordered_regs: Vec<u8> = canonical_order.iter()
.map(|fname| {
let expr = source_fields[fname.as_str()];
self.compile_expr(expr)
})
.collect();
let a = self.alloc_reg(); let fields_base = self.next_reg;
assert!((self.next_reg as usize) + ordered_regs.len() <= 255, "register overflow: record literal requires too many register slots");
self.next_reg += ordered_regs.len() as u8;
if self.next_reg > self.max_reg {
self.max_reg = self.next_reg;
}
for (i, &field_reg) in ordered_regs.iter().enumerate() {
let target = fields_base + i as u8;
if field_reg != target {
self.emit_abc(OP_MOVE, target, field_reg, 0);
}
}
assert!(type_id <= 255, "type_id {} exceeds 8-bit limit in OP_RECNEW", type_id);
let bx = (type_id << 8) | ordered_regs.len() as u16;
self.emit_abx(OP_RECNEW, a, bx);
self.reg_record_type[a as usize] = type_id;
a
}
Expr::Match { subject, arms } => {
let sub_reg = match subject {
Some(e) => self.compile_expr(e),
None => {
let r = self.alloc_reg();
let ki = self.current.add_const(Value::Nil);
self.emit_abx(OP_LOADK, r, ki);
r
}
};
let result_reg = self.alloc_reg();
self.compile_match_arms(sub_reg, result_reg, arms);
result_reg
}
Expr::NilCoalesce { value, default } => {
let val_reg = self.compile_expr(value);
let skip_jump = self.emit_abx(OP_JMPNN, val_reg, 0);
let def_reg = self.compile_expr(default);
if def_reg != val_reg {
self.emit_abc(OP_MOVE, val_reg, def_reg, 0);
}
self.current.patch_jump(skip_jump);
val_reg
}
Expr::Ternary { condition, then_expr, else_expr } => {
let cond_reg = self.compile_expr(condition);
let result_reg = self.alloc_reg();
let jump_to_else = self.emit_jmpf(cond_reg);
let then_reg = self.compile_expr(then_expr);
if then_reg != result_reg {
self.emit_abc(OP_MOVE, result_reg, then_reg, 0);
}
let jump_over_else = self.emit_jmp_placeholder();
self.current.patch_jump(jump_to_else);
self.next_reg = result_reg + 1;
let else_reg = self.compile_expr(else_expr);
if else_reg != result_reg {
self.emit_abc(OP_MOVE, result_reg, else_reg, 0);
}
self.current.patch_jump(jump_over_else);
self.next_reg = result_reg + 1;
result_reg
}
Expr::With { object, updates } => {
let obj_reg = self.compile_expr(object);
let obj_type = self.reg_record_type[obj_reg as usize];
let update_regs: Vec<u8> = updates.iter()
.map(|(_, val_expr)| self.compile_expr(val_expr))
.collect();
let update_indices: Vec<Option<u8>> = updates.iter().map(|(name, _)| {
let idx = if obj_type != u16::MAX {
self.type_registry.field_index(obj_type, name)
} else {
self.search_field_index(name)
};
idx.map(|i| i as u8)
}).collect();
let all_resolved = update_indices.iter().all(|i| i.is_some());
let const_val = if all_resolved {
Value::List(update_indices.iter().map(|i| Value::Number(i.unwrap() as f64)).collect())
} else {
Value::List(updates.iter().map(|(n, _)| Value::Text(n.clone())).collect())
};
let const_idx = self.current.add_const_raw(const_val);
let a = self.alloc_reg(); let updates_base = self.next_reg;
assert!((self.next_reg as usize) + updates.len() <= 255, "register overflow: 'with' expression requires too many register slots");
self.next_reg += updates.len() as u8;
if self.next_reg > self.max_reg {
self.max_reg = self.next_reg;
}
if obj_reg != a {
self.emit_abc(OP_MOVE, a, obj_reg, 0);
}
for (i, &val_reg) in update_regs.iter().enumerate() {
let target = updates_base + i as u8;
if val_reg != target {
self.emit_abc(OP_MOVE, target, val_reg, 0);
}
}
assert!(const_idx <= 255, "constant pool overflow: field data index {} exceeds 8-bit limit in OP_RECWITH", const_idx);
let bx = (const_idx << 8) | updates.len() as u16;
self.emit_abx(OP_RECWITH, a, bx);
self.reg_record_type[a as usize] = obj_type;
a
}
}
}
}
const QNAN: u64 = 0x7FFC_0000_0000_0000;
const TAG_NIL: u64 = QNAN;
const TAG_TRUE: u64 = QNAN | 1;
const TAG_FALSE: u64 = QNAN | 2;
const TAG_STRING: u64 = 0x7FFD_0000_0000_0000;
const TAG_LIST: u64 = 0x7FFE_0000_0000_0000;
const TAG_RECORD: u64 = 0x7FFF_0000_0000_0000;
const TAG_OK: u64 = 0xFFFC_0000_0000_0000;
const TAG_ERR: u64 = 0xFFFD_0000_0000_0000;
const TAG_MAP: u64 = 0xFFFF_0000_0000_0000;
pub(crate) const TAG_ARENA_REC: u64 = 0xFFFE_0000_0000_0000;
const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
const TAG_MASK: u64 = 0xFFFF_0000_0000_0000;
const ARENA_DEFAULT_SIZE: usize = 64 * 1024;
#[repr(C)]
pub(crate) struct ArenaRecord {
pub type_id: u16,
pub n_fields: u16,
_pad: u32,
}
impl ArenaRecord {
#[inline]
pub(crate) unsafe fn field_ptr(&self, idx: usize) -> *const u64 {
debug_assert!(idx < self.n_fields as usize, "field_ptr: idx {idx} >= n_fields {}", self.n_fields);
unsafe { (self as *const Self as *const u8).add(8).cast::<u64>().add(idx) }
}
#[inline]
pub(crate) unsafe fn field_ptr_mut(&mut self, idx: usize) -> *mut u64 {
debug_assert!(idx < self.n_fields as usize, "field_ptr_mut: idx {idx} >= n_fields {}", self.n_fields);
unsafe { (self as *mut Self as *mut u8).add(8).cast::<u64>().add(idx) }
}
}
#[repr(C)]
pub(crate) struct BumpArena {
pub(crate) buf_ptr: *mut u8, pub(crate) buf_cap: usize, pub(crate) offset: usize, }
impl BumpArena {
pub(crate) fn new() -> Self {
let layout = std::alloc::Layout::from_size_align(ARENA_DEFAULT_SIZE, 8).expect("valid arena layout");
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() { std::alloc::handle_alloc_error(layout); }
BumpArena { buf_ptr: ptr, buf_cap: ARENA_DEFAULT_SIZE, offset: 0 }
}
#[inline]
pub(crate) fn reset(&mut self) {
let mut off = 0usize;
while off + 8 <= self.offset {
let ptr = unsafe { self.buf_ptr.add(off) } as *const ArenaRecord;
let rec = unsafe { &*ptr };
let n = rec.n_fields as usize;
let record_size = 8 + n * 8;
if off + record_size > self.offset { break; }
for i in 0..n {
let v = NanVal(unsafe { *rec.field_ptr(i) });
v.drop_rc(); }
off += record_size;
off = (off + 7) & !7;
}
self.offset = 0;
}
#[inline]
pub(crate) fn alloc_record(&mut self, type_id: u16, n_fields: usize) -> Option<*mut ArenaRecord> {
let size = 8 + n_fields * 8; let aligned_offset = (self.offset + 7) & !7;
if aligned_offset + size > self.buf_cap {
return None; }
let ptr = unsafe { self.buf_ptr.add(aligned_offset) } as *mut ArenaRecord;
unsafe {
(*ptr).type_id = type_id;
(*ptr).n_fields = n_fields as u16;
(*ptr)._pad = 0;
}
self.offset = aligned_offset + size;
Some(ptr)
}
}
impl Drop for BumpArena {
fn drop(&mut self) {
self.reset(); unsafe {
let layout = std::alloc::Layout::from_size_align(self.buf_cap, 8).expect("valid arena layout");
std::alloc::dealloc(self.buf_ptr, layout);
}
}
}
thread_local! {
pub(crate) static JIT_ARENA: std::cell::RefCell<BumpArena> = std::cell::RefCell::new(BumpArena::new());
static ACTIVE_REGISTRY: std::cell::Cell<*const TypeRegistry> = const { std::cell::Cell::new(std::ptr::null()) };
}
pub fn with_active_registry<R>(program: &CompiledProgram, f: impl FnOnce() -> R) -> R {
struct ClearGuard;
impl Drop for ClearGuard {
fn drop(&mut self) {
ACTIVE_REGISTRY.with(|r| r.set(std::ptr::null()));
}
}
ACTIVE_REGISTRY.with(|r| r.set(&program.type_registry as *const TypeRegistry));
let _guard = ClearGuard;
f()
}
fn clear_active_registry() {
ACTIVE_REGISTRY.with(|r| r.set(std::ptr::null()));
}
pub(crate) struct ActiveRegistryGuard;
impl Drop for ActiveRegistryGuard {
fn drop(&mut self) {
clear_active_registry();
}
}
pub(crate) fn jit_arena_ptr() -> *mut BumpArena {
JIT_ARENA.with(|cell| cell.as_ptr())
}
pub(crate) fn jit_arena_reset() {
JIT_ARENA.with(|cell| cell.borrow_mut().reset());
}
enum HeapObj {
Str(String),
List(Vec<NanVal>),
Map(HashMap<String, NanVal>),
Record { type_info: Rc<TypeInfo>, fields: Box<[NanVal]> },
OkVal(NanVal),
ErrVal(NanVal),
}
impl Drop for HeapObj {
fn drop(&mut self) {
match self {
HeapObj::Str(_) => {}
HeapObj::List(items) => {
for item in items {
item.drop_rc();
}
}
HeapObj::Map(m) => {
for val in m.values() {
val.drop_rc();
}
}
HeapObj::Record { fields, .. } => {
for val in fields.iter() {
val.drop_rc();
}
}
HeapObj::OkVal(inner) | HeapObj::ErrVal(inner) => {
inner.drop_rc();
}
}
}
}
#[derive(Clone, Copy)]
pub struct NanVal(pub u64);
impl NanVal {
#[inline]
pub(crate) fn number(n: f64) -> Self {
if n.is_nan() {
NanVal(0x7FF8_0000_0000_0000) } else {
NanVal(n.to_bits())
}
}
#[inline]
fn nil() -> Self { NanVal(TAG_NIL) }
#[inline]
fn boolean(b: bool) -> Self {
NanVal(if b { TAG_TRUE } else { TAG_FALSE })
}
fn heap_string(s: String) -> Self {
let rc = Rc::new(HeapObj::Str(s));
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_STRING | (ptr & PTR_MASK))
}
fn heap_list(items: Vec<NanVal>) -> Self {
let rc = Rc::new(HeapObj::List(items));
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_LIST | (ptr & PTR_MASK))
}
fn heap_record(type_info: Rc<TypeInfo>, fields: Box<[NanVal]>) -> Self {
let rc = Rc::new(HeapObj::Record { type_info, fields });
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_RECORD | (ptr & PTR_MASK))
}
#[inline]
fn arena_record(ptr: *const ArenaRecord) -> Self {
NanVal(TAG_ARENA_REC | (ptr as u64 & PTR_MASK))
}
#[inline]
pub(crate) fn is_arena_record(self) -> bool {
(self.0 & TAG_MASK) == TAG_ARENA_REC
}
#[inline]
pub(crate) unsafe fn as_arena_record(&self) -> &ArenaRecord {
unsafe { &*((self.0 & PTR_MASK) as *const ArenaRecord) }
}
fn promote_arena_to_heap(self, registry: &TypeRegistry) -> Self {
debug_assert!(self.is_arena_record());
unsafe {
let rec = self.as_arena_record();
let type_info = Rc::clone(®istry.types[rec.type_id as usize]);
let n = rec.n_fields as usize;
let mut fields = Vec::with_capacity(n);
for i in 0..n {
let v = NanVal(*rec.field_ptr(i));
let v = if v.is_arena_record() {
v.promote_arena_to_heap(registry)
} else {
v.clone_rc();
v
};
fields.push(v);
}
NanVal::heap_record(type_info, fields.into_boxed_slice())
}
}
fn heap_ok(inner: NanVal) -> Self {
let rc = Rc::new(HeapObj::OkVal(inner));
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_OK | (ptr & PTR_MASK))
}
fn heap_err(inner: NanVal) -> Self {
let rc = Rc::new(HeapObj::ErrVal(inner));
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_ERR | (ptr & PTR_MASK))
}
fn heap_map(m: HashMap<String, NanVal>) -> Self {
let rc = Rc::new(HeapObj::Map(m));
let ptr = Rc::into_raw(rc) as u64;
NanVal(TAG_MAP | (ptr & PTR_MASK))
}
#[inline]
pub(crate) fn is_number(self) -> bool {
(self.0 & QNAN) != QNAN
}
#[inline]
pub(crate) fn as_number(self) -> f64 {
f64::from_bits(self.0)
}
#[inline]
fn is_heap(self) -> bool {
(self.0 & QNAN) == QNAN && self.0 != TAG_NIL && self.0 != TAG_TRUE && self.0 != TAG_FALSE
&& (self.0 & TAG_MASK) != TAG_ARENA_REC
}
#[inline]
fn is_string(self) -> bool {
(self.0 & TAG_MASK) == TAG_STRING
}
#[inline]
unsafe fn as_heap_ref<'a>(self) -> &'a HeapObj {
debug_assert!(self.is_heap(), "as_heap_ref called on non-heap NanVal {:#018x}", self.0);
let ptr = (self.0 & PTR_MASK) as *const HeapObj;
#[cfg(debug_assertions)]
{
let rc = unsafe { Rc::from_raw(ptr) };
let count = Rc::strong_count(&rc);
std::mem::forget(rc);
debug_assert!(count >= 1, "as_heap_ref: Rc strong count is 0 (use-after-free) for NanVal {:#018x}", self.0);
}
unsafe { &*ptr }
}
#[inline(always)]
fn clone_rc(self) {
if self.is_heap() {
let ptr = (self.0 & PTR_MASK) as *const HeapObj;
unsafe { Rc::increment_strong_count(ptr); }
}
}
#[inline(always)]
fn drop_rc(self) {
if self.is_heap() {
let ptr = (self.0 & PTR_MASK) as *const HeapObj;
unsafe { Rc::decrement_strong_count(ptr); }
}
}
pub fn from_value(val: &Value) -> Self {
match val {
Value::Number(n) => NanVal::number(*n),
Value::Bool(b) => NanVal::boolean(*b),
Value::Nil => NanVal::nil(),
Value::Text(s) => NanVal::heap_string(s.clone()),
Value::List(items) => {
NanVal::heap_list(items.iter().map(NanVal::from_value).collect())
}
Value::Map(m) => {
let nan_map: HashMap<String, NanVal> = m.iter()
.map(|(k, v)| (k.clone(), NanVal::from_value(v)))
.collect();
NanVal::heap_map(nan_map)
}
Value::Record { type_name, fields } => {
let field_names: Vec<String> = fields.keys().cloned().collect();
let type_info = Rc::new(TypeInfo { name: type_name.clone(), fields: field_names.clone(), num_fields: 0 });
let flat: Box<[NanVal]> = field_names.iter()
.map(|k| NanVal::from_value(&fields[k]))
.collect::<Vec<_>>()
.into_boxed_slice();
NanVal::heap_record(type_info, flat)
}
Value::Ok(inner) => NanVal::heap_ok(NanVal::from_value(inner)),
Value::Err(inner) => NanVal::heap_err(NanVal::from_value(inner)),
Value::FnRef(name) => NanVal::heap_string(format!("<fn:{}>", name)),
}
}
pub fn to_value(self) -> Value {
if self.is_number() {
return Value::Number(self.as_number());
}
if self.is_arena_record() {
return unsafe {
let rec = self.as_arena_record();
let n = rec.n_fields as usize;
let mut field_map = HashMap::new();
let registry_ptr = ACTIVE_REGISTRY.with(|r| r.get());
let (type_name, field_names) = if !registry_ptr.is_null() {
let registry = &*registry_ptr;
match registry.types.get(rec.type_id as usize) {
Some(ti) => (ti.name.clone(), Some(&ti.fields)),
None => (String::new(), None),
}
} else {
(String::new(), None)
};
for i in 0..n {
let v = NanVal(*rec.field_ptr(i));
let name = field_names
.and_then(|f| f.get(i).cloned())
.unwrap_or_else(|| format!("_{}", i));
field_map.insert(name, v.to_value());
}
Value::Record { type_name, fields: field_map }
};
}
match self.0 {
TAG_NIL => Value::Nil,
TAG_TRUE => Value::Bool(true),
TAG_FALSE => Value::Bool(false),
_ => unsafe {
debug_assert!(self.is_heap(), "to_value: unexpected non-heap NanVal tag {:#018x}", self.0);
match self.as_heap_ref() {
HeapObj::Str(s) => Value::Text(s.clone()),
HeapObj::List(items) => {
Value::List(items.iter().map(|v| v.to_value()).collect())
}
HeapObj::Map(m) => {
Value::Map(m.iter().map(|(k, v)| (k.clone(), v.to_value())).collect())
}
HeapObj::Record { type_info, fields } => Value::Record {
type_name: type_info.name.clone(),
fields: type_info.fields.iter().zip(fields.iter())
.map(|(k, v)| (k.clone(), v.to_value()))
.collect(),
},
HeapObj::OkVal(inner) => Value::Ok(Box::new(inner.to_value())),
HeapObj::ErrVal(inner) => Value::Err(Box::new(inner.to_value())),
}
}
}
}
#[allow(dead_code)]
pub(crate) fn to_value_with_registry(self, registry: &TypeRegistry) -> Value {
if self.is_arena_record() {
return unsafe {
let rec = self.as_arena_record();
let type_info = ®istry.types[rec.type_id as usize];
let n = rec.n_fields as usize;
let mut field_map = HashMap::new();
for i in 0..n {
let v = NanVal(*rec.field_ptr(i));
let name = type_info.fields.get(i).cloned().unwrap_or_else(|| format!("_{}", i));
field_map.insert(name, v.to_value_with_registry(registry));
}
Value::Record {
type_name: type_info.name.clone(),
fields: field_map,
}
};
}
self.to_value()
}
}
pub fn compile(program: &Program) -> Result<CompiledProgram, CompileError> {
let mut prog = RegCompiler::new().compile_program(program)?;
prog.nan_constants = prog.chunks.iter()
.map(|chunk| chunk.constants.iter().map(NanVal::from_value).collect())
.collect();
Ok(prog)
}
pub fn run(compiled: &CompiledProgram, func_name: Option<&str>, args: Vec<Value>) -> Result<Value, VmRuntimeError> {
let target = match func_name {
Some(name) => name.to_string(),
None => compiled.func_names.first().ok_or_else(|| VmRuntimeError {
error: VmError::NoFunctionsDefined,
span: None,
call_stack: Vec::new(),
})?.clone(),
};
let func_idx = compiled.func_index(&target)
.ok_or_else(|| VmRuntimeError {
error: VmError::UndefinedFunction { name: target.clone() },
span: None,
call_stack: Vec::new(),
})?;
VM::new(compiled).call(func_idx, args)
}
pub fn run_with_tools(
compiled: &CompiledProgram,
func_name: Option<&str>,
args: Vec<Value>,
provider: &dyn crate::tools::ToolProvider,
#[cfg(feature = "tools")] runtime: &tokio::runtime::Runtime,
) -> Result<Value, VmRuntimeError> {
let target = match func_name {
Some(name) => name.to_string(),
None => compiled.func_names.first().ok_or_else(|| VmRuntimeError {
error: VmError::NoFunctionsDefined,
span: None,
call_stack: Vec::new(),
})?.clone(),
};
let func_idx = compiled.func_index(&target)
.ok_or_else(|| VmRuntimeError {
error: VmError::UndefinedFunction { name: target.clone() },
span: None,
call_stack: Vec::new(),
})?;
VM::new_with_tools(
compiled,
provider,
#[cfg(feature = "tools")]
runtime,
).call(func_idx, args)
}
#[cfg(test)]
pub fn compile_and_run(program: &Program, func_name: Option<&str>, args: Vec<Value>) -> Result<Value, Box<dyn std::error::Error>> {
let compiled = compile(program)?;
Ok(run(&compiled, func_name, args).map_err(|e| e.error)?)
}
pub struct VmState<'a> {
vm: VM<'a>,
}
impl<'a> VmState<'a> {
pub fn new(compiled: &'a CompiledProgram) -> Self {
VmState { vm: VM::new(compiled) }
}
pub fn call(&mut self, func_name: &str, args: Vec<Value>) -> VmResult<Value> {
for v in self.vm.stack.drain(..) {
v.drop_rc();
}
self.vm.frames.clear();
let func_idx = self.vm.program.func_index(func_name)
.ok_or_else(|| VmError::UndefinedFunction { name: func_name.to_string() })?;
let nan_args: Vec<NanVal> = args.iter().map(NanVal::from_value).collect();
self.vm.setup_call(func_idx, nan_args, 0);
self.vm.execute() }
}
struct CallFrame {
chunk_idx: u16,
ip: usize,
stack_base: usize,
result_reg: u8,
}
struct VM<'a> {
program: &'a CompiledProgram,
stack: Vec<NanVal>,
frames: Vec<CallFrame>,
arena: BumpArena,
last_ci: usize,
last_ip: usize,
tool_provider: Option<&'a dyn crate::tools::ToolProvider>,
#[cfg(feature = "tools")]
tokio_runtime: Option<&'a tokio::runtime::Runtime>,
}
impl<'a> Drop for VM<'a> {
fn drop(&mut self) {
for v in &self.stack {
v.drop_rc();
}
}
}
impl<'a> VM<'a> {
fn new(program: &'a CompiledProgram) -> Self {
VM {
program,
stack: Vec::with_capacity(256),
frames: Vec::with_capacity(64),
arena: BumpArena::new(),
last_ci: 0,
last_ip: 0,
tool_provider: None,
#[cfg(feature = "tools")]
tokio_runtime: None,
}
}
fn new_with_tools(
program: &'a CompiledProgram,
provider: &'a dyn crate::tools::ToolProvider,
#[cfg(feature = "tools")] runtime: &'a tokio::runtime::Runtime,
) -> Self {
VM {
program,
stack: Vec::with_capacity(256),
frames: Vec::with_capacity(64),
arena: BumpArena::new(),
last_ci: 0,
last_ip: 0,
tool_provider: Some(provider),
#[cfg(feature = "tools")]
tokio_runtime: Some(runtime),
}
}
fn setup_call(&mut self, func_idx: u16, args: Vec<NanVal>, result_reg: u8) {
let chunk = &self.program.chunks[func_idx as usize];
let stack_base = self.stack.len();
for arg in args {
self.stack.push(arg);
}
while self.stack.len() < stack_base + chunk.reg_count as usize {
self.stack.push(NanVal::nil());
}
self.frames.push(CallFrame {
chunk_idx: func_idx,
ip: 0,
stack_base,
result_reg,
});
}
fn call(&mut self, func_idx: u16, args: Vec<Value>) -> Result<Value, VmRuntimeError> {
let nan_args: Vec<NanVal> = args.iter().map(NanVal::from_value).collect();
self.setup_call(func_idx, nan_args, 0);
self.execute().map_err(|e| self.make_runtime_error(e))
}
fn make_runtime_error(&self, error: VmError) -> VmRuntimeError {
let span = self.program.chunks.get(self.last_ci)
.and_then(|chunk| chunk.spans.get(self.last_ip))
.copied()
.filter(|s| *s != crate::ast::Span::UNKNOWN);
let call_stack: Vec<String> = self.frames.iter()
.filter_map(|f| self.program.func_names.get(f.chunk_idx as usize).cloned())
.collect();
VmRuntimeError { error, span, call_stack }
}
#[allow(unused_unsafe)]
fn execute(&mut self) -> VmResult<Value> {
ACTIVE_REGISTRY.with(|r| r.set(&self.program.type_registry as *const TypeRegistry));
let _registry_guard = ActiveRegistryGuard;
let frame = unsafe { self.frames.last().unwrap_unchecked() };
let mut ci = frame.chunk_idx as usize;
let mut ip = frame.ip;
let mut base = frame.stack_base;
loop {
let code = unsafe { &self.program.chunks.get_unchecked(ci).code };
let nan_consts = unsafe { self.program.nan_constants.get_unchecked(ci) };
if ip >= code.len() {
let result = NanVal::nil();
for i in base..self.stack.len() {
self.stack[i].drop_rc();
}
self.stack.truncate(base);
self.frames.pop();
if self.frames.is_empty() {
return Ok(result.to_value());
}
let f = unsafe { self.frames.last().unwrap_unchecked() };
let target = f.stack_base + self.frames.last().map(|f| f.result_reg).unwrap_or(0) as usize;
ci = f.chunk_idx as usize;
ip = f.ip;
base = f.stack_base;
if target < self.stack.len() {
self.stack[target].drop_rc();
self.stack[target] = result;
}
continue;
}
let inst = unsafe { *code.get_unchecked(ip) };
self.last_ci = ci;
self.last_ip = ip;
ip += 1;
let op = (inst >> 24) as u8;
macro_rules! reg {
($idx:expr) => {
unsafe { *self.stack.get_unchecked($idx) }
}
}
macro_rules! reg_set {
($idx:expr, $val:expr) => {
unsafe {
let slot = self.stack.as_mut_ptr().add($idx);
(*slot).drop_rc();
*slot = $val;
}
}
}
match op {
OP_ADD => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::number(bv.as_number() + cv.as_number()));
} else if bv.is_string() && cv.is_string() {
let result = unsafe {
let sb = match bv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let sc = match cv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let mut out = String::with_capacity(sb.len() + sc.len());
out.push_str(sb);
out.push_str(sc);
NanVal::heap_string(out)
};
reg_set!(a, result);
} else if bv.is_heap() && cv.is_heap() {
let bref = unsafe { bv.as_heap_ref() };
let cref = unsafe { cv.as_heap_ref() };
if let (HeapObj::List(left), HeapObj::List(right)) = (bref, cref) {
let mut new_items = Vec::with_capacity(left.len() + right.len());
for v in left {
v.clone_rc();
new_items.push(*v);
}
for v in right {
v.clone_rc();
new_items.push(*v);
}
reg_set!(a, NanVal::heap_list(new_items));
} else {
return Err(VmError::Type("cannot add non-matching types"));
}
} else {
return Err(VmError::Type("cannot add non-matching types"));
}
}
OP_SUB => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::number(bv.as_number() - cv.as_number()));
} else {
return Err(VmError::Type("cannot subtract non-numbers"));
}
}
OP_MUL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::number(bv.as_number() * cv.as_number()));
} else {
return Err(VmError::Type("cannot multiply non-numbers"));
}
}
OP_DIV => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
let dv = cv.as_number();
if dv == 0.0 {
return Err(VmError::DivisionByZero);
}
reg_set!(a, NanVal::number(bv.as_number() / dv));
} else {
return Err(VmError::Type("cannot divide non-numbers"));
}
}
OP_EQ => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let eq = nanval_equal(reg!(b), reg!(c));
reg_set!(a, NanVal::boolean(eq));
}
OP_NE => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let eq = nanval_equal(reg!(b), reg!(c));
reg_set!(a, NanVal::boolean(!eq));
}
OP_GT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::boolean(bv.as_number() > cv.as_number()));
} else if bv.is_string() && cv.is_string() {
let result = unsafe { nanval_str_cmp(bv, cv) == std::cmp::Ordering::Greater };
reg_set!(a, NanVal::boolean(result));
} else {
return Err(VmError::Type("cannot compare > : operands must be same type (n or t)"));
}
}
OP_LT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::boolean(bv.as_number() < cv.as_number()));
} else if bv.is_string() && cv.is_string() {
let result = unsafe { nanval_str_cmp(bv, cv) == std::cmp::Ordering::Less };
reg_set!(a, NanVal::boolean(result));
} else {
return Err(VmError::Type("cannot compare < : operands must be same type (n or t)"));
}
}
OP_GE => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::boolean(bv.as_number() >= cv.as_number()));
} else if bv.is_string() && cv.is_string() {
let result = unsafe { nanval_str_cmp(bv, cv) != std::cmp::Ordering::Less };
reg_set!(a, NanVal::boolean(result));
} else {
return Err(VmError::Type("cannot compare >= : operands must be same type (n or t)"));
}
}
OP_LE => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let bv = reg!(b);
let cv = reg!(c);
if bv.is_number() && cv.is_number() {
reg_set!(a, NanVal::boolean(bv.as_number() <= cv.as_number()));
} else if bv.is_string() && cv.is_string() {
let result = unsafe { nanval_str_cmp(bv, cv) != std::cmp::Ordering::Greater };
reg_set!(a, NanVal::boolean(result));
} else {
return Err(VmError::Type("cannot compare <= : operands must be same type (n or t)"));
}
}
OP_MOVE => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_number() { v.clone_rc(); }
reg_set!(a, v);
}
OP_NOT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let t = nanval_truthy(reg!(b));
reg_set!(a, NanVal::boolean(!t));
}
OP_NEG => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if v.is_number() {
reg_set!(a, NanVal::number(-v.as_number()));
} else {
return Err(VmError::Type("cannot negate non-number"));
}
}
OP_WRAPOK => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let mut v = reg!(b);
if v.is_arena_record() {
v = v.promote_arena_to_heap(&self.program.type_registry);
} else if !v.is_number() { v.clone_rc(); }
reg_set!(a, NanVal::heap_ok(v));
}
OP_WRAPERR => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let mut v = reg!(b);
if v.is_arena_record() {
v = v.promote_arena_to_heap(&self.program.type_registry);
} else if !v.is_number() { v.clone_rc(); }
reg_set!(a, NanVal::heap_err(v));
}
OP_ISOK => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let is_ok = (reg!(b).0 & TAG_MASK) == TAG_OK;
reg_set!(a, NanVal::boolean(is_ok));
}
OP_ISERR => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let is_err = (reg!(b).0 & TAG_MASK) == TAG_ERR;
reg_set!(a, NanVal::boolean(is_err));
}
OP_ISNUM => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let is_num = reg!(b).is_number();
reg_set!(a, NanVal::boolean(is_num));
}
OP_ISTEXT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let is_text = reg!(b).is_string();
reg_set!(a, NanVal::boolean(is_text));
}
OP_ISBOOL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b).0;
let is_bool = v == TAG_TRUE || v == TAG_FALSE;
reg_set!(a, NanVal::boolean(is_bool));
}
OP_ISLIST => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let is_list = (reg!(b).0 & TAG_MASK) == TAG_LIST;
reg_set!(a, NanVal::boolean(is_list));
}
OP_MAPNEW => {
let a = ((inst >> 16) & 0xFF) as usize + base;
reg_set!(a, NanVal::heap_map(HashMap::new()));
}
OP_MGET => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let map_v = reg!(b);
let key_v = reg!(c);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
match key_v.as_heap_ref() {
HeapObj::Str(k) => m.get(k.as_str())
.map(|v| { v.clone_rc(); *v })
.unwrap_or_else(NanVal::nil),
_ => return Err(VmError::Type("mget: key must be text")),
}
}
_ => return Err(VmError::Type("mget: first arg must be a map")),
}
};
reg_set!(a, result);
}
OP_MSET => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let map_v = reg!(b);
let key_v = reg!(c);
let val_v = reg!(c + 1);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
match key_v.as_heap_ref() {
HeapObj::Str(k) => {
let mut new_map = m.clone();
val_v.clone_rc();
new_map.insert(k.clone(), val_v);
NanVal::heap_map(new_map)
}
_ => return Err(VmError::Type("mset: key must be text")),
}
}
_ => return Err(VmError::Type("mset: first arg must be a map")),
}
};
reg_set!(a, result);
}
OP_MHAS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let map_v = reg!(b);
let key_v = reg!(c);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
match key_v.as_heap_ref() {
HeapObj::Str(k) => NanVal::boolean(m.contains_key(k.as_str())),
_ => return Err(VmError::Type("mhas: key must be text")),
}
}
_ => return Err(VmError::Type("mhas: first arg must be a map")),
}
};
reg_set!(a, result);
}
OP_MKEYS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let map_v = reg!(b);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
let mut keys: Vec<&String> = m.keys().collect();
keys.sort();
let nan_keys: Vec<NanVal> = keys.iter()
.map(|k| NanVal::heap_string((*k).clone()))
.collect();
NanVal::heap_list(nan_keys)
}
_ => return Err(VmError::Type("mkeys: expects a map")),
}
};
reg_set!(a, result);
}
OP_MVALS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let map_v = reg!(b);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
let mut pairs: Vec<(&String, &NanVal)> = m.iter().collect();
pairs.sort_by_key(|(k, _)| k.as_str());
let nan_vals: Vec<NanVal> = pairs.iter()
.map(|(_, v)| { v.clone_rc(); **v })
.collect();
NanVal::heap_list(nan_vals)
}
_ => return Err(VmError::Type("mvals: expects a map")),
}
};
reg_set!(a, result);
}
OP_MDEL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let map_v = reg!(b);
let key_v = reg!(c);
let result = unsafe {
match map_v.as_heap_ref() {
HeapObj::Map(m) => {
match key_v.as_heap_ref() {
HeapObj::Str(k) => {
let mut new_map = m.clone();
new_map.remove(k.as_str());
NanVal::heap_map(new_map)
}
_ => return Err(VmError::Type("mdel: key must be text")),
}
}
_ => return Err(VmError::Type("mdel: first arg must be a map")),
}
};
reg_set!(a, result);
}
OP_PRT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
println!("{}", v.to_value());
reg_set!(a, v); }
OP_RD => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("rd requires a string path"));
}
let path = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let fmt = std::path::Path::new(&path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("raw")
.to_lowercase();
let result = match std::fs::read_to_string(&path) {
Ok(content) => match vm_parse_format(&fmt, &content) {
Ok(v) => NanVal::heap_ok(v),
Err(e) => NanVal::heap_err(e),
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
};
reg_set!(a, result);
}
OP_RDL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("rdl requires a string path"));
}
let path = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let result = match std::fs::read_to_string(&path) {
Ok(content) => {
let lines: Vec<NanVal> = content
.lines()
.map(|l| NanVal::heap_string(l.to_string()))
.collect();
NanVal::heap_ok(NanVal::heap_list(lines))
}
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
};
reg_set!(a, result);
}
OP_WR => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() { return Err(VmError::Type("wr arg 1 must be a string path")); }
if !vc.is_string() { return Err(VmError::Type("wr arg 2 must be a string")); }
let (path, content) = unsafe {
let p = match vb.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() };
let c = match vc.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() };
(p, c)
};
let result = match std::fs::write(&path, &content) {
Ok(()) => NanVal::heap_ok(NanVal::heap_string(path)),
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
};
reg_set!(a, result);
}
OP_WRL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() { return Err(VmError::Type("wrl arg 1 must be a string path")); }
let path = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let result = if (vc.0 & TAG_MASK) == TAG_LIST {
let lines = unsafe { match vc.as_heap_ref() { HeapObj::List(l) => l.clone(), _ => unreachable!() } };
let mut buf = String::new();
for line in &lines {
if !line.is_string() { return Err(VmError::Type("wrl list elements must be strings")); }
let s = unsafe { match line.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
buf.push_str(&s);
buf.push('\n');
}
match std::fs::write(&path, &buf) {
Ok(()) => NanVal::heap_ok(NanVal::heap_string(path)),
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
}
} else {
return Err(VmError::Type("wrl arg 2 must be a list"));
};
reg_set!(a, result);
}
OP_TRM => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("trm requires a string"));
}
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().trim().to_owned(), _ => unreachable!() } };
reg_set!(a, NanVal::heap_string(s));
}
OP_UNQ => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let mut seen = std::collections::HashSet::new();
let deduped: String = s.chars().filter(|c| seen.insert(*c)).collect();
reg_set!(a, NanVal::heap_string(deduped));
} else if (v.0 & TAG_MASK) == TAG_LIST {
let items = unsafe { match v.as_heap_ref() { HeapObj::List(l) => l.clone(), _ => unreachable!() } };
let mut out: Vec<NanVal> = Vec::new();
for item in items {
if !out.iter().any(|existing| nanval_equal(*existing, item)) {
item.clone_rc(); out.push(item);
}
}
reg_set!(a, NanVal::heap_list(out));
} else {
return Err(VmError::Type("unq requires a list or string"));
}
}
OP_UNWRAP => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
debug_assert!(v.is_heap(), "OP_UNWRAP on non-heap value");
let inner = unsafe {
match v.as_heap_ref() {
HeapObj::OkVal(inner) | HeapObj::ErrVal(inner) => {
inner.clone_rc();
*inner
}
_ => return Err(VmError::Type("unwrap on non-Ok/Err")),
}
};
reg_set!(a, inner);
}
OP_RECFLD => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let field_idx = (inst & 0xFF) as usize;
let record = reg!(b);
if record.is_arena_record() {
let field_val = unsafe {
let rec = record.as_arena_record();
if field_idx < rec.n_fields as usize {
let v = NanVal(*rec.field_ptr(field_idx));
v.clone_rc(); v
} else {
return Err(VmError::FieldNotFound { field: format!("index {}", field_idx) });
}
};
reg_set!(a, field_val);
} else {
debug_assert!(record.is_heap(), "OP_RECFLD on non-heap value");
let field_val = unsafe {
match record.as_heap_ref() {
HeapObj::Record { fields, type_info } => {
if field_idx < fields.len() {
let val = fields[field_idx];
val.clone_rc();
val
} else {
let name = type_info.fields.get(field_idx)
.map(|s| s.as_str()).unwrap_or("?");
return Err(VmError::FieldNotFound { field: name.to_string() });
}
}
_ => return Err(VmError::Type("field access on non-record")),
}
};
reg_set!(a, field_val);
} }
OP_RECFLD_NAME => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let chunk = unsafe { self.program.chunks.get_unchecked(ci) };
let field_name = match &chunk.constants[c] {
Value::Text(s) => s.as_str(),
_ => return Err(VmError::Type("RecordField expects string constant")),
};
let record = reg!(b);
if record.is_arena_record() {
let field_val = unsafe {
let rec = record.as_arena_record();
let type_info = &self.program.type_registry.types[rec.type_id as usize];
match type_info.fields.iter().position(|f| f == field_name) {
Some(idx) if idx < rec.n_fields as usize => {
let v = NanVal(*rec.field_ptr(idx));
v.clone_rc();
v
}
_ => return Err(VmError::FieldNotFound { field: field_name.to_string() }),
}
};
reg_set!(a, field_val);
} else {
debug_assert!(record.is_heap(), "OP_RECFLD_NAME on non-heap value");
let field_val = unsafe {
match record.as_heap_ref() {
HeapObj::Record { type_info, fields } => {
match type_info.fields.iter().position(|f| f == field_name) {
Some(idx) if idx < fields.len() => {
let val = fields[idx];
val.clone_rc();
val
}
_ => return Err(VmError::FieldNotFound { field: field_name.to_string() }),
}
}
_ => return Err(VmError::Type("field access on non-record")),
}
};
reg_set!(a, field_val);
} }
OP_INDEX => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let obj = reg!(b);
debug_assert!(obj.is_heap(), "OP_INDEX on non-heap value");
let item = unsafe {
match obj.as_heap_ref() {
HeapObj::List(items) => {
if c < items.len() {
let v = items[c];
v.clone_rc();
v
} else {
return Err(VmError::Type("list index out of bounds"));
}
}
_ => return Err(VmError::Type("index access on non-list")),
}
};
reg_set!(a, item);
}
OP_LISTGET => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let list = reg!(b);
let idx_val = reg!(c);
if !list.is_heap() {
return Err(VmError::Type("foreach requires a list"));
}
if idx_val.is_number() {
debug_assert!(list.is_heap(), "OP_LISTGET on non-heap value");
unsafe {
match list.as_heap_ref() {
HeapObj::List(items) => {
let i = idx_val.as_number() as usize;
if i < items.len() {
let item = items[i];
item.clone_rc();
reg_set!(a, item);
ip += 1; }
}
_ => return Err(VmError::Type("foreach requires a list")),
}
}
} else {
return Err(VmError::Type("list index must be a number"));
}
}
OP_LOADK => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let bx = (inst & 0xFFFF) as usize;
let v = unsafe { *nan_consts.get_unchecked(bx) };
if !v.is_number() { v.clone_rc(); }
reg_set!(a, v);
}
OP_JMP => {
let sbx = (inst & 0xFFFF) as i16;
ip = (ip as isize + sbx as isize) as usize;
}
OP_JMPF => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let sbx = (inst & 0xFFFF) as i16;
if !nanval_truthy(reg!(a)) {
ip = (ip as isize + sbx as isize) as usize;
}
}
OP_JMPT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let sbx = (inst & 0xFFFF) as i16;
if nanval_truthy(reg!(a)) {
ip = (ip as isize + sbx as isize) as usize;
}
}
OP_JMPNN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let sbx = (inst & 0xFFFF) as i16;
if reg!(a).0 != TAG_NIL {
ip = (ip as isize + sbx as isize) as usize;
}
}
OP_CALL => {
let a = ((inst >> 16) & 0xFF) as u8;
let bx = (inst & 0xFFFF) as usize;
let func_idx = (bx >> 8) as u16;
let n_args = bx & 0xFF;
unsafe { self.frames.last_mut().unwrap_unchecked() }.ip = ip;
let is_tool_call = self.program.is_tool.get(func_idx as usize).copied().unwrap_or(false);
if let (true, Some(_provider)) = (is_tool_call, self.tool_provider) {
let _tool_name = &self.program.func_names[func_idx as usize];
let mut value_args = Vec::with_capacity(n_args);
for i in 0..n_args {
value_args.push(reg!(base + a as usize + 1 + i).to_value());
}
let result: Value = {
#[cfg(feature = "tools")]
{
if let Some(rt) = self.tokio_runtime {
rt.block_on(_provider.call(_tool_name, value_args))
.unwrap_or_else(|e| Value::Err(Box::new(Value::Text(e.to_string()))))
} else {
let _ = value_args;
Value::Ok(Box::new(Value::Nil))
}
}
#[cfg(not(feature = "tools"))]
{
let _ = value_args;
Value::Ok(Box::new(Value::Nil))
}
};
let nan_result = NanVal::from_value(&result);
reg_set!(base + a as usize, nan_result);
continue;
}
let new_base = self.stack.len();
for i in 0..n_args {
let v = reg!(base + a as usize + 1 + i);
if !v.is_number() { v.clone_rc(); }
self.stack.push(v);
}
let reg_count = self.program.chunks[func_idx as usize].reg_count as usize;
self.stack.resize(new_base + reg_count, NanVal::nil());
self.frames.push(CallFrame {
chunk_idx: func_idx,
ip: 0,
stack_base: new_base,
result_reg: a,
});
ci = func_idx as usize;
ip = 0;
base = new_base;
}
OP_RET => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let mut result = reg!(a);
if !result.is_number() && !result.is_arena_record() { result.clone_rc(); }
let result_reg = unsafe { self.frames.last().unwrap_unchecked() }.result_reg;
for i in base..self.stack.len() {
unsafe { self.stack.get_unchecked(i) }.drop_rc();
}
self.stack.truncate(base);
self.frames.pop();
if self.frames.is_empty() {
if result.is_arena_record() {
result = result.promote_arena_to_heap(&self.program.type_registry);
}
self.arena.reset();
let val = result.to_value();
result.drop_rc();
return Ok(val);
}
let f = unsafe { self.frames.last().unwrap_unchecked() };
ci = f.chunk_idx as usize;
ip = f.ip;
base = f.stack_base;
reg_set!(base + result_reg as usize, result);
}
OP_RECNEW => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let bx = (inst & 0xFFFF) as usize;
let type_id = (bx >> 8) as u16;
let n_fields = bx & 0xFF;
if let Some(rec_ptr) = self.arena.alloc_record(type_id, n_fields) {
unsafe {
let rec = &mut *rec_ptr;
for i in 0..n_fields {
let v = reg!(a + 1 + i);
v.clone_rc(); *rec.field_ptr_mut(i) = v.0;
}
}
reg_set!(a, NanVal::arena_record(rec_ptr));
} else {
let type_info = Rc::clone(&self.program.type_registry.types[type_id as usize]);
let mut fields = Vec::with_capacity(n_fields);
for i in 0..n_fields {
let v = reg!(a + 1 + i);
v.clone_rc();
fields.push(v);
}
reg_set!(a, NanVal::heap_record(type_info, fields.into_boxed_slice()));
}
}
OP_RECWITH => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let bx = (inst & 0xFFFF) as usize;
let const_idx = bx >> 8;
let n_updates = bx & 0xFF;
let chunk = unsafe { self.program.chunks.get_unchecked(ci) };
let const_val = &chunk.constants[const_idx];
let old_record = reg!(a);
if old_record.is_arena_record() {
let (type_id, old_n) = unsafe {
let rec = old_record.as_arena_record();
(rec.type_id, rec.n_fields as usize)
};
let slots: Vec<usize> = match const_val {
Value::List(items) => items.iter().map(|v| match v {
Value::Number(n) => *n as usize,
_ => 0,
}).collect(),
_ => vec![],
};
if let Some(new_ptr) = self.arena.alloc_record(type_id, old_n) {
unsafe {
let old_rec = old_record.as_arena_record();
let new_rec = &mut *new_ptr;
for i in 0..old_n {
let v = NanVal(*old_rec.field_ptr(i));
v.clone_rc();
*new_rec.field_ptr_mut(i) = v.0;
}
for (i, &slot) in slots.iter().enumerate().take(n_updates) {
if slot < old_n {
NanVal(*new_rec.field_ptr(slot)).drop_rc();
let val = reg!(a + 1 + i);
val.clone_rc();
*new_rec.field_ptr_mut(slot) = val.0;
}
}
}
reg_set!(a, NanVal::arena_record(new_ptr));
} else {
let type_info = Rc::clone(&self.program.type_registry.types[type_id as usize]);
unsafe {
let old_rec = old_record.as_arena_record();
let mut new_fields = Vec::with_capacity(old_n);
for i in 0..old_n {
let v = NanVal(*old_rec.field_ptr(i));
v.clone_rc();
new_fields.push(v);
}
for (i, &slot) in slots.iter().enumerate().take(n_updates) {
let val = reg!(a + 1 + i);
val.clone_rc();
if slot < new_fields.len() {
new_fields[slot].drop_rc();
new_fields[slot] = val;
}
}
reg_set!(a, NanVal::heap_record(type_info, new_fields.into_boxed_slice()));
}
}
} else {
debug_assert!(old_record.is_heap(), "OP_RECWITH on non-heap value");
let new_record = unsafe {
match old_record.as_heap_ref() {
HeapObj::Record { type_info, fields } => {
let mut new_fields: Vec<NanVal> = fields.to_vec();
for v in new_fields.iter() { v.clone_rc(); }
let slots: Vec<usize> = match const_val {
Value::List(items) => items.iter().map(|v| match v {
Value::Number(n) => *n as usize,
Value::Text(name) => type_info.fields.iter()
.position(|f| f == name).unwrap_or(0),
_ => 0,
}).collect(),
_ => vec![],
};
for (i, &slot) in slots.iter().enumerate().take(n_updates) {
let val = reg!(a + 1 + i);
val.clone_rc();
if slot < new_fields.len() {
new_fields[slot].drop_rc();
new_fields[slot] = val;
}
}
NanVal::heap_record(Rc::clone(type_info), new_fields.into_boxed_slice())
}
_ => return Err(VmError::Type("'with' requires a record")),
}
};
reg_set!(a, new_record);
} }
OP_LISTNEW => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let n = (inst & 0xFFFF) as usize;
let mut items = Vec::with_capacity(n);
for i in 0..n {
let v = reg!(a + 1 + i);
v.clone_rc();
items.push(v);
}
reg_set!(a, NanVal::heap_list(items));
}
OP_ADDK_N => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let kv = unsafe { *nan_consts.get_unchecked(c) };
let result = NanVal::number(reg!(b).as_number() + kv.as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_SUBK_N => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let kv = unsafe { *nan_consts.get_unchecked(c) };
let result = NanVal::number(reg!(b).as_number() - kv.as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_MULK_N => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let kv = unsafe { *nan_consts.get_unchecked(c) };
let result = NanVal::number(reg!(b).as_number() * kv.as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_DIVK_N => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize;
let kv = unsafe { *nan_consts.get_unchecked(c) };
let dv = kv.as_number();
if dv == 0.0 {
return Err(VmError::DivisionByZero);
}
let result = NanVal::number(reg!(b).as_number() / dv);
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_ADD_NN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let result = NanVal::number(reg!(b).as_number() + reg!(c).as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_SUB_NN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let result = NanVal::number(reg!(b).as_number() - reg!(c).as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_MUL_NN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let result = NanVal::number(reg!(b).as_number() * reg!(c).as_number());
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_DIV_NN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let dv = reg!(c).as_number();
if dv == 0.0 {
return Err(VmError::DivisionByZero);
}
let result = NanVal::number(reg!(b).as_number() / dv);
unsafe { *self.stack.as_mut_ptr().add(a) = result; }
}
OP_LEN => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
let length = if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
s.len() as f64
} else if v.is_heap() {
match unsafe { v.as_heap_ref() } {
HeapObj::List(items) => items.len() as f64,
HeapObj::Map(m) => m.len() as f64,
_ => return Err(VmError::Type("len requires string, list, or map")),
}
} else {
return Err(VmError::Type("len requires string, list, or map"));
};
reg_set!(a, NanVal::number(length));
}
OP_STR => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_number() {
return Err(VmError::Type("str requires a number"));
}
let n = v.as_number();
let s = if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{}", n as i64)
} else {
format!("{}", n)
};
reg_set!(a, NanVal::heap_string(s));
}
OP_NUM => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("num requires a string"));
}
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let result = match s.parse::<f64>() {
Ok(n) => NanVal::heap_ok(NanVal::number(n)),
Err(_) => {
v.clone_rc();
NanVal::heap_err(v)
}
};
reg_set!(a, result);
}
OP_ABS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_number() {
return Err(VmError::Type("abs requires a number"));
}
reg_set!(a, NanVal::number(v.as_number().abs()));
}
OP_MIN | OP_MAX => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_number() || !vc.is_number() {
return Err(VmError::Type("min/max require numbers"));
}
let nb = vb.as_number();
let nc = vc.as_number();
let result = if op == OP_MIN { nb.min(nc) } else { nb.max(nc) };
reg_set!(a, NanVal::number(result));
}
OP_MOD => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_number() || !vc.is_number() {
return Err(VmError::Type("mod requires numbers"));
}
let nc = vc.as_number();
if nc == 0.0 {
return Err(VmError::Type("modulo by zero"));
}
reg_set!(a, NanVal::number(vb.as_number() % nc));
}
OP_FLR | OP_CEL | OP_ROU => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_number() {
return Err(VmError::Type("flr/cel/rou requires a number"));
}
let n = v.as_number();
let result = if op == OP_FLR { n.floor() } else if op == OP_CEL { n.ceil() } else { n.round() };
reg_set!(a, NanVal::number(result));
}
OP_RND0 => {
let a = ((inst >> 16) & 0xFF) as usize + base;
reg_set!(a, NanVal::number(fastrand::f64()));
}
OP_RND2 => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_number() || !vc.is_number() {
return Err(VmError::Type("rnd requires two numbers"));
}
let lo = vb.as_number() as i64;
let hi = vc.as_number() as i64;
if lo > hi {
return Err(VmError::Type("rnd: lower bound > upper bound"));
}
reg_set!(a, NanVal::number(fastrand::i64(lo..=hi) as f64));
}
OP_NOW => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
reg_set!(a, NanVal::number(ts));
}
OP_ENV => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("env requires a string"));
}
let key_str: String = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let result = match std::env::var(&key_str) {
Ok(val) => NanVal::heap_ok(NanVal::heap_string(val)),
Err(_) => NanVal::heap_err(NanVal::heap_string(format!("env var '{}' not set", key_str))),
};
reg_set!(a, result);
}
OP_GET => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("get requires a string"));
}
#[cfg(feature = "http")]
let result = {
let url = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match minreq::get(url.as_str()).send() {
Ok(resp) => match resp.as_str() {
Ok(body) => NanVal::heap_ok(NanVal::heap_string(body.to_string())),
Err(e) => NanVal::heap_err(NanVal::heap_string(format!("response is not valid UTF-8: {e}"))),
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
}
};
#[cfg(not(feature = "http"))]
let result = NanVal::heap_err(NanVal::heap_string("http feature not enabled".to_string()));
reg_set!(a, result);
}
OP_POST => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() || !vc.is_string() {
return Err(VmError::Type("post requires two strings (url, body)"));
}
#[cfg(feature = "http")]
let result = {
let url = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let body = unsafe { match vc.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match minreq::post(url.as_str()).with_body(body.as_str()).send() {
Ok(resp) => match resp.as_str() {
Ok(b) => NanVal::heap_ok(NanVal::heap_string(b.to_string())),
Err(e) => NanVal::heap_err(NanVal::heap_string(format!("response is not valid UTF-8: {e}"))),
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
}
};
#[cfg(not(feature = "http"))]
let result = NanVal::heap_err(NanVal::heap_string("http feature not enabled".to_string()));
reg_set!(a, result);
}
OP_GETH => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() {
return Err(VmError::Type("get requires a string url"));
}
#[cfg(feature = "http")]
let result = {
let url = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let mut req = minreq::get(url.as_str());
if vc.is_heap()
&& let HeapObj::Map(m) = unsafe { vc.as_heap_ref() } {
for (k, v) in m.iter() {
if v.is_string() {
let vs = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
req = req.with_header(k.as_str(), &vs);
}
}
}
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(body) => NanVal::heap_ok(NanVal::heap_string(body.to_string())),
Err(e) => NanVal::heap_err(NanVal::heap_string(format!("response is not valid UTF-8: {e}"))),
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
}
};
#[cfg(not(feature = "http"))]
let result = NanVal::heap_err(NanVal::heap_string("http feature not enabled".to_string()));
reg_set!(a, result);
}
OP_POSTH => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let data_inst = unsafe { *code.get_unchecked(ip) };
ip += 1;
let d = ((data_inst >> 16) & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
let vd = reg!(d);
if !vb.is_string() || !vc.is_string() {
return Err(VmError::Type("post requires string url and body"));
}
#[cfg(feature = "http")]
let result = {
let url = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let body_str = unsafe { match vc.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
let mut req = minreq::post(url.as_str()).with_body(body_str.as_str());
if vd.is_heap()
&& let HeapObj::Map(m) = unsafe { vd.as_heap_ref() } {
for (k, v) in m.iter() {
if v.is_string() {
let vs = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s.as_str().to_owned(), _ => unreachable!() } };
req = req.with_header(k.as_str(), &vs);
}
}
}
match req.send() {
Ok(resp) => match resp.as_str() {
Ok(b) => NanVal::heap_ok(NanVal::heap_string(b.to_string())),
Err(e) => NanVal::heap_err(NanVal::heap_string(format!("response is not valid UTF-8: {e}"))),
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
}
};
#[cfg(not(feature = "http"))]
let result = NanVal::heap_err(NanVal::heap_string("http feature not enabled".to_string()));
reg_set!(a, result);
}
OP_JPTH => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() || !vc.is_string() {
return Err(VmError::Type("jpth requires two strings"));
}
let json_str = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let path_str = unsafe { match vc.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let result = match serde_json::from_str::<serde_json::Value>(json_str) {
Ok(parsed) => {
let mut current = &parsed;
let mut found = true;
let mut missing_key = String::new();
for key in path_str.split('.') {
if let Ok(idx) = key.parse::<usize>() {
if let Some(v) = current.as_array().and_then(|a| a.get(idx)) {
current = v;
} else {
found = false;
missing_key = key.to_string();
break;
}
} else if let Some(v) = current.get(key) {
current = v;
} else {
found = false;
missing_key = key.to_string();
break;
}
}
if found {
let result_str = match current {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
NanVal::heap_ok(NanVal::heap_string(result_str))
} else {
NanVal::heap_err(NanVal::heap_string(format!("key not found: {missing_key}")))
}
}
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
};
reg_set!(a, result);
}
OP_JDMP => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
let json_val = nanval_to_json(v);
reg_set!(a, NanVal::heap_string(json_val.to_string()));
}
OP_JPAR => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if !v.is_string() {
return Err(VmError::Type("jpar requires a string"));
}
let text = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let result = match serde_json::from_str::<serde_json::Value>(text) {
Ok(parsed) => NanVal::heap_ok(serde_json_to_nanval(parsed)),
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())),
};
reg_set!(a, result);
}
OP_SPL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vb.is_string() || !vc.is_string() {
return Err(VmError::Type("spl requires two strings"));
}
let text = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let sep = unsafe { match vc.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let items: Vec<NanVal> = text.split(sep.as_str())
.map(|p| NanVal::heap_string(p.to_string()))
.collect();
reg_set!(a, NanVal::heap_list(items));
}
OP_CAT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let vb = reg!(b);
let vc = reg!(c);
if !vc.is_string() {
return Err(VmError::Type("cat requires a text separator"));
}
if !vb.is_heap() {
return Err(VmError::Type("cat requires a list"));
}
let sep = unsafe { match vc.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let items = unsafe { match vb.as_heap_ref() { HeapObj::List(l) => l, _ => return Err(VmError::Type("cat requires a list")) } };
let mut parts = Vec::with_capacity(items.len());
for item in items {
if !item.is_string() {
return Err(VmError::Type("cat: list items must be text"));
}
let s = unsafe { match item.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
parts.push(s.as_str());
}
let result = parts.join(sep.as_str());
reg_set!(a, NanVal::heap_string(result));
}
OP_HAS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let collection = reg!(b);
let needle = reg!(c);
let found = if collection.is_string() {
if !needle.is_string() {
return Err(VmError::Type("has: text search requires text needle"));
}
unsafe {
let haystack = match collection.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let needle_s = match needle.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
haystack.contains(needle_s.as_str())
}
} else if collection.is_heap() {
match unsafe { collection.as_heap_ref() } {
HeapObj::List(items) => {
items.iter().any(|item| nanval_equal(*item, needle))
}
_ => return Err(VmError::Type("has requires a list or text")),
}
} else {
return Err(VmError::Type("has requires a list or text"));
};
reg_set!(a, NanVal::boolean(found));
}
OP_HD => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
let result = if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
if s.is_empty() {
return Err(VmError::Type("hd: empty text"));
}
NanVal::heap_string(s.chars().next().expect("non-empty checked above").to_string())
} else if v.is_heap() {
match unsafe { v.as_heap_ref() } {
HeapObj::List(items) => {
if items.is_empty() {
return Err(VmError::Type("hd: empty list"));
}
items[0].clone_rc();
items[0]
}
_ => return Err(VmError::Type("hd requires a list or text")),
}
} else {
return Err(VmError::Type("hd requires a list or text"));
};
reg_set!(a, result);
}
OP_TL => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
let result = if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
if s.is_empty() {
return Err(VmError::Type("tl: empty text"));
}
let mut chars = s.chars();
chars.next();
NanVal::heap_string(chars.collect())
} else if v.is_heap() {
match unsafe { v.as_heap_ref() } {
HeapObj::List(items) => {
if items.is_empty() {
return Err(VmError::Type("tl: empty list"));
}
let tail: Vec<NanVal> = items[1..].iter().map(|item| {
item.clone_rc();
*item
}).collect();
NanVal::heap_list(tail)
}
_ => return Err(VmError::Type("tl requires a list or text")),
}
} else {
return Err(VmError::Type("tl requires a list or text"));
};
reg_set!(a, result);
}
OP_REV => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
let result = if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
NanVal::heap_string(s.chars().rev().collect::<String>())
} else if v.is_heap() {
match unsafe { v.as_heap_ref() } {
HeapObj::List(items) => {
let mut reversed: Vec<NanVal> = items.iter().map(|item| { item.clone_rc(); *item }).collect();
reversed.reverse();
NanVal::heap_list(reversed)
}
_ => return Err(VmError::Type("rev requires a list or text")),
}
} else {
return Err(VmError::Type("rev requires a list or text"));
};
reg_set!(a, result);
}
OP_SRT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let v = reg!(b);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let mut chars: Vec<char> = s.chars().collect();
chars.sort();
let sorted: String = chars.into_iter().collect();
reg_set!(a, NanVal::heap_string(sorted));
} else if v.is_heap() {
match unsafe { v.as_heap_ref() } {
HeapObj::List(items) => {
if items.is_empty() {
reg_set!(a, NanVal::heap_list(vec![]));
} else {
let all_numbers = items.iter().all(|v| v.is_number());
let all_strings = items.iter().all(|v| v.is_string());
if all_numbers {
let mut sorted: Vec<NanVal> = items.iter().map(|v| { v.clone_rc(); *v }).collect();
sorted.sort_by(|a, b| {
a.as_number().partial_cmp(&b.as_number()).unwrap_or(std::cmp::Ordering::Equal)
});
reg_set!(a, NanVal::heap_list(sorted));
} else if all_strings {
let mut sorted: Vec<NanVal> = items.iter().map(|v| { v.clone_rc(); *v }).collect();
sorted.sort_by(|a, b| unsafe { nanval_str_cmp(*a, *b) });
reg_set!(a, NanVal::heap_list(sorted));
} else {
return Err(VmError::Type("srt: list must contain all numbers or all text"));
}
}
}
_ => return Err(VmError::Type("srt requires a list or text")),
}
} else {
return Err(VmError::Type("srt requires a list or text"));
}
}
OP_SLC => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
let d = c + 1;
let vb = reg!(b);
let vc = reg!(c);
let vd = reg!(d);
if !vc.is_number() || !vd.is_number() {
return Err(VmError::Type("slc: indices must be numbers"));
}
let start = vc.as_number() as usize;
let end = vd.as_number() as usize;
if vb.is_string() {
let s = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let chars: Vec<char> = s.chars().collect();
let end = end.min(chars.len());
let start = start.min(end);
let result: String = chars[start..end].iter().collect();
reg_set!(a, NanVal::heap_string(result));
} else if vb.is_heap() {
match unsafe { vb.as_heap_ref() } {
HeapObj::List(items) => {
let end = end.min(items.len());
let start = start.min(end);
let mut sliced = Vec::with_capacity(end - start);
for v in &items[start..end] {
v.clone_rc();
sliced.push(*v);
}
reg_set!(a, NanVal::heap_list(sliced));
}
_ => return Err(VmError::Type("slc requires a list or text")),
}
} else {
return Err(VmError::Type("slc requires a list or text"));
}
}
OP_LISTAPPEND => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
let c = (inst & 0xFF) as usize + base;
if reg!(c).is_arena_record() {
let promoted = reg!(c).promote_arena_to_heap(&self.program.type_registry);
reg_set!(c, promoted);
}
let list_val = reg!(b);
let item_val = reg!(c);
if !list_val.is_heap() {
return Err(VmError::Type("+= requires a list"));
}
match unsafe { list_val.as_heap_ref() } {
HeapObj::List(items) => {
let mut new_items = Vec::with_capacity(items.len() + 1);
for v in items {
v.clone_rc();
new_items.push(*v);
}
item_val.clone_rc();
new_items.push(item_val);
reg_set!(a, NanVal::heap_list(new_items));
}
_ => return Err(VmError::Type("+= requires a list")),
}
}
_ => return Err(VmError::UnknownOpcode { op }),
}
}
}
}
unsafe fn nanval_str_cmp(a: NanVal, b: NanVal) -> std::cmp::Ordering {
unsafe {
let sa = match a.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let sb = match b.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
sa.cmp(sb)
}
}
fn nanval_to_json(v: NanVal) -> serde_json::Value {
if v.is_number() {
let n = v.as_number();
if n.fract() == 0.0 && n.abs() < 1e15 {
return serde_json::Value::Number(serde_json::Number::from(n as i64));
}
return serde_json::Number::from_f64(n)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null);
}
if v.is_arena_record() {
unsafe {
let rec = v.as_arena_record();
let n = rec.n_fields as usize;
let mut map = serde_json::Map::new();
let registry_ptr = ACTIVE_REGISTRY.with(|r| r.get());
for i in 0..n {
let fv = NanVal(*rec.field_ptr(i));
let name = if !registry_ptr.is_null() {
let registry = &*registry_ptr;
registry.types.get(rec.type_id as usize)
.and_then(|ti| ti.fields.get(i).cloned())
.unwrap_or_else(|| format!("_{}", i))
} else {
format!("_{}", i)
};
map.insert(name, nanval_to_json(fv));
}
return serde_json::Value::Object(map);
}
}
match v.0 {
TAG_NIL => serde_json::Value::Null,
TAG_TRUE => serde_json::Value::Bool(true),
TAG_FALSE => serde_json::Value::Bool(false),
_ if v.is_heap() => {
unsafe {
match v.as_heap_ref() {
HeapObj::Str(s) => serde_json::Value::String(s.clone()),
HeapObj::List(items) => {
serde_json::Value::Array(items.iter().map(|i| nanval_to_json(*i)).collect())
}
HeapObj::Record { type_info, fields } => {
let map: serde_json::Map<String, serde_json::Value> = type_info.fields.iter()
.zip(fields.iter())
.map(|(k, v)| (k.clone(), nanval_to_json(*v)))
.collect();
serde_json::Value::Object(map)
}
HeapObj::OkVal(inner) => nanval_to_json(*inner),
HeapObj::ErrVal(inner) => nanval_to_json(*inner),
HeapObj::Map(m) => {
let obj: serde_json::Map<String, serde_json::Value> = m.iter()
.map(|(k, v)| (k.clone(), nanval_to_json(*v)))
.collect();
serde_json::Value::Object(obj)
}
}
}
}
_ => serde_json::Value::Null,
}
}
fn serde_json_to_nanval(v: serde_json::Value) -> NanVal {
match v {
serde_json::Value::Object(map) => {
let field_names: Vec<String> = map.keys().cloned().collect();
let field_vals: Box<[NanVal]> = map.into_iter()
.map(|(_, v)| serde_json_to_nanval(v))
.collect::<Vec<_>>()
.into_boxed_slice();
let type_info = Rc::new(TypeInfo { name: "json".to_string(), fields: field_names, num_fields: 0 });
NanVal::heap_record(type_info, field_vals)
}
serde_json::Value::Array(arr) => {
NanVal::heap_list(arr.into_iter().map(serde_json_to_nanval).collect())
}
serde_json::Value::String(s) => NanVal::heap_string(s),
serde_json::Value::Number(n) => NanVal::number(n.as_f64().unwrap_or(0.0)),
serde_json::Value::Bool(b) => NanVal::boolean(b),
serde_json::Value::Null => NanVal::nil(),
}
}
fn vm_parse_format(fmt: &str, content: &str) -> Result<NanVal, NanVal> {
match fmt {
"csv" | "tsv" => {
let sep = if fmt == "tsv" { '\t' } else { ',' };
let rows: Vec<NanVal> = content
.lines()
.map(|line| {
let fields: Vec<NanVal> = vm_parse_csv_row(line, sep)
.into_iter()
.map(NanVal::heap_string)
.collect();
NanVal::heap_list(fields)
})
.collect();
Ok(NanVal::heap_list(rows))
}
"json" => {
serde_json::from_str::<serde_json::Value>(content)
.map(serde_json_to_nanval)
.map_err(|e| NanVal::heap_string(e.to_string()))
}
_ => Ok(NanVal::heap_string(content.to_string())),
}
}
fn vm_parse_csv_row(line: &str, sep: char) -> Vec<String> {
let mut fields = Vec::new();
let mut field = String::new();
let mut in_quotes = false;
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if in_quotes {
if c == '"' {
if chars.peek() == Some(&'"') {
chars.next();
field.push('"');
} else {
in_quotes = false;
}
} else {
field.push(c);
}
} else if c == '"' {
in_quotes = true;
} else if c == sep {
fields.push(std::mem::take(&mut field));
} else {
field.push(c);
}
}
fields.push(field);
fields
}
fn nanval_equal(a: NanVal, b: NanVal) -> bool {
if a.is_number() && b.is_number() {
(a.as_number() - b.as_number()).abs() < f64::EPSILON
} else if a.0 == b.0 {
true
} else if a.is_string() && b.is_string() {
unsafe {
let sa = match a.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let sb = match b.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
sa == sb
}
} else {
false
}
}
fn nanval_truthy(v: NanVal) -> bool {
if v.is_number() {
v.as_number() != 0.0
} else {
match v.0 {
TAG_NIL | TAG_FALSE => false,
TAG_TRUE => true,
_ => unsafe {
debug_assert!(v.is_heap(), "nanval_truthy: unexpected non-heap NanVal tag {:#018x}", v.0);
match v.as_heap_ref() {
HeapObj::Str(s) => !s.is_empty(),
HeapObj::List(l) => !l.is_empty(),
_ => true,
}
}
}
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_add(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::number(av.as_number() + bv.as_number()).0;
}
if av.is_string() && bv.is_string() {
let result = unsafe {
let sa = match av.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let sb = match bv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let mut out = String::with_capacity(sa.len() + sb.len());
out.push_str(sa);
out.push_str(sb);
NanVal::heap_string(out)
};
return result.0;
}
if av.is_heap() && bv.is_heap() {
let aref = unsafe { av.as_heap_ref() };
let bref = unsafe { bv.as_heap_ref() };
if let (HeapObj::List(left), HeapObj::List(right)) = (aref, bref) {
let mut new_items = Vec::with_capacity(left.len() + right.len());
for v in left { v.clone_rc(); new_items.push(*v); }
for v in right { v.clone_rc(); new_items.push(*v); }
return NanVal::heap_list(new_items).0;
}
}
TAG_NIL }
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_sub(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::number(av.as_number() - bv.as_number()).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_mul(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::number(av.as_number() * bv.as_number()).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_div(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
let dv = bv.as_number();
if dv == 0.0 { return TAG_NIL; }
return NanVal::number(av.as_number() / dv).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_eq(a: u64, b: u64) -> u64 {
NanVal::boolean(nanval_equal(NanVal(a), NanVal(b))).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_ne(a: u64, b: u64) -> u64 {
NanVal::boolean(!nanval_equal(NanVal(a), NanVal(b))).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_gt(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::boolean(av.as_number() > bv.as_number()).0;
}
if av.is_string() && bv.is_string() {
return NanVal::boolean(unsafe { nanval_str_cmp(av, bv) == std::cmp::Ordering::Greater }).0;
}
TAG_FALSE
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_lt(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::boolean(av.as_number() < bv.as_number()).0;
}
if av.is_string() && bv.is_string() {
return NanVal::boolean(unsafe { nanval_str_cmp(av, bv) == std::cmp::Ordering::Less }).0;
}
TAG_FALSE
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_ge(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::boolean(av.as_number() >= bv.as_number()).0;
}
if av.is_string() && bv.is_string() {
return NanVal::boolean(unsafe { nanval_str_cmp(av, bv) != std::cmp::Ordering::Less }).0;
}
TAG_FALSE
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_le(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
return NanVal::boolean(av.as_number() <= bv.as_number()).0;
}
if av.is_string() && bv.is_string() {
return NanVal::boolean(unsafe { nanval_str_cmp(av, bv) != std::cmp::Ordering::Greater }).0;
}
TAG_FALSE
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_not(a: u64) -> u64 {
NanVal::boolean(!nanval_truthy(NanVal(a))).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_neg(a: u64) -> u64 {
let v = NanVal(a);
if v.is_number() {
return NanVal::number(-v.as_number()).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_truthy(a: u64) -> u64 {
if nanval_truthy(NanVal(a)) { 1 } else { 0 }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_wrapok(v: u64) -> u64 {
let nv = NanVal(v);
if !nv.is_number() { nv.clone_rc(); }
NanVal::heap_ok(nv).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_wraperr(v: u64) -> u64 {
let nv = NanVal(v);
if !nv.is_number() { nv.clone_rc(); }
NanVal::heap_err(nv).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_isok(v: u64) -> u64 {
NanVal::boolean((v & TAG_MASK) == TAG_OK).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_iserr(v: u64) -> u64 {
NanVal::boolean((v & TAG_MASK) == TAG_ERR).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_unwrap(v: u64) -> u64 {
let nv = NanVal(v);
if !nv.is_heap() { return TAG_NIL; }
unsafe {
match nv.as_heap_ref() {
HeapObj::OkVal(inner) | HeapObj::ErrVal(inner) => {
inner.clone_rc();
inner.0
}
_ => TAG_NIL,
}
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_move(v: u64) -> u64 {
let nv = NanVal(v);
nv.clone_rc(); v
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_clone_rc(v: u64) {
NanVal(v).clone_rc();
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_drop_rc(v: u64) {
NanVal(v).drop_rc();
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_len(a: u64) -> u64 {
let v = NanVal(a);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
return NanVal::number(s.len() as f64).0;
}
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() } {
return NanVal::number(items.len() as f64).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_str(a: u64) -> u64 {
let v = NanVal(a);
if !v.is_number() { return TAG_NIL; }
let n = v.as_number();
let s = if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{}", n as i64)
} else {
format!("{}", n)
};
NanVal::heap_string(s).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_num(a: u64) -> u64 {
let v = NanVal(a);
if !v.is_string() { return TAG_NIL; }
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match s.parse::<f64>() {
Ok(n) => NanVal::heap_ok(NanVal::number(n)).0,
Err(_) => {
v.clone_rc();
NanVal::heap_err(v).0
}
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_abs(a: u64) -> u64 {
let v = NanVal(a);
if v.is_number() { NanVal::number(v.as_number().abs()).0 } else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_min(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
NanVal::number(av.as_number().min(bv.as_number())).0
} else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_max(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
NanVal::number(av.as_number().max(bv.as_number())).0
} else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_flr(a: u64) -> u64 {
let v = NanVal(a);
if v.is_number() { NanVal::number(v.as_number().floor()).0 } else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_cel(a: u64) -> u64 {
let v = NanVal(a);
if v.is_number() { NanVal::number(v.as_number().ceil()).0 } else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_rou(a: u64) -> u64 {
let v = NanVal(a);
if v.is_number() { NanVal::number(v.as_number().round()).0 } else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_rnd0() -> u64 {
NanVal::number(fastrand::f64()).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_rnd2(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if av.is_number() && bv.is_number() {
let lo = av.as_number() as i64;
let hi = bv.as_number() as i64;
if lo > hi { return TAG_NIL; }
NanVal::number(fastrand::i64(lo..=hi) as f64).0
} else { TAG_NIL }
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_now() -> u64 {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
NanVal::number(ts).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_env(a: u64) -> u64 {
let v = NanVal(a);
if !v.is_string() { return TAG_NIL; }
let key = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match std::env::var(key.as_str()) {
Ok(val) => NanVal::heap_ok(NanVal::heap_string(val)).0,
Err(_) => NanVal::heap_err(NanVal::heap_string(format!("env var '{}' not set", key))).0,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_get(a: u64) -> u64 {
let v = NanVal(a);
if !v.is_string() { return TAG_NIL; }
#[cfg(feature = "http")]
{
let url = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match minreq::get(url.as_str()).send() {
Ok(resp) => match resp.as_str() {
Ok(body) => NanVal::heap_ok(NanVal::heap_string(body.to_string())).0,
Err(e) => NanVal::heap_err(NanVal::heap_string(format!("response is not valid UTF-8: {e}"))).0,
},
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())).0,
}
}
#[cfg(not(feature = "http"))]
{
NanVal::heap_err(NanVal::heap_string("http feature not enabled".to_string())).0
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_spl(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if !av.is_string() || !bv.is_string() { return TAG_NIL; }
let text = unsafe { match av.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let sep = unsafe { match bv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let items: Vec<NanVal> = text.split(sep.as_str()).map(|p| NanVal::heap_string(p.to_string())).collect();
NanVal::heap_list(items).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_cat(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if !bv.is_string() || !av.is_heap() { return TAG_NIL; }
let sep = unsafe { match bv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let items = match unsafe { av.as_heap_ref() } {
HeapObj::List(l) => l,
_ => return TAG_NIL,
};
let mut parts = Vec::with_capacity(items.len());
for item in items {
if !item.is_string() { return TAG_NIL; }
let s = unsafe { match item.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
parts.push(s.as_str());
}
NanVal::heap_string(parts.join(sep.as_str())).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_has(a: u64, b: u64) -> u64 {
let collection = NanVal(a);
let needle = NanVal(b);
if collection.is_string() {
if !needle.is_string() { return TAG_FALSE; }
let found = unsafe {
let haystack = match collection.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
let needle_s = match needle.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() };
haystack.contains(needle_s.as_str())
};
return NanVal::boolean(found).0;
}
if collection.is_heap()
&& let HeapObj::List(items) = unsafe { collection.as_heap_ref() } {
let found = items.iter().any(|item| nanval_equal(*item, needle));
return NanVal::boolean(found).0;
}
TAG_FALSE
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_hd(a: u64) -> u64 {
let v = NanVal(a);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
if s.is_empty() { return TAG_NIL; }
return NanVal::heap_string(s.chars().next().expect("non-empty checked above").to_string()).0;
}
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() } {
if items.is_empty() { return TAG_NIL; }
items[0].clone_rc();
return items[0].0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_tl(a: u64) -> u64 {
let v = NanVal(a);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
if s.is_empty() { return TAG_NIL; }
let mut chars = s.chars();
chars.next();
return NanVal::heap_string(chars.collect()).0;
}
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() } {
if items.is_empty() { return TAG_NIL; }
let tail: Vec<NanVal> = items[1..].iter().map(|item| { item.clone_rc(); *item }).collect();
return NanVal::heap_list(tail).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_rev(a: u64) -> u64 {
let v = NanVal(a);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
return NanVal::heap_string(s.chars().rev().collect::<String>()).0;
}
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() } {
let mut reversed: Vec<NanVal> = items.iter().map(|item| { item.clone_rc(); *item }).collect();
reversed.reverse();
return NanVal::heap_list(reversed).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_srt(a: u64) -> u64 {
let v = NanVal(a);
if v.is_string() {
let s = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let mut chars: Vec<char> = s.chars().collect();
chars.sort();
return NanVal::heap_string(chars.into_iter().collect()).0;
}
if v.is_heap()
&& let HeapObj::List(items) = unsafe { v.as_heap_ref() } {
if items.is_empty() { return NanVal::heap_list(vec![]).0; }
let all_numbers = items.iter().all(|v| v.is_number());
let all_strings = items.iter().all(|v| v.is_string());
if all_numbers {
let mut sorted: Vec<NanVal> = items.iter().map(|v| { v.clone_rc(); *v }).collect();
sorted.sort_by(|a, b| a.as_number().partial_cmp(&b.as_number()).unwrap_or(std::cmp::Ordering::Equal));
return NanVal::heap_list(sorted).0;
}
if all_strings {
let mut sorted: Vec<NanVal> = items.iter().map(|v| { v.clone_rc(); *v }).collect();
sorted.sort_by(|a, b| unsafe { nanval_str_cmp(*a, *b) });
return NanVal::heap_list(sorted).0;
}
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_slc(a: u64, start: u64, end: u64) -> u64 {
let vb = NanVal(a);
let vc = NanVal(start);
let vd = NanVal(end);
if !vc.is_number() || !vd.is_number() { return TAG_NIL; }
let s_idx = vc.as_number() as usize;
let e_idx = vd.as_number() as usize;
if vb.is_string() {
let s = unsafe { match vb.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let chars: Vec<char> = s.chars().collect();
let e = e_idx.min(chars.len());
let s = s_idx.min(e);
return NanVal::heap_string(chars[s..e].iter().collect()).0;
}
if vb.is_heap()
&& let HeapObj::List(items) = unsafe { vb.as_heap_ref() } {
let e = e_idx.min(items.len());
let s = s_idx.min(e);
let mut sliced = Vec::with_capacity(e - s);
for v in &items[s..e] { v.clone_rc(); sliced.push(*v); }
return NanVal::heap_list(sliced).0;
}
TAG_NIL
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_listappend(a: u64, b: u64) -> u64 {
let list_val = NanVal(a);
let item_val = NanVal(b);
if !list_val.is_heap() { return TAG_NIL; }
match unsafe { list_val.as_heap_ref() } {
HeapObj::List(items) => {
let mut new_items = Vec::with_capacity(items.len() + 1);
for v in items { v.clone_rc(); new_items.push(*v); }
if item_val.is_arena_record() {
let registry_ptr = ACTIVE_REGISTRY.with(|r| r.get());
if !registry_ptr.is_null() {
let promoted = item_val.promote_arena_to_heap(unsafe { &*registry_ptr });
new_items.push(promoted);
} else {
return TAG_NIL;
}
} else {
item_val.clone_rc();
new_items.push(item_val);
}
NanVal::heap_list(new_items).0
}
_ => TAG_NIL,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_index(a: u64, idx: u64) -> u64 {
let obj = NanVal(a);
let i = idx as usize;
if !obj.is_heap() { return TAG_NIL; }
match unsafe { obj.as_heap_ref() } {
HeapObj::List(items) => {
if i < items.len() {
items[i].clone_rc();
items[i].0
} else { TAG_NIL }
}
_ => TAG_NIL,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_recfld(rec: u64, field_idx: u64) -> u64 {
let rv = NanVal(rec);
let idx = field_idx as usize;
if rv.is_arena_record() {
unsafe {
let r = rv.as_arena_record();
if idx < r.n_fields as usize {
let v = NanVal(*r.field_ptr(idx));
v.clone_rc();
return v.0;
}
}
return TAG_NIL;
}
if !rv.is_heap() { return TAG_NIL; }
match unsafe { rv.as_heap_ref() } {
HeapObj::Record { fields, .. } => {
if idx < fields.len() {
let val = fields[idx];
val.clone_rc();
val.0
} else {
TAG_NIL
}
}
_ => TAG_NIL,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_recnew(arena_ptr: u64, type_id_and_nfields: u64, regs: *const u64, registry_ptr: u64) -> u64 {
let tid = (type_id_and_nfields >> 16) as u16;
let n = (type_id_and_nfields & 0xFFFF) as usize;
let arena = unsafe { &mut *(arena_ptr as *mut BumpArena) };
if let Some(rec_ptr) = arena.alloc_record(tid, n) {
unsafe {
let rec = &mut *rec_ptr;
for i in 0..n {
let v = NanVal(*regs.add(i));
v.clone_rc();
*rec.field_ptr_mut(i) = v.0;
}
}
return NanVal::arena_record(rec_ptr).0;
}
let registry = unsafe { &*(registry_ptr as *const TypeRegistry) };
let type_info = Rc::clone(®istry.types[tid as usize]);
let mut fields = Vec::with_capacity(n);
for i in 0..n {
let v = NanVal(unsafe { *regs.add(i) });
v.clone_rc();
fields.push(v);
}
NanVal::heap_record(type_info, fields.into_boxed_slice()).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_recwith(rec: u64, indices_ptr: *const u8, n_updates: u64, regs: *const u64) -> u64 {
let rv = NanVal(rec);
let n = n_updates as usize;
if rv.is_arena_record() {
unsafe {
let old_rec = rv.as_arena_record();
let old_n = old_rec.n_fields as usize;
let tid = old_rec.type_id;
let arena_result = JIT_ARENA.with(|cell| {
let mut arena = cell.borrow_mut();
arena.alloc_record(tid, old_n)
});
if let Some(new_ptr) = arena_result {
let new_rec = &mut *new_ptr;
for i in 0..old_n {
let v = NanVal(*old_rec.field_ptr(i));
v.clone_rc();
*new_rec.field_ptr_mut(i) = v.0;
}
for i in 0..n {
let slot = *indices_ptr.add(i) as usize;
if slot < old_n {
NanVal(*new_rec.field_ptr(slot)).drop_rc();
let val = NanVal(*regs.add(i));
val.clone_rc();
*new_rec.field_ptr_mut(slot) = val.0;
}
}
return NanVal::arena_record(new_ptr).0;
}
}
}
if !rv.is_heap() && !rv.is_arena_record() { return TAG_NIL; }
if rv.is_arena_record() {
unsafe {
let old_rec = rv.as_arena_record();
let old_n = old_rec.n_fields as usize;
let registry_ptr = ACTIVE_REGISTRY.with(|r| r.get());
if registry_ptr.is_null() { return TAG_NIL; }
let registry = &*registry_ptr;
let type_info = Rc::clone(®istry.types[old_rec.type_id as usize]);
let mut new_fields = Vec::with_capacity(old_n);
for i in 0..old_n {
let v = NanVal(*old_rec.field_ptr(i));
v.clone_rc();
new_fields.push(v);
}
for i in 0..n {
let slot = *indices_ptr.add(i) as usize;
if slot < new_fields.len() {
let val = NanVal(*regs.add(i));
val.clone_rc();
new_fields[slot].drop_rc();
new_fields[slot] = val;
}
}
return NanVal::heap_record(type_info, new_fields.into_boxed_slice()).0;
}
}
match unsafe { rv.as_heap_ref() } {
HeapObj::Record { type_info, fields } => {
let mut new_fields: Vec<NanVal> = fields.to_vec();
for v in new_fields.iter() { v.clone_rc(); }
for i in 0..n {
let slot = unsafe { *indices_ptr.add(i) } as usize;
let val = NanVal(unsafe { *regs.add(i) });
val.clone_rc();
new_fields[slot].drop_rc();
new_fields[slot] = val;
}
NanVal::heap_record(Rc::clone(type_info), new_fields.into_boxed_slice()).0
}
_ => TAG_NIL,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_listnew(regs: *const u64, n: u64) -> u64 {
let count = n as usize;
let mut items = Vec::with_capacity(count);
for i in 0..count {
let v = NanVal(unsafe { *regs.add(i) });
v.clone_rc();
items.push(v);
}
NanVal::heap_list(items).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_listget(list: u64, idx: u64) -> u64 {
let lv = NanVal(list);
let iv = NanVal(idx);
if !lv.is_heap() || !iv.is_number() { return TAG_NIL; }
let i = iv.as_number() as usize;
match unsafe { lv.as_heap_ref() } {
HeapObj::List(items) => {
if i < items.len() {
items[i].clone_rc();
NanVal::heap_ok(items[i]).0
} else {
TAG_NIL
}
}
_ => TAG_NIL,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_jpth(a: u64, b: u64) -> u64 {
let av = NanVal(a);
let bv = NanVal(b);
if !av.is_string() || !bv.is_string() { return TAG_NIL; }
let json_str = unsafe { match av.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
let path_str = unsafe { match bv.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match serde_json::from_str::<serde_json::Value>(json_str) {
Ok(parsed) => {
let mut current = &parsed;
let mut found = true;
let mut missing_key = String::new();
for key in path_str.split('.') {
if let Ok(idx) = key.parse::<usize>() {
if let Some(v) = current.as_array().and_then(|a| a.get(idx)) {
current = v;
} else {
found = false; missing_key = key.to_string(); break;
}
} else if let Some(v) = current.get(key) {
current = v;
} else {
found = false; missing_key = key.to_string(); break;
}
}
if found {
let result_str = match current {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
NanVal::heap_ok(NanVal::heap_string(result_str)).0
} else {
NanVal::heap_err(NanVal::heap_string(format!("key not found: {missing_key}"))).0
}
}
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())).0,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_jdmp(a: u64) -> u64 {
let v = NanVal(a);
let json_val = nanval_to_json(v);
NanVal::heap_string(json_val.to_string()).0
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_jpar(a: u64) -> u64 {
let v = NanVal(a);
if !v.is_string() { return TAG_NIL; }
let text = unsafe { match v.as_heap_ref() { HeapObj::Str(s) => s, _ => unreachable!() } };
match serde_json::from_str::<serde_json::Value>(text) {
Ok(parsed) => NanVal::heap_ok(serde_json_to_nanval(parsed)).0,
Err(e) => NanVal::heap_err(NanVal::heap_string(e.to_string())).0,
}
}
#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_call(program_ptr: *const CompiledProgram, func_idx: u64, regs: *const u64, n_args: u64) -> u64 {
let program = unsafe { &*program_ptr };
let n = n_args as usize;
let mut nan_args = Vec::with_capacity(n);
for i in 0..n {
let v = NanVal(unsafe { *regs.add(i) });
v.clone_rc();
nan_args.push(v);
}
let mut vm = VM::new(program);
vm.setup_call(func_idx as u16, nan_args, 0);
match vm.execute() {
Ok(val) => NanVal::from_value(&val).0,
Err(e) => {
let msg = format!("{:?}", e);
NanVal::heap_err(NanVal::heap_string(msg)).0
}
}
}
#[cfg(feature = "cranelift")]
pub(crate) fn find_block_leaders(code: &[u32]) -> Vec<usize> {
let mut leaders = std::collections::BTreeSet::new();
leaders.insert(0);
for (i, &inst) in code.iter().enumerate() {
let op = (inst >> 24) as u8;
match op {
OP_JMP | OP_JMPF | OP_JMPT | OP_JMPNN => {
let sbx = (inst & 0xFFFF) as i16;
let target = (i as isize + 1 + sbx as isize) as usize;
leaders.insert(target);
leaders.insert(i + 1);
}
OP_LISTGET => {
leaders.insert(i + 1);
leaders.insert(i + 2);
}
_ => {}
}
}
leaders.into_iter().filter(|&l| l <= code.len()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer;
use crate::parser;
static ENV_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn parse_program(source: &str) -> Program {
let tokens = lexer::lex(source).unwrap();
let token_spans: Vec<(crate::lexer::Token, crate::ast::Span)> = tokens
.into_iter()
.map(|(t, r)| (t, crate::ast::Span { start: r.start, end: r.end }))
.collect();
let (prog, errors) = parser::parse(token_spans);
assert!(errors.is_empty(), "parse errors: {:?}", errors);
prog
}
fn vm_run(source: &str, func: Option<&str>, args: Vec<Value>) -> Value {
let prog = parse_program(source);
compile_and_run(&prog, func, args).unwrap()
}
#[test]
fn vm_tot() {
let source = std::fs::read_to_string("examples/01-simple-function.ilo").unwrap();
let result = vm_run(
&source,
Some("tot"),
vec![Value::Number(10.0), Value::Number(20.0), Value::Number(30.0)],
);
assert_eq!(result, Value::Number(6200.0));
}
#[test]
fn vm_tot_different_args() {
let source = "tot p:n q:n r:n>n;s=*p q;t=*s r;+s t";
let result = vm_run(
source,
Some("tot"),
vec![Value::Number(2.0), Value::Number(3.0), Value::Number(4.0)],
);
assert_eq!(result, Value::Number(30.0));
}
#[test]
fn vm_cls_gold() {
let source = r#"cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze""#;
let result = vm_run(source, Some("cls"), vec![Value::Number(1000.0)]);
assert_eq!(result, Value::Text("gold".to_string()));
}
#[test]
fn vm_cls_silver() {
let source = r#"cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze""#;
let result = vm_run(source, Some("cls"), vec![Value::Number(500.0)]);
assert_eq!(result, Value::Text("silver".to_string()));
}
#[test]
fn vm_cls_bronze() {
let source = r#"cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze""#;
let result = vm_run(source, Some("cls"), vec![Value::Number(100.0)]);
assert_eq!(result, Value::Text("bronze".to_string()));
}
#[test]
fn vm_match_stmt() {
let source = r#"f x:t>n;?x{"a":1;"b":2;_:0}"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("a".to_string())]),
Value::Number(1.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("b".to_string())]),
Value::Number(2.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("z".to_string())]),
Value::Number(0.0)
);
}
#[test]
fn vm_ok_err() {
let source = "f x:n>R n t;~x";
let result = vm_run(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Ok(Box::new(Value::Number(42.0))));
}
#[test]
fn vm_tool_call() {
let source = "tool fetch\"HTTP GET\" url:t>R _ t timeout:30\nf>R _ t;fetch \"http://example.com\"";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn vm_tool_call_multi_param() {
let source = "tool send\"send msg\" to:t body:t>R _ t\nf>R _ t;send \"alice\" \"hello\"";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn vm_tool_call_unwrap() {
let source = "tool fetch\"get\" url:t>R _ t\nf>R _ t;v=fetch! \"http://x\";~v";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn vm_tool_call_match() {
let source = "tool fetch\"get\" url:t>R _ t\nf>t;r=fetch \"http://x\";?r{~v:\"ok\";^e:\"err\"}";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Text("ok".into()));
}
#[test]
fn vm_tool_mixed_with_functions() {
let source = "add a:n b:n>n;+a b\ntool fetch\"get\" url:t>R _ t\nf>n;add 1 2";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_multiple_tools() {
let source = "tool a\"first\" x:t>R _ t\ntool b\"second\" x:t>R _ t\nf>R _ t;b \"test\"";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn vm_err_constructor() {
let source = r#"f x:n>R n t;^"bad""#;
let result = vm_run(source, Some("f"), vec![Value::Number(0.0)]);
assert_eq!(result, Value::Err(Box::new(Value::Text("bad".to_string()))));
}
#[test]
fn vm_match_ok_err_patterns() {
let source = r#"f x:R n t>n;?x{^e:0;~v:v}"#;
let ok_result = vm_run(
source,
Some("f"),
vec![Value::Ok(Box::new(Value::Number(42.0)))],
);
assert_eq!(ok_result, Value::Number(42.0));
let err_result = vm_run(
source,
Some("f"),
vec![Value::Err(Box::new(Value::Text("oops".to_string())))],
);
assert_eq!(err_result, Value::Number(0.0));
}
#[test]
fn vm_negated_guard() {
let source = r#"f x:b>t;!x{"nope"};"yes""#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false)]),
Value::Text("nope".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true)]),
Value::Text("yes".to_string())
);
}
#[test]
fn vm_logical_not() {
let source = "f x:b>b;!x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true)]),
Value::Bool(false)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false)]),
Value::Bool(true)
);
}
#[test]
fn vm_unary_negate() {
let source = "f x:n>n;-x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(-5.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(-3.0)]),
Value::Number(3.0)
);
}
#[test]
fn vm_unary_negate_in_expr() {
let source = "f x:n>n;y=-x;+y 10";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0)]),
Value::Number(5.0)
);
}
#[test]
fn vm_record_and_field() {
let source = "f x:n>n;r=point x:x y:10;r.y";
let result = vm_run(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_with_expr() {
let source = "f>n;r=point x:1 y:2;r2=r with y:10;r2.y";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_string_concat() {
let source = r#"f a:t b:t>t;+a b"#;
let result = vm_run(
source,
Some("f"),
vec![Value::Text("hello ".to_string()), Value::Text("world".to_string())],
);
assert_eq!(result, Value::Text("hello world".to_string()));
}
#[test]
fn vm_list_literal() {
let source = "f>n;xs=[10, 20, 30];@x xs{x};0";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(0.0));
let source = "f>L n;[1, 2, 3]";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::List(vec![
Value::Number(1.0), Value::Number(2.0), Value::Number(3.0),
]));
}
#[test]
fn vm_empty_list() {
let source = "f>L n;[]";
let result = vm_run(source, Some("f"), vec![]);
assert!(matches!(result, Value::List(items) if items.is_empty()));
}
#[test]
fn vm_string_comparison() {
let source = r#"f a:t b:t>b;>a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("banana".into()), Value::Text("apple".into())]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("apple".into()), Value::Text("banana".into())]),
Value::Bool(false)
);
let source = r#"f a:t b:t>b;<a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("apple".into()), Value::Text("banana".into())]),
Value::Bool(true)
);
let source = r#"f a:t b:t>b;>=a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("apple".into()), Value::Text("apple".into())]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("apple".into()), Value::Text("banana".into())]),
Value::Bool(false)
);
let source = r#"f a:t b:t>b;<=a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("banana".into()), Value::Text("banana".into())]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("zebra".into()), Value::Text("banana".into())]),
Value::Bool(false)
);
}
#[test]
fn vm_multi_function() {
let source = "double x:n>n;*x 2\nf x:n>n;double x";
let result = vm_run(source, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_match_expr_in_let() {
let source = r#"f x:t>n;y=?x{"a":1;"b":2;_:0};y"#;
let result = vm_run(source, Some("f"), vec![Value::Text("b".to_string())]);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn vm_default_first_function() {
let source = "f>n;42";
let result = vm_run(source, None, vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn vm_division_by_zero() {
let source = "f x:n>n;/x 0";
let prog = parse_program(source);
let result = compile_and_run(&prog, Some("f"), vec![Value::Number(10.0)]);
assert!(result.is_err());
}
#[test]
fn vm_logical_and() {
let source = "f a:b b:b>b;&a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true), Value::Bool(true)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true), Value::Bool(false)]),
Value::Bool(false)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false), Value::Bool(true)]),
Value::Bool(false)
);
}
#[test]
fn vm_logical_or() {
let source = "f a:b b:b>b;|a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false), Value::Bool(false)]),
Value::Bool(false)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true), Value::Bool(false)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false), Value::Bool(true)]),
Value::Bool(true)
);
}
#[test]
fn vm_logical_and_short_circuit() {
let source = r#"f>b;&false true"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_logical_or_short_circuit() {
let source = r#"f>b;|true false"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_and_does_not_clobber_left_operand() {
let source = r#"f n:n>t;a=flr /n 3;b=flr /n 5;c=*a 3;d=*b 5;e= =c n;f= =d n;&e f{"FizzBuzz"};e{"Fizz"};f{"Buzz"};str n"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0)]),
Value::Text("Fizz".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0)]),
Value::Text("Buzz".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(15.0)]),
Value::Text("FizzBuzz".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(7.0)]),
Value::Text("7".to_string())
);
}
#[test]
fn vm_or_does_not_clobber_left_operand() {
let source = r#"f>t;a=true;b=false;r= |a b;a{"a is still true"};"nope""#;
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::Text("a is still true".to_string())
);
}
#[test]
fn vm_len_string() {
let source = r#"f s:t>n;len s"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hello".to_string())]),
Value::Number(5.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("".to_string())]),
Value::Number(0.0)
);
}
#[test]
fn vm_len_list() {
let source = "f>n;xs=[1, 2, 3];len xs";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_len_empty_list() {
let source = "f>n;xs=[];len xs";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(0.0));
}
#[test]
fn vm_list_append() {
let source = "f>L n;xs=[1, 2];+=xs 3";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)])
);
}
#[test]
fn vm_list_append_empty() {
let source = "f>L n;xs=[];+=xs 42";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(42.0)])
);
}
#[test]
fn vm_list_concat() {
let source = "f>L n;a=[1, 2];b=[3, 4];+a b";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0), Value::Number(4.0)])
);
}
#[test]
fn vm_list_concat_empty() {
let source = "f>L n;a=[1, 2];b=[];+a b";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(1.0), Value::Number(2.0)])
);
}
#[test]
fn vm_index_access() {
let source = "f>n;xs=[10, 20, 30];xs.0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn vm_index_access_second() {
let source = "f>n;xs=[10, 20, 30];xs.2";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(30.0));
}
#[test]
fn vm_str_integer() {
let source = "f>t;str 42";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("42".into()));
}
#[test]
fn vm_str_float() {
let source = "f>t;str 3.14";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("3.14".into()));
}
#[test]
fn vm_num_ok() {
let source = "f>R n t;num \"42\"";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Ok(Box::new(Value::Number(42.0))));
}
#[test]
fn vm_num_float() {
let source = "f>R n t;num \"3.14\"";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Ok(Box::new(Value::Number(3.14))));
}
#[test]
fn vm_num_err() {
let source = "f>R n t;num \"abc\"";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Err(Box::new(Value::Text("abc".into()))));
}
#[test]
fn vm_abs_positive() {
let source = "f>n;abs 5";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_abs_negative() {
let source = "f>n;abs -3";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_min() {
let source = "f>n;min 3 7";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_max() {
let source = "f>n;max 3 7";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_min_negative() {
let source = "f>n;min -5 2";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(-5.0));
}
#[test]
fn vm_mod() {
assert_eq!(vm_run("f>n;mod 10 3", Some("f"), vec![]), Value::Number(1.0));
}
#[test]
fn vm_mod_negative() {
assert_eq!(vm_run("f>n;mod -7 3", Some("f"), vec![]), Value::Number(-1.0));
}
#[test]
fn vm_mod_float() {
assert_eq!(vm_run("f>n;mod 5.5 2.0", Some("f"), vec![]), Value::Number(1.5));
}
#[test]
fn vm_flr() {
let source = "f>n;flr 3.7";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_flr_negative() {
let source = "f>n;flr -2.3";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(-3.0));
}
#[test]
fn vm_cel() {
let source = "f>n;cel 3.2";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn vm_cel_negative() {
let source = "f>n;cel -2.7";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(-2.0));
}
#[test]
fn vm_index_access_string_list() {
let source = "f>t;xs=[\"a\", \"b\"];xs.1";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("b".into()));
}
#[test]
fn vm_nested_multiply_add() {
let source = "f a:n b:n c:n>n;+*a b c";
let result = vm_run(source, Some("f"), vec![Value::Number(2.0), Value::Number(3.0), Value::Number(4.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_nested_compare() {
let source = "f x:n y:n>b;>=+x y 100";
let result = vm_run(source, Some("f"), vec![Value::Number(60.0), Value::Number(50.0)]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_not_as_and_operand() {
let source = "f x:b y:b>b;&!x y";
let result = vm_run(source, Some("f"), vec![Value::Bool(false), Value::Bool(true)]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_negate_product() {
let source = "f a:n b:n>n;-*a b";
let result = vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(4.0)]);
assert_eq!(result, Value::Number(-12.0));
}
#[test]
fn nanval_roundtrip() {
let v = Value::Number(42.5);
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
nv.drop_rc();
let v = Value::Number(-0.0);
let nv = NanVal::from_value(&v);
assert!(nv.is_number());
let rt = nv.to_value();
let Value::Number(n) = rt else { panic!("expected Number") };
assert!(n.to_bits() == (-0.0f64).to_bits());
nv.drop_rc();
let v = Value::Number(f64::INFINITY);
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
nv.drop_rc();
let v = Value::Bool(true);
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
let v = Value::Bool(false);
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
let v = Value::Nil;
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
let v = Value::Text("hello".to_string());
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
nv.drop_rc();
let v = Value::Ok(Box::new(Value::Number(7.0)));
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
nv.drop_rc();
let v = Value::Err(Box::new(Value::Text("bad".to_string())));
let nv = NanVal::from_value(&v);
assert_eq!(nv.to_value(), v);
nv.drop_rc();
}
fn vm_run_err(source: &str, func: Option<&str>, args: Vec<Value>) -> String {
let prog = parse_program(source);
compile_and_run(&prog, func, args).unwrap_err().to_string()
}
fn compile_err(source: &str) -> String {
let prog = parse_program(source);
compile_and_run(&prog, None, vec![]).unwrap_err().to_string()
}
#[test]
fn vm_state_reusable() {
let prog = parse_program("f x:n>n;*x 2");
let compiled = compile(&prog).unwrap();
let mut state = VmState::new(&compiled);
assert_eq!(state.call("f", vec![Value::Number(5.0)]).unwrap(), Value::Number(10.0));
assert_eq!(state.call("f", vec![Value::Number(3.0)]).unwrap(), Value::Number(6.0));
}
#[test]
fn vm_state_undefined_function() {
let prog = parse_program("f x:n>n;*x 2");
let compiled = compile(&prog).unwrap();
let mut state = VmState::new(&compiled);
let err = state.call("nonexistent", vec![]).unwrap_err();
assert!(err.to_string().contains("undefined function"));
}
#[test]
fn vm_sub_nn() {
let source = "f a:n b:n>n;-a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(10.0), Value::Number(3.0)]),
Value::Number(7.0)
);
}
#[test]
fn vm_div_nn() {
let source = "f a:n b:n>n;/a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(15.0), Value::Number(3.0)]),
Value::Number(5.0)
);
}
#[test]
fn vm_equals_prefix() {
let source = "f a:n b:n>b;=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(5.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_equals_double_eq_sugar() {
let source = "f a:n b:n>b;==a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(5.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_double_eq_in_guard() {
let source = "f x:n>t;==x 3{\"match\"};\"nope\"";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0)]),
Value::Text("match".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0)]),
Value::Text("nope".to_string())
);
}
#[test]
fn vm_assign_equality_with_double_eq() {
let source = "f x:n>t;e= ==x 3;e{\"match\"};\"nope\"";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0)]),
Value::Text("match".to_string())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0)]),
Value::Text("nope".to_string())
);
}
#[test]
fn vm_not_equals_prefix() {
let source = "f a:n b:n>b;!=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(5.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_const_fold_add() {
let source = "f>n;x=+2 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_const_fold_subtract() {
let source = "f>n;x=-+5 5 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_const_fold_multiply() {
let source = "f>n;x=*4 5;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(20.0));
}
#[test]
fn vm_const_fold_divide() {
let source = "f>n;x=/10 2;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_const_fold_equals() {
let source = "f>b;x= ==3 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_not_equals() {
let source = "f>b;x=!=3 4;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_comparison() {
let source = "f>b;x=>5 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_text_concat() {
let source = r#"f>t;x=+"hello " "world";x"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("hello world".into()));
}
#[test]
fn vm_const_fold_bool_and() {
let source = "f>b;x=&true false;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_const_fold_bool_or() {
let source = "f>b;x=|true false;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_bool_eq() {
let source = "f>b;x= ==true true;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_bool_ne() {
let source = "f>b;x=!=true false;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_negate() {
let source = "f>n;x=-5;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(-5.0));
}
#[test]
fn vm_const_fold_not() {
let source = "f>b;x=!true;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_match_bool_pattern() {
let source = r#"f x:b>t;?x{true:"yes";_:"no"}"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true)]),
Value::Text("yes".into())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(false)]),
Value::Text("no".into())
);
}
#[test]
fn vm_match_number_pattern() {
let source = r#"f x:n>t;?x{0:"zero";1:"one";_:"other"}"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(0.0)]),
Value::Text("zero".into())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0)]),
Value::Text("one".into())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(42.0)]),
Value::Text("other".into())
);
}
#[test]
fn vm_match_no_subject() {
let source = r#"f>t;?{_:"always"}"#;
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::Text("always".into())
);
}
#[test]
fn vm_foreach_basic() {
let source = "f>L n;xs=[10, 20, 30];@x xs{x};xs";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(10.0), Value::Number(20.0), Value::Number(30.0)])
);
}
#[test]
fn vm_foreach_empty() {
let source = "f>n;xs=[];s=99;@x xs{s=+s x};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_bool_literal_true() {
let source = "f>b;true";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_bool_literal_false() {
let source = "f>b;false";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_nanval_equal_numbers() {
let source = "f a:n b:n>b;=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(3.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(4.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_nanval_equal_strings() {
let source = r#"f a:t b:t>b;=a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hi".into()), Value::Text("hi".into())]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hi".into()), Value::Text("bye".into())]),
Value::Bool(false)
);
}
#[test]
fn vm_nanval_equal_different_types() {
let source = "f a:b b:b>b;=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true), Value::Bool(true)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Bool(true), Value::Bool(false)]),
Value::Bool(false)
);
}
#[test]
fn vm_nanval_not_equal() {
let source = "f a:n b:n>b;!=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(4.0)]),
Value::Bool(true)
);
}
#[test]
fn vm_nanval_truthy_number_and() {
let source = "f a:n b:n>n;&a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Number(3.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(0.0), Value::Number(3.0)]),
Value::Number(0.0)
);
}
#[test]
fn vm_nanval_truthy_number_or() {
let source = "f a:n b:n>n;|a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Number(5.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(0.0), Value::Number(3.0)]),
Value::Number(3.0)
);
}
#[test]
fn vm_nanval_truthy_string() {
let source = r#"f a:t b:t>t;&a b"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hi".into()), Value::Text("there".into())]),
Value::Text("there".into())
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("".into()), Value::Text("there".into())]),
Value::Text("".into())
);
}
#[test]
fn vm_nanval_truthy_list() {
let source = "f>L n;xs=[1, 2];ys=[3];|xs ys";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(1.0), Value::Number(2.0)])
);
}
#[test]
fn vm_nanval_truthy_heap_other() {
let source = "f x:t>n;x{1}";
let result = vm_run(source, Some("f"), vec![Value::Ok(Box::new(Value::Number(1.0)))]);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn vm_nanval_record_roundtrip() {
let source = "f>n;r=point x:5 y:10;r.x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_nanval_record_return() {
let source = "f>point;r=point x:1 y:2;r";
let result = vm_run(source, Some("f"), vec![]);
let Value::Record { type_name, fields } = result else { panic!("expected record") };
assert_eq!(type_name, "point");
assert_eq!(fields.get("x"), Some(&Value::Number(1.0)));
assert_eq!(fields.get("y"), Some(&Value::Number(2.0)));
}
#[test]
fn vm_err_negate_non_number() {
let source = "f x:b>n;y=0;-y x";
let err = vm_run_err(source, Some("f"), vec![Value::Bool(true)]);
assert!(err.contains("subtract") || err.contains("negate") || err.contains("number"),
"unexpected error: {}", err);
}
#[test]
fn vm_err_add_incompatible() {
let source = "f x:n y:b>n;-x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(5.0), Value::Bool(true)]);
assert!(err.contains("subtract") || err.contains("number"),
"unexpected error: {}", err);
}
#[test]
fn vm_err_field_not_found() {
let source = "f>n;r=point x:1 y:2;r.z";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("field") || err.contains("z"),
"unexpected error: {}", err);
}
#[test]
fn vm_err_undefined_variable() {
let err = compile_err("f>n;x");
assert!(err.contains("undefined variable"),
"unexpected error: {}", err);
}
#[test]
fn vm_err_undefined_function() {
let err = compile_err("f>n;nonexistent 5");
assert!(err.contains("undefined function"),
"unexpected error: {}", err);
}
#[test]
fn vm_err_division_by_zero() {
let source = "f a:n b:n>n;/a b";
let err = vm_run_err(source, Some("f"), vec![Value::Number(10.0), Value::Number(0.0)]);
assert!(err.contains("division by zero"),
"unexpected error: {}", err);
}
#[test]
fn vm_match_expr_no_subject() {
let source = r#"f x:n>t;y=?{_:"default"};y"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0)]),
Value::Text("default".into())
);
}
#[test]
fn vm_foreach_single_element() {
let source = "f>n;xs=[42];@x xs{x};0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(0.0));
}
#[test]
fn vm_const_fold_lt() {
let source = "f>b;x=<3 5;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_le() {
let source = "f>b;x=<=3 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_ge() {
let source = "f>b;x=>=5 3;x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_fold_div_by_zero_no_fold() {
let source = "f>n;/10 0";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("division by zero"), "unexpected error: {}", err);
}
#[test]
fn vm_typedef_in_program() {
let source = "f x:n>n;*x 2\ntype point{x:n;y:n}";
let result = vm_run(source, Some("f"), vec![Value::Number(3.0)]);
assert_eq!(result, Value::Number(6.0));
}
#[test]
fn vm_index_out_of_bounds() {
let source = "f>n;xs=[1, 2];xs.5";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(
err.contains("out of bounds") || err.contains("index"),
"unexpected error: {}", err
);
}
#[test]
fn vm_subk_n() {
let source = "f x:n>n;-x 3";
let result = vm_run(source, Some("f"), vec![Value::Number(10.0)]);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn vm_divk_n() {
let source = "f x:n>n;/x 2";
let result = vm_run(source, Some("f"), vec![Value::Number(10.0)]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_state_with_heap_values() {
let source = "f x:t>t;x";
let prog = parse_program(source);
let compiled = compile(&prog).unwrap();
let mut state = VmState::new(&compiled);
let r1 = state.call("f", vec![Value::Text("hello".into())]).unwrap();
assert_eq!(r1, Value::Text("hello".into()));
let r2 = state.call("f", vec![Value::Text("world".into())]).unwrap();
assert_eq!(r2, Value::Text("world".into()));
}
#[test]
fn vm_const_dedup_bool_true() {
let source = "f>b;x=true;y=true;=x y";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_dedup_bool_false() {
let source = "f>b;x=false;y=false;=x y";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_const_dedup_bool_mixed() {
let source = "f>b;x=true;y=false;=x y";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_const_dedup_nil_via_match() {
let source = "f>b;x=?{_:true};x";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_nil_fallback_function_body() {
let source = "f x:n>n;>=x 0{0}";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(0.0));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(-1.0)]), Value::Nil);
}
#[test]
fn vm_nil_fallback_guard_body() {
let source = "f x:n>n;>=x 0{>=x 5{10}}";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(10.0)]), Value::Number(10.0));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Nil);
}
#[test]
fn vm_const_fold_negate_number() {
let source = "f>n;-(+3 2)";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(-5.0));
}
#[test]
fn vm_addk_n_const_left() {
let source = "f x:n>n;+2 x";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(3.0)]), Value::Number(5.0));
}
#[test]
fn vm_mulk_n_const_left() {
let source = "f x:n>n;*3 x";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(4.0)]), Value::Number(12.0));
}
#[test]
fn vm_nanval_from_value_record() {
let source = "f x:n>n;x";
let prog = parse_program(source);
let compiled = compile(&prog).unwrap();
let mut state = VmState::new(&compiled);
let rec = Value::Record {
type_name: "point".to_string(),
fields: std::collections::HashMap::from([
("x".to_string(), Value::Number(42.0)),
]),
};
let nv = NanVal::from_value(&rec);
let roundtrip = nv.to_value();
let Value::Record { type_name, fields } = roundtrip else { panic!("expected Record") };
assert_eq!(type_name, "point");
assert_eq!(fields.get("x"), Some(&Value::Number(42.0)));
let r = state.call("f", vec![Value::Number(1.0)]).unwrap();
assert_eq!(r, Value::Number(1.0));
}
#[test]
fn vm_divide_non_numeric_register() {
let source = "f x:n>n;r=?x{1:2;_:3};/r x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0)]),
Value::Number(2.0)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(2.0)]),
Value::Number(1.5)
);
}
#[test]
fn vm_div_non_numeric_division_by_zero() {
let source = "f x:n>n;r=?x{1:0;_:2};/x r";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("zero") || err.contains("Div"), "expected division-by-zero error, got: {err}");
}
#[test]
fn vm_gt_non_numeric_registers() {
let source = "f x:n>b;r=?x{1:5;_:2};s=?x{1:3;_:8};>r s";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Bool(true)); assert_eq!(vm_run(source, Some("f"), vec![Value::Number(2.0)]), Value::Bool(false)); }
#[test]
fn vm_lt_non_numeric_registers() {
let source = "f x:n>b;r=?x{1:5;_:2};s=?x{1:3;_:8};<r s";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Bool(false)); assert_eq!(vm_run(source, Some("f"), vec![Value::Number(2.0)]), Value::Bool(true)); }
#[test]
fn vm_le_non_numeric_registers() {
let source = "f x:n>b;r=?x{1:5;_:3};s=?x{1:5;_:8};<=r s";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Bool(true)); assert_eq!(vm_run(source, Some("f"), vec![Value::Number(2.0)]), Value::Bool(true)); }
#[test]
fn vm_multiply_non_numeric_register() {
let source = "f x:n>n;r=?x{1:2;_:3};*r x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0)]),
Value::Number(2.0) );
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(4.0)]),
Value::Number(12.0) );
}
#[test]
fn vm_subtract_non_numeric_register() {
let source = "f x:n>n;r=?x{1:10;_:20};-r x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0)]),
Value::Number(9.0) );
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0)]),
Value::Number(17.0) );
}
#[test]
fn vm_add_number_text_type_error() {
let source = "f x:n y:t>n;+x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Text("hi".to_string())]);
assert!(err.contains("cannot add"), "got: {err}");
}
#[test]
fn vm_add_heap_non_list_type_error() {
let source = "f x:t y:t>t;+x y";
let list = Value::List(vec![Value::Number(1.0)]);
let ok_val = Value::Ok(Box::new(Value::Number(1.0)));
let err = vm_run_err(source, Some("f"), vec![list, ok_val]);
assert!(err.contains("cannot add"), "got: {err}");
}
#[test]
fn vm_mul_type_error() {
let source = "f x:t y:n>n;*x y";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string()), Value::Number(2.0)]);
assert!(err.contains("multiply"), "got: {err}");
}
#[test]
fn vm_gt_type_error() {
let source = "f x:n y:t>b;>x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Text("hi".to_string())]);
assert!(err.contains("compare"), "got: {err}");
}
#[test]
fn vm_lt_type_error() {
let source = "f x:n y:t>b;<x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Text("hi".to_string())]);
assert!(err.contains("compare"), "got: {err}");
}
#[test]
fn vm_ge_type_error() {
let source = "f x:n y:t>b;>=x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Text("hi".to_string())]);
assert!(err.contains("compare"), "got: {err}");
}
#[test]
fn vm_le_type_error() {
let source = "f x:n y:t>b;<=x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Text("hi".to_string())]);
assert!(err.contains("compare"), "got: {err}");
}
#[test]
fn vm_neg_type_error() {
let source = "f x:t>n;-x";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string())]);
assert!(err.contains("negate"), "got: {err}");
}
#[test]
fn vm_str_non_number_type_error() {
let source = "f x:t>t;str x";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string())]);
assert!(err.contains("str"), "got: {err}");
}
#[test]
fn vm_num_non_string_type_error() {
let source = "f x:n>n;num x";
let err = vm_run_err(source, Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("num"), "got: {err}");
}
#[test]
fn vm_abs_non_number_type_error() {
let source = "f x:t>n;abs x";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string())]);
assert!(err.contains("abs"), "got: {err}");
}
#[test]
fn vm_min_non_number_type_error() {
let source = "f x:t y:n>n;min x y";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string()), Value::Number(2.0)]);
assert!(err.contains("min") || err.contains("max") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_flr_non_number_type_error() {
let source = "f x:t>n;flr x";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".to_string())]);
assert!(err.contains("flr") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_nan_value_number() {
let result = vm_run("f x:n>n;x", Some("f"), vec![Value::Number(f64::NAN)]);
assert!(matches!(result, Value::Number(n) if n.is_nan()), "expected NaN, got: {:?}", result);
}
#[test]
fn vm_state_call_after_error() {
let source = "f x:n>n;/x 0";
let prog = parse_program(source);
let compiled = compile(&prog).unwrap();
let mut state = VmState::new(&compiled);
let err1 = state.call("f", vec![Value::Number(5.0)]);
assert!(err1.is_err(), "expected DivisionByZero error");
let err2 = state.call("f", vec![Value::Number(3.0)]);
assert!(err2.is_err(), "expected DivisionByZero error again");
}
#[test]
fn vm_div_type_error() {
let source = "f x:t y:t>t;/x y";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".into()), Value::Text("lo".into())]);
assert!(err.contains("divide"), "got: {err}");
}
#[test]
fn vm_recfld_on_non_record() {
let source = "f x:t>t;x.name";
let err = vm_run_err(source, Some("f"), vec![Value::List(vec![])]);
assert!(err.contains("field access") || err.contains("record"), "got: {err}");
}
#[test]
fn vm_index_on_non_list() {
let source = "f x:t>t;x.0";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("index") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_foreach_on_non_heap() {
let source = "f x:n>n;@elem x{elem}";
let err = vm_run_err(source, Some("f"), vec![Value::Number(5.0)]);
assert!(err.contains("list") || err.contains("foreach"), "got: {err}");
}
#[test]
fn vm_foreach_on_heap_non_list() {
let source = "f x:t>t;@elem x{elem}";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("list") || err.contains("foreach"), "got: {err}");
}
#[test]
fn vm_with_on_non_record() {
let source = "f x:t>t;x with name:\"bob\"";
let err = vm_run_err(source, Some("f"), vec![Value::List(vec![])]);
assert!(err.contains("record") || err.contains("with"), "got: {err}");
}
#[test]
fn vm_len_on_heap_non_string_non_list() {
let source = "f x:t>n;len x";
let err = vm_run_err(source, Some("f"), vec![Value::Ok(Box::new(Value::Number(1.0)))]);
assert!(err.contains("len") || err.contains("string") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_len_on_non_heap() {
let source = "f x:t>n;len x";
let err = vm_run_err(source, Some("f"), vec![Value::Number(5.0)]);
assert!(err.contains("len") || err.contains("string") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_listappend_on_non_heap() {
let source = "f x:t y:t>t;+=x y";
let err = vm_run_err(source, Some("f"), vec![Value::Number(1.0), Value::Number(2.0)]);
assert!(err.contains("list") || err.contains("+="), "got: {err}");
}
#[test]
fn vm_listappend_on_heap_non_list() {
let source = "f x:t y:t>t;+=x y";
let err = vm_run_err(source, Some("f"), vec![Value::Text("hi".into()), Value::Number(1.0)]);
assert!(err.contains("list") || err.contains("+="), "got: {err}");
}
#[test]
#[should_panic(expected = "function has 256 parameters")]
fn vm_too_many_params_panics() {
use crate::ast::{Decl, Param, Program, Span, Type};
let params: Vec<Param> = (0..256)
.map(|i| Param { name: format!("p{i}"), ty: Type::Number })
.collect();
let prog = Program {
declarations: vec![Decl::Function {
name: "f".to_string(),
params,
body: vec![],
return_type: Type::Number,
span: Span::UNKNOWN,
}],
source: None,
};
let _ = compile(&prog);
}
#[test]
fn vm_const_fold_text_eq_no_fold() {
let result = vm_run(r#"f>b;="hello" "world""#, Some("f"), vec![]);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_const_fold_bool_lt_no_fold() {
let err = vm_run_err("f>b;<true false", Some("f"), vec![]);
assert!(err.contains("compare") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_const_fold_mixed_types_no_fold() {
let err = vm_run_err("f>n;+true 3", Some("f"), vec![]);
assert!(err.contains("add") || err.contains("type") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_const_fold_not_on_number_no_fold() {
let result = vm_run("f>b;!3", Some("f"), vec![]);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_get_compiles() {
let prog = parse_program(r#"f url:t>R t t;get url"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_get_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_GET);
assert!(has_get_op, "expected OP_GET in bytecode");
}
#[test]
fn vm_dollar_desugars_to_get() {
let prog = parse_program(r#"f url:t>R t t;$url"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_get_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_GET);
assert!(has_get_op, "expected OP_GET in bytecode from $ syntax");
}
#[test]
fn vm_post_compiles_to_op_post() {
let prog = parse_program(r#"f url:t body:t>R t t;post url body"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_post_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_POST);
assert!(has_post_op, "expected OP_POST in bytecode");
}
#[test]
fn vm_post_unwrap_compiles_to_op_post() {
let prog = parse_program(r#"f url:t body:t>t;post! url body"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_post_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_POST);
assert!(has_post_op, "expected OP_POST in bytecode for post!");
}
#[test]
fn vm_get_with_headers_compiles_to_op_geth() {
let prog = parse_program(r#"f url:t hdrs:M t t>R t t;get url hdrs"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_geth = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_GETH);
assert!(has_geth, "expected OP_GETH in bytecode");
}
#[test]
fn vm_post_with_headers_compiles_to_op_posth() {
let prog = parse_program(r#"f url:t body:t hdrs:M t t>R t t;post url body hdrs"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_posth = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_POSTH);
assert!(has_posth, "expected OP_POSTH in bytecode");
}
#[test]
fn vm_get_with_headers_bad_host_returns_err() {
let src = r#"f url:t hdrs:M t t>R t t;get url hdrs"#;
let mut headers = std::collections::HashMap::new();
headers.insert("x-api-key".to_string(), Value::Text("tok".to_string()));
let result = vm_run(src, Some("f"), vec![
Value::Text("http://127.0.0.1:1".to_string()),
Value::Map(headers),
]);
let Value::Err(_) = result else { panic!("expected Err") };
}
#[test]
fn vm_post_with_headers_bad_host_returns_err() {
let src = r#"f url:t body:t hdrs:M t t>R t t;post url body hdrs"#;
let mut headers = std::collections::HashMap::new();
headers.insert("x-api-key".to_string(), Value::Text("tok".to_string()));
let result = vm_run(src, Some("f"), vec![
Value::Text("http://127.0.0.1:1".to_string()),
Value::Text("body".to_string()),
Value::Map(headers),
]);
let Value::Err(_) = result else { panic!("expected Err") };
}
#[test]
fn vm_braceless_guard() {
let source = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
assert_eq!(
vm_run(source, Some("cls"), vec![Value::Number(1500.0)]),
Value::Text("gold".to_string())
);
assert_eq!(
vm_run(source, Some("cls"), vec![Value::Number(750.0)]),
Value::Text("silver".to_string())
);
assert_eq!(
vm_run(source, Some("cls"), vec![Value::Number(100.0)]),
Value::Text("bronze".to_string())
);
}
#[test]
fn vm_braceless_guard_factorial() {
let source = "fac n:n>n;<=n 1 1;r=fac -n 1;*n r";
assert_eq!(
vm_run(source, Some("fac"), vec![Value::Number(5.0)]),
Value::Number(120.0)
);
}
#[test]
fn vm_braceless_guard_fibonacci() {
let source = "fib n:n>n;<=n 1 n;a=fib -n 1;b=fib -n 2;+a b";
assert_eq!(
vm_run(source, Some("fib"), vec![Value::Number(10.0)]),
Value::Number(55.0)
);
}
#[test]
fn vm_spl_basic() {
let source = r#"f>L t;spl "a,b,c" ",""#;
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![
Value::Text("a".to_string()),
Value::Text("b".to_string()),
Value::Text("c".to_string()),
])
);
}
#[test]
fn vm_spl_empty() {
let source = r#"f>L t;spl "" ",""#;
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Text("".to_string())])
);
}
#[test]
fn vm_cat_basic() {
let source = "f items:L t>t;cat items \",\"";
assert_eq!(
vm_run(source, Some("f"), vec![Value::List(vec![
Value::Text("a".into()), Value::Text("b".into()), Value::Text("c".into()),
])]),
Value::Text("a,b,c".into())
);
}
#[test]
fn vm_cat_empty_list() {
let source = "f items:L t>t;cat items \"-\"";
assert_eq!(vm_run(source, Some("f"), vec![Value::List(vec![])]), Value::Text("".into()));
}
#[test]
fn vm_has_list() {
let source = "f xs:L n x:n>b;has xs x";
assert_eq!(
vm_run(source, Some("f"), vec![Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]), Value::Number(2.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::List(vec![Value::Number(1.0)]), Value::Number(5.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_has_text() {
let source = r#"f s:t needle:t>b;has s needle"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hello world".into()), Value::Text("world".into())]),
Value::Bool(true)
);
}
#[test]
fn vm_hd_list() {
let source = "f>n;xs=[10, 20, 30];hd xs";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn vm_tl_list() {
let source = "f>L n;xs=[10, 20, 30];tl xs";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(20.0), Value::Number(30.0)])
);
}
#[test]
fn vm_hd_text() {
let source = r#"f s:t>t;hd s"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hello".into())]),
Value::Text("h".into())
);
}
#[test]
fn vm_tl_text() {
let source = r#"f s:t>t;tl s"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("hello".into())]),
Value::Text("ello".into())
);
}
#[test]
fn vm_rev_list() {
let source = "f>L n;rev [1, 2, 3]";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(3.0), Value::Number(2.0), Value::Number(1.0)])
);
}
#[test]
fn vm_rev_text() {
let source = r#"f>t;rev "abc""#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("cba".into()));
}
#[test]
fn vm_srt_numbers() {
let source = "f>L n;srt [3, 1, 2]";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)])
);
}
#[test]
fn vm_srt_text_list() {
let source = r#"f>L t;srt ["c", "a", "b"]"#;
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Text("a".into()), Value::Text("b".into()), Value::Text("c".into())])
);
}
#[test]
fn vm_slc_list() {
let source = "f>L n;slc [1, 2, 3, 4, 5] 1 3";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(2.0), Value::Number(3.0)])
);
}
#[test]
fn vm_slc_text() {
let source = r#"f>t;slc "hello" 1 4"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("ell".into()));
}
#[test]
fn vm_ternary_true() {
let source = r#"f x:n>t;=x 1{"yes"}{"no"}"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Text("yes".into()));
}
#[test]
fn vm_ternary_false() {
let source = r#"f x:n>t;=x 1{"yes"}{"no"}"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(2.0)]), Value::Text("no".into()));
}
#[test]
fn vm_ternary_no_early_return() {
let source = r#"f x:n>n;=x 0{10}{20};+x 1"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(0.0)]), Value::Number(1.0));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(6.0));
}
#[test]
fn vm_ret_early_return() {
let source = r#"f x:n>n;>x 0{ret x};0"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(5.0));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(-1.0)]), Value::Number(0.0));
}
#[test]
fn vm_ret_in_foreach() {
let source = "f xs:L n>n;@x xs{>=x 10{ret x}};0";
let list = Value::List(vec![Value::Number(1.0), Value::Number(15.0), Value::Number(3.0)]);
assert_eq!(vm_run(source, Some("f"), vec![list]), Value::Number(15.0));
}
#[test]
fn vm_pipe_simple() {
let source = "f x:n>n;str x>>len";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(42.0)]), Value::Number(2.0));
}
#[test]
fn vm_pipe_chain() {
let source = "dbl x:n>n;*x 2\nadd1 x:n>n;+x 1\nf x:n>n;dbl x>>add1";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(11.0));
}
#[test]
fn vm_pipe_with_extra_args() {
let source = "add a:n b:n>n;+a b\nf x:n>n;add x 1>>add 2";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(8.0));
}
#[test]
fn vm_while_basic() {
let source = "f>n;i=0;s=0;wh <i 5{i=+i 1;s=+s i};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(15.0));
}
#[test]
fn vm_while_zero_iterations() {
let source = "f>n;wh false{42};0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(0.0));
}
#[test]
fn vm_while_with_ret() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{ret i}};0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_nil_coalesce_nil() {
let source = "mk x:n>n;>=x 1{x}\nf>n;x=mk 0;x??42";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(42.0));
}
#[test]
fn vm_nil_coalesce_non_nil() {
let source = "f>n;x=10;x??42";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn vm_nil_coalesce_chain() {
let source = "mk x:n>n;>=x 1{x}\nf>n;a=mk 0;b=mk 0;a??b??99";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_safe_field_on_nil() {
let source = "mk x:n>n;>=x 1{x}\nf>n;v=mk 0;v.?name??99";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_safe_field_on_value() {
let source = "f>n;p=pt x:5;p.?x\ntype pt{x:n}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_safe_field_chained() {
let source = "mk x:n>n;>=x 1{x}\nf>n;v=mk 0;v.?a.?b??77";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(77.0));
}
#[test]
fn vm_while_brk() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{brk}};i";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_while_brk_value() {
let source = "f>n;i=0;wh true{i=+i 1;>=i 3{brk 99}};i";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_while_cnt() {
let source = "f>n;i=0;s=0;wh <i 5{i=+i 1;>=i 3{cnt};s=+s i};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_foreach_brk() {
let source = "f>n;@x [1,2,3,4,5]{>=x 3{brk x};x}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_foreach_cnt() {
let source = "f>n;@x [1,2,3,4,5]{>=x 3{cnt};*x 2}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn vm_rnd_no_args() {
let source = "f>n;rnd";
let result = vm_run(source, Some("f"), vec![]);
let Value::Number(n) = result else { panic!("expected Number") };
assert!(n >= 0.0 && n < 1.0, "rnd should be in [0,1), got {n}");
}
#[test]
fn vm_rnd_two_args() {
let source = "f>n;rnd 1 10";
let result = vm_run(source, Some("f"), vec![]);
let Value::Number(n) = result else { panic!("expected Number") };
assert!(n >= 1.0 && n <= 10.0, "rnd 1 10 should be in [1,10], got {n}");
assert_eq!(n, n.floor(), "rnd with two args should return integer");
}
#[test]
fn vm_rnd_same_bounds() {
let source = "f>n;rnd 5 5";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_rnd_non_number_type_error() {
let source = "f>n;rnd \"hello\" 5";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("rnd") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_now() {
let source = "f>n;now";
let result = vm_run(source, Some("f"), vec![]);
let Value::Number(n) = result else { panic!("expected Number") };
assert!(n > 1_000_000_000.0, "now should be a reasonable unix timestamp, got {n}");
}
#[test]
fn vm_env_existing_var() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
unsafe { std::env::set_var("ILO_VM_TEST", "vmval"); }
let source = r#"f k:t>R t t;env k"#;
let result = vm_run(source, Some("f"), vec![Value::Text("ILO_VM_TEST".into())]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("vmval".into()))));
unsafe { std::env::remove_var("ILO_VM_TEST"); }
}
#[test]
fn vm_env_missing_var() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
let source = r#"f k:t>R t t;env k"#;
let result = vm_run(source, Some("f"), vec![Value::Text("ILO_VM_NONEXIST_999".into())]);
let Value::Err(inner) = result else { panic!("expected Err") };
let Value::Text(s) = *inner else { panic!("expected Text") };
assert!(s.contains("not set"), "got: {s}");
}
#[test]
fn vm_env_compiles_to_op_env() {
let source = r#"f k:t>R t t;env k"#;
let prog = parse_program(source);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_env_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_ENV);
assert!(has_env_op, "expected OP_ENV in bytecode");
}
#[test]
fn vm_range_basic() {
let source = "f>n;@i 0..3{i}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(2.0));
}
#[test]
fn vm_range_accumulate() {
let source = "f>n;s=0;@i 0..3{s=+s i};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_range_empty() {
let source = "f>n;s=99;@i 5..3{s=0};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_range_dynamic_end() {
let source = "f n:n>n;s=0;@i 0..n{s=+s i};s";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(4.0)]),
Value::Number(6.0)
);
}
#[test]
fn vm_range_brk() {
let source = "f>n;@i 0..10{>=i 3{brk i};i}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_range_cnt() {
let source = "f>n;s=0;@i 0..5{=i 2{cnt};s=+s i};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(8.0));
}
#[test]
fn vm_range_nonzero_start() {
let source = "f>n;s=0;@i 2..5{s=+s i};s";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(9.0));
}
#[test]
fn vm_safe_index_on_nil() {
let source = "mk x:n>n;>=x 1{x}\nf>n;v=mk 0;v.?0??99";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_safe_index_on_value() {
let source = "f>n;xs=[10,20,30];xs.?0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(10.0));
}
#[test]
fn vm_destructure_basic() {
let source = "type pt{x:n;y:n} f>n;p=pt x:3 y:4;{x;y}=p;+x y";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_destructure_single_field() {
let source = "type pt{x:n;y:n} f>n;p=pt x:10 y:20;{y}=p;y";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(20.0));
}
#[test]
fn vm_destructure_in_loop() {
let source = "type pt{x:n;y:n} f>n;ps=[pt x:1 y:2,pt x:3 y:4];@p ps{{x;y}=p;+x y}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_jp_basic() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"name":"alice"}"#.to_string()),
Value::Text("name".to_string()),
]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("alice".to_string()))));
}
#[test]
fn vm_jp_nested() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"user":{"name":"bob"}}"#.to_string()),
Value::Text("user.name".to_string()),
]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("bob".to_string()))));
}
#[test]
fn vm_jp_array_index() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"[10,20,30]"#.to_string()),
Value::Text("1".to_string()),
]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("20".to_string()))));
}
#[test]
fn vm_jp_missing_key() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"a":1}"#.to_string()),
Value::Text("b".to_string()),
]);
let Value::Err(e) = result else { panic!("expected Err") };
assert!(e.to_string().contains("key not found"), "got: {}", e);
}
#[test]
fn vm_jp_unwrap() {
let source = r#"f j:t p:t>t;jpth! j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"x":"hello"}"#.to_string()),
Value::Text("x".to_string()),
]);
assert_eq!(result, Value::Text("hello".to_string()));
}
#[test]
fn vm_jp_compiles_to_opcode() {
let prog = parse_program(r#"f j:t p:t>R t t;jpth j p"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_jp_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_JPTH);
assert!(has_jp_op, "expected OP_JPTH in bytecode");
}
#[test]
fn vm_jd_number() {
let source = "f x:n>t;jdmp x";
let result = vm_run(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Text("42".to_string()));
}
#[test]
fn vm_jd_text() {
let source = r#"f x:t>t;jdmp x"#;
let result = vm_run(source, Some("f"), vec![Value::Text("hello".to_string())]);
assert_eq!(result, Value::Text(r#""hello""#.to_string()));
}
#[test]
fn vm_jd_list() {
let source = "f>t;xs=[1, 2, 3];jdmp xs";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Text("[1,2,3]".to_string()));
}
#[test]
fn vm_jd_record() {
let source = "type pt{x:n;y:n} f>t;p=pt x:1 y:2;jdmp p";
let result = vm_run(source, Some("f"), vec![]);
let Value::Text(ref s) = result else { panic!("expected text") };
let text = s.clone();
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(parsed["x"], 1);
assert_eq!(parsed["y"], 2);
}
#[test]
fn vm_jd_compiles_to_opcode() {
let prog = parse_program("f x:n>t;jdmp x");
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_jd_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_JDMP);
assert!(has_jd_op, "expected OP_JDMP in bytecode");
}
#[test]
fn vm_jparse_object() {
let source = r#"f j:t>R t t;jpar j"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"a":1,"b":"two"}"#.to_string()),
]);
let Value::Ok(inner) = result else { panic!("expected Ok") };
let Value::Record { type_name, fields } = *inner else { panic!("expected record") };
assert_eq!(type_name, "json");
assert_eq!(fields.get("a"), Some(&Value::Number(1.0)));
assert_eq!(fields.get("b"), Some(&Value::Text("two".to_string())));
}
#[test]
fn vm_jparse_array() {
let source = r#"f j:t>R t t;jpar j"#;
let result = vm_run(source, Some("f"), vec![
Value::Text("[1,2,3]".to_string()),
]);
let Value::Ok(inner) = result else { panic!("expected Ok") };
assert_eq!(*inner, Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]));
}
#[test]
fn vm_jparse_invalid() {
let source = r#"f j:t>R t t;jpar j"#;
let result = vm_run(source, Some("f"), vec![
Value::Text("not json".to_string()),
]);
assert!(matches!(result, Value::Err(_)));
}
#[test]
fn vm_jparse_unwrap() {
let source = r#"f j:t>t;jpar! j"#;
let result = vm_run(source, Some("f"), vec![Value::Text(r#"{"x":1}"#.to_string())]);
let Value::Record { type_name, fields } = result else { panic!("expected record") };
assert_eq!(type_name, "json");
assert_eq!(fields.get("x"), Some(&Value::Number(1.0)));
}
#[test]
fn vm_jparse_compiles_to_opcode() {
let prog = parse_program(r#"f j:t>R t t;jpar j"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_jparse_op = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_JPAR);
assert!(has_jparse_op, "expected OP_JPAR in bytecode");
}
#[test]
fn vm_jparse_then_field_access() {
let source = r#"f j:t>n;r=jpar! j;r.x"#;
let result = vm_run(source, Some("f"), vec![Value::Text(r#"{"x":42}"#.to_string())]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn vm_trm_basic() {
let result = vm_run("f s:t>t;trm s", Some("f"), vec![Value::Text(" hello ".into())]);
assert_eq!(result, Value::Text("hello".into()));
}
#[test]
fn vm_trm_no_whitespace() {
let result = vm_run("f s:t>t;trm s", Some("f"), vec![Value::Text("hi".into())]);
assert_eq!(result, Value::Text("hi".into()));
}
#[test]
fn vm_trm_only_whitespace() {
let result = vm_run("f s:t>t;trm s", Some("f"), vec![Value::Text(" ".into())]);
assert_eq!(result, Value::Text("".into()));
}
#[test]
fn vm_trm_compiles_to_opcode() {
let prog = parse_program("f s:t>t;trm s");
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
assert!(chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_TRM), "expected OP_TRM in bytecode");
}
#[test]
fn vm_unq_text() {
let result = vm_run("f s:t>t;unq s", Some("f"), vec![Value::Text("aabbc".into())]);
assert_eq!(result, Value::Text("abc".into()));
}
#[test]
fn vm_unq_list_numbers() {
let result = vm_run("f xs:L n>L n;unq xs", Some("f"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(1.0), Value::Number(3.0)]),
]);
assert_eq!(result, Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]));
}
#[test]
fn vm_unq_list_strings_dedup() {
let result = vm_run("f xs:L t>L t;unq xs", Some("f"), vec![
Value::List(vec![
Value::Text("a".into()),
Value::Text("b".into()),
Value::Text("a".into()),
Value::Text("c".into()),
Value::Text("b".into()),
]),
]);
assert_eq!(result, Value::List(vec![
Value::Text("a".into()),
Value::Text("b".into()),
Value::Text("c".into()),
]));
}
#[test]
fn vm_unq_preserves_order() {
let result = vm_run("f xs:L n>L n;unq xs", Some("f"), vec![
Value::List(vec![Value::Number(3.0), Value::Number(1.0), Value::Number(2.0), Value::Number(1.0)]),
]);
assert_eq!(result, Value::List(vec![Value::Number(3.0), Value::Number(1.0), Value::Number(2.0)]));
}
#[test]
fn vm_unq_empty_list() {
let result = vm_run("f xs:L n>L n;unq xs", Some("f"), vec![Value::List(vec![])]);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_unq_compiles_to_opcode() {
let prog = parse_program("f xs:L n>L n;unq xs");
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
assert!(chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNQ), "expected OP_UNQ in bytecode");
}
#[test]
fn vm_prnt_returns_value() {
let result = vm_run("f x:n>n;prnt x", Some("f"), vec![Value::Number(7.0)]);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn vm_rd_file_not_found() {
let result = vm_run(
"f p:t>t;rd p",
Some("f"),
vec![Value::Text("/nonexistent/ilo_test.txt".into())],
);
assert!(matches!(result, Value::Err(_)), "expected Err, got {:?}", result);
}
#[test]
fn vm_mapnew_empty() {
let result = vm_run("f>M t n;mmap", Some("f"), vec![]);
assert!(matches!(result, Value::Map(_)), "expected Map, got {result:?}");
if let Value::Map(m) = result { assert!(m.is_empty()); }
}
#[test]
fn vm_mset_and_mget_roundtrip() {
let result = vm_run(
r#"f>O n;m=mset mmap "x" 7;mget m "x""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn vm_mset_multiple_keys() {
let result = vm_run(
r#"f>O n;m=mset mmap "a" 1;m=mset m "b" 2;mget m "b""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn vm_mget_missing_key_returns_nil() {
let result = vm_run(
r#"f>O n;m=mset mmap "x" 1;mget m "y""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Nil);
}
#[test]
fn vm_mhas_present() {
let result = vm_run(
r#"f>b;m=mset mmap "k" 99;mhas m "k""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_mhas_absent() {
let result = vm_run(
r#"f>b;m=mset mmap "k" 99;mhas m "z""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_mkeys_sorted() {
let result = vm_run(
r#"f>L t;m=mset mmap "b" 2;m=mset m "a" 1;m=mset m "c" 3;mkeys m"#,
Some("f"), vec![],
);
assert_eq!(result, Value::List(vec![
Value::Text("a".into()),
Value::Text("b".into()),
Value::Text("c".into()),
]));
}
#[test]
fn vm_mvals_sorted_by_key() {
let result = vm_run(
r#"f>L n;m=mset mmap "b" 2;m=mset m "a" 1;m=mset m "c" 3;mvals m"#,
Some("f"), vec![],
);
assert_eq!(result, Value::List(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
]));
}
#[test]
fn vm_mdel_removes_key() {
let result = vm_run(
r#"f>b;m=mset mmap "k" 1;m=mdel m "k";mhas m "k""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_mdel_nonexistent_key_noop() {
let result = vm_run(
r#"f>O n;m=mset mmap "k" 42;m=mdel m "z";mget m "k""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn vm_mset_immutable_original() {
let result = vm_run(
r#"f>b;orig=mset mmap "k" 1;upd=mset orig "k" 99;mhas orig "k""#,
Some("f"), vec![],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_mkeys_empty_map() {
let result = vm_run("f>L t;mkeys mmap", Some("f"), vec![]);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_mvals_empty_map() {
let result = vm_run("f>L n;mvals mmap", Some("f"), vec![]);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_map_compiles_to_opcode() {
let prog = parse_program(r#"f>O n;m=mset mmap "k" 1;mget m "k""#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_mapnew = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_MAPNEW);
let has_mset = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_MSET);
let has_mget = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_MGET);
assert!(has_mapnew, "expected OP_MAPNEW");
assert!(has_mset, "expected OP_MSET");
assert!(has_mget, "expected OP_MGET");
}
#[test]
fn vm_hd_empty_list_is_error() {
let err = vm_run_err("f xs:L n>n;hd xs", Some("f"), vec![Value::List(vec![])]);
assert!(err.contains("hd"), "expected hd error, got: {err}");
}
#[test]
fn vm_hd_empty_text_is_error() {
let err = vm_run_err(
"f s:t>t;hd s", Some("f"), vec![Value::Text(String::new())],
);
assert!(err.contains("hd"), "expected hd error, got: {err}");
}
#[test]
fn vm_tl_empty_list_is_error() {
let err = vm_run_err("f xs:L n>n;tl xs", Some("f"), vec![Value::List(vec![])]);
assert!(err.contains("tl"), "expected tl error, got: {err}");
}
#[test]
fn vm_tl_empty_text_is_error() {
let err = vm_run_err(
"f s:t>t;tl s", Some("f"), vec![Value::Text(String::new())],
);
assert!(err.contains("tl"), "expected tl error, got: {err}");
}
#[test]
fn vm_srt_mixed_types_is_error() {
let err = vm_run_err(
"f xs:L n>t;srt xs", Some("f"),
vec![Value::List(vec![Value::Number(1.0), Value::Text("a".into())])],
);
assert!(err.contains("srt"), "expected srt error, got: {err}");
}
#[test]
fn vm_cat_empty_separator() {
let result = vm_run(
"f items:L t>t;cat items \"\"", Some("f"),
vec![Value::List(vec![
Value::Text("a".into()), Value::Text("b".into()), Value::Text("c".into()),
])],
);
assert_eq!(result, Value::Text("abc".into()));
}
#[test]
fn vm_has_number_in_list() {
let result = vm_run(
"f xs:L n x:n>b;has xs x", Some("f"),
vec![Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]),
Value::Number(2.0)],
);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_has_number_not_in_list() {
let result = vm_run(
"f xs:L n x:n>b;has xs x", Some("f"),
vec![Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]),
Value::Number(9.0)],
);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_slc_out_of_bounds_clamped() {
let result = vm_run(
"f xs:L n>L n;slc xs 0 100", Some("f"),
vec![Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)])],
);
assert_eq!(result, Value::List(vec![
Value::Number(1.0), Value::Number(2.0), Value::Number(3.0),
]));
}
#[test]
fn vm_rev_empty_list() {
let result = vm_run(
"f xs:L n>L n;rev xs", Some("f"),
vec![Value::List(vec![])],
);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_srt_empty_list() {
let result = vm_run(
"f xs:L n>L n;srt xs", Some("f"),
vec![Value::List(vec![])],
);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_srt_text_chars() {
let result = vm_run(r#"f>t;srt "bac""#, Some("f"), vec![]);
assert_eq!(result, Value::Text("abc".into()));
}
#[test]
fn vm_rdl_file_not_found() {
let result = vm_run(
"f p:t>t;rdl p", Some("f"),
vec![Value::Text("/nonexistent/ilo_rdl_test.txt".into())],
);
assert!(matches!(result, Value::Err(_)), "expected Err, got {result:?}");
}
#[test]
fn vm_wr_and_rdl_roundtrip() {
let path = "/tmp/ilo_vm_rdl_test.txt";
std::fs::write(path, "line1\nline2\n").unwrap();
let result = vm_run(
"f p:t>t;rdl p", Some("f"),
vec![Value::Text(path.into())],
);
let Value::Ok(inner) = result else { panic!("expected Ok") };
let Value::List(lines) = *inner else { panic!("expected List inside Ok") };
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], Value::Text("line1".into()));
let _ = std::fs::remove_file(path);
}
#[test]
fn vm_type_check_isnum_match() {
let src = r#"f x:t>b;?x{n _:true;_:false}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(42.0)]), Value::Bool(true));
assert_eq!(vm_run(src, Some("f"), vec![Value::Text("hi".into())]), Value::Bool(false));
}
#[test]
fn vm_type_check_istext_match() {
let src = r#"f x:n>b;?x{t _:true;_:false}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Text("hello".into())]), Value::Bool(true));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(5.0)]), Value::Bool(false));
}
#[test]
fn vm_type_check_isbool_match() {
let src = r#"f x:n>b;?x{b _:true;_:false}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Bool(true)]), Value::Bool(true));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(1.0)]), Value::Bool(false));
}
#[test]
fn vm_type_check_islist_match() {
let src = r#"f x:n>b;?x{l _:true;_:false}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::List(vec![Value::Number(1.0)])]),
Value::Bool(true)
);
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(5.0)]), Value::Bool(false));
}
#[test]
fn vm_type_check_isnum_with_binding() {
let src = r#"f x:t>n;?x{n v:v;_:0}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(99.0)]), Value::Number(99.0));
assert_eq!(vm_run(src, Some("f"), vec![Value::Text("x".into())]), Value::Number(0.0));
}
#[test]
fn vm_type_check_istext_with_binding() {
let src = r#"f x:n>t;?x{t v:v;_:"nope"}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Text("yes".into())]), Value::Text("yes".into()));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(1.0)]), Value::Text("nope".into()));
}
#[test]
fn vm_type_check_isbool_with_binding() {
let src = r#"f x:n>b;?x{b v:v;_:false}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Bool(true)]), Value::Bool(true));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(0.0)]), Value::Bool(false));
}
#[test]
fn vm_type_check_compiles_to_opcode() {
let prog = parse_program(r#"f x:t>b;?x{n _:true;_:false}"#);
let compiled = compile(&prog).unwrap();
let idx = compiled.func_index("f").unwrap() as usize;
let chunk = &compiled.chunks[idx];
let has_isnum = chunk.code.iter().any(|&inst| (inst >> 24) as u8 == OP_ISNUM);
assert!(has_isnum, "expected OP_ISNUM in compiled chunk");
}
#[test]
fn vm_env_non_string_error() {
let err = vm_run_err("f x:n>R t t;env x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("env") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_jpar_non_string_error() {
let err = vm_run_err("f x:n>R t t;jpar x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("jpar") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_jpth_non_string_json_error() {
let err = vm_run_err(
"f j:n p:t>R t t;jpth j p",
Some("f"),
vec![Value::Number(42.0), Value::Text("key".into())],
);
assert!(err.contains("jpth") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_jpth_non_string_path_error() {
let err = vm_run_err(
r#"f j:t p:n>R t t;jpth j p"#,
Some("f"),
vec![Value::Text("{}".into()), Value::Number(1.0)],
);
assert!(err.contains("jpth") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_trm_non_string_error() {
let err = vm_run_err("f x:n>t;trm x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("trm") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_unq_non_string_non_list_error() {
let err = vm_run_err("f x:n>t;unq x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("unq") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_rd_non_string_path_error() {
let err = vm_run_err("f x:n>R t t;rd x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("rd") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_rdl_non_string_error() {
let err = vm_run_err("f x:n>R t t;rdl x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("rdl") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_wr_non_string_path_error() {
let err = vm_run_err(
"f x:n c:t>R t t;wr x c",
Some("f"),
vec![Value::Number(42.0), Value::Text("content".into())],
);
assert!(err.contains("wr") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_wr_non_string_content_error() {
let err = vm_run_err(
"f p:t x:n>R t t;wr p x",
Some("f"),
vec![Value::Text("/tmp/test".into()), Value::Number(42.0)],
);
assert!(err.contains("wr") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_wrl_non_string_path_error() {
let err = vm_run_err(
"f x:n xs:L t>R t t;wrl x xs",
Some("f"),
vec![Value::Number(42.0), Value::List(vec![Value::Text("a".into())])],
);
assert!(err.contains("wrl") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_wr_creates_file() {
let path = "/tmp/ilo_vm_wr_test.txt";
let result = vm_run(
"f p:t c:t>R t t;wr p c",
Some("f"),
vec![Value::Text(path.into()), Value::Text("hello from ilo".into())],
);
assert!(matches!(result, Value::Ok(_)), "wr should succeed, got {result:?}");
let content = std::fs::read_to_string(path).unwrap();
assert_eq!(content, "hello from ilo");
let _ = std::fs::remove_file(path);
}
#[test]
fn vm_wrl_creates_file() {
let path = std::env::temp_dir().join("ilo_vm_wrl_test.txt");
let path_str = path.to_str().unwrap();
let result = vm_run(
"f p:t xs:L t>R t t;wrl p xs",
Some("f"),
vec![
Value::Text(path_str.into()),
Value::List(vec![Value::Text("line1".into()), Value::Text("line2".into())]),
],
);
assert!(matches!(result, Value::Ok(_)), "wrl should succeed, got {result:?}");
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("line1"), "got: {content}");
let _ = std::fs::remove_file(&path);
}
#[test]
fn vm_recwith_multiple_fields() {
let src = "type pt{x:n;y:n;z:n} f>n;p=pt x:1 y:2 z:3;q=p with x:10;q.x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_recwith_preserves_other_fields() {
let src = "type pt{x:n;y:n} f>n;p=pt x:1 y:2;q=p with x:99;q.y";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2.0), "y should be unchanged");
}
#[test]
fn vm_recwith_original_unchanged() {
let src = "type pt{x:n;y:n} f>n;orig=pt x:1 y:2;upd=orig with x:99;orig.x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(1.0), "original should be unchanged");
}
#[test]
fn vm_rd_json_file() {
let path = "/tmp/ilo_vm_rd_json.json";
std::fs::write(path, r#"{"key":"value"}"#).unwrap();
let result = vm_run("f p:t>R t t;rd p", Some("f"), vec![Value::Text(path.into())]);
assert!(matches!(result, Value::Ok(_)), "rd json should succeed, got {result:?}");
let _ = std::fs::remove_file(path);
}
#[test]
fn vm_rd_csv_file() {
let path = "/tmp/ilo_vm_rd_csv.csv";
std::fs::write(path, "a,b,c\n1,2,3\n").unwrap();
let result = vm_run("f p:t>R t t;rd p", Some("f"), vec![Value::Text(path.into())]);
assert!(matches!(result, Value::Ok(_)), "rd csv should succeed, got {result:?}");
let _ = std::fs::remove_file(path);
}
#[test]
fn vm_nil_coalesce_on_nil_uses_default() {
let src = "f>n;m=mmap;v=mget m \"x\";v??99";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(99.0));
}
#[test]
fn vm_nil_coalesce_on_non_nil_skips_default() {
let src = "f>n;m=mset mmap \"x\" 5;mget m \"x\"??99";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_text_greater_than() {
let result = vm_run("f a:t b:t>b;>a b", Some("f"), vec![
Value::Text("b".into()), Value::Text("a".into()),
]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_text_less_than() {
let result = vm_run("f a:t b:t>b;<a b", Some("f"), vec![
Value::Text("a".into()), Value::Text("b".into()),
]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_text_greater_or_equal() {
let result = vm_run("f a:t b:t>b;>=a b", Some("f"), vec![
Value::Text("a".into()), Value::Text("a".into()),
]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_text_less_or_equal() {
let result = vm_run("f a:t b:t>b;<=a b", Some("f"), vec![
Value::Text("a".into()), Value::Text("b".into()),
]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_rnd_two_args_range() {
let result = vm_run("f a:n b:n>n;rnd a b", Some("f"), vec![
Value::Number(5.0), Value::Number(5.0),
]);
assert_eq!(result, Value::Number(5.0)); }
#[test]
fn vm_slc_basic() {
let result = vm_run(
"f xs:L n>L n;slc xs 1 3",
Some("f"),
vec![Value::List(vec![
Value::Number(10.0), Value::Number(20.0),
Value::Number(30.0), Value::Number(40.0),
])],
);
assert_eq!(result, Value::List(vec![Value::Number(20.0), Value::Number(30.0)]));
}
#[test]
fn vm_cat_non_string_list_error() {
let err = vm_run_err(
"f xs:L n sep:t>t;cat xs sep",
Some("f"),
vec![
Value::List(vec![Value::Number(1.0)]),
Value::Text(",".into()),
],
);
assert!(err.contains("cat") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_spl_non_string_error() {
let err = vm_run_err(
"f x:n sep:t>L t;spl x sep",
Some("f"),
vec![Value::Number(42.0), Value::Text(",".into())],
);
assert!(err.contains("spl") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_hd_on_number_error() {
let err = vm_run_err("f x:n>n;hd x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("hd") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_tl_on_number_error() {
let err = vm_run_err("f x:n>n;tl x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("tl") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_rev_on_number_error() {
let err = vm_run_err("f x:n>n;rev x", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("rev") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_has_text_non_text_needle_error() {
let err = vm_run_err(
"f s:t x:n>b;has s x",
Some("f"),
vec![Value::Text("hello".into()), Value::Number(42.0)],
);
assert!(err.contains("has") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_has_non_collection_error() {
let err = vm_run_err(
"f x:n y:n>b;has x y",
Some("f"),
vec![Value::Number(42.0), Value::Number(10.0)],
);
assert!(err.contains("has") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_slc_on_number_error() {
let err = vm_run_err("f x:n>n;slc x 0 1", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("slc") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_srt_single_element() {
let result = vm_run("f>L n;srt [42]", Some("f"), vec![]);
assert_eq!(result, Value::List(vec![Value::Number(42.0)]));
}
#[test]
fn vm_cat_string_first_arg_error() {
let err = vm_run_err(
r#"f s:t sep:t>t;cat s sep"#,
Some("f"),
vec![Value::Text("hello".into()), Value::Text(",".into())],
);
assert!(err.contains("cat") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_not_on_non_empty_text_is_false() {
let result = vm_run(r#"f s:t>b;!s"#, Some("f"), vec![Value::Text("hi".into())]);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_not_on_empty_list_is_true() {
let result = vm_run("f xs:L n>b;!xs", Some("f"), vec![Value::List(vec![])]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_neg_on_text_error() {
let err = vm_run_err(r#"f x:t>n;neg x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("neg") || err.contains("number") || err.contains("n"), "got: {err}");
}
#[test]
fn vm_let_rebind_accumulates() {
let result = vm_run("f>n;x=1;x=+x 1;x=+x 1;x", Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_match_no_subject_wildcard() {
let result = vm_run(r#"f x:n>t;?{_:"default"}"#, Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Text("default".into()));
}
#[test]
fn vm_abs_on_text_error() {
let err = vm_run_err(r#"f x:t>n;abs x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("abs") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_flr_on_text_error() {
let err = vm_run_err(r#"f x:t>n;flr x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("flr") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_cel_on_text_error() {
let err = vm_run_err(r#"f x:t>n;cel x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("cel") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_min_on_text_error() {
let err = vm_run_err(r#"f x:t y:t>n;min x y"#, Some("f"), vec![
Value::Text("a".into()), Value::Text("b".into()),
]);
assert!(err.contains("min") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_rnd2_lo_greater_than_hi_error() {
let err = vm_run_err(
"f a:n b:n>n;rnd a b",
Some("f"),
vec![Value::Number(10.0), Value::Number(5.0)],
);
assert!(err.contains("rnd") || err.contains("bound") || err.contains("lower"), "got: {err}");
}
#[test]
fn vm_match_literal_number_hit() {
let src = r#"f x:n>t;?x{42:"found";_:"other"}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Number(42.0)]),
Value::Text("found".into())
);
}
#[test]
fn vm_match_literal_number_miss() {
let src = r#"f x:n>t;?x{42:"found";_:"other"}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Number(7.0)]),
Value::Text("other".into())
);
}
#[test]
fn vm_match_multiple_literal_numbers() {
let src = r#"f x:n>t;?x{1:"one";2:"two";3:"three";_:"many"}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Number(2.0)]),
Value::Text("two".into())
);
assert_eq!(
vm_run(src, Some("f"), vec![Value::Number(99.0)]),
Value::Text("many".into())
);
}
#[test]
fn vm_match_literal_bool_true() {
let src = r#"f x:b>t;?x{true:"yes";false:"no"}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Bool(true)]),
Value::Text("yes".into())
);
}
#[test]
fn vm_match_literal_bool_false() {
let src = r#"f x:b>t;?x{true:"yes";false:"no"}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Bool(false)]),
Value::Text("no".into())
);
}
#[test]
fn vm_safe_field_on_record_non_nil_returns_value() {
let src = "f>t;p=rec name:\"alice\";p.?name\ntype rec{name:t}";
assert_eq!(
vm_run(src, Some("f"), vec![]),
Value::Text("alice".into())
);
}
#[test]
fn vm_safe_field_chain_nil_propagates() {
let src = "mk x:n>n;>=x 1{x}\nf>t;v=mk 0;v.?a.?b??\"default\"";
assert_eq!(
vm_run(src, Some("f"), vec![]),
Value::Text("default".into())
);
}
#[test]
fn vm_safe_index_on_list_index_1() {
let src = "f>n;xs=[10,20,30];xs.?1";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(20.0));
}
#[test]
fn vm_nil_coalesce_text_nil_uses_default() {
let src = "mk x:n>n;>=x 1{x}\nf>t;v=mk 0;v??\"fallback\"";
assert_eq!(
vm_run(src, Some("f"), vec![]),
Value::Text("fallback".into())
);
}
#[test]
fn vm_nil_coalesce_text_non_nil_passes_through() {
let src = "f>t;v=\"hello\";v??\"fallback\"";
assert_eq!(
vm_run(src, Some("f"), vec![]),
Value::Text("hello".into())
);
}
#[test]
fn vm_foreach_brk_no_value_exits_loop() {
let src = "f>n;tot=0;@x [1,2,3,4,5]{>=x 3{brk};tot=+tot x};tot";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_foreach_cnt_accumulate_sum() {
let src = "f>n;s=0;@x [1,2,3,4,5]{>x 3{cnt};s=+s x};s";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(6.0));
}
#[test]
fn vm_post_bad_host_returns_err() {
let src = r#"f url:t body:t>R t t;post url body"#;
let result = vm_run(
src,
Some("f"),
vec![
Value::Text("http://ilo-lang-test-nonexistent.invalid/endpoint".into()),
Value::Text("{}".into()),
],
);
assert!(
matches!(result, Value::Err(_)),
"expected Err from bad host, got {:?}", result
);
}
#[test]
fn vm_match_result_ok_arm() {
let src = r#"f r:R n t>n;?r{~v:v;^_:0}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Ok(Box::new(Value::Number(42.0)))]),
Value::Number(42.0)
);
}
#[test]
fn vm_match_result_err_arm() {
let src = r#"f r:R n t>n;?r{~_:1;^_:0}"#;
assert_eq!(
vm_run(src, Some("f"), vec![Value::Err(Box::new(Value::Text("oops".into())))]),
Value::Number(0.0)
);
}
#[test]
fn vm_range_cnt_skip_middle() {
let src = "f>n;s=0;@i 0..6{=i 3{cnt};s=+s i};s";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(12.0));
}
#[test]
fn vm_while_brk_expr_value_discarded() {
let src = "f>n;i=0;wh true{i=+i 1;>=i 5{brk 999}};i";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(5.0));
}
#[test]
fn vm_mget_nil_coalesce_default() {
let src = r#"f>n;m=mset mmap "a" 1;v=mget m "missing";v??42"#;
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(42.0));
}
#[test]
fn vm_safe_index_non_nil_with_coalesce() {
let src = "f>n;xs=[5,10,15];xs.?2??99";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(15.0));
}
#[test]
fn vm_foreach_with_inner_while_brk() {
let src = "f>n;s=0;@x [1,2,3]{i=0;wh true{i=+i x;>=i x{brk}};s=+s i};s";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(6.0));
}
#[test]
fn vm_nil_coalesce_bool_default() {
let src = "mk x:n>n;>=x 1{x}\nf>b;v=mk 0;v??true";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Bool(true));
}
#[test]
fn vm_jdmp_number_arg() {
let source = "f x:n>t;jdmp x";
let result = vm_run(source, Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Text("42".to_string()));
}
#[test]
fn vm_jdmp_list_arg() {
let source = "f xs:L n>t;jdmp xs";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0)]),
]);
assert_eq!(result, Value::Text("[1,2]".to_string()));
}
#[test]
fn vm_jdmp_nil() {
let source = "f>t;m=mmap;v=mget m \"missing\";jdmp v";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Text("null".to_string()));
}
#[test]
fn vm_has_map_heap_returns_error() {
let err = vm_run_err(
"f coll:n needle:t>b;has coll needle",
Some("f"),
vec![
Value::Map({
let mut m = std::collections::HashMap::new();
m.insert("x".to_string(), Value::Text("1".to_string()));
m
}),
Value::Text("x".to_string()),
],
);
assert!(err.contains("has") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_cat_number_list_element_error() {
let err = vm_run_err(
"f xs:L n sep:t>t;cat xs sep",
Some("f"),
vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0)]),
Value::Text(" ".to_string()),
],
);
assert!(
err.contains("cat") || err.contains("text") || err.contains("string"),
"got: {err}"
);
}
#[test]
fn vm_geth_empty_map_headers_bad_host() {
let src = r#"f url:t hdrs:M t t>R t t;get url hdrs"#;
let result = vm_run(src, Some("f"), vec![
Value::Text("http://127.0.0.1:1".to_string()),
Value::Map(std::collections::HashMap::new()),
]);
assert!(matches!(result, Value::Err(_)), "expected Err, got {result:?}");
}
#[test]
fn vm_posth_empty_map_headers_bad_host() {
let src = r#"f url:t body:t hdrs:M t t>R t t;post url body hdrs"#;
let result = vm_run(src, Some("f"), vec![
Value::Text("http://127.0.0.1:1".to_string()),
Value::Text("{}".to_string()),
Value::Map(std::collections::HashMap::new()),
]);
assert!(matches!(result, Value::Err(_)), "expected Err, got {result:?}");
}
#[test]
fn vm_flr_integer_valued_float() {
let result = vm_run("f x:n>n;flr x", Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_cel_integer_valued_float() {
let result = vm_run("f x:n>n;cel x", Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_flr_negative_fraction() {
let result = vm_run("f x:n>n;flr x", Some("f"), vec![Value::Number(-2.3)]);
assert_eq!(result, Value::Number(-3.0));
}
#[test]
fn vm_runtime_error_display_formats_message() {
let err = VmRuntimeError {
error: VmError::Type("test error message"),
span: None,
call_stack: vec!["f".to_string()],
};
let s = format!("{err}");
assert!(s.contains("test error message"), "got: {s}");
}
#[test]
fn vm_runtime_error_source_is_some() {
use std::error::Error;
let err = VmRuntimeError {
error: VmError::Type("inner"),
span: None,
call_stack: vec![],
};
assert!(err.source().is_some());
}
#[test]
fn vm_env_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f k:t>t;env! k"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_env = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_ENV);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_env, "expected OP_ENV");
assert!(has_unwrap, "expected OP_UNWRAP for env!");
}
#[test]
fn vm_get_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f url:t>t;get! url"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_get = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_GET);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_get, "expected OP_GET");
assert!(has_unwrap, "expected OP_UNWRAP for get!");
}
#[test]
fn vm_get_with_headers_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f url:t hdrs:M t t>t;get! url hdrs"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_geth = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_GETH);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_geth, "expected OP_GETH");
assert!(has_unwrap, "expected OP_UNWRAP for get! with headers");
}
#[test]
fn vm_post_with_headers_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f url:t body:t hdrs:M t t>t;post! url body hdrs"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_posth = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_POSTH);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_posth, "expected OP_POSTH");
assert!(has_unwrap, "expected OP_UNWRAP for post! with headers");
}
#[test]
fn vm_rd_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f path:t>t;rd! path"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_rd = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_RD);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_rd, "expected OP_RD");
assert!(has_unwrap, "expected OP_UNWRAP for rd!");
}
#[test]
fn vm_rdl_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f path:t>L t;rdl! path"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_rdl = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_RDL);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_rdl, "expected OP_RDL");
assert!(has_unwrap, "expected OP_UNWRAP for rdl!");
}
#[test]
fn vm_wr_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f path:t data:t>t;wr! path data"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_wr = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_WR);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_wr, "expected OP_WR");
assert!(has_unwrap, "expected OP_UNWRAP for wr!");
}
#[test]
fn vm_wrl_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f path:t data:t>t;wrl! path data"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_wrl = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_WRL);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_wrl, "expected OP_WRL");
assert!(has_unwrap, "expected OP_UNWRAP for wrl!");
}
#[test]
fn vm_jpar_bang_compiles_unwrap_sequence() {
let prog = parse_program(r#"f s:t>t;jpar! s"#);
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_jpar = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_JPAR);
let has_unwrap = chunk.code.iter().any(|inst| (inst >> 24) as u8 == OP_UNWRAP);
assert!(has_jpar, "expected OP_JPAR");
assert!(has_unwrap, "expected OP_UNWRAP for jpar!");
}
#[test]
fn vm_ternary_then_empty_body_yields_nil() {
let result = vm_run("f x:n>n;>x 0{}{99}", Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Nil);
}
#[test]
fn vm_ternary_else_empty_body_yields_nil() {
let result = vm_run("f x:n>n;>x 0{99}{}", Some("f"), vec![Value::Number(-1.0)]);
assert_eq!(result, Value::Nil);
}
#[test]
fn vm_destructure_into_existing_local() {
let src = "type pt{x:n;y:n} f>n;x=0;p=pt x:10 y:20;{x}=p;x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_foreach_cnt_skips_iteration() {
let src = "f>n;s=0;@x [1,2,3,4,5]{>x 3{cnt};s=+s x};s";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(6.0));
}
#[test]
fn vm_forrange_cnt_skips_iteration() {
let src = "f>n;s=0;@i 0..6{>i 3{cnt};s=+s i};s";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(6.0));
}
#[test]
fn vm_nanval_from_fnref() {
let val = Value::FnRef("my_fn".to_string());
let nv = NanVal::from_value(&val);
let back = nv.to_value();
let Value::Text(s) = back else { panic!("expected Text") };
assert!(s.contains("my_fn"), "got: {s}");
}
#[cfg(feature = "cranelift")]
mod jit_helpers {
use super::super::*;
fn num(v: f64) -> u64 { NanVal::number(v).0 }
fn is_num(v: u64) -> bool { NanVal(v).is_number() }
fn as_num(v: u64) -> f64 { NanVal(v).as_number() }
fn is_bool(v: u64) -> bool { v == TAG_TRUE || v == TAG_FALSE }
fn as_bool(v: u64) -> bool { v == TAG_TRUE }
fn is_nil(v: u64) -> bool { v == TAG_NIL }
#[test]
fn jit_sub_numbers() {
let r = jit_sub(num(10.0), num(3.0));
assert!(is_num(r));
assert_eq!(as_num(r), 7.0);
}
#[test]
fn jit_sub_non_numbers_returns_nil() {
let s = NanVal::heap_string("hello".into());
let r = jit_sub(s.0, num(1.0));
assert!(is_nil(r));
}
#[test]
fn jit_mul_numbers() {
let r = jit_mul(num(4.0), num(5.0));
assert!(is_num(r));
assert_eq!(as_num(r), 20.0);
}
#[test]
fn jit_div_numbers() {
let r = jit_div(num(10.0), num(4.0));
assert!(is_num(r));
assert_eq!(as_num(r), 2.5);
}
#[test]
fn jit_div_by_zero_returns_nil() {
let r = jit_div(num(5.0), num(0.0));
assert!(is_nil(r));
}
#[test]
fn jit_eq_equal_numbers() {
let r = jit_eq(num(3.0), num(3.0));
assert!(is_bool(r));
assert!(as_bool(r));
}
#[test]
fn jit_eq_unequal_numbers() {
let r = jit_eq(num(3.0), num(4.0));
assert!(is_bool(r));
assert!(!as_bool(r));
}
#[test]
fn jit_ne_numbers() {
assert!(as_bool(jit_ne(num(1.0), num(2.0))));
assert!(!as_bool(jit_ne(num(2.0), num(2.0))));
}
#[test]
fn jit_gt_numbers() {
assert!(as_bool(jit_gt(num(5.0), num(3.0))));
assert!(!as_bool(jit_gt(num(3.0), num(5.0))));
}
#[test]
fn jit_lt_numbers() {
assert!(as_bool(jit_lt(num(2.0), num(7.0))));
assert!(!as_bool(jit_lt(num(7.0), num(2.0))));
}
#[test]
fn jit_ge_numbers() {
assert!(as_bool(jit_ge(num(5.0), num(5.0))));
assert!(as_bool(jit_ge(num(6.0), num(5.0))));
assert!(!as_bool(jit_ge(num(4.0), num(5.0))));
}
#[test]
fn jit_le_numbers() {
assert!(as_bool(jit_le(num(3.0), num(3.0))));
assert!(as_bool(jit_le(num(2.0), num(3.0))));
assert!(!as_bool(jit_le(num(4.0), num(3.0))));
}
#[test]
fn jit_not_true_returns_false() {
let r = jit_not(NanVal::boolean(true).0);
assert!(is_bool(r));
assert!(!as_bool(r));
}
#[test]
fn jit_not_false_returns_true() {
let r = jit_not(NanVal::boolean(false).0);
assert!(is_bool(r));
assert!(as_bool(r));
}
#[test]
fn jit_neg_number() {
let r = jit_neg(num(5.0));
assert!(is_num(r));
assert_eq!(as_num(r), -5.0);
}
#[test]
fn jit_neg_non_number_returns_nil() {
let s = NanVal::heap_string("x".into());
let r = jit_neg(s.0);
assert!(is_nil(r));
}
#[test]
fn jit_truthy_number_nonzero() {
let r = jit_truthy(num(42.0));
assert_eq!(r, 1);
}
#[test]
fn jit_truthy_number_zero() {
let r = jit_truthy(num(0.0));
assert_eq!(r, 0);
}
#[test]
fn jit_truthy_bool_true() {
let r = jit_truthy(NanVal::boolean(true).0);
assert_eq!(r, 1);
}
#[test]
fn jit_truthy_nil_false() {
let r = jit_truthy(TAG_NIL);
assert_eq!(r, 0);
}
#[test]
fn jit_wrapok_wraps_value() {
let r = jit_wrapok(num(7.0));
let v = NanVal(r).to_value();
assert!(matches!(v, Value::Ok(_)));
}
#[test]
fn jit_wraperr_wraps_value() {
let r = jit_wraperr(num(7.0));
let v = NanVal(r).to_value();
assert!(matches!(v, Value::Err(_)));
}
#[test]
fn jit_isok_on_ok_value() {
let ok_val = NanVal::from_value(&Value::Ok(Box::new(Value::Number(1.0))));
let r = jit_isok(ok_val.0);
assert!(as_bool(r));
}
#[test]
fn jit_isok_on_non_ok() {
let r = jit_isok(num(42.0));
assert!(!as_bool(r));
}
#[test]
fn jit_iserr_on_err_value() {
let err_val = NanVal::from_value(&Value::Err(Box::new(Value::Text("oops".into()))));
let r = jit_iserr(err_val.0);
assert!(as_bool(r));
}
#[test]
fn jit_iserr_on_non_err() {
let r = jit_iserr(num(1.0));
assert!(!as_bool(r));
}
#[test]
fn jit_unwrap_ok_value() {
let ok_val = NanVal::from_value(&Value::Ok(Box::new(Value::Number(3.14))));
let r = jit_unwrap(ok_val.0);
assert!(is_num(r));
assert!((as_num(r) - 3.14).abs() < 1e-9);
}
#[test]
fn jit_move_clones_value() {
let v = num(99.0);
let r = jit_move(v);
assert_eq!(r, v);
}
#[test]
fn jit_gt_non_numbers_returns_false() {
let r = jit_gt(TAG_NIL, TAG_NIL);
assert!(!as_bool(r));
}
#[test]
fn jit_lt_non_numbers_returns_false() {
let r = jit_lt(TAG_NIL, num(1.0));
assert!(!as_bool(r));
}
#[test]
fn jit_ge_non_numbers_returns_false() {
let r = jit_ge(TAG_NIL, num(1.0));
assert!(!as_bool(r));
}
#[test]
fn jit_le_non_numbers_returns_false() {
let r = jit_le(TAG_NIL, num(1.0));
assert!(!as_bool(r));
}
fn str_val(s: &str) -> u64 { NanVal::heap_string(s.to_string()).0 }
#[test]
fn jit_gt_strings_true() {
let r = jit_gt(str_val("b"), str_val("a"));
assert!(as_bool(r));
}
#[test]
fn jit_gt_strings_false() {
let r = jit_gt(str_val("a"), str_val("b"));
assert!(!as_bool(r));
}
#[test]
fn jit_lt_strings_true() {
let r = jit_lt(str_val("a"), str_val("b"));
assert!(as_bool(r));
}
#[test]
fn jit_ge_strings_equal() {
let r = jit_ge(str_val("a"), str_val("a"));
assert!(as_bool(r));
}
#[test]
fn jit_le_strings_less() {
let r = jit_le(str_val("a"), str_val("b"));
assert!(as_bool(r));
}
#[test]
fn jit_add_strings_concat() {
let r = jit_add(str_val("hello "), str_val("world"));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "hello world");
}
#[test]
fn jit_add_non_numeric_non_string_returns_nil() {
let r = jit_add(TAG_NIL, num(1.0));
assert!(is_nil(r));
}
#[test]
fn jit_len_string() {
let r = jit_len(str_val("hello"));
assert!(is_num(r));
assert_eq!(as_num(r), 5.0);
}
#[test]
fn jit_len_list() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0), NanVal::number(3.0)];
let list = NanVal::heap_list(items);
let r = jit_len(list.0);
assert!(is_num(r));
assert_eq!(as_num(r), 3.0);
}
#[test]
fn jit_len_non_string_non_list_returns_nil() {
let r = jit_len(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_str_number_to_string() {
let r = jit_str(num(42.0));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "42");
}
#[test]
fn jit_str_float_to_string() {
let r = jit_str(num(3.14));
let rv = NanVal(r);
assert!(rv.is_string());
}
#[test]
fn jit_str_non_number_returns_nil() {
let r = jit_str(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_hd_string_returns_first_char() {
let r = jit_hd(str_val("hello"));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "h");
}
#[test]
fn jit_hd_empty_string_returns_nil() {
let r = jit_hd(str_val(""));
assert!(is_nil(r));
}
#[test]
fn jit_hd_list_returns_first() {
let items = vec![NanVal::number(10.0), NanVal::number(20.0)];
let list = NanVal::heap_list(items);
let r = jit_hd(list.0);
assert!(is_num(r));
assert_eq!(as_num(r), 10.0);
}
#[test]
fn jit_hd_empty_list_returns_nil() {
let list = NanVal::heap_list(vec![]);
let r = jit_hd(list.0);
assert!(is_nil(r));
}
#[test]
fn jit_hd_non_string_non_list_returns_nil() {
let r = jit_hd(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_tl_string_returns_tail() {
let r = jit_tl(str_val("hello"));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "ello");
}
#[test]
fn jit_tl_empty_string_returns_nil() {
let r = jit_tl(str_val(""));
assert!(is_nil(r));
}
#[test]
fn jit_tl_list_returns_tail() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0), NanVal::number(3.0)];
let list = NanVal::heap_list(items);
let r = jit_tl(list.0);
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_tl_empty_list_returns_nil() {
let list = NanVal::heap_list(vec![]);
let r = jit_tl(list.0);
assert!(is_nil(r));
}
#[test]
fn jit_rev_string() {
let r = jit_rev(str_val("hello"));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "olleh");
}
#[test]
fn jit_rev_list() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0), NanVal::number(3.0)];
let list = NanVal::heap_list(items);
let r = jit_rev(list.0);
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_rev_non_string_non_list_returns_nil() {
let r = jit_rev(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_srt_string_sorts_chars() {
let r = jit_srt(str_val("cab"));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "abc");
}
#[test]
fn jit_srt_number_list() {
let items = vec![NanVal::number(3.0), NanVal::number(1.0), NanVal::number(2.0)];
let list = NanVal::heap_list(items);
let r = jit_srt(list.0);
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_srt_string_list() {
let items = vec![
NanVal::heap_string("c".into()),
NanVal::heap_string("a".into()),
NanVal::heap_string("b".into()),
];
let list = NanVal::heap_list(items);
let r = jit_srt(list.0);
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_srt_empty_list_returns_list() {
let list = NanVal::heap_list(vec![]);
let r = jit_srt(list.0);
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_srt_non_string_non_list_returns_nil() {
let r = jit_srt(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_slc_string_slice() {
let r = jit_slc(str_val("hello"), num(1.0), num(3.0));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "el");
}
#[test]
fn jit_slc_list_slice() {
let items = vec![NanVal::number(0.0), NanVal::number(1.0), NanVal::number(2.0), NanVal::number(3.0)];
let list = NanVal::heap_list(items);
let r = jit_slc(list.0, num(1.0), num(3.0));
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_slc_non_number_indices_returns_nil() {
let r = jit_slc(str_val("hello"), TAG_NIL, num(3.0));
assert!(is_nil(r));
}
#[test]
fn jit_slc_non_string_non_list_returns_nil() {
let r = jit_slc(TAG_NIL, num(0.0), num(2.0));
assert!(is_nil(r));
}
#[test]
fn jit_has_text_found() {
let r = jit_has(str_val("hello world"), str_val("world"));
assert!(as_bool(r));
}
#[test]
fn jit_has_text_not_found() {
let r = jit_has(str_val("hello"), str_val("xyz"));
assert!(!as_bool(r));
}
#[test]
fn jit_has_list_found() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0), NanVal::number(3.0)];
let list = NanVal::heap_list(items);
let r = jit_has(list.0, num(2.0));
assert!(as_bool(r));
}
#[test]
fn jit_has_list_not_found() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0)];
let list = NanVal::heap_list(items);
let r = jit_has(list.0, num(5.0));
assert!(!as_bool(r));
}
#[test]
fn jit_has_text_non_string_needle_returns_false() {
let r = jit_has(str_val("hello"), TAG_NIL);
assert!(!as_bool(r));
}
#[test]
fn jit_has_non_string_non_list_returns_false() {
let r = jit_has(TAG_NIL, num(1.0));
assert!(!as_bool(r));
}
#[test]
fn jit_spl_string_splits() {
let r = jit_spl(str_val("a,b,c"), str_val(","));
let rv = NanVal(r);
assert!(rv.is_heap());
let HeapObj::List(items) = (unsafe { rv.as_heap_ref() }) else { panic!("expected list") };
assert_eq!(items.len(), 3);
}
#[test]
fn jit_spl_non_string_returns_nil() {
let r = jit_spl(TAG_NIL, str_val(","));
assert!(is_nil(r));
}
#[test]
fn jit_cat_list_with_sep() {
let items = vec![
NanVal::heap_string("a".into()),
NanVal::heap_string("b".into()),
NanVal::heap_string("c".into()),
];
let list = NanVal::heap_list(items);
let r = jit_cat(list.0, str_val(","));
let rv = NanVal(r);
assert!(rv.is_string());
let HeapObj::Str(s) = (unsafe { rv.as_heap_ref() }) else { panic!("expected Str") };
let s = s.clone();
assert_eq!(s, "a,b,c");
}
#[test]
fn jit_cat_non_list_returns_nil() {
let r = jit_cat(TAG_NIL, str_val(","));
assert!(is_nil(r));
}
#[test]
fn jit_listappend_appends_item() {
let items = vec![NanVal::number(1.0), NanVal::number(2.0)];
let list = NanVal::heap_list(items);
let r = jit_listappend(list.0, num(3.0));
let rv = NanVal(r);
assert!(rv.is_heap());
let HeapObj::List(items) = (unsafe { rv.as_heap_ref() }) else { panic!("expected list") };
assert_eq!(items.len(), 3);
}
#[test]
fn jit_listappend_non_list_returns_nil() {
let r = jit_listappend(TAG_NIL, num(1.0));
assert!(is_nil(r));
}
#[test]
fn jit_index_list_in_bounds() {
let items = vec![NanVal::number(10.0), NanVal::number(20.0), NanVal::number(30.0)];
let list = NanVal::heap_list(items);
let r = jit_index(list.0, 1u64);
assert!(is_num(r));
assert_eq!(as_num(r), 20.0);
}
#[test]
fn jit_index_out_of_bounds_returns_nil() {
let items = vec![NanVal::number(1.0)];
let list = NanVal::heap_list(items);
let r = jit_index(list.0, 5u64);
assert!(is_nil(r));
}
#[test]
fn jit_index_non_list_returns_nil() {
let r = jit_index(TAG_NIL, num(0.0));
assert!(is_nil(r));
}
#[test]
fn jit_jdmp_number() {
let r = jit_jdmp(num(42.0));
let rv = NanVal(r);
assert!(rv.is_string());
}
#[test]
fn jit_jpar_valid_json() {
let r = jit_jpar(str_val(r#"{"x":1}"#));
let rv = NanVal(r);
assert!(rv.is_heap());
}
#[test]
fn jit_jpar_invalid_json() {
let r = jit_jpar(str_val("not json"));
let rv = NanVal(r);
assert!(rv.is_heap());
let HeapObj::ErrVal(_) = (unsafe { rv.as_heap_ref() }) else { panic!("expected ErrVal") };
}
#[test]
fn jit_jpar_non_string_returns_nil() {
let r = jit_jpar(TAG_NIL);
assert!(is_nil(r));
}
#[test]
fn jit_jpth_object_key() {
let r = jit_jpth(str_val(r#"{"x":"hello"}"#), str_val("x"));
let rv = NanVal(r);
assert!(rv.is_heap());
let HeapObj::OkVal(inner) = (unsafe { rv.as_heap_ref() }) else { panic!("expected OkVal") };
assert!(inner.is_string());
}
#[test]
fn jit_jpth_missing_key() {
let r = jit_jpth(str_val(r#"{"a":1}"#), str_val("b"));
let rv = NanVal(r);
let HeapObj::ErrVal(_) = (unsafe { rv.as_heap_ref() }) else { panic!("expected ErrVal") };
}
#[test]
fn jit_jpth_invalid_json() {
let r = jit_jpth(str_val("not json"), str_val("x"));
let rv = NanVal(r);
let HeapObj::ErrVal(_) = (unsafe { rv.as_heap_ref() }) else { panic!("expected ErrVal") };
}
#[test]
fn jit_jpth_non_string_args_returns_nil() {
let r = jit_jpth(TAG_NIL, str_val("x"));
assert!(is_nil(r));
}
#[test]
fn jit_clone_rc_and_drop_rc_no_panic() {
let s = NanVal::heap_string("test".into());
jit_clone_rc(s.0);
jit_drop_rc(s.0); }
#[test]
fn jit_listget_in_bounds() {
let items = vec![NanVal::number(10.0), NanVal::number(20.0)];
let list = NanVal::heap_list(items);
let r = jit_listget(list.0, num(0.0));
let rv = NanVal(r);
let HeapObj::OkVal(inner) = (unsafe { rv.as_heap_ref() }) else { panic!("expected OkVal") };
assert_eq!(inner.as_number(), 10.0);
}
#[test]
fn jit_listget_out_of_bounds_returns_nil() {
let items = vec![NanVal::number(1.0)];
let list = NanVal::heap_list(items);
let r = jit_listget(list.0, num(10.0));
assert!(is_nil(r));
}
#[test]
fn jit_listget_non_list_returns_nil() {
let r = jit_listget(str_val("hello"), num(0.0));
assert!(is_nil(r));
}
#[test]
fn jit_listget_non_number_idx_returns_nil() {
let items = vec![NanVal::number(1.0)];
let list = NanVal::heap_list(items);
let r = jit_listget(list.0, TAG_NIL);
assert!(is_nil(r));
}
}
#[test]
fn vm_dynamic_destructure_field_name_lookup() {
let result = vm_run(
"type a{x:n;y:n} type b{y:n;x:n} f>n;v=a x:5 y:3;{y}=v;y",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_while_continue_skips_body() {
let result = vm_run(
"f>n;i=0;s=0;wh <i 5{i=+i 1;=i 3{cnt};s=+s i};s",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(12.0));
}
#[test]
fn vm_while_break_with_value() {
let result = vm_run(
"f>n;i=0;wh <i 10{i=+i 1;=i 5{brk i}};i",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_typeis_list_pattern_in_match() {
let result = vm_run(
r#"f xs:L n>t;?xs{l v:"got list";_:"other"}"#,
Some("f"),
vec![Value::List(vec![Value::Number(1.0)])],
);
assert_eq!(result, Value::Text("got list".into()));
}
#[test]
fn vm_divk_n_constant_divisor() {
let result = vm_run("f x:n>n;/x 2", Some("f"), vec![Value::Number(10.0)]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_addk_n_constant_on_left() {
let result = vm_run("f x:n>n;+2 x", Some("f"), vec![Value::Number(8.0)]);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_mulk_n_constant_on_left() {
let result = vm_run("f x:n>n;*3 x", Some("f"), vec![Value::Number(7.0)]);
assert_eq!(result, Value::Number(21.0));
}
#[test]
fn vm_to_value_with_registry_via_record() {
let result = vm_run(
"type pt{x:n;y:n} f>n;p=pt x:3 y:4;p.x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_run_no_function_name_with_empty_program_errors() {
use crate::vm::{compile, run};
let prog = parse_program("type x{a:n}");
match compile(&prog) {
Ok(compiled) => {
let result = run(&compiled, None, vec![]);
assert!(result.is_err());
}
Err(_) => {
}
}
}
#[test]
fn vm_run_with_tools_undefined_function() {
use crate::vm::{compile, run_with_tools};
use crate::interpreter::Value;
use crate::tools::{ToolProvider, ToolError};
use std::future::Future;
use std::pin::Pin;
struct DummyProvider;
impl ToolProvider for DummyProvider {
fn call(&self, _name: &str, _args: Vec<Value>) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + '_>> {
Box::pin(async { Ok(Value::Nil) })
}
}
let prog = parse_program("f>n;42");
let compiled = compile(&prog).expect("compile ok");
let provider = DummyProvider;
#[cfg(feature = "tools")]
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let result = run_with_tools(
&compiled,
Some("nonexistent_function"),
vec![],
&provider,
#[cfg(feature = "tools")]
&runtime,
);
assert!(result.is_err());
}
#[test]
fn vm_destructure_ambiguous_field_uses_name_lookup() {
let result = vm_run(
"type a{x:n;y:n} type b{z:n;x:n} f>n;v=a x:10 y:20;{x}=v;x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_guard_with_else_body_false_branch() {
let result = vm_run("f x:n>n;>x 10{1}{-1}", Some("f"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(-1.0));
}
#[test]
fn vm_match_type_is_bool_pattern() {
let result = vm_run(r#"f x:b>t;?x{b v:"bool";_:"other"}"#, Some("f"), vec![Value::Bool(true)]);
assert_eq!(result, Value::Text("bool".into()));
}
#[test]
fn vm_match_type_is_list_pattern() {
let result = vm_run(r#"f xs:L n>t;?xs{l v:"list";_:"other"}"#, Some("f"), vec![Value::List(vec![])]);
assert_eq!(result, Value::Text("list".into()));
}
#[test]
fn vm_search_field_index_ambiguous_returns_none() {
let result = vm_run(
"type p{x:n;y:n} type q{y:n;x:n} f>n;v=p x:5 y:3;{x}=v;x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_type_registry_register_dedup() {
let prog = parse_program("type pt{x:n;y:n} f>n;p=pt x:3 y:4;p.x");
let compiled = compile(&prog).unwrap();
let result = run(&compiled, Some("f"), vec![]).unwrap();
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_search_field_same_index_multiple_types() {
let result = vm_run(
"type a{x:n;y:n} type b{x:n;z:n} f>n;v=a x:7 y:2;{x}=v;x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(7.0));
}
#[test]
fn vm_destructure_name_lookup_existing_binding() {
let result = vm_run(
"type a{x:n;y:n} type b{y:n;x:n} f>n;y=0;v=a x:5 y:9;{y}=v;y",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(9.0));
}
#[test]
fn vm_foreach_cnt_patches_correctly() {
let result = vm_run(
"f>n;s=0;@x [10,20,30,40,50]{>x 25{cnt};s=+s x};s",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(30.0));
}
#[test]
fn vm_forrange_cnt_patches_correctly() {
let result = vm_run(
"f>n;s=0;@i 0..8{>i 4{cnt};s=+s i};s",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_foreach_brk_with_same_reg() {
let result = vm_run(
"f>n;@x [1,2,3,4,5]{>=x 4{brk x};x}",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(4.0));
}
#[test]
fn vm_while_cnt_jumps_to_top() {
let result = vm_run(
"f>n;i=0;s=0;wh <i 6{i=+i 1;=i 4{cnt};s=+s i};s",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(17.0));
}
#[test]
fn vm_subk_n_constant_on_right() {
let result = vm_run("f x:n>n;-x 7", Some("f"), vec![Value::Number(20.0)]);
assert_eq!(result, Value::Number(13.0));
}
#[test]
fn vm_mulk_n_constant_on_right() {
let result = vm_run("f x:n>n;*x 5", Some("f"), vec![Value::Number(6.0)]);
assert_eq!(result, Value::Number(30.0));
}
#[test]
fn vm_addk_n_left_constant_commutative() {
let result = vm_run("f x:n>n;+100 x", Some("f"), vec![Value::Number(42.0)]);
assert_eq!(result, Value::Number(142.0));
}
#[test]
fn vm_mulk_n_left_constant_commutative() {
let result = vm_run("f x:n>n;*10 x", Some("f"), vec![Value::Number(9.0)]);
assert_eq!(result, Value::Number(90.0));
}
#[test]
fn vm_nested_record_in_list_promotes_arena() {
let result = vm_run(
"type pt{x:n;y:n} f>n;xs=[pt x:1 y:2, pt x:3 y:4];xs.0",
Some("f"), vec![],
);
match result {
Value::Record { type_name, .. } => assert_eq!(type_name, "pt"),
Value::Number(n) => assert_eq!(n, 1.0), other => panic!("expected record or number, got {:?}", other),
}
}
#[test]
fn vm_run_no_functions_defined_error() {
use crate::ast::{Decl, Param, Program, Span, Type};
let prog = Program {
declarations: vec![Decl::TypeDef {
name: "pt".to_string(),
fields: vec![Param { name: "x".to_string(), ty: Type::Number }],
span: Span::UNKNOWN,
}],
source: None,
};
let compiled = compile(&prog).unwrap();
let result = run(&compiled, None, vec![]);
assert!(result.is_err(), "expected NoFunctionsDefined error");
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("no functions") || err_str.contains("undefined"),
"unexpected error: {err_str}"
);
}
#[test]
fn vm_run_with_tools_no_functions_defined() {
use crate::vm::{compile, run_with_tools};
use crate::interpreter::Value;
use crate::tools::{ToolProvider, ToolError};
use crate::ast::{Decl, Param, Program, Span, Type};
use std::future::Future;
use std::pin::Pin;
struct DummyProvider;
impl ToolProvider for DummyProvider {
fn call(&self, _name: &str, _args: Vec<Value>) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + '_>> {
Box::pin(async { Ok(Value::Nil) })
}
}
let prog = Program {
declarations: vec![Decl::TypeDef {
name: "pt".to_string(),
fields: vec![Param { name: "x".to_string(), ty: Type::Number }],
span: Span::UNKNOWN,
}],
source: None,
};
let compiled = compile(&prog).unwrap();
let provider = DummyProvider;
#[cfg(feature = "tools")]
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let result = run_with_tools(
&compiled,
None,
vec![],
&provider,
#[cfg(feature = "tools")]
&runtime,
);
assert!(result.is_err(), "expected error for no functions");
}
#[test]
fn vm_run_with_tools_calls_function_successfully() {
use crate::vm::{compile, run_with_tools};
use crate::interpreter::Value;
use crate::tools::{ToolProvider, ToolError};
use std::future::Future;
use std::pin::Pin;
struct DummyProvider;
impl ToolProvider for DummyProvider {
fn call(&self, _name: &str, _args: Vec<Value>) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + '_>> {
Box::pin(async { Ok(Value::Nil) })
}
}
let prog = parse_program("f>n;42");
let compiled = compile(&prog).unwrap();
let provider = DummyProvider;
#[cfg(feature = "tools")]
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let result = run_with_tools(
&compiled,
Some("f"),
vec![],
&provider,
#[cfg(feature = "tools")]
&runtime,
);
assert_eq!(result.unwrap(), Value::Number(42.0));
}
#[test]
fn vm_to_value_with_registry_string_field() {
let result = vm_run(
"type person{name:t;age:n} f>t;p=person name:\"alice\" age:30;p.name",
Some("f"), vec![],
);
assert_eq!(result, Value::Text("alice".into()));
}
#[test]
fn vm_to_value_with_registry_multiple_records() {
let result = vm_run(
"type color{r:n;g:n;b:n} f>n;c=color r:255 g:128 b:0;c.g",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(128.0));
}
#[test]
fn vm_recfld_heap_record_field_access() {
let source = "f r:pt>n;r.x\ntype pt{x:n}";
let mut fields = std::collections::HashMap::new();
fields.insert("x".to_string(), Value::Number(77.0));
let result = vm_run(source, Some("f"), vec![
Value::Record { type_name: "pt".to_string(), fields },
]);
assert_eq!(result, Value::Number(77.0));
}
#[test]
fn vm_recwith_heap_record_arg() {
let source = "f r:pt>n;r2=r with x:99;r2.x\ntype pt{x:n;y:n}";
let mut fields = std::collections::HashMap::new();
fields.insert("x".to_string(), Value::Number(1.0));
fields.insert("y".to_string(), Value::Number(2.0));
let result = vm_run(source, Some("f"), vec![
Value::Record { type_name: "pt".to_string(), fields },
]);
assert_eq!(result, Value::Number(99.0));
}
#[test]
fn vm_recfld_name_heap_record() {
let source = "type a{x:n;y:n} type b{y:n;x:n} f s:t>n;r=jpar! s;{y}=r;y";
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"x": 10, "y": 20}"#.to_string()),
]);
assert_eq!(result, Value::Number(20.0));
}
#[test]
fn vm_recwith_heap_preserves_unchanged_fields() {
let source = "type box{v:n} f r:box>n;r2=r with v:55;r2.v";
let mut fields = std::collections::HashMap::new();
fields.insert("v".to_string(), Value::Number(0.0));
let result = vm_run(source, Some("f"), vec![
Value::Record { type_name: "box".to_string(), fields },
]);
assert_eq!(result, Value::Number(55.0));
}
#[test]
fn vm_arena_record_to_value_with_heap_string_field() {
let result = vm_run(
"type item{label:t;count:n} f>t;r=item label:\"widget\" count:5;r.label",
Some("f"), vec![],
);
assert_eq!(result, Value::Text("widget".into()));
}
#[test]
fn vm_match_type_is_all_patterns() {
let num_src = r#"f x:t>b;?x{n _:true;_:false}"#;
assert_eq!(vm_run(num_src, Some("f"), vec![Value::Number(1.0)]), Value::Bool(true));
assert_eq!(vm_run(num_src, Some("f"), vec![Value::Text("a".into())]), Value::Bool(false));
let text_src = r#"f x:n>b;?x{t _:true;_:false}"#;
assert_eq!(vm_run(text_src, Some("f"), vec![Value::Text("x".into())]), Value::Bool(true));
assert_eq!(vm_run(text_src, Some("f"), vec![Value::Number(0.0)]), Value::Bool(false));
let bool_src = r#"f x:n>b;?x{b _:true;_:false}"#;
assert_eq!(vm_run(bool_src, Some("f"), vec![Value::Bool(false)]), Value::Bool(true));
let list_src = r#"f x:n>b;?x{l _:true;_:false}"#;
let list = Value::List(vec![Value::Number(1.0)]);
assert_eq!(vm_run(list_src, Some("f"), vec![list]), Value::Bool(true));
}
#[test]
fn vm_mulk_n_right_side_constant_explicit() {
let result = vm_run("f x:n>n;*x 4", Some("f"), vec![Value::Number(7.0)]);
assert_eq!(result, Value::Number(28.0));
}
#[test]
fn vm_addk_n_right_side_constant_explicit() {
let result = vm_run("f x:n>n;+x 15", Some("f"), vec![Value::Number(10.0)]);
assert_eq!(result, Value::Number(25.0));
}
#[test]
fn vm_binop_append_emits_listappend() {
let prog = parse_program("f xs:L n x:n>L n;+=xs x");
let compiled = compile(&prog).unwrap();
let chunk = &compiled.chunks[0];
let has_listappend = chunk.code.iter().any(|&inst| (inst >> 24) as u8 == OP_LISTAPPEND);
assert!(has_listappend, "expected OP_LISTAPPEND for += operator");
}
#[test]
fn vm_record_text_field_roundtrip() {
let result = vm_run(
"type greeting{msg:t} f>t;g=greeting msg:\"hello world\";g.msg",
Some("f"), vec![],
);
assert_eq!(result, Value::Text("hello world".into()));
}
#[test]
fn vm_guard_ternary_chained() {
let src = r#"f x:n>t;>=x 10{"large"}{"small"}"#;
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(10.0)]), Value::Text("large".into()));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(5.0)]), Value::Text("small".into()));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(15.0)]), Value::Text("large".into()));
}
#[test]
fn vm_safe_field_on_list_returns_nil() {
let src = "f xs:L n>n;xs.?0??77";
assert_eq!(vm_run(src, Some("f"), vec![Value::List(vec![Value::Number(99.0)])]), Value::Number(99.0));
}
#[test]
fn vm_while_brk_no_expr_exits_loop() {
let src = "f>n;i=0;wh <i 100{i=+i 1;>=i 7{brk}};i";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_foreach_brk_no_expr_result() {
let src = "f>n;@x [1,2,3,4,5]{>=x 3{brk};x}";
let result = vm_run(src, Some("f"), vec![]);
assert!(matches!(result, Value::Number(n) if n == 2.0 || n == 3.0),
"expected 2.0 or 3.0, got {:?}", result);
}
#[test]
fn vm_recursive_call_stack_captured() {
let src = "fib n:n>n;<=n 1 n;a=fib -n 1;b=fib -n 2;+a b";
let result = vm_run(src, Some("fib"), vec![Value::Number(6.0)]);
assert_eq!(result, Value::Number(8.0));
}
#[test]
fn vm_recfld_name_field_not_found_heap_record() {
let err = vm_run_err(
"type a{x:n;y:n} type b{y:n;x:n} f r:a>n;{z}=r;z",
Some("f"),
vec![Value::Record {
type_name: "a".to_string(),
fields: {
let mut m = std::collections::HashMap::new();
m.insert("x".to_string(), Value::Number(1.0));
m.insert("y".to_string(), Value::Number(2.0));
m
},
}],
);
assert!(err.contains("z") || err.contains("field"), "got: {err}");
}
#[test]
fn vm_recfld_index_out_of_bounds_heap_record() {
let err = vm_run_err(
"type pt{x:n;y:n;z:n} f r:pt>n;r.z",
Some("f"),
vec![Value::Record {
type_name: "pt".to_string(),
fields: {
let mut m = std::collections::HashMap::new();
m.insert("x".to_string(), Value::Number(1.0));
m.insert("y".to_string(), Value::Number(2.0));
m
},
}],
);
assert!(err.contains("z") || err.contains("field") || err.contains("not found"), "got: {err}");
}
#[test]
fn vm_state_call_drain_after_error_then_success() {
let prog1 = parse_program("f x:n>n;/x 0");
let compiled1 = compile(&prog1).unwrap();
let mut state = VmState::new(&compiled1);
let err = state.call("f", vec![Value::Number(10.0)]);
assert!(err.is_err());
let prog2 = parse_program("g x:n>n;+x 1");
let compiled2 = compile(&prog2).unwrap();
let mut state2 = VmState::new(&compiled2);
let ok = state2.call("g", vec![Value::Number(5.0)]);
assert_eq!(ok.unwrap(), Value::Number(6.0));
}
#[test]
fn vm_run_explicit_undefined_function_name() {
let prog = parse_program("f>n;42");
let compiled = compile(&prog).unwrap();
let result = run(&compiled, Some("does_not_exist"), vec![]);
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("does_not_exist") || err_str.contains("undefined"),
"unexpected error: {err_str}"
);
}
#[test]
fn vm_large_record_multiple_fields() {
let src = "type big{a:n;b:n;c:n;d:n;e:n} f>n;r=big a:1 b:2 c:3 d:4 e:5;r.c";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_recwith_two_field_updates() {
let src = "type pt{x:n;y:n} f>n;p=pt x:1 y:2;q=p with x:10;+q.x q.y";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(12.0)); }
#[test]
fn vm_list_of_records_field_access() {
let src = "type pt{x:n;y:n} f>n;xs=[pt x:1 y:2,pt x:10 y:20];xs.1";
let result = vm_run(src, Some("f"), vec![]);
let Value::Record { type_name, fields } = result else { panic!("expected Record") };
assert_eq!(type_name, "pt");
assert_eq!(fields.get("x"), Some(&Value::Number(10.0)));
}
#[test]
fn vm_run_with_tools_with_tool_declaration() {
use crate::vm::{compile, run_with_tools};
use crate::interpreter::Value;
use crate::tools::{ToolProvider, ToolError};
use std::future::Future;
use std::pin::Pin;
struct DummyProvider;
impl ToolProvider for DummyProvider {
fn call(&self, _name: &str, _args: Vec<Value>) -> Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + '_>> {
Box::pin(async { Ok(Value::Nil) })
}
}
let prog = parse_program("tool fetch\"HTTP GET\" url:t>R _ t\nf>R _ t;fetch \"http://x\"");
let compiled = compile(&prog).unwrap();
let provider = DummyProvider;
#[cfg(feature = "tools")]
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let result = run_with_tools(
&compiled,
Some("f"),
vec![],
&provider,
#[cfg(feature = "tools")]
&runtime,
);
assert_eq!(result.unwrap(), Value::Ok(Box::new(Value::Nil)));
}
#[test]
fn vm_ternary_else_computation() {
let src = "f x:n>n;>x 0{x}{-x}"; assert_eq!(vm_run(src, Some("f"), vec![Value::Number(5.0)]), Value::Number(5.0));
assert_eq!(vm_run(src, Some("f"), vec![Value::Number(-3.0)]), Value::Number(3.0));
}
#[test]
fn vm_while_cnt_accumulates_correctly() {
let src = "f>n;i=0;s=0;wh <i 5{i=+i 1;<i 3{cnt};s=+s i};s";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(12.0));
}
#[test]
fn vm_type_registry_field_index() {
let result = vm_run(
"type a{x:n;y:n} type b{y:n;x:n} f>n;r=b y:7 x:3;{x}=r;x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_multi_function_with_records() {
let src = "type pt{x:n;y:n} mk a:n b:n>pt;pt x:a y:b\nf>n;p=mk 5 10;p.x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_mget_non_text_key_error() {
let err = vm_run_err(
"f m:z k:z>n;mget m k",
Some("f"),
vec![
Value::Map(std::collections::HashMap::new()),
Value::List(vec![Value::Number(1.0)]),
],
);
assert!(err.contains("mget") || err.contains("key") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_mget_non_map_first_arg_error() {
let err = vm_run_err(
"f x:z k:t>n;mget x k",
Some("f"),
vec![
Value::List(vec![Value::Number(1.0)]),
Value::Text("key".into()),
],
);
assert!(err.contains("mget") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mset_non_text_key_error() {
let err = vm_run_err(
"f m:z k:z v:t>n;mset m k v",
Some("f"),
vec![
Value::Map(std::collections::HashMap::new()),
Value::List(vec![Value::Number(1.0)]),
Value::Text("val".into()),
],
);
assert!(err.contains("mset") || err.contains("key") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_mset_non_map_first_arg_error() {
let err = vm_run_err(
"f x:z k:t v:t>n;mset x k v",
Some("f"),
vec![
Value::Text("not-a-map".into()),
Value::Text("key".into()),
Value::Text("val".into()),
],
);
assert!(err.contains("mset") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mhas_non_text_key_error() {
let err = vm_run_err(
"f m:z k:z>n;mhas m k",
Some("f"),
vec![
Value::Map(std::collections::HashMap::new()),
Value::List(vec![Value::Number(1.0)]),
],
);
assert!(err.contains("mhas") || err.contains("key") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_mhas_non_map_first_arg_error() {
let err = vm_run_err(
"f x:z k:t>n;mhas x k",
Some("f"),
vec![
Value::List(vec![Value::Number(1.0)]),
Value::Text("k".into()),
],
);
assert!(err.contains("mhas") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mkeys_non_map_arg_error() {
let err = vm_run_err(
"f x:z>n;mkeys x",
Some("f"),
vec![Value::List(vec![Value::Text("a".into())])],
);
assert!(err.contains("mkeys") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mvals_non_map_arg_error() {
let err = vm_run_err(
"f x:z>n;mvals x",
Some("f"),
vec![Value::Text("not-a-map".into())],
);
assert!(err.contains("mvals") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mdel_non_text_key_error() {
let err = vm_run_err(
"f m:z k:z>n;mdel m k",
Some("f"),
vec![
Value::Map(std::collections::HashMap::new()),
Value::List(vec![Value::Number(7.0)]),
],
);
assert!(err.contains("mdel") || err.contains("key") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_mdel_non_map_first_arg_error() {
let err = vm_run_err(
"f x:z k:t>n;mdel x k",
Some("f"),
vec![
Value::List(vec![Value::Number(1.0)]),
Value::Text("k".into()),
],
);
assert!(err.contains("mdel") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_rd_bad_json_returns_err() {
let path = "/tmp/ilo_vm_rd_badjson.json";
std::fs::write(path, "{ this is not valid json }").unwrap();
let result = vm_run("f p:t>R t t;rd p", Some("f"), vec![Value::Text(path.into())]);
assert!(matches!(result, Value::Err(_)), "expected Err from bad JSON, got {result:?}");
let _ = std::fs::remove_file(path);
}
#[test]
fn vm_wr_bad_path_returns_err() {
let result = vm_run(
"f p:t c:t>R t t;wr p c",
Some("f"),
vec![
Value::Text("/nonexistent_dir_ilo/output.txt".into()),
Value::Text("hello".into()),
],
);
assert!(matches!(result, Value::Err(_)), "expected Err from bad path, got {result:?}");
}
#[test]
fn vm_wrl_bad_path_returns_err() {
let result = vm_run(
"f p:t xs:L t>R t t;wrl p xs",
Some("f"),
vec![
Value::Text("/nonexistent_dir_ilo/lines.txt".into()),
Value::List(vec![Value::Text("line1".into())]),
],
);
assert!(matches!(result, Value::Err(_)), "expected Err from bad wrl path, got {result:?}");
}
#[test]
fn vm_wrl_non_list_second_arg_error() {
let err = vm_run_err(
"f p:t x:z>R t t;wrl p x",
Some("f"),
vec![
Value::Text("/tmp/ilo_wrl_nonlist.txt".into()),
Value::Text("not-a-list".into()),
],
);
assert!(err.contains("wrl") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_wrapok_unwrap_roundtrip_via_match() {
let src = "wrap x:n>R n n;~x\nf>n;r=wrap 42;?r{^_:0;~v:v}";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn vm_recfld_arena_out_of_bounds() {
let err = vm_run_err(
"type pt{x:n;y:n;z:n} f r:pt>n;r.z",
Some("f"),
vec![Value::Record {
type_name: "pt".to_string(),
fields: {
let mut m = std::collections::HashMap::new();
m.insert("x".to_string(), Value::Number(1.0));
m.insert("y".to_string(), Value::Number(2.0));
m
},
}],
);
assert!(
err.contains("z") || err.contains("field") || err.contains("not found") || err.contains("index"),
"got: {err}"
);
}
#[test]
fn vm_recfld_non_record_heap_value_error() {
let err = vm_run_err(
"type pt{x:n} f r:z>n;r.x",
Some("f"),
vec![Value::Text("not-a-record".into())],
);
assert!(
err.contains("field") || err.contains("record") || err.contains("not found") || err.contains("x"),
"got: {err}"
);
}
#[test]
fn vm_wrapok_arena_record_promotes_to_heap() {
let src = "type pt{x:n;y:n} wrap a:n>R pt n;~pt x:a y:7\nf>n;r=wrap 3;?r{^_:0;~p:p.x}";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_wraperr_arena_record_promotes_to_heap() {
let src = "type info{code:n} wrap a:n>R n info;^info code:a\nf>n;r=wrap 99;?r{^e:e.code;~_:0}";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(99.0));
}
#[test]
fn vm_foreach_on_non_list_error() {
let err = vm_run_err(
"f xs:z>n;@x xs{x};0",
Some("f"),
vec![Value::Text("not-a-list".into())],
);
assert!(
err.contains("list") || err.contains("foreach"),
"got: {err}"
);
}
#[test]
fn vm_len_on_heap_ok_value_error() {
let err = vm_run_err(
"f x:z>n;len x",
Some("f"),
vec![Value::Ok(Box::new(Value::Number(5.0)))],
);
assert!(
err.contains("len") || err.contains("string") || err.contains("list") || err.contains("map"),
"got: {err}"
);
}
#[test]
fn vm_len_on_number_error() {
let err = vm_run_err(
"f x:z>n;len x",
Some("f"),
vec![Value::Number(42.0)],
);
assert!(
err.contains("len") || err.contains("string") || err.contains("list"),
"got: {err}"
);
}
#[test]
fn vm_index_on_non_list_heap_value_error() {
let err = vm_run_err(
"f x:z>n;x.0",
Some("f"),
vec![Value::Map(std::collections::HashMap::new())],
);
assert!(
err.contains("list") || err.contains("index"),
"got: {err}"
);
}
#[test]
fn vm_divk_n_div_by_zero() {
let src = "f x:n>n;/x 0";
let err = vm_run_err(src, Some("f"), vec![Value::Number(10.0)]);
assert!(err.contains("division by zero"), "got: {err}");
}
#[test]
fn vm_div_nn_div_by_zero() {
let src = "f a:n b:n>n;/a b";
let err = vm_run_err(src, Some("f"), vec![Value::Number(5.0), Value::Number(0.0)]);
assert!(err.contains("division by zero"), "got: {err}");
}
#[test]
fn vm_wrl_non_string_list_element_error() {
let err = vm_run_err(
"f p:t xs:L n>R t t;wrl p xs",
Some("f"),
vec![
Value::Text("/tmp/ilo_wrl_elem.txt".into()),
Value::List(vec![Value::Number(42.0)]),
],
);
assert!(err.contains("wrl") || err.contains("string") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_len_map() {
let src = "f>n;m=mset mmap \"a\" 1;len m";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn vm_recwith_on_non_record_heap_error() {
let err = vm_run_err(
"type pt{x:n;y:n} f r:z>n;q=r with x:5;q.x",
Some("f"),
vec![Value::Text("not-a-record".into())],
);
assert!(
err.contains("record") || err.contains("with") || err.contains("field"),
"got: {err}"
);
}
#[test]
fn vm_jpth_invalid_json_returns_err() {
let result = vm_run(r#"f s:t>R t t;jpth s "a""#, Some("f"),
vec![Value::Text("not json at all".into())]);
let Value::Err(_) = result else { panic!("expected Err") };
}
#[test]
fn vm_jpth_array_index_not_found() {
let result = vm_run(r#"f s:t>R t t;jpth s "a.5""#, Some("f"),
vec![Value::Text(r#"{"a":[1,2]}"#.into())]);
let Value::Err(_) = result else { panic!("expected Err") };
}
#[test]
fn vm_cat_non_text_separator_error() {
let err = vm_run_err(r#"f xs:L t>t;cat xs 42"#, Some("f"),
vec![Value::List(vec![Value::Text("a".into())])]);
assert!(err.contains("cat") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_cat_non_list_first_arg_error() {
let err = vm_run_err(r#"f n:n>t;cat n ",""#, Some("f"),
vec![Value::Number(42.0)]);
assert!(err.contains("cat") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_hd_on_map_error() {
let err = vm_run_err(r#"f m:_>t;hd m"#, Some("f"),
vec![Value::Map(std::collections::HashMap::new())]);
assert!(err.contains("hd") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_tl_on_map_error() {
let err = vm_run_err(r#"f m:_>t;tl m"#, Some("f"),
vec![Value::Map(std::collections::HashMap::new())]);
assert!(err.contains("tl") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_rev_on_map_error() {
let err = vm_run_err(r#"f m:_>t;rev m"#, Some("f"),
vec![Value::Map(std::collections::HashMap::new())]);
assert!(err.contains("rev") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_srt_on_map_error() {
let err = vm_run_err(r#"f m:_>L t;srt m"#, Some("f"),
vec![Value::Map(std::collections::HashMap::new())]);
assert!(err.contains("srt") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_srt_on_number_error() {
let err = vm_run_err("f x:n>L n;srt x", Some("f"),
vec![Value::Number(42.0)]);
assert!(err.contains("srt") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_slc_non_number_indices_error() {
let err = vm_run_err(r#"f xs:L n s:t e:t>L n;slc xs s e"#, Some("f"),
vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0)]),
Value::Text("a".into()),
Value::Text("b".into()),
]);
assert!(err.contains("slc") || err.contains("indices") || err.contains("number"), "got: {err}");
}
#[test]
fn vm_listappend_arena_record_promotes_to_heap() {
let source = "type pt{x:n;y:n} f>n;xs=[];r=pt x:1 y:2;ys=+=xs r;len ys";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(1.0));
}
#[test]
fn vm_jdmp_float_number() {
let result = vm_run("f>t;jdmp 3.14", Some("f"), vec![]);
assert_eq!(result, Value::Text("3.14".into()));
}
#[test]
fn vm_jdmp_heap_record() {
let result = vm_run(r#"f s:t>t;r=jpar! s;jdmp r"#, Some("f"),
vec![Value::Text(r#"{"x":10}"#.into())]);
let Value::Text(s) = result else { panic!("expected Text") };
assert!(s.contains("10"), "got: {s}");
}
#[test]
fn vm_jdmp_ok_value() {
let result = vm_run(r#"f s:t>t;r=jpar s;jdmp r"#, Some("f"),
vec![Value::Text(r#"{"v":5}"#.into())]);
let Value::Text(s) = result else { panic!("expected Text") };
assert!(s.contains("5"), "got: {s}");
}
#[test]
fn vm_jdmp_map_value() {
let result = vm_run(r#"f>t;m=mset mmap "k" 42;jdmp m"#, Some("f"), vec![]);
let Value::Text(s) = result else { panic!("expected Text") };
assert!(s.contains("42"), "got: {s}");
}
#[test]
fn vm_jdmp_bool_true() {
let result = vm_run("f>t;jdmp true", Some("f"), vec![]);
assert_eq!(result, Value::Text("true".into()));
}
#[test]
fn vm_jdmp_bool_false() {
let result = vm_run("f>t;jdmp false", Some("f"), vec![]);
assert_eq!(result, Value::Text("false".into()));
}
#[test]
fn vm_jdmp_err_value() {
let result = vm_run(r#"f s:t>t;e=jpar s;jdmp e"#, Some("f"),
vec![Value::Text("not json".into())]);
let Value::Text(_) = result else { panic!("expected Text") };
}
#[test]
fn vm_slc_on_map_heap_error() {
let err = vm_run_err(r#"f m:_ i:n j:n>L t;slc m i j"#, Some("f"),
vec![
Value::Map(std::collections::HashMap::new()),
Value::Number(0.0),
Value::Number(1.0),
]);
assert!(err.contains("slc") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_rd_txt_extension_raw_format() {
let path = "/tmp/ilo_vm_test_raw.txt";
std::fs::write(path, "hello raw").unwrap();
let source = format!(r#"f>R t t;rd "{path}""#);
let result = vm_run(&source, Some("f"), vec![]);
let Value::Ok(inner) = result else { panic!("expected Ok") };
assert_eq!(*inner, Value::Text("hello raw".into()));
}
#[test]
fn vm_rd_csv_quoted_fields() {
let path = "/tmp/ilo_vm_test_quoted.csv";
std::fs::write(path, "\"hello, world\",\"say \"\"hi\"\"\"").unwrap();
let source = format!(r#"f>n;rows=rd! "{path}";len rows"#);
let result = vm_run(&source, Some("f"), vec![]);
assert_eq!(result, Value::Number(1.0)); }
#[test]
fn vm_execute_fallthrough_returns_nil() {
let chunk = Chunk {
code: vec![],
constants: vec![],
param_count: 0,
reg_count: 0,
spans: vec![],
};
let program = CompiledProgram {
chunks: vec![chunk],
func_names: vec!["f".to_string()],
nan_constants: vec![vec![]],
type_registry: TypeRegistry::default(),
is_tool: vec![false],
};
let result = run(&program, Some("f"), vec![]).expect("fallthrough should succeed");
assert_eq!(result, Value::Nil);
}
#[test]
fn vm_unknown_opcode_error_has_span_and_stack() {
let inst = (0xFEu32) << 24;
let chunk = Chunk {
code: vec![inst],
constants: vec![],
param_count: 0,
reg_count: 0,
spans: vec![crate::ast::Span { start: 1, end: 2 }],
};
let program = CompiledProgram {
chunks: vec![chunk],
func_names: vec!["f".to_string()],
nan_constants: vec![vec![]],
type_registry: TypeRegistry::default(),
is_tool: vec![false],
};
let err = run(&program, Some("f"), vec![]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("unknown opcode") || msg.contains("opcode"), "got: {msg}");
assert!(err.span.is_some(), "expected span to be captured");
assert_eq!(err.call_stack, vec!["f".to_string()]);
}
#[test]
fn vm_error_call_stack_includes_caller_and_callee() {
let prog = parse_program("g x:n>n;/x 0 f>n;g 1");
let compiled = compile(&prog).unwrap();
let err = run(&compiled, Some("f"), vec![]).unwrap_err();
assert!(err.call_stack.contains(&"f".to_string()));
assert!(err.call_stack.contains(&"g".to_string()));
let f_pos = err.call_stack.iter().position(|n| n == "f").unwrap();
let g_pos = err.call_stack.iter().position(|n| n == "g").unwrap();
assert!(f_pos < g_pos, "expected f before g in call stack: {:?}", err.call_stack);
}
#[test]
fn vm_foreach_cnt_skips_elements() {
let src = "f>n;s=0;@x [1,2,3,4,5]{=x 3{cnt};s=+s x};s";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(12.0));
}
#[test]
fn vm_range_cnt_patches_applied() {
let src = "f>n;s=0;@i 0..5{=i 2{cnt};s=+s i};s";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(8.0));
}
#[test]
fn vm_foreach_brk_with_expr() {
let src = "f>n;@x [1,2,3,4,5]{=x 3{brk 99};x}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(99.0));
}
#[test]
fn vm_get_non_string_url_error() {
let err = vm_run_err("f u:z>R t t;get u", Some("f"), vec![Value::Number(42.0)]);
assert!(err.contains("get") || err.contains("string") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_post_non_string_args_error() {
let err = vm_run_err(
"f u:z b:z>R t t;post u b",
Some("f"),
vec![Value::Number(1.0), Value::Text("body".into())],
);
assert!(err.contains("post") || err.contains("string") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_geth_non_string_url_error() {
let err = vm_run_err(
"f u:z h:M t t>R t t;get u h",
Some("f"),
vec![Value::Number(42.0), Value::Map(std::collections::HashMap::new())],
);
assert!(err.contains("get") || err.contains("string") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_posth_non_string_url_error() {
let err = vm_run_err(
"f u:z b:z h:M t t>R t t;post u b h",
Some("f"),
vec![Value::Number(1.0), Value::Text("body".into()), Value::Map(std::collections::HashMap::new())],
);
assert!(err.contains("post") || err.contains("string") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_destructure_existing_binding_ambiguous_field() {
let src = "type a{x:n;y:n} type b{y:n;x:n} f>n;r=a x:1 y:2;{y}=r;r2=a x:3 y:4;{y}=r2;y";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn vm_search_field_same_index_arm() {
let result = vm_run(
"type a{x:n;y:n} type b{x:n;z:n} f>n;r=a x:5 y:10;{x}=r;x",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_search_field_not_in_all_types_phantom() {
let result = vm_run(
"type a{x:n;y:n} type b{x:n;z:n} f>n;r=a x:5 y:10;{y}=r;y",
Some("f"), vec![],
);
assert_eq!(result, Value::Number(10.0));
}
#[test]
fn vm_type_registry_register_duplicate_name() {
use crate::ast::{Decl, Param, Program, Span, Type};
use crate::vm::compile;
let prog = Program {
declarations: vec![
Decl::TypeDef {
name: "pt".to_string(),
fields: vec![Param { name: "x".to_string(), ty: Type::Number }],
span: Span::UNKNOWN,
},
Decl::TypeDef {
name: "pt".to_string(), fields: vec![Param { name: "x".to_string(), ty: Type::Number }],
span: Span::UNKNOWN,
},
],
source: None,
};
let compiled = compile(&prog).expect("compile ok");
assert_eq!(compiled.type_registry.types.len(), 1);
}
#[test]
fn vm_subtract() {
let source = "f a:n b:n>n;-a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(10.0), Value::Number(3.0)]),
Value::Number(7.0)
);
}
#[test]
fn vm_divide() {
let source = "f a:n b:n>n;/a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(10.0), Value::Number(4.0)]),
Value::Number(2.5)
);
}
#[test]
fn vm_equals() {
let source = "f a:n b:n>b;=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0), Value::Number(1.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0), Value::Number(2.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_not_equals() {
let source = "f a:n b:n>b;!=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0), Value::Number(2.0)]),
Value::Bool(true)
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(1.0), Value::Number(1.0)]),
Value::Bool(false)
);
}
#[test]
fn vm_greater_than() {
let source = "f a:n b:n>b;>a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(5.0), Value::Number(3.0)]),
Value::Bool(true)
);
}
#[test]
fn vm_less_than() {
let source = "f a:n b:n>b;<a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(5.0)]),
Value::Bool(true)
);
}
#[test]
fn vm_less_or_equal() {
let source = "f a:n b:n>b;<=a b";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(3.0), Value::Number(3.0)]),
Value::Bool(true)
);
}
#[test]
fn vm_literal_bool() {
assert_eq!(vm_run("f>b;true", Some("f"), vec![]), Value::Bool(true));
assert_eq!(vm_run("f>b;false", Some("f"), vec![]), Value::Bool(false));
}
#[test]
fn vm_abs() {
assert_eq!(vm_run("f>n;abs -7", Some("f"), vec![]), Value::Number(7.0));
}
#[test]
fn vm_foreach() {
let source = "f>n;s=0;@x [1, 2, 3]{+s x}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_foreach_early_return() {
let source = "f xs:L n>n;@x xs{>=x 3{x}};0";
let result = vm_run(
source,
Some("f"),
vec![Value::List(vec![
Value::Number(1.0),
Value::Number(5.0),
Value::Number(2.0),
])],
);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_foreach_on_non_list() {
let err = vm_run_err("f x:n>n;@i x{i}", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("foreach") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_foreach_return_from_nested_match() {
let source = "f xs:L n>n;@x xs{?x{5:x;_:0}}";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(5.0), Value::Number(9.0)]),
]);
assert_eq!(result, Value::Number(0.0));
}
#[test]
fn vm_guard_still_returns_early() {
let source = "f x:n>n;=x 0{99};+x 1";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(0.0)]), Value::Number(99.0));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(6.0));
}
#[test]
fn vm_ternary_negated() {
let source = r#"f x:n>t;!=x 1{"not one"}{"one"}"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Text("one".into()));
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(2.0)]), Value::Text("not one".into()));
}
#[test]
fn vm_guard_ternary_in_foreach() {
let source = "f xs:L n>n;@x xs{=x 0{10}{20}}";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![Value::Number(0.0), Value::Number(1.0)]),
]);
assert_eq!(result, Value::Number(20.0));
}
#[test]
fn vm_match_not_last_stmt() {
let source = "f x:n>n;?x{0:x;_:x};+x 1";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(6.0));
}
#[test]
fn vm_match_expr_no_arm_matches() {
let source = r#"f>n;y=?1{2:99};y"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Nil);
}
#[test]
fn vm_match_expr_with_bindings() {
let source = "f x:R n t>n;y=?x{~v:v;_:0};y";
let result = vm_run(source, Some("f"), vec![Value::Ok(Box::new(Value::Number(99.0)))]);
assert_eq!(result, Value::Number(99.0));
}
#[test]
fn vm_match_stmt_no_arm_matches() {
let source = "f x:n>n;?x{1:99};0";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(0.0));
}
#[test]
fn vm_match_arm_body_with_guard_return() {
let source = "f x:n>n;y=0;?x{1:>=x 0{42};_:0}";
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(1.0)]), Value::Number(42.0));
}
#[test]
fn vm_match_continue_arm_returns_nil() {
let source = "f xs:L n>n;@x xs{?x{1:cnt;_:x}}";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0)]),
]);
assert_eq!(result, Value::Number(2.0));
}
#[test]
fn vm_match_stmt_continue_propagates() {
let source = "f xs:L n>n;@x xs{?x{1:cnt;_:x}}";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(5.0)]),
]);
assert_eq!(result, Value::Number(5.0));
}
#[test]
fn vm_pattern_literal_no_match() {
let source = r#"f x:n>n;?x{1:10;2:20;_:0}"#;
assert_eq!(vm_run(source, Some("f"), vec![Value::Number(5.0)]), Value::Number(0.0));
}
#[test]
fn vm_pattern_ok_no_match() {
let source = r#"f>t;x=^"err";?x{~v:v;_:"default"}"#;
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("default".to_string()));
}
#[test]
fn vm_type_is_number_match() {
let result = vm_run(
r#"f x:n>t;?x{n v:"num";_:"other"}"#,
Some("f"),
vec![Value::Number(42.0)],
);
assert_eq!(result, Value::Text("num".into()));
}
#[test]
fn vm_type_is_text_match() {
let result = vm_run(
r#"f x:t>t;?x{t v:v;_:"other"}"#,
Some("f"),
vec![Value::Text("hello".into())],
);
assert_eq!(result, Value::Text("hello".into()));
}
#[test]
fn vm_type_is_bool_match() {
let result = vm_run(
r#"f x:b>t;?x{b v:"bool";_:"other"}"#,
Some("f"),
vec![Value::Bool(true)],
);
assert_eq!(result, Value::Text("bool".into()));
}
#[test]
fn vm_type_is_list_match() {
let result = vm_run(
r#"f x:L n>t;?x{l v:"list";_:"other"}"#,
Some("f"),
vec![Value::List(vec![Value::Number(1.0)])],
);
assert_eq!(result, Value::Text("list".into()));
}
#[test]
fn vm_type_is_no_match_falls_through() {
let result = vm_run(
r#"f x:n>t;?x{t v:"text";_:"other"}"#,
Some("f"),
vec![Value::Number(1.0)],
);
assert_eq!(result, Value::Text("other".into()));
}
#[test]
fn vm_type_is_wildcard_binding() {
let result = vm_run(
r#"f x:n>t;?x{n _:"matched";_:"other"}"#,
Some("f"),
vec![Value::Number(5.0)],
);
assert_eq!(result, Value::Text("matched".into()));
}
#[test]
fn vm_typeis_pattern_non_basic_type_no_match() {
let source = "f x:z>b;?x{n _:true;_:false}";
let result = vm_run(source, Some("f"), vec![
Value::Record { type_name: "pt".into(), fields: std::collections::HashMap::new() },
]);
assert_eq!(result, Value::Bool(false));
}
#[test]
fn vm_index_access_string() {
let source = "f>t;xs=[\"hello\", \"world\"];xs.0";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("hello".into()));
}
#[test]
fn vm_unsupported_binop() {
let source = "f a:b b:b>b;-a b";
let err = vm_run_err(
source,
Some("f"),
vec![Value::Bool(true), Value::Bool(false)],
);
assert!(
err.contains("unsupported") || err.contains("subtract") || err.contains("type"),
"unexpected error: {}", err
);
}
#[test]
fn vm_typedef_in_declarations() {
let source = "type point{x:n;y:n}\nf>n;42";
assert_eq!(vm_run(source, None, vec![]), Value::Number(42.0));
}
#[test]
fn vm_typedef_not_callable() {
let source = "type point{x:n;y:n}\nf>n;point 1 2";
let prog = parse_program(source);
let result = compile_and_run(&prog, Some("f"), vec![]);
assert!(result.is_err(), "expected error calling typedef");
}
#[test]
fn vm_destructure_with_text_fields() {
let source = "type usr{name:t;email:t} f>t;u=usr name:\"alice\" email:\"a@b\";{name;email}=u;name";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("alice".to_string()));
}
#[test]
fn vm_destructure_missing_field_error() {
let source = "type pt{x:n;y:n} f>n;p=pt x:3 y:4;{x;z}=p;x";
let prog = parse_program(source);
let result = compile_and_run(&prog, Some("f"), vec![]);
assert!(result.is_err(), "expected error for missing field in destructure");
}
#[test]
#[ignore] fn vm_destructure_non_record_error() {
let source = "type pt{x:n;y:n} f p:pt>n;{x;y}=p;+x y";
let prog = parse_program(source);
let result = compile_and_run(&prog, Some("f"), vec![Value::Number(42.0)]);
assert!(result.is_err(), "expected error for destructure on non-record");
}
#[test]
fn vm_index_access_string_list_second() {
let source = "f>t;xs=[\"hello\", \"world\"];xs.1";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Text("world".into()));
}
#[test]
fn vm_env_unwrap() {
let _guard = ENV_TEST_MUTEX.lock().unwrap();
unsafe { std::env::set_var("ILO_TEST_UNWRAP_VM", "world"); }
let source = r#"f k:t>R t t;~(env! k)"#;
let result = vm_run(source, Some("f"), vec![Value::Text("ILO_TEST_UNWRAP_VM".into())]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("world".into()))));
unsafe { std::env::remove_var("ILO_TEST_UNWRAP_VM"); }
}
#[test]
fn vm_env_wrong_arg_type() {
let err = vm_run_err("f>t;env 42", Some("f"), vec![]);
assert!(err.contains("env") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_range_as_index() {
let source = "f>n;@i 0..3{*i i}";
assert_eq!(vm_run(source, Some("f"), vec![]), Value::Number(4.0));
}
#[test]
fn vm_range_end_not_number() {
let source = "f s:n e:n>n;@i s..e{i}";
assert_eq!(
vm_run(source, Some("f"), vec![Value::Number(0.0), Value::Number(3.0)]),
Value::Number(2.0)
);
}
#[test]
fn vm_for_range_early_return_via_guard() {
let result = vm_run("f>n;@i 0..5{>=i 3{i};i}", Some("f"), vec![]);
assert_eq!(result, Value::Number(3.0));
}
#[test]
fn vm_for_range_non_number_start_error() {
let err = vm_run_err("f s:t>n;@i s..3{i}", Some("f"), vec![Value::Text("a".into())]);
assert!(
err.contains("range") || err.contains("number") || err.contains("start") || err.contains("type"),
"got: {err}"
);
}
#[test]
fn vm_for_range_non_number_end_error() {
let err = vm_run_err("f e:t>n;@i 0..e{i}", Some("f"), vec![Value::Text("b".into())]);
assert!(
err.contains("range") || err.contains("number") || err.contains("end") || err.contains("type"),
"got: {err}"
);
}
#[test]
fn vm_err_abs_wrong_arg_count() {
let err = vm_run_err("f>n;abs 1 2", Some("f"), vec![]);
assert!(err.contains("abs") || err.contains("arg") || err.contains("expect"), "got: {err}");
}
#[test]
fn vm_err_abs_wrong_type() {
let err = vm_run_err(r#"f x:t>n;abs x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("abs") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_cat_non_text_items() {
let err = vm_run_err("f>t;cat [1,2,3] \",\"", Some("f"), vec![]);
assert!(err.contains("cat") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_err_cat_wrong_arg_types() {
let err = vm_run_err("f x:n y:n>t;cat x y", Some("f"), vec![Value::Number(1.0), Value::Number(2.0)]);
assert!(err.contains("cat") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_err_cel_non_number() {
let err = vm_run_err(r#"f x:t>n;cel x"#, Some("f"), vec![Value::Text("a".into())]);
assert!(err.contains("cel") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_err_field_access_on_non_record() {
let err = vm_run_err("f x:n>n;x.y", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("field") || err.contains("record"), "got: {err}");
}
#[test]
fn vm_err_field_not_found_on_record() {
let err = vm_run_err("f>n;r=point x:1 y:2;r.z", Some("f"), vec![]);
assert!(err.contains("field") || err.contains("z") || err.contains("not found"), "got: {err}");
}
#[test]
fn vm_err_flr_non_number() {
let err = vm_run_err(r#"f x:t>n;flr x"#, Some("f"), vec![Value::Text("a".into())]);
assert!(err.contains("flr") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_get_non_text_arg() {
let err = vm_run_err("f x:n>R t t;get x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("get") || err.contains("text") || err.contains("string"), "got: {err}");
}
#[test]
fn vm_err_has_text_non_text_needle() {
let err = vm_run_err("f x:t y:n>b;has x y", Some("f"), vec![Value::Text("hello".into()), Value::Number(1.0)]);
assert!(err.contains("has") || err.contains("text") || err.contains("needle"), "got: {err}");
}
#[test]
fn vm_err_has_wrong_first_arg() {
let err = vm_run_err("f x:n y:n>b;has x y", Some("f"), vec![Value::Number(1.0), Value::Number(2.0)]);
assert!(err.contains("has") || err.contains("list") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_err_hd_empty_list() {
let err = vm_run_err("f>n;hd []", Some("f"), vec![]);
assert!(err.contains("hd") || err.contains("empty"), "got: {err}");
}
#[test]
fn vm_err_hd_empty_text() {
let err = vm_run_err("f>t;hd \"\"", Some("f"), vec![]);
assert!(err.contains("hd") || err.contains("empty"), "got: {err}");
}
#[test]
fn vm_err_hd_wrong_type() {
let err = vm_run_err("f x:n>n;hd x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("hd") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_err_index_on_non_list() {
let err = vm_run_err("f x:n>n;x.0", Some("f"), vec![Value::Number(1.0)]);
assert!(
err.contains("index") || err.contains("field") || err.contains("list") || err.contains("record"),
"got: {}", err
);
}
#[test]
fn vm_err_index_out_of_bounds() {
let err = vm_run_err("f>n;xs=[1, 2];xs.5", Some("f"), vec![]);
assert!(err.contains("bound") || err.contains("index") || err.contains("5"), "got: {err}");
}
#[test]
fn vm_err_len_wrong_arg_count() {
let err = vm_run_err("f>n;len 1 2", Some("f"), vec![]);
assert!(err.contains("len") || err.contains("arg") || err.contains("expect"), "got: {err}");
}
#[test]
fn vm_err_len_wrong_type() {
let err = vm_run_err("f x:n>n;len x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("len") || err.contains("string") || err.contains("list") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_max_non_number() {
let err = vm_run_err(
r#"f a:t b:t>n;max a b"#,
Some("f"),
vec![Value::Text("a".into()), Value::Text("b".into())],
);
assert!(err.contains("max") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_min_non_number() {
let err = vm_run_err(
r#"f a:t b:t>n;min a b"#,
Some("f"),
vec![Value::Text("a".into()), Value::Text("b".into())],
);
assert!(err.contains("min") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_num_wrong_arg_count() {
let err = vm_run_err(r#"f>R n t;num "1" "2""#, Some("f"), vec![]);
assert!(err.contains("num") || err.contains("arg") || err.contains("expect"), "got: {err}");
}
#[test]
fn vm_err_num_wrong_type() {
let err = vm_run_err("f x:n>R n t;num x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("num") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_rev_wrong_type() {
let err = vm_run_err("f x:n>n;rev x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("rev") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_rnd_lower_gt_upper() {
let err = vm_run_err("f>n;rnd 10 1", Some("f"), vec![]);
assert!(err.contains("rnd") || err.contains("bound"), "got: {err}");
}
#[test]
fn vm_err_rnd_wrong_arg_types() {
let err = vm_run_err("f x:t y:t>n;rnd x y", Some("f"), vec![Value::Text("a".into()), Value::Text("b".into())]);
assert!(err.contains("rnd") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_err_slc_non_number_end() {
let err = vm_run_err("f x:t y:t>t;slc x 0 y", Some("f"), vec![Value::Text("hi".into()), Value::Text("a".into())]);
assert!(err.contains("slc") || err.contains("number") || err.contains("index") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_slc_non_number_start() {
let err = vm_run_err("f x:t y:t>t;slc x y 1", Some("f"), vec![Value::Text("hi".into()), Value::Text("a".into())]);
assert!(err.contains("slc") || err.contains("number") || err.contains("index") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_slc_wrong_first_arg() {
let err = vm_run_err("f x:n>n;slc x 0 1", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("slc") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_spl_non_text_first() {
let err = vm_run_err("f x:n y:t>L t;spl x y", Some("f"), vec![Value::Number(1.0), Value::Text("a".into())]);
assert!(err.contains("spl") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_spl_non_text_second() {
let err = vm_run_err("f x:t y:n>L t;spl x y", Some("f"), vec![Value::Text("a-b".into()), Value::Number(1.0)]);
assert!(err.contains("spl") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_srt_mixed_types() {
let err = vm_run_err("f>L n;srt [1,\"a\"]", Some("f"), vec![]);
assert!(err.contains("srt") || err.contains("mixed") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_srt_wrong_type() {
let err = vm_run_err("f x:n>n;srt x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("srt") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_str_wrong_arg_count() {
let err = vm_run_err("f>t;str 1 2", Some("f"), vec![]);
assert!(err.contains("str") || err.contains("arg") || err.contains("expect"), "got: {err}");
}
#[test]
fn vm_err_str_wrong_type() {
let err = vm_run_err(r#"f x:t>t;str x"#, Some("f"), vec![Value::Text("hi".into())]);
assert!(err.contains("str") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_tl_empty_list() {
let err = vm_run_err("f>L n;tl []", Some("f"), vec![]);
assert!(err.contains("tl") || err.contains("empty"), "got: {err}");
}
#[test]
fn vm_err_tl_empty_text() {
let err = vm_run_err("f>t;tl \"\"", Some("f"), vec![]);
assert!(err.contains("tl") || err.contains("empty"), "got: {err}");
}
#[test]
fn vm_err_tl_wrong_type() {
let err = vm_run_err("f x:n>n;tl x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("tl") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_err_trm_wrong_type() {
let err = vm_run_err("f x:n>t;trm x", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("trm") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_err_with_on_non_record() {
let err = vm_run_err("f x:n>n;x with y:1", Some("f"), vec![Value::Number(1.0)]);
assert!(err.contains("with") || err.contains("record"), "got: {err}");
}
#[test]
#[ignore] fn vm_err_wrong_arity() {
let err = vm_run_err("f x:n>n;x", Some("f"), vec![]);
assert!(err.contains("expected") || err.contains("arg") || err.contains("arity") || err.contains("1"), "got: {err}");
}
#[test]
#[ignore] fn vm_map_squares() {
let source = "sq x:n>n;*x x main xs:L n>L n;map sq xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![1.0, 2.0, 3.0, 4.0, 5.0].into_iter().map(Value::Number).collect())
]);
assert_eq!(result, Value::List(vec![1.0, 4.0, 9.0, 16.0, 25.0].into_iter().map(Value::Number).collect()));
}
#[test]
fn vm_map_wrong_fn_arg() {
let err = vm_run_err("f>t;map 42 [1, 2]", Some("f"), vec![]);
assert!(err.contains("map") || err.contains("fn") || err.contains("function"), "got: {err}");
}
#[test]
#[ignore] fn vm_map_wrong_list_arg() {
let source = "sq x:n>n;*x x f>t;map sq 42";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("map") || err.contains("list"), "got: {err}");
}
#[test]
#[ignore] fn vm_map_with_text_fn_name() {
let source = "sq x:n>n;*x x f cb:t xs:L n>L n;map cb xs";
let result = vm_run(source, Some("f"), vec![
Value::Text("sq".into()),
Value::List(vec![Value::Number(3.0)]),
]);
assert_eq!(result, Value::List(vec![Value::Number(9.0)]));
}
#[test]
#[ignore] fn vm_flt_positive() {
let source = "pos x:n>b;>x 0 main xs:L n>L n;flt pos xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![-3.0, -1.0, 0.0, 2.0, 4.0].into_iter().map(Value::Number).collect())
]);
assert_eq!(result, Value::List(vec![2.0, 4.0].into_iter().map(Value::Number).collect()));
}
#[test]
#[ignore] fn vm_flt_predicate_returns_non_bool() {
let source = "id x:n>n;x f xs:L n>L n;flt id xs";
let err = vm_run_err(source, Some("f"), vec![Value::List(vec![Value::Number(1.0)])]);
assert!(err.contains("flt") || err.contains("bool"), "got: {err}");
}
#[test]
#[ignore] fn vm_flt_wrong_list_arg() {
let source = "pos x:n>b;>x 0 f>t;flt pos 42";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("flt") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_flt_key_not_fn_ref() {
let err = vm_run_err("f xs:L n>L n;flt 42 xs", Some("f"),
vec![Value::List(vec![Value::Number(1.0)])]);
assert!(err.contains("flt") || err.contains("fn") || err.contains("function"), "got: {err}");
}
#[test]
#[ignore] fn vm_fld_sum() {
let source = "add a:n b:n>n;+a b main xs:L n>n;fld add xs 0";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![1.0, 2.0, 3.0, 4.0, 5.0].into_iter().map(Value::Number).collect())
]);
assert_eq!(result, Value::Number(15.0));
}
#[test]
fn vm_fld_wrong_fn_arg() {
let err = vm_run_err("f>n;fld 42 [1, 2] 0", Some("f"), vec![]);
assert!(err.contains("fld") || err.contains("fn") || err.contains("function"), "got: {err}");
}
#[test]
#[ignore] fn vm_fld_wrong_list_arg() {
let source = "add a:n b:n>n;+a b f>n;fld add 42 0";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("fld") || err.contains("list"), "got: {err}");
}
#[test]
#[ignore] fn vm_grp_by_string_key() {
let source = r#"cl x:n>t;>x 5{"big"}{"small"} main xs:L n>M t L n;grp cl xs"#;
let result = vm_run(source, Some("main"), vec![
Value::List(vec![1.0, 8.0, 3.0, 9.0, 2.0].into_iter().map(Value::Number).collect())
]);
let Value::Map(m) = result else { panic!("expected Map") };
assert_eq!(m.get("small").unwrap(), &Value::List(vec![1.0, 3.0, 2.0].into_iter().map(Value::Number).collect()));
assert_eq!(m.get("big").unwrap(), &Value::List(vec![8.0, 9.0].into_iter().map(Value::Number).collect()));
}
#[test]
#[ignore] fn vm_grp_by_numeric_key() {
let source = "key x:n>t;str x main xs:L n>M t L n;grp key xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![1.0, 2.0, 1.0, 3.0, 2.0].into_iter().map(Value::Number).collect())
]);
let Value::Map(m) = result else { panic!("expected Map") };
assert_eq!(m.get("1").unwrap(), &Value::List(vec![1.0, 1.0].into_iter().map(Value::Number).collect()));
assert_eq!(m.get("2").unwrap(), &Value::List(vec![2.0, 2.0].into_iter().map(Value::Number).collect()));
assert_eq!(m.get("3").unwrap(), &Value::List(vec![3.0].into_iter().map(Value::Number).collect()));
}
#[test]
#[ignore] fn vm_grp_empty_list() {
let source = "id x:n>t;str x main xs:L n>M t L n;grp id xs";
let result = vm_run(source, Some("main"), vec![Value::List(vec![])]);
assert_eq!(result, Value::Map(std::collections::HashMap::new()));
}
#[test]
fn vm_grp_wrong_fn_arg() {
let err = vm_run_err("f>t;grp 42 [1, 2, 3]", Some("f"), vec![]);
assert!(err.contains("grp") || err.contains("fn") || err.contains("function"), "got: {err}");
}
#[test]
#[ignore] fn vm_grp_wrong_list_arg() {
let err = vm_run_err("id x:n>n;x f>t;grp id 42", Some("f"), vec![]);
assert!(err.contains("grp") || err.contains("list"), "got: {err}");
}
#[test]
#[ignore] fn vm_grp_number_key() {
let source = "id x:n>n;x g xs:L n>_;grp id xs";
let result = vm_run(source, Some("g"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(1.0)]),
]);
let Value::Map(m) = result else { panic!("expected map") };
assert_eq!(m.len(), 2);
}
#[test]
#[ignore] fn vm_grp_bool_key() {
let source = "pos x:n>b;>x 0 g xs:L n>_;grp pos xs";
let result = vm_run(source, Some("g"), vec![
Value::List(vec![Value::Number(-1.0), Value::Number(1.0), Value::Number(2.0)]),
]);
let Value::Map(m) = result else { panic!("expected map") };
assert!(m.contains_key("true"));
assert!(m.contains_key("false"));
}
#[test]
#[ignore] fn vm_grp_float_key() {
let source = "half x:n>n;/x 2 g xs:L n>_;grp half xs";
let result = vm_run(source, Some("g"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0), Value::Number(3.0)]),
]);
let Value::Map(m) = result else { panic!("expected Map") };
assert!(m.contains_key("0.5") || m.contains_key("1.5"),
"expected float key, got: {:?}", m.keys().collect::<Vec<_>>());
}
#[test]
#[ignore] fn vm_grp_key_returns_list_error() {
let source = "mk x:n>L n;[x] g xs:L n>_;grp mk xs";
let err = vm_run_err(source, Some("g"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(2.0)]),
]);
assert!(err.contains("grp") || err.contains("key") || err.contains("string"), "got: {err}");
}
#[test]
#[ignore] fn vm_sum_basic() {
let source = "f xs:L n>n;sum xs";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![1.0, 2.0, 3.0, 4.0, 5.0].into_iter().map(Value::Number).collect())
]);
assert_eq!(result, Value::Number(15.0));
}
#[test]
#[ignore] fn vm_sum_empty() {
let source = "f xs:L n>n;sum xs";
assert_eq!(vm_run(source, Some("f"), vec![Value::List(vec![])]), Value::Number(0.0));
}
#[test]
fn vm_sum_wrong_arg() {
let err = vm_run_err("f>n;sum 42", Some("f"), vec![]);
assert!(err.contains("sum") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_sum_non_numeric_element() {
let err = vm_run_err(r#"f>n;sum ["a", "b"]"#, Some("f"), vec![]);
assert!(err.contains("sum") || err.contains("number"), "got: {err}");
}
#[test]
#[ignore] fn vm_avg_basic() {
let source = "f xs:L n>n;avg xs";
let result = vm_run(source, Some("f"), vec![
Value::List(vec![2.0, 4.0, 6.0].into_iter().map(Value::Number).collect())
]);
assert_eq!(result, Value::Number(4.0));
}
#[test]
fn vm_avg_empty_error() {
let err = vm_run_err("f>n;avg []", Some("f"), vec![]);
assert!(err.contains("avg") || err.contains("empty"), "got: {err}");
}
#[test]
fn vm_avg_wrong_arg() {
let err = vm_run_err("f>n;avg 42", Some("f"), vec![]);
assert!(err.contains("avg") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_avg_non_number_element() {
let err = vm_run_err("f xs:L n>n;avg xs", Some("f"),
vec![Value::List(vec![Value::Text("x".into())])]);
assert!(err.contains("avg") || err.contains("number"), "got: {err}");
}
#[test]
#[ignore] fn vm_flat_nested() {
let source = "f>L n;flat [[1, 2], [3], [4, 5]]";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::List(vec![1.0, 2.0, 3.0, 4.0, 5.0].into_iter().map(Value::Number).collect()));
}
#[test]
#[ignore] fn vm_flat_mixed() {
let source = "f>L n;flat [[1, 2], 3]";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::List(vec![1.0, 2.0, 3.0].into_iter().map(Value::Number).collect()));
}
#[test]
#[ignore] fn vm_flat_empty() {
assert_eq!(vm_run("f>L n;flat []", Some("f"), vec![]), Value::List(vec![]));
}
#[test]
fn vm_flat_wrong_arg() {
let err = vm_run_err("f>L n;flat 42", Some("f"), vec![]);
assert!(err.contains("flat") || err.contains("list"), "got: {err}");
}
#[test]
#[ignore] fn vm_srt_fn_by_length() {
let source = "ln s:t>n;len s main xs:L t>L t;srt ln xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![
Value::Text("banana".into()),
Value::Text("a".into()),
Value::Text("cc".into()),
]),
]);
assert_eq!(result, Value::List(vec![
Value::Text("a".into()),
Value::Text("cc".into()),
Value::Text("banana".into()),
]));
}
#[test]
#[ignore] fn vm_srt_fn_numeric_key() {
let source = "neg x:n>n;-x main xs:L n>L n;srt neg xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![Value::Number(1.0), Value::Number(3.0), Value::Number(2.0)]),
]);
assert_eq!(result, Value::List(vec![Value::Number(3.0), Value::Number(2.0), Value::Number(1.0)]));
}
#[test]
#[ignore] fn vm_srt_key_fn_text_keys() {
let source = "id x:t>t;x main xs:L t>L t;srt id xs";
let result = vm_run(source, Some("main"), vec![
Value::List(vec![
Value::Text("banana".into()),
Value::Text("apple".into()),
Value::Text("cherry".into()),
]),
]);
assert_eq!(result, Value::List(vec![
Value::Text("apple".into()),
Value::Text("banana".into()),
Value::Text("cherry".into()),
]));
}
#[test]
#[ignore] fn vm_srt_key_fn_wrong_second_arg() {
let source = "sq x:n>n;*x x f>n;srt sq 42";
let err = vm_run_err(source, Some("f"), vec![]);
assert!(err.contains("srt") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_srt_key_not_fn_ref() {
let err = vm_run_err("f xs:L n>L n;srt 42 xs", Some("f"),
vec![Value::List(vec![Value::Number(1.0)])]);
assert!(err.contains("srt") || err.contains("fn") || err.contains("function"), "got: {err}");
}
#[test]
fn vm_srt_text_string() {
assert_eq!(vm_run(r#"f>t;srt "cab""#, Some("f"), vec![]), Value::Text("abc".into()));
}
#[test]
#[ignore] fn vm_srt_bool_key_equal_ordering() {
let source = "pos x:n>b;> x 0 f>L n;srt pos [3,-1,2,-2]";
let result = vm_run(source, Some("f"), vec![]);
let Value::List(items) = result else { panic!("expected List, got {:?}", result) };
assert_eq!(items.len(), 4);
}
#[test]
fn vm_ok_srt_empty_list() {
assert_eq!(vm_run("f>L n;srt []", Some("f"), vec![]), Value::List(vec![]));
}
#[test]
fn vm_slc_clamped() {
let source = "f>L n;slc [1, 2, 3] 1 100";
assert_eq!(
vm_run(source, Some("f"), vec![]),
Value::List(vec![Value::Number(2.0), Value::Number(3.0)])
);
}
#[test]
fn vm_unq_list_strings() {
let result = vm_run("f xs:L t>L t;unq xs", Some("f"), vec![
Value::List(vec![Value::Text("a".into()), Value::Text("b".into()), Value::Text("a".into())]),
]);
assert_eq!(result, Value::List(vec![Value::Text("a".into()), Value::Text("b".into())]));
}
#[test]
fn vm_unq_text_chars() {
assert_eq!(
vm_run("f s:t>t;unq s", Some("f"), vec![Value::Text("aabbc".into())]),
Value::Text("abc".into())
);
}
#[test]
fn vm_unq_wrong_type() {
let err = vm_run_err("f>n;unq 42", Some("f"), vec![]);
assert!(err.contains("unq") || err.contains("list") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_fmt_basic() {
let result = vm_run(
r#"f a:t b:t>t;fmt "{} + {}" a b"#,
Some("f"),
vec![Value::Text("1".into()), Value::Text("2".into())],
);
assert_eq!(result, Value::Text("1 + 2".into()));
}
#[test]
#[ignore] fn vm_fmt_template_only() {
assert_eq!(vm_run(r#"f>t;fmt "hello""#, Some("f"), vec![]), Value::Text("hello".into()));
}
#[test]
#[ignore] fn vm_fmt_fewer_args_than_slots() {
let result = vm_run(
r#"f a:t>t;fmt "{} and {}" a"#,
Some("f"),
vec![Value::Text("x".into())],
);
assert_eq!(result, Value::Text("x and {}".into()));
}
#[test]
#[ignore] fn vm_fmt_number_arg() {
let result = vm_run(
r#"f n:n>t;fmt "value: {}" n"#,
Some("f"),
vec![Value::Number(42.0)],
);
assert_eq!(result, Value::Text("value: 42".into()));
}
#[test]
fn vm_fmt_wrong_first_arg() {
let err = vm_run_err("f>n;fmt 42", Some("f"), vec![]);
assert!(err.contains("fmt") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_prnt_text_passthrough() {
assert_eq!(
vm_run("f s:t>t;prnt s", Some("f"), vec![Value::Text("hi".into())]),
Value::Text("hi".into())
);
}
#[test]
#[ignore] fn vm_rgx_find_all() {
let source = r#"f s:t>L t;rgx "\d+" s"#;
let result = vm_run(source, Some("f"), vec![Value::Text("abc 123 def 456".into())]);
assert_eq!(result, Value::List(vec![
Value::Text("123".into()),
Value::Text("456".into()),
]));
}
#[test]
#[ignore] fn vm_rgx_capture_groups() {
let source = r#"f s:t>L t;rgx "(\w+)=(\w+)" s"#;
let result = vm_run(source, Some("f"), vec![Value::Text("name=alice age=30".into())]);
assert_eq!(result, Value::List(vec![
Value::Text("name".into()),
Value::Text("alice".into()),
]));
}
#[test]
#[ignore] fn vm_rgx_no_match() {
let source = r#"f s:t>L t;rgx "\d+" s"#;
let result = vm_run(source, Some("f"), vec![Value::Text("no numbers here".into())]);
assert_eq!(result, Value::List(vec![]));
}
#[test]
fn vm_rgx_invalid_pattern() {
let err = vm_run_err(r#"f>L t;rgx "[invalid" "test""#, Some("f"), vec![]);
assert!(err.contains("rgx") || err.contains("regex") || err.contains("pattern"), "got: {err}");
}
#[test]
fn vm_rgx_wrong_arg_types() {
let err = vm_run_err(r#"f>L t;rgx 42 "test""#, Some("f"), vec![]);
assert!(err.contains("rgx") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_rgx_non_text_second_arg() {
let err = vm_run_err(r#"f>L t;rgx "." 42"#, Some("f"), vec![]);
assert!(err.contains("rgx") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_jp_object() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"{"name":"alice"}"#.to_string()),
Value::Text("name".to_string()),
]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("alice".to_string()))));
}
#[test]
fn vm_jp_invalid_json() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text("not json".to_string()),
Value::Text("x".to_string()),
]);
assert!(matches!(result, Value::Err(_)));
}
#[test]
fn vm_jparse_scalar() {
let source = r#"f j:t>R t t;jpar j"#;
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("42".to_string())]),
Value::Ok(Box::new(Value::Number(42.0)))
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("true".to_string())]),
Value::Ok(Box::new(Value::Bool(true)))
);
assert_eq!(
vm_run(source, Some("f"), vec![Value::Text("null".to_string())]),
Value::Ok(Box::new(Value::Nil))
);
}
#[test]
fn vm_jpar_wrong_arg_type() {
let err = vm_run_err("f>t;jpar 42", Some("f"), vec![]);
assert!(err.contains("jpar") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_jpth_array_index() {
let source = r#"f j:t p:t>R t t;jpth j p"#;
let result = vm_run(source, Some("f"), vec![
Value::Text(r#"[10,20,30]"#.to_string()),
Value::Text("1".to_string()),
]);
assert_eq!(result, Value::Ok(Box::new(Value::Text("20".into()))));
}
#[test]
fn vm_jpth_array_index_out_of_bounds() {
let source = r#"f>R t t;jpth "[1,2,3]" "5""#;
let result = vm_run(source, Some("f"), vec![]);
let Value::Err(inner) = result else { panic!("expected Err, got {:?}", result) };
let s = inner.to_string();
assert!(s.contains("not found") || s.contains("5") || s.contains("key"), "got: {s}");
}
#[test]
fn vm_jpth_wrong_args() {
let err = vm_run_err(r#"f>t;jpth 42 "path""#, Some("f"), vec![]);
assert!(err.contains("jpth") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_jdmp_bool_value() {
assert_eq!(vm_run("f>t;jdmp true", Some("f"), vec![]), Value::Text("true".into()));
}
#[test]
fn vm_jdmp_nil_value() {
let result = vm_run(r#"f>t;jdmp (mget mmap "k")"#, Some("f"), vec![]);
assert_eq!(result, Value::Text("null".into()));
}
#[test]
#[ignore] fn vm_jdmp_fnref() {
let source = "sq x:n>n;*x x f>t;r=sq;jdmp r";
let result = vm_run(source, Some("f"), vec![]);
let Value::Text(s) = result else { panic!("expected Text") };
assert!(s.contains("fn:sq") || s.contains("sq"), "got: {s}");
}
#[test]
fn vm_jdmp_large_float() {
let source = "f x:n>t;jdmp x";
let result = vm_run(source, Some("f"), vec![Value::Number(1.23456789e20)]);
assert!(matches!(result, Value::Text(_)));
}
#[test]
fn vm_mhas_found() {
let result = vm_run(r#"f>b;m=mset mmap "x" 1;mhas m "x""#, Some("f"), vec![]);
assert_eq!(result, Value::Bool(true));
}
#[test]
fn vm_mhas_not_found() {
let result = vm_run(r#"f>b;m=mset mmap "x" 1;mhas m "y""#, Some("f"), vec![]);
assert_eq!(result, Value::Bool(false));
}
#[test]
#[ignore] fn vm_mhas_wrong_args() {
let err = vm_run_err("f>n;mhas 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mhas") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mkeys_happy_path() {
let result = vm_run(r#"f>L t;m=mset (mset mmap "b" 2) "a" 1;mkeys m"#, Some("f"), vec![]);
assert_eq!(result, Value::List(vec![Value::Text("a".into()), Value::Text("b".into())]));
}
#[test]
#[ignore] fn vm_mkeys_wrong_args() {
let err = vm_run_err("f>n;mkeys 42", Some("f"), vec![]);
assert!(err.contains("mkeys") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mvals_happy_path() {
let result = vm_run(r#"f>L n;m=mset (mset mmap "b" 2) "a" 1;mvals m"#, Some("f"), vec![]);
assert_eq!(result, Value::List(vec![Value::Number(1.0), Value::Number(2.0)]));
}
#[test]
#[ignore] fn vm_mvals_wrong_args() {
let err = vm_run_err("f>n;mvals 42", Some("f"), vec![]);
assert!(err.contains("mvals") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_mdel_happy_path() {
let result = vm_run(r#"f>n;m=mset (mset mmap "a" 1) "b" 2;m2=mdel m "a";len m2"#, Some("f"), vec![]);
assert_eq!(result, Value::Number(1.0));
}
#[test]
#[ignore] fn vm_mdel_wrong_args() {
let err = vm_run_err("f>n;mdel 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mdel") || err.contains("map"), "got: {err}");
}
#[test]
#[ignore] fn vm_mget_wrong_args() {
let err = vm_run_err("f>n;mget 42 \"key\"", Some("f"), vec![]);
assert!(err.contains("mget") || err.contains("map"), "got: {err}");
}
#[test]
#[ignore] fn vm_mset_wrong_args() {
let err = vm_run_err("f>n;mset 42 \"key\" 1", Some("f"), vec![]);
assert!(err.contains("mset") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_rnd_wrong_types() {
let err = vm_run_err(r#"f>n;rnd "a" "b""#, Some("f"), vec![]);
assert!(err.contains("rnd") || err.contains("number") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_safe_field_on_nil_returns_nil() {
let result = vm_run("f>n;x=mget mmap \"key\";x.?field", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
fn vm_safe_index_on_nil_returns_nil() {
let result = vm_run("f>n;xs=mget mmap \"key\";xs.?0", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
#[ignore] fn vm_fnref_callee_from_scope() {
let source = "sq x:n>n;*x x f cb:z>n;cb 3";
let result = vm_run(source, Some("f"), vec![Value::FnRef("sq".into())]);
assert_eq!(result, Value::Number(9.0));
}
#[test]
#[ignore] fn vm_fn_ref_via_ref_expr() {
let source = "dbl x:n>n;*x 2 main>n;f=dbl;f 10";
assert_eq!(vm_run(source, Some("main"), vec![]), Value::Number(20.0));
}
#[test]
#[ignore] fn vm_text_callee_from_scope() {
let source = "sq x:n>n;*x x f cb:z>n;cb 3";
let result = vm_run(source, Some("f"), vec![Value::Text("sq".into())]);
assert_eq!(result, Value::Number(9.0));
}
#[test]
#[ignore] fn vm_user_hof_fn_type() {
let source = "sq x:n>n;*x x apl f:F n n x:n>n;f x";
let result = vm_run(source, Some("apl"), vec![
Value::FnRef("sq".to_string()),
Value::Number(7.0),
]);
assert_eq!(result, Value::Number(49.0));
}
#[test]
fn vm_bang_on_non_result_passes_through() {
let source = "id x:n>z;x f>z;id! 42";
let result = vm_run(source, Some("f"), vec![]);
assert_eq!(result, Value::Number(42.0));
}
#[test]
fn vm_brk_inside_guard_body_propagates() {
let src = "f>n;@x [1,2,3,4]{>x 2{brk x};x}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_cnt_inside_guard_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 1{cnt};x}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_brk_inside_ternary_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 2{brk x}{0};0}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(2.0));
}
#[test]
fn vm_cnt_inside_ternary_body_propagates() {
let src = "f>n;@x [1,2,3]{=x 1{cnt}{0};x}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_brk_inside_match_arm_propagates() {
let src = "f>n;@x [1,2,3]{?x{2:brk x;_:x};x}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(2.0));
}
#[test]
fn vm_cnt_in_match_expr_arm_returns_nil() {
let src = "f>n;@x [1,2,3]{r=?x{1:cnt;_:x};r}";
assert_eq!(vm_run(src, Some("f"), vec![]), Value::Number(3.0));
}
#[test]
fn vm_continue_in_function_body_returns_nil() {
let result = vm_run("f>_;cnt", Some("f"), vec![]);
assert_eq!(result, Value::Nil);
}
#[test]
#[ignore] fn vm_rdb_csv() {
let result = vm_run(
r#"f s:t>t;rdb s "csv""#,
Some("f"),
vec![Value::Text("a,b\n1,2".into())],
);
let Value::Ok(inner) = result else { panic!("expected Ok") };
let Value::List(rows) = *inner else { panic!("expected list") };
assert_eq!(rows.len(), 2);
}
#[test]
#[ignore] fn vm_rdb_csv_single_row() {
let result = vm_run(
r#"f s:t>t;rdb s "csv""#,
Some("f"),
vec![Value::Text("a,b,c".into())],
);
let Value::Ok(inner) = result else { panic!("expected Ok") };
let Value::List(rows) = *inner else { panic!("expected list") };
assert_eq!(rows.len(), 1);
}
#[test]
#[ignore] fn vm_rdb_json() {
let result = vm_run(
r#"f s:t>t;rdb s "json""#,
Some("f"),
vec![Value::Text(r#"{"x":1}"#.into())],
);
assert!(matches!(result, Value::Ok(_)), "expected Ok, got {:?}", result);
}
#[test]
#[ignore] fn vm_rdb_invalid_json_is_err() {
let result = vm_run(
r#"f s:t>t;rdb s "json""#,
Some("f"),
vec![Value::Text("not json".into())],
);
assert!(matches!(result, Value::Err(_)), "expected Err, got {:?}", result);
}
#[test]
#[ignore] fn vm_rdb_raw_passthrough() {
let result = vm_run(
r#"f s:t>t;rdb s "raw""#,
Some("f"),
vec![Value::Text("hello".into())],
);
assert_eq!(result, Value::Ok(Box::new(Value::Text("hello".into()))));
}
#[test]
fn vm_rdb_wrong_first_arg() {
let err = vm_run_err(r#"f>t;rdb 42 "raw""#, Some("f"), vec![]);
assert!(err.contains("rdb") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_rdb_wrong_format_arg() {
let err = vm_run_err(r#"f>t;rdb "hello" 42"#, Some("f"), vec![]);
assert!(err.contains("rdb") || err.contains("format") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_rd_wrong_arg_type() {
let err = vm_run_err("f>t;rd 42", Some("f"), vec![]);
assert!(err.contains("rd") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_rd_with_wrong_format_type() {
let err = vm_run_err("f>t;rd \"/tmp\" 42", Some("f"), vec![]);
assert!(err.contains("rd") || err.contains("format") || err.contains("text"), "got: {err}");
}
#[test]
#[ignore] fn vm_rd_explicit_raw_format() {
let path = "/tmp/ilo_test_vm_rd_explicit.txt";
std::fs::write(path, "hello").unwrap();
let source = format!(r#"f>R t t;rd "{path}" "raw""#);
let result = vm_run(&source, Some("f"), vec![]);
let Value::Ok(inner) = result else { panic!("expected Ok") };
assert_eq!(*inner, Value::Text("hello".into()));
}
#[test]
#[ignore] fn vm_rd_explicit_format_parse_error() {
let path = "/tmp/ilo_test_vm_rd_badjson.txt";
std::fs::write(path, "not json at all!!!").unwrap();
let source = format!(r#"f>R t t;rd "{path}" "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Err(_)));
}
#[test]
fn vm_rdl_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_vm_rdl_test.txt");
std::fs::write(&path, "line1\nline2\nline3").unwrap();
let path_str = path.to_str().unwrap().to_string();
let result = vm_run("f p:t>t;rdl p", Some("f"), vec![Value::Text(path_str)]);
std::fs::remove_file(&path).ok();
let Value::Ok(inner) = result else { panic!("expected Ok") };
let Value::List(lines) = *inner else { panic!("expected list") };
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], Value::Text("line1".into()));
}
#[test]
fn vm_rdl_not_found() {
let result = vm_run(
"f p:t>t;rdl p",
Some("f"),
vec![Value::Text("/nonexistent/ilo_rdl_test.txt".into())],
);
assert!(matches!(result, Value::Err(_)), "expected Err, got {:?}", result);
}
#[test]
fn vm_rdl_wrong_arg() {
let err = vm_run_err("f>t;rdl 42", Some("f"), vec![]);
assert!(err.contains("rdl") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_wr_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_vm_wr_test.txt");
let path_str = path.to_str().unwrap().to_string();
let result = vm_run(
"f p:t>t;wr p \"hello\"",
Some("f"),
vec![Value::Text(path_str.clone())],
);
std::fs::remove_file(&path).ok();
assert!(matches!(result, Value::Ok(_)), "expected Ok, got {:?}", result);
}
#[test]
fn vm_wr_wrong_args() {
let err = vm_run_err("f>t;wr 42 \"hello\"", Some("f"), vec![]);
assert!(err.contains("wr") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_wr_csv_format() {
let path = "/tmp/ilo_test_vm_wr.csv";
let source = format!(r#"f>R t t;wr "{path}" [[1,2],[3,4]] "csv""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("1,2"));
}
#[test]
#[ignore] fn vm_wr_csv_bool_field() {
let path = "/tmp/ilo_test_vm_wr_bool.csv";
let source = format!(r#"f>R t t;wr "{path}" [[true,false]] "csv""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("true"));
}
#[test]
#[ignore] fn vm_wr_json_format() {
let path = "/tmp/ilo_test_vm_wr.json";
let source = format!(r#"f>R t t;wr "{path}" [1,2,3] "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("1"));
}
#[test]
#[ignore] fn vm_wr_csv_output() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_vm_wr_csv.csv");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [["name", "age"], ["alice", 30], ["bob", 25]] "csv""#,
path_str.replace('\\', "\\\\")
);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "name,age\nalice,30\nbob,25\n");
let _ = std::fs::remove_file(&path);
}
#[test]
#[ignore] fn vm_wr_csv_quoted_fields() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_vm_wr_csv_quoted.csv");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [["a,b", "c\"d"]] "csv""#,
path_str.replace('\\', "\\\\")
);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "\"a,b\",\"c\"\"d\"\n");
let _ = std::fs::remove_file(&path);
}
#[test]
#[ignore] fn vm_wr_json_output() {
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_vm_wr_json.json");
let path_str = path.to_str().unwrap();
let source = format!(
r#"f>R t t;wr "{}" [1, 2, 3] "json""#,
path_str.replace('\\', "\\\\")
);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed, serde_json::json!([1.0, 2.0, 3.0]));
let _ = std::fs::remove_file(&path);
}
#[test]
fn vm_wr_unknown_format() {
let err = vm_run_err(r#"f>R t t;wr "/tmp/x" "data" "xml""#, Some("f"), vec![]);
assert!(err.contains("unknown") || err.contains("format") || err.contains("wr"), "got: {err}");
}
#[test]
#[ignore] fn vm_wr_json_text_value() {
let path = "/tmp/ilo_test_vm_wr_json_text.json";
let source = format!(r#"f>R t t;wr "{path}" "hello world" "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("hello world"));
}
#[test]
#[ignore] fn vm_wr_json_bool_value() {
let path = "/tmp/ilo_test_vm_wr_json_bool.json";
let source = format!(r#"f>R t t;wr "{path}" true "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("true"));
}
#[test]
#[ignore] fn vm_wr_json_map_value() {
let path = "/tmp/ilo_test_vm_wr_json_map.json";
let source = format!(r#"f>R t t;m=mset mmap "k" 42;wr "{path}" m "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert!(content.contains("\"k\""));
assert!(content.contains("42"));
}
#[test]
#[ignore] fn vm_wr_json_nil_value() {
let path = "/tmp/ilo_test_vm_wr_json_nil.json";
let source = format!(r#"f>R t t;v=mget mmap "x";wr "{path}" v "json""#);
let result = vm_run(&source, Some("f"), vec![]);
assert!(matches!(result, Value::Ok(_)));
let content = std::fs::read_to_string(path).unwrap();
assert_eq!(content.trim(), "null");
}
#[test]
#[ignore] fn vm_wr_json_with_ok_value() {
let path = "/tmp/ilo_test_vm_wr_ok.json";
let source = format!(r#"f x:z>R t t;wr "{path}" x "json""#);
let result = vm_run(&source, Some("f"), vec![
Value::Ok(Box::new(Value::Number(1.0))),
]);
assert!(matches!(result, Value::Ok(_)));
}
#[test]
fn vm_wr_non_text_format_arg_errors() {
let path = "/tmp/ilo_test_vm_wr_fmt_err.csv";
let source = format!(r#"f>R t t;wr "{path}" [1] 42"#);
let err = vm_run_err(&source, Some("f"), vec![]);
assert!(err.contains("wr") || err.contains("format") || err.contains("text"), "got: {err}");
}
#[test]
fn vm_wr_csv_non_list_data_errors() {
let path = "/tmp/ilo_test_vm_wr_csv_nonlist.csv";
let source = format!(r#"f>R t t;wr "{path}" 42 "csv""#);
let err = vm_run_err(&source, Some("f"), vec![]);
assert!(err.contains("wr") || err.contains("csv") || err.contains("list"), "got: {err}");
}
#[test]
fn vm_wr_csv_row_not_a_list_errors() {
let path = "/tmp/ilo_test_vm_wr_csv_row_err.csv";
let source = format!(r#"f>R t t;wr "{path}" [42] "csv""#);
let err = vm_run_err(&source, Some("f"), vec![]);
assert!(err.contains("wr") || err.contains("csv") || err.contains("list") || err.contains("row"), "got: {err}");
}
#[test]
#[ignore] fn vm_wr_csv_nil_field() {
let path = "/tmp/ilo_test_vm_wr_nil.csv";
let source = format!(r#"f x:z>R t t;wr "{path}" [[x,1]] "csv""#);
let result = vm_run(&source, Some("f"), vec![Value::Nil]);
assert!(matches!(result, Value::Ok(_)), "expected Ok, got {:?}", result);
let content = std::fs::read_to_string(path).unwrap();
assert!(!content.is_empty());
}
#[test]
fn vm_wr_two_arg_non_text_content_error() {
let err = vm_run_err(
r#"f>R t t;wr "/tmp/ilo_test_bad_wr.txt" 42"#,
Some("f"), vec![],
);
assert!(err.contains("wr") || err.contains("text") || err.contains("content"), "got: {err}");
}
#[test]
fn vm_wr_write_failure_returns_err() {
let source = r#"f>R t t;wr "/no/such/dir/ilo_test.txt" "hello""#;
let result = vm_run(source, Some("f"), vec![]);
assert!(matches!(result, Value::Err(_)), "expected Err for bad path, got {:?}", result);
}
#[test]
fn vm_wrl_basic() {
let mut path = std::env::temp_dir();
path.push("ilo_vm_wrl_test.txt");
let path_str = path.to_str().unwrap().to_string();
let result = vm_run(
"f p:t>t;wrl p [\"a\", \"b\", \"c\"]",
Some("f"),
vec![Value::Text(path_str.clone())],
);
std::fs::remove_file(&path).ok();
assert!(matches!(result, Value::Ok(_)), "expected Ok, got {:?}", result);
}
#[test]
fn vm_wrl_non_text_item() {
let path = "/tmp/ilo_test_vm_wrl_nontxt.txt";
let source = format!(r#"f>R t t;wrl "{path}" ["ok", 99]"#);
let prog = parse_program(&source);
let result = compile_and_run(&prog, Some("f"), vec![]);
std::fs::remove_file(path).ok();
assert!(result.is_err(), "expected error for non-text wrl item");
}
#[test]
fn vm_wrl_wrong_args() {
let err = vm_run_err("f>t;wrl 42 [\"a\"]", Some("f"), vec![]);
assert!(err.contains("wrl") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
fn vm_wrl_write_failure_returns_err() {
let source = r#"f>R t t;wrl "/no/such/dir/ilo_test.txt" ["a","b"]"#;
let result = vm_run(source, Some("f"), vec![]);
assert!(matches!(result, Value::Err(_)), "expected Err for bad path, got {:?}", result);
}
#[test]
#[ignore] fn vm_get_invalid_headers() {
let err = vm_run_err(r#"f>t;get "http://x" 42"#, Some("f"), vec![]);
assert!(err.contains("headers") || err.contains("get") || err.contains("map") || err.contains("M t t"), "got: {err}");
}
#[test]
fn vm_post_wrong_arg_types() {
let err = vm_run_err(r#"f>t;post 42 "body""#, Some("f"), vec![]);
assert!(err.contains("post") || err.contains("text") || err.contains("type"), "got: {err}");
}
#[test]
#[ignore] fn vm_post_invalid_headers() {
let err = vm_run_err(r#"f>t;post "http://x" "body" 42"#, Some("f"), vec![]);
assert!(err.contains("headers") || err.contains("post") || err.contains("map"), "got: {err}");
}
#[test]
fn vm_arena_full_recnew_fallback_to_heap() {
let src = "type pt{x:n;y:n} f>n;i=0;r=pt x:0 y:0;wh <i 3000{j=+i 1;r=pt x:i y:j;i=j};r.x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2999.0));
}
#[test]
fn vm_arena_full_recwith_fallback_to_heap() {
let src = "type pt{x:n;y:n} f>n;r=pt x:0 y:0;i=0;wh <i 3000{r=r with x:i;i=+i 1};r.x";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2999.0));
}
#[test]
fn vm_arena_full_recnew_with_string_field() {
let src = r#"type msg{text:t;val:n} f>n;i=0;r=msg text:"a" val:0;wh <i 3000{r=msg text:"hello" val:i;i=+i 1};r.val"#;
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(2999.0));
}
#[test]
fn vm_arena_full_recwith_preserves_string_fields() {
let src = r#"type msg{text:t;val:n} f>t;r=msg text:"hello" val:0;i=0;wh <i 3000{r=r with val:i;i=+i 1};r.text"#;
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Text("hello".into()));
}
#[test]
fn vm_arena_full_record_returned_as_value() {
let src = "type pt{x:n;y:n} f>pt;i=0;r=pt x:0 y:0;wh <i 3000{j=+i 1;r=pt x:i y:j;i=j};r";
let result = vm_run(src, Some("f"), vec![]);
match result {
Value::Record { type_name, fields } => {
assert_eq!(type_name, "pt");
assert_eq!(fields.get("x"), Some(&Value::Number(2999.0)));
assert_eq!(fields.get("y"), Some(&Value::Number(3000.0)));
}
other => panic!("expected Record, got {:?}", other),
}
}
#[test]
fn vm_arena_record_to_value_returns_record() {
let src = "type pt{x:n;y:n} f>pt;pt x:42 y:99";
let result = vm_run(src, Some("f"), vec![]);
match result {
Value::Record { type_name, fields } => {
assert_eq!(type_name, "pt");
assert_eq!(fields.get("x"), Some(&Value::Number(42.0)));
assert_eq!(fields.get("y"), Some(&Value::Number(99.0)));
}
other => panic!("expected Record, got {:?}", other),
}
}
#[test]
fn vm_to_value_with_registry_resolves_field_names() {
use crate::vm::compile;
let prog = parse_program("type pt{x:n;y:n} f>pt;pt x:7 y:8");
let compiled = compile(&prog).unwrap();
let result = crate::vm::run(&compiled, Some("f"), vec![]).unwrap();
match result {
Value::Record { type_name, fields } => {
assert_eq!(type_name, "pt");
assert_eq!(fields.get("x"), Some(&Value::Number(7.0)));
assert_eq!(fields.get("y"), Some(&Value::Number(8.0)));
}
other => panic!("expected Record, got {:?}", other),
}
}
#[test]
fn vm_to_value_with_registry_nested_record() {
let src = "type inner{v:n} type outer{a:inner;b:n} f>outer;i=inner v:42;outer a:i b:99";
let result = vm_run(src, Some("f"), vec![]);
match result {
Value::Record { type_name, fields } => {
assert_eq!(type_name, "outer");
assert_eq!(fields.get("b"), Some(&Value::Number(99.0)));
match fields.get("a") {
Some(Value::Record { type_name: inner_name, fields: inner_fields }) => {
assert_eq!(inner_name, "inner");
assert_eq!(inner_fields.get("v"), Some(&Value::Number(42.0)));
}
other => panic!("expected inner Record, got {:?}", other),
}
}
other => panic!("expected outer Record, got {:?}", other),
}
}
#[test]
fn vm_multi_frame_return_chain() {
let src = "c x:n>n;+x 100\nb x:n>n;c +x 10\na x:n>n;b +x 1";
let result = vm_run(src, Some("a"), vec![Value::Number(5.0)]);
assert_eq!(result, Value::Number(116.0));
}
#[test]
fn vm_multi_frame_return_with_records() {
let src = "type pt{x:n;y:n} mk a:n b:n>pt;pt x:a y:b\nwrap x:n>pt;y=+x 1;mk x y\nf>n;p=wrap 10;+p.x p.y";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(21.0));
}
#[test]
fn vm_deeply_nested_calls() {
let src = "d x:n>n;*x 2\nc x:n>n;d +x 1\nb x:n>n;c +x 1\na x:n>n;b +x 1";
let result = vm_run(src, Some("a"), vec![Value::Number(1.0)]);
assert_eq!(result, Value::Number(8.0));
}
#[test]
fn vm_arena_full_recwith_multiple_updates() {
let src = "type pt{x:n;y:n} f>n;r=pt x:0 y:0;i=0;wh <i 3000{j=+i 1;r=r with x:i y:j;i=j};+r.x r.y";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(5999.0));
}
#[test]
fn vm_arena_full_large_record() {
let src = "type big{a:n;b:n;c:n;d:n;e:n} f>n;i=0;r=big a:0 b:0 c:0 d:0 e:0;wh <i 1500{b=+i 1;c=+i 2;d=+i 3;e=+i 4;r=big a:i b:b c:c d:d e:e;i=b};r.a";
let result = vm_run(src, Some("f"), vec![]);
assert_eq!(result, Value::Number(1499.0));
}
}