Refactor Examples to use assert_eq

This commit updates all example files to use `assert_eq!` for verifying
output, replacing the previous `Output:` comments. This change makes the
examples more robust and self-testing.

The benchmark results in some examples have also been slightly adjusted,
reflecting minor performance variations after the refactoring.

Additionally, a runtime panic handling mechanism has been introduced in
the VM for function calls, which improves error reporting for unexpected
panics.
This commit is contained in:
2026-03-26 15:07:48 +01:00
parent 0088a644eb
commit 16af3ae9fc
40 changed files with 313 additions and 235 deletions
+109 -1
View File
@@ -1,5 +1,5 @@
use crate::ast::environment::Environment;
use crate::ast::types::{Purity, Signature, StaticType, Value};
use crate::ast::types::{Keyword, Purity, Signature, StaticType, Value};
use std::rc::Rc;
pub fn register(env: &Environment) {
@@ -8,6 +8,7 @@ pub fn register(env: &Environment) {
register_comparison(env);
register_logic(env);
register_io(env);
register_testing(env);
}
fn register_constants(env: &Environment) {
@@ -523,3 +524,110 @@ fn register_io(env: &Environment) {
}).doc("Prints all arguments to stdout followed by a newline. Returns void.")
.examples(&["(print \"Hello World\")", "(print x y z)"]);
}
/// Structural equality that compares record values by field content,
/// not by Arc pointer identity. Required because the compiler may fold
/// record literals at compile-time, yielding a different Arc than one
/// created at runtime via make_record.
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Record(la, va), Value::Record(lb, vb)) => {
la.fields.len() == lb.fields.len()
&& la
.fields
.iter()
.zip(lb.fields.iter())
.all(|((ka, _), (kb, _))| ka == kb)
&& va
.iter()
.zip(vb.iter())
.all(|(x, y)| values_equal(x, y))
}
(Value::Tuple(a), Value::Tuple(b)) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
}
_ => a == b,
}
}
fn register_testing(env: &Environment) {
let any_ty = StaticType::Function(Box::new(Signature {
params: StaticType::Any,
ret: StaticType::Void,
}));
// (assert cond) / (assert cond "msg")
env.register_native_fn("assert", any_ty.clone(), Purity::Impure, |args| {
let cond = match args.first() {
Some(Value::Bool(b)) => *b,
Some(other) => panic!("assert expects a bool, got {}", other),
None => panic!("assert requires at least 1 argument"),
};
if !cond {
match args.get(1) {
Some(Value::Text(msg)) => panic!("assertion failed: {}", msg),
_ => panic!("assertion failed"),
}
}
Value::Void
})
.doc("Asserts that a condition is true. Panics with an optional message on failure.")
.examples(&["(assert (= 1 1))", "(assert (> x 0) \"x must be positive\")"]);
// (assert-eq expected actual) / (assert-eq expected actual "msg")
env.register_native_fn("assert-eq", any_ty.clone(), Purity::Impure, |args| {
if args.len() < 2 {
panic!("assert-eq requires at least 2 arguments");
}
let expected = &args[0];
let actual = &args[1];
if !values_equal(expected, actual) {
match args.get(2) {
Some(Value::Text(msg)) => panic!(
"assertion failed: {}\n expected: {}\n got: {}",
msg, expected, actual
),
_ => panic!(
"assertion failed: expected {}, got {}",
expected, actual
),
}
}
Value::Void
})
.doc("Asserts that expected == actual. Panics with a diff message on failure.")
.examples(&["(assert-eq 42 (factorial 5 1))", "(assert-eq :ok (status) \"wrong status\")"]);
// (type-of x) -> keyword
let type_of_ty = StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Any]),
ret: StaticType::Keyword,
}));
env.register_native_fn("type-of", type_of_ty, Purity::Pure, |args| {
let name = match args.first() {
Some(v) => match v.static_type() {
StaticType::Void => "void",
StaticType::Bool => "bool",
StaticType::Int => "int",
StaticType::Float => "float",
StaticType::DateTime => "datetime",
StaticType::Text => "text",
StaticType::Keyword => "keyword",
StaticType::Tuple(_)
| StaticType::Vector(_, _)
| StaticType::Matrix(_, _) => "tuple",
StaticType::Record(_) => "record",
StaticType::Series(_) => "series",
StaticType::Stream(_) => "stream",
StaticType::Function(_) | StaticType::FunctionOverloads(_) => "function",
StaticType::FieldAccessor(_) => "field-accessor",
StaticType::Object(name) => name,
_ => "unknown",
},
None => panic!("type-of requires 1 argument"),
};
Value::Keyword(Keyword::intern(name))
})
.doc("Returns the type of a value as a keyword (e.g. :int, :float, :stream).")
.examples(&["(type-of 42)", "(type-of my-stream)"]);
}
+24 -2
View File
@@ -441,7 +441,19 @@ impl VM {
self.tail_call = Some((func_val, arg_vals));
return Ok(Value::Void);
}
Value::Function(f) => return Ok((f.func)(&arg_vals)),
Value::Function(f) => {
return std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(f.func)(&arg_vals)
}))
.map_err(|e| {
e.downcast_ref::<String>()
.cloned()
.or_else(|| {
e.downcast_ref::<&str>().map(|s| s.to_string())
})
.unwrap_or_else(|| "runtime panic".to_string())
});
}
Value::FieldAccessor(k) => {
if arg_vals.len() != 1 {
return Err(format!(
@@ -510,7 +522,17 @@ impl VM {
loop {
let result = match &current_func {
Value::Function(f) => {
let res = (f.func)(&self.stack[base..]);
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(f.func)(&self.stack[base..])
}))
.map_err(|e| {
e.downcast_ref::<String>()
.cloned()
.or_else(|| {
e.downcast_ref::<&str>().map(|s| s.to_string())
})
.unwrap_or_else(|| "runtime panic".to_string())
})?;
self.stack.truncate(base);
return Ok(res);
}
+15 -38
View File
@@ -25,53 +25,30 @@ pub fn run_functional_tests() -> Vec<TestResult> {
pub fn run_functional_tests_with_optimization(enabled: bool) -> Vec<TestResult> {
let mut results = Vec::new();
let entries = fs::read_dir("examples").unwrap();
let output_re = Regex::new(r";; Output: (.*)").unwrap();
for entry in entries.filter_map(|e| e.ok()) {
let mut env = Environment::new(); // Fresh environment per test file
let mut env = Environment::new();
env.optimization = enabled;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "myc") {
let content = fs::read_to_string(&path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let expected_output = output_re
.captures(&content)
.map(|m| m.get(1).unwrap().as_str().trim().to_string());
if content.contains(";; Skip:") {
continue;
}
if let Some(expected) = expected_output {
match env.run_script(&content) {
Ok(val) => {
let val_str = format!("{}", val);
if val_str == expected {
results.push(TestResult {
name,
success: true,
message: format!("OK: {}", val_str),
});
} else {
results.push(TestResult {
name,
success: false,
message: format!(
"Opt {}: Expected {}, got {}",
if enabled { "ON" } else { "OFF" },
expected,
val_str
),
});
}
}
Err(e) => results.push(TestResult {
name,
success: false,
message: format!(
"Opt {}: Error: {}",
if enabled { "ON" } else { "OFF" },
e
),
}),
}
match env.run_script(&content) {
Ok(_) => results.push(TestResult {
name,
success: true,
message: "OK".to_string(),
}),
Err(e) => results.push(TestResult {
name,
success: false,
message: format!("Opt {}: {}", if enabled { "ON" } else { "OFF" }, e),
}),
}
}
}