use crate::errors::QalaError;
use crate::opcode::STDLIB_FN_BASE;
use crate::value::Value;
use crate::vm::{HeapObject, Vm};
#[allow(dead_code)]
const VARIANT_ID_OK: u16 = 0;
pub fn dispatch(vm: &mut Vm, fn_id: u16, args: &[Value]) -> Result<Value, QalaError> {
if fn_id < STDLIB_FN_BASE {
return Err(vm.runtime_err(&format!("internal error: fn-id {fn_id} is not a stdlib id")));
}
match fn_id {
40000 => print(vm, args),
40001 => println(vm, args),
40002 => sqrt(vm, args),
40003 => abs(vm, args),
40004 => assert(vm, args),
40005 => len(vm, args),
40006 => push(vm, args),
40007 => pop(vm, args),
40008 => type_of(vm, args),
40009 => open(vm, args),
40010 => close(vm, args),
40011 => map(vm, args),
40012 => filter(vm, args),
40013 => reduce(vm, args),
40014 => read_all(vm, args),
other => Err(vm.runtime_err(&format!("unknown stdlib function {other}"))),
}
}
fn one_arg(vm: &Vm, args: &[Value], name: &str) -> Result<Value, QalaError> {
match args {
[a] => Ok(*a),
_ => Err(vm.runtime_err(&format!("{name} expects 1 argument, got {}", args.len()))),
}
}
fn alloc_int(vm: &mut Vm, n: i64) -> Result<Value, QalaError> {
let slot = vm
.heap
.alloc(HeapObject::Int(n))
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn alloc_str(vm: &mut Vm, s: String) -> Result<Value, QalaError> {
let slot = vm
.heap
.alloc(HeapObject::Str(s))
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn alloc_array(vm: &mut Vm, items: Vec<Value>) -> Result<Value, QalaError> {
let slot = vm
.heap
.alloc(HeapObject::Array(items))
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn make_option(vm: &mut Vm, payload: Option<Value>) -> Result<Value, QalaError> {
let object = match payload {
Some(v) => HeapObject::EnumVariant {
type_name: "Option".to_string(),
variant: "Some".to_string(),
payload: vec![v],
},
None => HeapObject::EnumVariant {
type_name: "Option".to_string(),
variant: "None".to_string(),
payload: Vec::new(),
},
};
let slot = vm
.heap
.alloc(object)
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn make_ok(vm: &mut Vm, payload: Value) -> Result<Value, QalaError> {
let slot = vm
.heap
.alloc(HeapObject::EnumVariant {
type_name: "Result".to_string(),
variant: "Ok".to_string(),
payload: vec![payload],
})
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn print(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "print")?;
let text = vm.value_to_string(arg);
vm.console.push(text);
Ok(Value::void())
}
fn println(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "println")?;
let mut text = vm.value_to_string(arg);
text.push('\n');
vm.console.push(text);
Ok(Value::void())
}
fn sqrt(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "sqrt")?;
let x = arg
.as_f64()
.ok_or_else(|| vm.runtime_err("sqrt: expected a float"))?;
Ok(Value::from_f64(x.sqrt()))
}
fn abs(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "abs")?;
if let Some(x) = arg.as_f64() {
return Ok(Value::from_f64(x.abs()));
}
let int = arg
.as_pointer()
.and_then(|slot| match vm.heap.get(slot) {
Some(HeapObject::Int(n)) => Some(*n),
_ => None,
})
.ok_or_else(|| vm.runtime_err("abs: expected an integer or a float"))?;
let result = int
.checked_abs()
.ok_or_else(|| vm.runtime_err("abs: integer overflow"))?;
alloc_int(vm, result)
}
fn assert(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "assert")?;
let condition = arg
.as_bool()
.ok_or_else(|| vm.runtime_err("assert: expected a boolean"))?;
if condition {
Ok(Value::void())
} else {
Err(vm.runtime_err("assertion failed"))
}
}
fn len(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "len")?;
let slot = arg
.as_pointer()
.ok_or_else(|| vm.runtime_err("len: expected an array or string"))?;
let length = match vm.heap.get(slot) {
Some(HeapObject::Array(items)) | Some(HeapObject::Tuple(items)) => items.len(),
Some(HeapObject::Str(s)) => s.chars().count(),
_ => return Err(vm.runtime_err("len: expected an array or string")),
};
alloc_int(vm, length as i64)
}
fn push(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let (array, value) = match args {
[a, v] => (*a, *v),
_ => {
return Err(vm.runtime_err(&format!("push expects 2 arguments, got {}", args.len())));
}
};
let slot = array
.as_pointer()
.ok_or_else(|| vm.runtime_err("push: expected an array"))?;
match vm.heap.get_mut(slot) {
Some(HeapObject::Array(items)) => {
items.push(value);
Ok(Value::void())
}
_ => Err(vm.runtime_err("push: expected an array")),
}
}
fn pop(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "pop")?;
let slot = arg
.as_pointer()
.ok_or_else(|| vm.runtime_err("pop: expected an array"))?;
let removed = match vm.heap.get_mut(slot) {
Some(HeapObject::Array(items)) => items.pop(),
_ => return Err(vm.runtime_err("pop: expected an array")),
};
make_option(vm, removed)
}
fn type_of(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "type_of")?;
let name = vm.runtime_type_name(arg);
alloc_str(vm, name)
}
fn open(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "open")?;
let path_slot = arg
.as_pointer()
.ok_or_else(|| vm.runtime_err("open: expected a string path"))?;
let path = match vm.heap.get(path_slot) {
Some(HeapObject::Str(s)) => s.clone(),
_ => return Err(vm.runtime_err("open: expected a string path")),
};
let slot = vm
.heap
.alloc(HeapObject::FileHandle {
path,
content: String::new(),
closed: false,
})
.ok_or_else(|| vm.runtime_err("heap exhausted"))?;
Ok(Value::pointer(slot))
}
fn close(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "close")?;
let slot = arg
.as_pointer()
.ok_or_else(|| vm.runtime_err("close: expected a file handle"))?;
match vm.heap.get_mut(slot) {
Some(HeapObject::FileHandle { closed, .. }) => {
*closed = true;
Ok(Value::void())
}
_ => Err(vm.runtime_err("close: expected a file handle")),
}
}
fn map(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let (array, callback) = match args {
[a, c] => (*a, *c),
_ => {
return Err(vm.runtime_err(&format!("map expects 2 arguments, got {}", args.len())));
}
};
let elements = heap_array_elements(vm, array, "map")?;
let mut results: Vec<Value> = Vec::with_capacity(elements.len());
for element in elements {
let mapped = vm.call_function_value(callback, &[element])?;
results.push(mapped);
}
alloc_array(vm, results)
}
fn filter(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let (array, callback) = match args {
[a, c] => (*a, *c),
_ => {
return Err(vm.runtime_err(&format!("filter expects 2 arguments, got {}", args.len())));
}
};
let elements = heap_array_elements(vm, array, "filter")?;
let mut kept: Vec<Value> = Vec::new();
for element in elements {
let verdict = vm.call_function_value(callback, &[element])?;
let keep = verdict
.as_bool()
.ok_or_else(|| vm.runtime_err("filter: the callback must return a boolean"))?;
if keep {
kept.push(element);
}
}
alloc_array(vm, kept)
}
fn reduce(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let (array, initial, callback) = match args {
[a, i, c] => (*a, *i, *c),
_ => {
return Err(vm.runtime_err(&format!("reduce expects 3 arguments, got {}", args.len())));
}
};
let elements = heap_array_elements(vm, array, "reduce")?;
let mut accumulator = initial;
for element in elements {
accumulator = vm.call_function_value(callback, &[accumulator, element])?;
}
Ok(accumulator)
}
fn read_all(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
let arg = one_arg(vm, args, "read_all")?;
let slot = arg
.as_pointer()
.ok_or_else(|| vm.runtime_err("read_all: expected a file handle"))?;
let content = match vm.heap.get(slot) {
Some(HeapObject::FileHandle { content, .. }) => content.clone(),
_ => return Err(vm.runtime_err("read_all: expected a file handle")),
};
let payload = alloc_str(vm, content)?;
make_ok(vm, payload)
}
fn heap_array_elements(vm: &Vm, value: Value, name: &str) -> Result<Vec<Value>, QalaError> {
let slot = value
.as_pointer()
.ok_or_else(|| vm.runtime_err(&format!("{name}: expected an array")))?;
match vm.heap.get(slot) {
Some(HeapObject::Array(items)) => Ok(items.clone()),
_ => Err(vm.runtime_err(&format!("{name}: expected an array"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::{Chunk, Program};
use crate::codegen::compile_program;
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::typechecker::check_program;
fn bare_vm() -> Vm {
let mut chunk = Chunk::new();
chunk.write_op(crate::opcode::Opcode::Return, 1);
let mut program = Program::new();
program.chunks.push(chunk);
program.fn_names.push("main".to_string());
program.main_index = 0;
Vm::new(program, String::new())
}
fn compile(src: &str) -> Program {
let tokens = Lexer::tokenize(src).expect("lex");
let ast = Parser::parse(&tokens).expect("parse");
let (typed, errors, _) = check_program(&ast, src);
assert!(errors.is_empty(), "typecheck errors: {errors:?}");
compile_program(&typed, src).expect("codegen")
}
fn fn_id(program: &Program, name: &str) -> u16 {
program
.fn_names
.iter()
.position(|n| n == name)
.unwrap_or_else(|| panic!("no function named {name}")) as u16
}
fn int(vm: &mut Vm, n: i64) -> Value {
alloc_int(vm, n).expect("alloc int")
}
fn string(vm: &mut Vm, s: &str) -> Value {
alloc_str(vm, s.to_string()).expect("alloc str")
}
fn result_i64(vm: &Vm, value: Value) -> i64 {
let slot = value.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Int(n)) => *n,
_ => panic!("the value does not point at a heap Int"),
}
}
fn result_str(vm: &Vm, value: Value) -> String {
let slot = value.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Str(s)) => s.clone(),
_ => panic!("the value does not point at a heap Str"),
}
}
fn result_array(vm: &Vm, value: Value) -> Vec<Value> {
let slot = value.as_pointer().expect("a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::Array(items)) => items.clone(),
_ => panic!("the value does not point at a heap array"),
}
}
fn program_result(vm: &Vm) -> String {
vm.get_state()
.stack
.last()
.expect("a finished program left a result value")
.rendered
.clone()
}
#[test]
fn len_of_an_array_and_of_a_string() {
let mut vm = bare_vm();
let a = int(&mut vm, 10);
let b = int(&mut vm, 20);
let c = int(&mut vm, 30);
let array = alloc_array(&mut vm, vec![a, b, c]).expect("alloc array");
let array_len = len(&mut vm, &[array]).expect("len of an array");
assert_eq!(result_i64(&vm, array_len), 3, "an array of 3 has len 3");
let s = string(&mut vm, "hello");
let str_len = len(&mut vm, &[s]).expect("len of a string");
assert_eq!(result_i64(&vm, str_len), 5, "\"hello\" has len 5");
}
#[test]
fn push_appends_and_pop_returns_some_then_none() {
let mut vm = bare_vm();
let array = alloc_array(&mut vm, Vec::new()).expect("alloc array");
let one = int(&mut vm, 1);
let two = int(&mut vm, 2);
push(&mut vm, &[array, one]).expect("push 1");
push(&mut vm, &[array, two]).expect("push 2");
assert_eq!(result_array(&vm, array).len(), 2, "two pushes -> length 2");
let popped = pop(&mut vm, &[array]).expect("pop");
let popped_slot = popped.as_pointer().expect("Some is a heap pointer");
match vm.heap.get(popped_slot) {
Some(HeapObject::EnumVariant {
variant, payload, ..
}) => {
assert_eq!(variant, "Some", "a non-empty pop returns Some");
assert_eq!(result_i64(&vm, payload[0]), 2, "the last element popped");
}
_ => panic!("pop must return an Option enum variant"),
}
pop(&mut vm, &[array]).expect("pop the last element");
let none = pop(&mut vm, &[array]).expect("pop an empty array");
let none_slot = none.as_pointer().expect("None is a heap pointer");
match vm.heap.get(none_slot) {
Some(HeapObject::EnumVariant { variant, .. }) => {
assert_eq!(variant, "None", "an empty pop returns None");
}
_ => panic!("pop of an empty array must return None"),
}
}
#[test]
fn sqrt_of_four_is_two() {
let mut vm = bare_vm();
let result = sqrt(&mut vm, &[Value::from_f64(4.0)]).expect("sqrt");
assert_eq!(result.as_f64(), Some(2.0), "sqrt(4.0) == 2.0");
}
#[test]
fn abs_of_a_negative_int_and_a_negative_float() {
let mut vm = bare_vm();
let neg_three = int(&mut vm, -3);
let abs_int = abs(&mut vm, &[neg_three]).expect("abs of an int");
assert_eq!(result_i64(&vm, abs_int), 3, "abs(-3) == 3");
let abs_float = abs(&mut vm, &[Value::from_f64(-1.5)]).expect("abs of a float");
assert_eq!(abs_float.as_f64(), Some(1.5), "abs(-1.5) == 1.5");
}
#[test]
fn assert_true_is_a_no_op_and_assert_false_is_a_runtime_error() {
let mut vm = bare_vm();
let ok = assert(&mut vm, &[Value::bool(true)]).expect("assert(true)");
assert!(ok.as_void(), "assert(true) returns void");
match assert(&mut vm, &[Value::bool(false)]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("assertion failed"), "got: {message}");
}
Err(other) => panic!("expected an assertion-failed Runtime error, got {other:?}"),
Ok(_) => panic!("assert(false) must error"),
}
}
#[test]
fn type_of_returns_the_runtime_type_name_for_each_kind() {
let mut vm = bare_vm();
let i64_value = int(&mut vm, 7);
let str_value = string(&mut vm, "hi");
let e0 = int(&mut vm, 1);
let e1 = int(&mut vm, 2);
let i64_array = alloc_array(&mut vm, vec![e0, e1]).expect("alloc array");
let shape_struct = vm
.heap
.alloc(HeapObject::Struct {
type_name: "Shape".to_string(),
fields: Vec::new(),
})
.expect("alloc struct");
let shape_variant = vm
.heap
.alloc(HeapObject::EnumVariant {
type_name: "Shape".to_string(),
variant: "Circle".to_string(),
payload: Vec::new(),
})
.expect("alloc variant");
let cases: Vec<(Value, &str)> = vec![
(i64_value, "i64"),
(Value::from_f64(1.5), "f64"),
(Value::bool(true), "bool"),
(str_value, "str"),
(Value::byte(65), "byte"),
(Value::void(), "void"),
(i64_array, "[i64]"),
(Value::pointer(shape_struct), "Shape"),
(Value::pointer(shape_variant), "Shape::Circle"),
];
for (value, expected) in cases {
let result = type_of(&mut vm, &[value]).expect("type_of");
assert_eq!(result_str(&vm, result), expected, "type_of mismatch");
}
}
#[test]
fn print_and_println_append_to_the_console() {
let mut vm = bare_vm();
let line = string(&mut vm, "first line");
println(&mut vm, &[line]).expect("println");
assert_eq!(vm.console, vec!["first line\n".to_string()]);
let a = string(&mut vm, "a");
let b = string(&mut vm, "b");
print(&mut vm, &[a]).expect("print a");
print(&mut vm, &[b]).expect("print b");
assert_eq!(
vm.console,
vec!["first line\n".to_string(), "a".to_string(), "b".to_string(),],
"println entries end with newline; print entries do not"
);
}
#[test]
fn println_adds_newline_print_does_not() {
let mut vm = bare_vm();
let a = string(&mut vm, "a");
let b = string(&mut vm, "b");
let c = string(&mut vm, "c");
println(&mut vm, &[a]).expect("println a");
println(&mut vm, &[b]).expect("println b");
print(&mut vm, &[c]).expect("print c");
let joined: String = vm.console.concat();
assert_eq!(
joined, "a\nb\nc",
"two println then print joined is a\\nb\\nc"
);
}
#[test]
fn open_returns_a_file_handle_and_close_marks_it_closed() {
let mut vm = bare_vm();
let path = string(&mut vm, "data.txt");
let handle = open(&mut vm, &[path]).expect("open");
let slot = handle.as_pointer().expect("open returns a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::FileHandle { path, closed, .. }) => {
assert_eq!(path, "data.txt", "the handle carries the path");
assert!(!closed, "a freshly opened handle is open");
}
_ => panic!("open must return a FileHandle"),
}
let void = close(&mut vm, &[handle]).expect("close");
assert!(void.as_void(), "close returns void");
match vm.heap.get(slot) {
Some(HeapObject::FileHandle { closed, .. }) => {
assert!(closed, "close marks the handle closed");
}
_ => panic!("the handle is still a FileHandle after close"),
}
}
#[test]
fn read_all_returns_ok_with_the_handle_content() {
let mut vm = bare_vm();
let path = string(&mut vm, "data.txt");
let handle = open(&mut vm, &[path]).expect("open");
let result = read_all(&mut vm, &[handle]).expect("read_all");
let slot = result.as_pointer().expect("Ok is a heap pointer");
match vm.heap.get(slot) {
Some(HeapObject::EnumVariant {
type_name,
variant,
payload,
}) => {
assert_eq!(type_name, "Result", "read_all returns a Result");
assert_eq!(variant, "Ok", "the mock read succeeds");
assert_eq!(
result_str(&vm, payload[0]),
"",
"the v1 mock content is empty"
);
}
_ => panic!("read_all must return a Result enum variant"),
}
}
#[test]
fn map_applies_a_user_callback_to_each_element() {
let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
fn main() is io {}\n";
let program = compile(src);
let double = fn_id(&program, "double");
let mut vm = Vm::new(program, src.to_string());
let e0 = int(&mut vm, 1);
let e1 = int(&mut vm, 2);
let e2 = int(&mut vm, 3);
let array = alloc_array(&mut vm, vec![e0, e1, e2]).expect("alloc array");
let mapped = map(&mut vm, &[array, Value::function(double)]).expect("map");
let items = result_array(&vm, mapped);
let values: Vec<i64> = items.iter().map(|v| result_i64(&vm, *v)).collect();
assert_eq!(values, vec![2, 4, 6], "map(double) doubles every element");
}
#[test]
fn filter_keeps_the_elements_the_callback_accepts() {
let src = "fn is_even(x: i64) -> bool is pure { return x % 2 == 0 }\n\
fn main() is io {}\n";
let program = compile(src);
let is_even = fn_id(&program, "is_even");
let mut vm = Vm::new(program, src.to_string());
let mut elements = Vec::new();
for n in 1..=6 {
elements.push(int(&mut vm, n));
}
let array = alloc_array(&mut vm, elements).expect("alloc array");
let filtered = filter(&mut vm, &[array, Value::function(is_even)]).expect("filter");
let items = result_array(&vm, filtered);
let values: Vec<i64> = items.iter().map(|v| result_i64(&vm, *v)).collect();
assert_eq!(values, vec![2, 4, 6], "filter(is_even) keeps the evens");
}
#[test]
fn reduce_folds_an_array_with_the_callback() {
let src = "fn add(a: i64, b: i64) -> i64 is pure { return a + b }\n\
fn main() is io {}\n";
let program = compile(src);
let add = fn_id(&program, "add");
let mut vm = Vm::new(program, src.to_string());
let mut elements = Vec::new();
for n in 1..=4 {
elements.push(int(&mut vm, n));
}
let array = alloc_array(&mut vm, elements).expect("alloc array");
let initial = int(&mut vm, 0);
let total = reduce(&mut vm, &[array, initial, Value::function(add)]).expect("reduce");
assert_eq!(result_i64(&vm, total), 10, "reduce(+) of 1..=4 is 10");
}
#[test]
fn dispatch_routes_each_fn_id_and_rejects_an_unknown_one() {
let mut vm = bare_vm();
let via_dispatch = dispatch(&mut vm, 40002, &[Value::from_f64(9.0)]).expect("sqrt");
assert_eq!(via_dispatch.as_f64(), Some(3.0), "dispatch(40002) is sqrt");
match dispatch(&mut vm, 49999, &[]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(
message.contains("unknown stdlib function"),
"got: {message}"
);
}
Err(other) => panic!("expected an unknown-stdlib Runtime error, got {other:?}"),
Ok(_) => panic!("an unknown fn-id must error"),
}
match dispatch(&mut vm, 5, &[]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("not a stdlib id"), "got: {message}");
}
Err(other) => panic!("expected an invariant Runtime error, got {other:?}"),
Ok(_) => panic!("a user fn-id must not dispatch as stdlib"),
}
}
#[test]
fn a_wrong_argument_count_is_a_runtime_error_not_a_panic() {
let mut vm = bare_vm();
match sqrt(&mut vm, &[]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(message.contains("expects 1 argument"), "got: {message}");
}
Err(other) => panic!("expected an arity Runtime error, got {other:?}"),
Ok(_) => panic!("sqrt with no argument must error"),
}
match len(&mut vm, &[Value::bool(true)]) {
Err(QalaError::Runtime { message, .. }) => {
assert!(
message.contains("expected an array or string"),
"got: {message}"
);
}
Err(other) => panic!("expected a wrong-type Runtime error, got {other:?}"),
Ok(_) => panic!("len of a bool must error"),
}
}
#[test]
fn map_reentrant_a_nested_map_inside_the_callback_works() {
let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
fn double_row(row: [i64]) -> [i64] is pure {\n \
return map(row, double)\n}\n\
fn main() -> i64 is pure {\n \
let grid = [[1, 2], [3, 4]]\n \
let doubled = map(grid, double_row)\n \
return doubled[1][1]\n}\n";
let program = compile(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("a nested-map program runs clean");
assert_eq!(
program_result(&vm),
"8",
"the nested map doubled [3,4] to [6,8]; [1][1] is 8"
);
}
#[test]
fn fibonacci_smoke_computes_the_correct_numeric_result() {
let src = "fn fib(n: i64) -> i64 is pure {\n \
if n <= 1 { return n }\n \
return fib(n - 1) + fib(n - 2)\n}\n\
fn main() -> i64 is pure {\n return fib(10)\n}\n";
let program = compile(src);
let mut vm = Vm::new(program, src.to_string());
vm.run().expect("the fibonacci program runs clean");
assert_eq!(program_result(&vm), "55", "fib(10) must be 55");
}
fn example_source(name: &str) -> String {
let path = format!(
"{}/../../playground/public/examples/{name}.qala",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
}
fn run_example(name: &str) -> Vm {
let src = example_source(name);
let program = compile(&src);
let mut vm = Vm::new(program, src);
vm.run()
.unwrap_or_else(|e| panic!("{name}.qala did not run to completion: {e:?}"));
vm
}
#[test]
fn six_examples_run_to_completion() {
let hello = run_example("hello");
assert!(
hello
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "hello, world!"),
"hello.qala console: {:?}",
hello.console,
);
let fibonacci = run_example("fibonacci");
assert!(
fibonacci
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "fib(0) = 0"),
"fibonacci.qala console: {:?}",
fibonacci.console,
);
assert!(
fibonacci
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "fib(10) = 55"),
"fibonacci.qala must print fib(10) = 55, console: {:?}",
fibonacci.console,
);
assert!(
fibonacci
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "fib(14) = 377"),
"fibonacci.qala must print fib(14) = 377, console: {:?}",
fibonacci.console,
);
let effects = run_example("effects");
assert!(
effects
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "7 squared: 49"),
"effects.qala console: {:?}",
effects.console,
);
let pattern_matching = run_example("pattern-matching");
assert!(
!pattern_matching.console.is_empty(),
"pattern-matching.qala must print something",
);
assert!(
pattern_matching
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "positive"),
"pattern-matching.qala classify(42) must print positive, console: {:?}",
pattern_matching.console,
);
assert!(
pattern_matching
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "negative"),
"pattern-matching.qala classify(-7) must print negative, console: {:?}",
pattern_matching.console,
);
assert!(
pattern_matching
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "zero"),
"pattern-matching.qala classify(0) must print zero, console: {:?}",
pattern_matching.console,
);
let pipeline = run_example("pipeline");
assert!(
pipeline.console.iter().any(|l| l.contains("= 22")),
"pipeline.qala must print the pipeline result 22, console: {:?}",
pipeline.console,
);
assert!(
pipeline
.console
.iter()
.any(|l| l.trim_end_matches('\n') == "20"),
"pipeline.qala filter+map must print the doubled even 20, console: {:?}",
pipeline.console,
);
let defer_demo = run_example("defer-demo");
assert!(
defer_demo.console.iter().any(|l| l.starts_with("got: ")),
"defer-demo.qala must print a got: line, console: {:?}",
defer_demo.console,
);
assert!(
defer_demo.leak_log.is_empty(),
"defer-demo.qala closes its handle via defer -- no leak, got: {:?}",
defer_demo.leak_log,
);
}
}