feat: add Test effect and native testing framework

- Add Test effect with operations: assert, assertEqual, assertNotEqual,
  assertTrue, assertFalse, fail
- Implement Test effect handlers in interpreter with TestResults tracking
- Add values_equal method for comparing Value types in tests
- Update lux test command to discover and run test_* functions
- Create example test files: test_math.lux, test_lists.lux
- Add TESTING_DESIGN.md documentation
- Fix AST mismatches in C backend and compiler.rs for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 01:20:30 -05:00
parent 9a42a7f540
commit ee9acce6ec
10 changed files with 866 additions and 107 deletions

View File

@@ -5,7 +5,7 @@
//! no garbage collector needed with Perceus-style reference counting.
use crate::ast::*;
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use std::fmt::Write;
/// C code generation errors
@@ -81,8 +81,8 @@ impl CBackend {
Declaration::Function(f) => {
self.emit_function(f)?;
}
Declaration::Let { name, value, .. } => {
self.emit_global_let(name, value)?;
Declaration::Let(let_decl) => {
self.emit_global_let(&let_decl.name, &let_decl.value)?;
}
_ => {}
}
@@ -170,18 +170,18 @@ impl CBackend {
self.types_emitted.insert(name.clone());
match &type_decl.definition {
TypeDefinition::Record(fields) => {
TypeDef::Record(fields) => {
self.writeln(&format!("typedef struct {} {{", name));
self.indent += 1;
for field in fields {
let c_type = self.type_to_c(&field.field_type)?;
let c_type = self.type_to_c(&field.typ)?;
self.writeln(&format!("{} {};", c_type, field.name.name));
}
self.indent -= 1;
self.writeln(&format!("}} {};", name));
self.writeln("");
}
TypeDefinition::Adt(variants) => {
TypeDef::Enum(variants) => {
// Emit tag enum
self.writeln(&format!("typedef enum {}_Tag {{", name));
self.indent += 1;
@@ -195,12 +195,24 @@ impl CBackend {
// Emit variant structs
for variant in variants {
if !variant.fields.is_empty() {
let has_fields = !matches!(&variant.fields, VariantFields::Unit);
if has_fields {
self.writeln(&format!("typedef struct {}_{}_Data {{", name, variant.name.name));
self.indent += 1;
for (i, field) in variant.fields.iter().enumerate() {
let c_type = self.type_to_c(field)?;
self.writeln(&format!("{} field{};", c_type, i));
match &variant.fields {
VariantFields::Tuple(fields) => {
for (i, field) in fields.iter().enumerate() {
let c_type = self.type_to_c(field)?;
self.writeln(&format!("{} field{};", c_type, i));
}
}
VariantFields::Record(fields) => {
for field in fields {
let c_type = self.type_to_c(&field.typ)?;
self.writeln(&format!("{} {};", c_type, field.name.name));
}
}
VariantFields::Unit => {}
}
self.indent -= 1;
self.writeln(&format!("}} {}_{}_Data;", name, variant.name.name));
@@ -215,7 +227,8 @@ impl CBackend {
self.writeln("union {");
self.indent += 1;
for variant in variants {
if !variant.fields.is_empty() {
let has_fields = !matches!(&variant.fields, VariantFields::Unit);
if has_fields {
self.writeln(&format!("{}_{}_Data {};", name, variant.name.name, variant.name.name.to_lowercase()));
}
}
@@ -225,7 +238,7 @@ impl CBackend {
self.writeln(&format!("}} {};", name));
self.writeln("");
}
TypeDefinition::Alias(_) => {
TypeDef::Alias(_) => {
// Type aliases are handled during type resolution
}
}
@@ -270,7 +283,7 @@ impl CBackend {
}
let param_strs: Result<Vec<_>, _> = params.iter().map(|p| {
let c_type = self.type_expr_to_c(&p.param_type)?;
let c_type = self.type_expr_to_c(&p.typ)?;
Ok(format!("{} {}", c_type, p.name.name))
}).collect();
@@ -301,13 +314,10 @@ impl CBackend {
BinaryOp::Ge => ">=",
BinaryOp::And => "&&",
BinaryOp::Or => "||",
BinaryOp::Concat => {
return Ok(format!("lux_string_concat({}, {})", l, r));
BinaryOp::Pipe => {
// Pipe operator - for now, just call the right side with left as argument
return Ok(format!("{}({})", r, l));
}
_ => return Err(CGenError {
message: format!("Unsupported binary operator: {:?}", op),
span: None,
}),
};
Ok(format!("({} {} {})", l, op_str, r))
@@ -404,19 +414,8 @@ impl CBackend {
Ok(format!("{}.{}", obj, field.name))
}
Expr::Match { expr, arms, .. } => {
self.emit_match(expr, arms)
}
Expr::Constructor { name, args, .. } => {
// ADT constructor - need to determine the type
// For now, assume it's a simple constructor call
if args.is_empty() {
Ok(format!("/* {} */ 0", name.name))
} else {
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
Ok(format!("/* {}({}) */", name.name, arg_strs?.join(", ")))
}
Expr::Match { scrutinee, arms, .. } => {
self.emit_match(scrutinee, arms)
}
_ => Err(CGenError {
@@ -462,9 +461,9 @@ impl CBackend {
fn pattern_to_condition(&self, pattern: &Pattern, scrutinee: &str) -> Result<String, CGenError> {
match pattern {
Pattern::Wildcard(_) => Ok("1".to_string()),
Pattern::Var(ident, _) => Ok(format!("(1) /* bind {} = {} */", ident.name, scrutinee)),
Pattern::Literal(lit, _) => {
let lit_val = self.emit_literal_value(lit)?;
Pattern::Var(ident) => Ok(format!("(1) /* bind {} = {} */", ident.name, scrutinee)),
Pattern::Literal(lit) => {
let lit_val = self.emit_literal_value(&lit.kind)?;
Ok(format!("{} == {}", scrutinee, lit_val))
}
Pattern::Constructor { name, .. } => {
@@ -504,7 +503,7 @@ impl CBackend {
// Check for top-level run expressions
let has_run = program.declarations.iter().any(|d| {
matches!(d, Declaration::Let { value, .. } if matches!(value.as_ref(), Expr::Run { .. }))
matches!(d, Declaration::Let(let_decl) if matches!(&let_decl.value, Expr::Run { .. }))
});
if has_main || has_run {
@@ -513,9 +512,9 @@ impl CBackend {
// Execute top-level let bindings with run expressions
for decl in &program.declarations {
if let Declaration::Let { name, value, .. } = decl {
if matches!(value.as_ref(), Expr::Run { .. }) {
if let Expr::Run { expr, .. } = value.as_ref() {
if let Declaration::Let(let_decl) = decl {
if matches!(&let_decl.value, Expr::Run { .. }) {
if let Expr::Run { expr, .. } = &let_decl.value {
if let Expr::Call { func, .. } = expr.as_ref() {
if let Expr::Var(fn_name) = func.as_ref() {
self.writeln(&format!("{}();", fn_name.name));
@@ -549,18 +548,23 @@ impl CBackend {
other => Ok(other.to_string()),
}
}
TypeExpr::Generic { name, .. } => {
TypeExpr::App(base, _) => {
// For now, use void* for generic types
match name.name.as_str() {
"List" => Ok("void*".to_string()),
"Option" => Ok("void*".to_string()),
_ => Ok("void*".to_string()),
if let TypeExpr::Named(name) = base.as_ref() {
match name.name.as_str() {
"List" => Ok("void*".to_string()),
"Option" => Ok("void*".to_string()),
_ => Ok("void*".to_string()),
}
} else {
Ok("void*".to_string())
}
}
TypeExpr::Unit => Ok("void".to_string()),
TypeExpr::Versioned { base, .. } => self.type_expr_to_c(base),
TypeExpr::Function { .. } => Ok("void*".to_string()),
TypeExpr::Tuple(_) => Ok("void*".to_string()),
TypeExpr::Record(_) => Ok("void*".to_string()),
_ => Ok("void*".to_string()),
}
}