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:
307
docs/TESTING_DESIGN.md
Normal file
307
docs/TESTING_DESIGN.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# Lux Testing Framework Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Lux needs a native testing framework that leverages its effect system. The key insight: **testing is an effect** - assertions, test discovery, and reporting are all side effects.
|
||||||
|
|
||||||
|
## Survey of Testing Approaches
|
||||||
|
|
||||||
|
### 1. Rust Style (`#[test]` attributes)
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_addition() {
|
||||||
|
assert_eq!(2 + 2, 4);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Pros:** Familiar, built-in, zero runtime overhead
|
||||||
|
**Cons:** Requires attribute/macro system
|
||||||
|
|
||||||
|
### 2. Elm Style (describe/test/expect)
|
||||||
|
```elm
|
||||||
|
suite = describe "Math"
|
||||||
|
[ test "addition" <| \_ -> Expect.equal 4 (2 + 2)
|
||||||
|
, test "subtraction" <| \_ -> Expect.equal 0 (2 - 2)
|
||||||
|
]
|
||||||
|
```
|
||||||
|
**Pros:** Readable, hierarchical
|
||||||
|
**Cons:** Verbose, needs custom runner
|
||||||
|
|
||||||
|
### 3. Zig Style (inline test blocks)
|
||||||
|
```zig
|
||||||
|
test "addition" {
|
||||||
|
try std.testing.expect(2 + 2 == 4);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Pros:** Inline with code, comptime evaluated
|
||||||
|
**Cons:** Needs language support
|
||||||
|
|
||||||
|
### 4. Property-Based (QuickCheck/Hypothesis)
|
||||||
|
```haskell
|
||||||
|
prop_reverse :: [Int] -> Bool
|
||||||
|
prop_reverse xs = reverse (reverse xs) == xs
|
||||||
|
```
|
||||||
|
**Pros:** Finds edge cases automatically
|
||||||
|
**Cons:** Complex to implement
|
||||||
|
|
||||||
|
### 5. Expect Tests (OCaml/Jane Street)
|
||||||
|
```ocaml
|
||||||
|
let%expect_test "hello" =
|
||||||
|
print_endline "Hello";
|
||||||
|
[%expect {| Hello |}]
|
||||||
|
```
|
||||||
|
**Pros:** Self-updating, snapshot testing
|
||||||
|
**Cons:** Requires tooling
|
||||||
|
|
||||||
|
## Recommended: Effect-Based Testing
|
||||||
|
|
||||||
|
Since Lux has effects, testing should be an effect. This is idiomatic and enables powerful features.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Built-in Test effect
|
||||||
|
effect Test {
|
||||||
|
fn assert(condition: Bool, message: String): Unit
|
||||||
|
fn assertEqual<T>(expected: T, actual: T): Unit
|
||||||
|
fn assertNotEqual<T>(a: T, b: T): Unit
|
||||||
|
fn fail(message: String): Unit
|
||||||
|
fn skip(reason: String): Unit
|
||||||
|
fn todo(description: String): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test declaration syntax (new keyword)
|
||||||
|
test "addition works" {
|
||||||
|
Test.assertEqual(4, 2 + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "string concatenation" {
|
||||||
|
let result = "hello" + " " + "world"
|
||||||
|
Test.assertEqual("hello world", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grouped tests
|
||||||
|
describe "List operations" {
|
||||||
|
test "map doubles values" {
|
||||||
|
let input = [1, 2, 3]
|
||||||
|
let output = List.map(input, fn(x) => x * 2)
|
||||||
|
Test.assertEqual([2, 4, 6], output)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "filter keeps matches" {
|
||||||
|
let input = [1, 2, 3, 4, 5]
|
||||||
|
let output = List.filter(input, fn(x) => x > 2)
|
||||||
|
Test.assertEqual([3, 4, 5], output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Design?
|
||||||
|
|
||||||
|
1. **Idiomatic**: Testing is just another effect
|
||||||
|
2. **Mockable**: Can provide different Test handlers for different scenarios
|
||||||
|
3. **Parallel-safe**: Effect isolation prevents test interference
|
||||||
|
4. **Extensible**: Users can define custom assertion effects
|
||||||
|
5. **Property testing**: Can add `Test.forAll` for property-based testing
|
||||||
|
|
||||||
|
### Test Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests in a file
|
||||||
|
lux test examples/math_test.lux
|
||||||
|
|
||||||
|
# Run all tests in directory
|
||||||
|
lux test tests/
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
lux test tests/math.lux --filter "addition"
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
lux test --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative: Annotation-Based (Simpler)
|
||||||
|
|
||||||
|
If we want simpler implementation:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Functions prefixed with test_ are tests
|
||||||
|
fn test_addition(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(4, 2 + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_strings(): Unit with {Test} = {
|
||||||
|
Test.assertEqual("hello", "hel" + "lo")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
```bash
|
||||||
|
lux test file.lux # Finds all test_* functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chosen Approach: Hybrid
|
||||||
|
|
||||||
|
Combine both for flexibility:
|
||||||
|
|
||||||
|
### Phase 1: Function-Based (Simple, Immediate)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Any function starting with test_ is a test
|
||||||
|
fn test_addition(): Unit with {Test} =
|
||||||
|
Test.assertEqual(4, 2 + 2)
|
||||||
|
|
||||||
|
fn test_factorial(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(1, factorial(0))
|
||||||
|
Test.assertEqual(1, factorial(1))
|
||||||
|
Test.assertEqual(120, factorial(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for multiple assertions
|
||||||
|
fn test_list_operations(): Unit with {Test} = {
|
||||||
|
// Each assertion is independent
|
||||||
|
Test.assertEqual([2, 4], List.map([1, 2], fn(x) => x * 2))
|
||||||
|
Test.assertEqual([2, 3], List.filter([1, 2, 3], fn(x) => x > 1))
|
||||||
|
Test.assertEqual(6, List.fold([1, 2, 3], 0, fn(a, b) => a + b))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Block Syntax (Later)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
test "descriptive name" {
|
||||||
|
Test.assertEqual(4, 2 + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "module name" {
|
||||||
|
test "feature 1" { ... }
|
||||||
|
test "feature 2" { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Step 1: Test Effect
|
||||||
|
|
||||||
|
Add built-in `Test` effect to the type system:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// In types.rs, add Test effect
|
||||||
|
EffectDef {
|
||||||
|
name: "Test".to_string(),
|
||||||
|
operations: vec![
|
||||||
|
EffectOpDef { name: "assert", params: vec![("cond", Bool), ("msg", String)], return_type: Unit },
|
||||||
|
EffectOpDef { name: "assertEqual", params: vec![("expected", T), ("actual", T)], return_type: Unit },
|
||||||
|
EffectOpDef { name: "fail", params: vec![("msg", String)], return_type: Unit },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Test Handler (Interpreter)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// In interpreter.rs
|
||||||
|
fn handle_test_effect(&mut self, op: &str, args: &[Value]) -> Result<Value, RuntimeError> {
|
||||||
|
match op {
|
||||||
|
"assert" => {
|
||||||
|
let cond = args[0].as_bool()?;
|
||||||
|
let msg = args[1].as_string()?;
|
||||||
|
if !cond {
|
||||||
|
self.test_failures.push(TestFailure { message: msg, .. });
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
"assertEqual" => {
|
||||||
|
let expected = &args[0];
|
||||||
|
let actual = &args[1];
|
||||||
|
if expected != actual {
|
||||||
|
self.test_failures.push(TestFailure {
|
||||||
|
message: format!("Expected {:?}, got {:?}", expected, actual),
|
||||||
|
..
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Test Discovery
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Find all test_* functions in a file
|
||||||
|
fn discover_tests(program: &Program) -> Vec<&FunctionDecl> {
|
||||||
|
program.declarations.iter().filter_map(|d| {
|
||||||
|
if let Declaration::Function(f) = d {
|
||||||
|
if f.name.name.starts_with("test_") {
|
||||||
|
return Some(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Test Runner CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lux test file.lux
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
Running tests in file.lux...
|
||||||
|
|
||||||
|
test_addition ... OK
|
||||||
|
test_factorial ... OK
|
||||||
|
test_strings ... FAIL
|
||||||
|
Expected: "hello world"
|
||||||
|
Actual: "helloworld"
|
||||||
|
at file.lux:15
|
||||||
|
|
||||||
|
Results: 2 passed, 1 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Property-Based Testing (Future)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Properties are functions that return Bool
|
||||||
|
fn prop_reverse_twice<T>(xs: List<T>): Bool =
|
||||||
|
List.reverse(List.reverse(xs)) == xs
|
||||||
|
|
||||||
|
fn prop_sort_idempotent(xs: List<Int>): Bool =
|
||||||
|
List.sort(List.sort(xs)) == List.sort(xs)
|
||||||
|
|
||||||
|
// Test runner generates random inputs
|
||||||
|
test "reverse is involution" {
|
||||||
|
Test.forAll(prop_reverse_twice)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expect Tests (Future)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
test "json output" {
|
||||||
|
let data = { name: "Alice", age: 30 }
|
||||||
|
let json = Json.stringify(data)
|
||||||
|
|
||||||
|
// This gets auto-updated by the test runner
|
||||||
|
Test.expect(json, |||
|
||||||
|
{"name":"Alice","age":30}
|
||||||
|
|||)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Phase | Feature | Effort |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| 1 | Test effect + test_* discovery | 1-2 days |
|
||||||
|
| 2 | Test runner CLI (`lux test`) | 1 day |
|
||||||
|
| 3 | Nice output formatting | 0.5 day |
|
||||||
|
| 4 | `test "name" {}` syntax | 1 day |
|
||||||
|
| 5 | `describe` blocks | 0.5 day |
|
||||||
|
| 6 | Property-based testing | 2-3 days |
|
||||||
|
| 7 | Expect/snapshot tests | 2 days |
|
||||||
|
|
||||||
|
**Recommendation:** Start with Phase 1-3 (function-based tests with CLI) for immediate value.
|
||||||
57
examples/test_lists.lux
Normal file
57
examples/test_lists.lux
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// List Operations Test Suite
|
||||||
|
// Run with: lux test examples/test_lists.lux
|
||||||
|
|
||||||
|
fn test_list_length(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(0, List.length([]))
|
||||||
|
Test.assertEqual(1, List.length([1]))
|
||||||
|
Test.assertEqual(3, List.length([1, 2, 3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_head(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(Some(1), List.head([1, 2, 3]))
|
||||||
|
Test.assertEqual(None, List.head([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_tail(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(Some([2, 3]), List.tail([1, 2, 3]))
|
||||||
|
Test.assertEqual(None, List.tail([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_get(): Unit with {Test} = {
|
||||||
|
let xs = [10, 20, 30]
|
||||||
|
Test.assertEqual(Some(10), List.get(xs, 0))
|
||||||
|
Test.assertEqual(Some(20), List.get(xs, 1))
|
||||||
|
Test.assertEqual(Some(30), List.get(xs, 2))
|
||||||
|
Test.assertEqual(None, List.get(xs, 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_map(): Unit with {Test} = {
|
||||||
|
let double = fn(x: Int): Int => x * 2
|
||||||
|
Test.assertEqual([2, 4, 6], List.map([1, 2, 3], double))
|
||||||
|
Test.assertEqual([], List.map([], double))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_filter(): Unit with {Test} = {
|
||||||
|
let isEven = fn(x: Int): Bool => x % 2 == 0
|
||||||
|
Test.assertEqual([2, 4], List.filter([1, 2, 3, 4, 5], isEven))
|
||||||
|
Test.assertEqual([], List.filter([1, 3, 5], isEven))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_fold(): Unit with {Test} = {
|
||||||
|
let sum = fn(acc: Int, x: Int): Int => acc + x
|
||||||
|
Test.assertEqual(6, List.fold([1, 2, 3], 0, sum))
|
||||||
|
Test.assertEqual(10, List.fold([1, 2, 3], 4, sum))
|
||||||
|
Test.assertEqual(0, List.fold([], 0, sum))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_reverse(): Unit with {Test} = {
|
||||||
|
Test.assertEqual([3, 2, 1], List.reverse([1, 2, 3]))
|
||||||
|
Test.assertEqual([], List.reverse([]))
|
||||||
|
Test.assertEqual([1], List.reverse([1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_list_concat(): Unit with {Test} = {
|
||||||
|
Test.assertEqual([1, 2, 3, 4], List.concat([1, 2], [3, 4]))
|
||||||
|
Test.assertEqual([1, 2], List.concat([1, 2], []))
|
||||||
|
Test.assertEqual([3, 4], List.concat([], [3, 4]))
|
||||||
|
}
|
||||||
51
examples/test_math.lux
Normal file
51
examples/test_math.lux
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Math Test Suite
|
||||||
|
// Run with: lux test examples/test_math.lux
|
||||||
|
|
||||||
|
fn test_addition(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(4, 2 + 2)
|
||||||
|
Test.assertEqual(0, 0 + 0)
|
||||||
|
Test.assertEqual(100, 50 + 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_subtraction(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(0, 2 - 2)
|
||||||
|
Test.assertEqual(5, 10 - 5)
|
||||||
|
Test.assertEqual(-5, 0 - 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_multiplication(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(6, 2 * 3)
|
||||||
|
Test.assertEqual(0, 0 * 100)
|
||||||
|
Test.assertEqual(100, 10 * 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_division(): Unit with {Test} = {
|
||||||
|
Test.assertEqual(2, 6 / 3)
|
||||||
|
Test.assertEqual(0, 0 / 5)
|
||||||
|
Test.assertEqual(5, 25 / 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_comparisons(): Unit with {Test} = {
|
||||||
|
Test.assertTrue(5 > 3)
|
||||||
|
Test.assertTrue(3 < 5)
|
||||||
|
Test.assertTrue(5 >= 5)
|
||||||
|
Test.assertTrue(5 <= 5)
|
||||||
|
Test.assertFalse(3 > 5)
|
||||||
|
Test.assertFalse(5 < 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_equality(): Unit with {Test} = {
|
||||||
|
Test.assertTrue(42 == 42)
|
||||||
|
Test.assertFalse(42 == 43)
|
||||||
|
Test.assertTrue(42 != 43)
|
||||||
|
Test.assertFalse(42 != 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_boolean_logic(): Unit with {Test} = {
|
||||||
|
Test.assertTrue(true && true)
|
||||||
|
Test.assertFalse(true && false)
|
||||||
|
Test.assertTrue(true || false)
|
||||||
|
Test.assertFalse(false || false)
|
||||||
|
Test.assertTrue(!false)
|
||||||
|
Test.assertFalse(!true)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//! no garbage collector needed with Perceus-style reference counting.
|
//! no garbage collector needed with Perceus-style reference counting.
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
/// C code generation errors
|
/// C code generation errors
|
||||||
@@ -81,8 +81,8 @@ impl CBackend {
|
|||||||
Declaration::Function(f) => {
|
Declaration::Function(f) => {
|
||||||
self.emit_function(f)?;
|
self.emit_function(f)?;
|
||||||
}
|
}
|
||||||
Declaration::Let { name, value, .. } => {
|
Declaration::Let(let_decl) => {
|
||||||
self.emit_global_let(name, value)?;
|
self.emit_global_let(&let_decl.name, &let_decl.value)?;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -170,18 +170,18 @@ impl CBackend {
|
|||||||
self.types_emitted.insert(name.clone());
|
self.types_emitted.insert(name.clone());
|
||||||
|
|
||||||
match &type_decl.definition {
|
match &type_decl.definition {
|
||||||
TypeDefinition::Record(fields) => {
|
TypeDef::Record(fields) => {
|
||||||
self.writeln(&format!("typedef struct {} {{", name));
|
self.writeln(&format!("typedef struct {} {{", name));
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
for field in fields {
|
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.writeln(&format!("{} {};", c_type, field.name.name));
|
||||||
}
|
}
|
||||||
self.indent -= 1;
|
self.indent -= 1;
|
||||||
self.writeln(&format!("}} {};", name));
|
self.writeln(&format!("}} {};", name));
|
||||||
self.writeln("");
|
self.writeln("");
|
||||||
}
|
}
|
||||||
TypeDefinition::Adt(variants) => {
|
TypeDef::Enum(variants) => {
|
||||||
// Emit tag enum
|
// Emit tag enum
|
||||||
self.writeln(&format!("typedef enum {}_Tag {{", name));
|
self.writeln(&format!("typedef enum {}_Tag {{", name));
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
@@ -195,13 +195,25 @@ impl CBackend {
|
|||||||
|
|
||||||
// Emit variant structs
|
// Emit variant structs
|
||||||
for variant in variants {
|
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.writeln(&format!("typedef struct {}_{}_Data {{", name, variant.name.name));
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
for (i, field) in variant.fields.iter().enumerate() {
|
match &variant.fields {
|
||||||
|
VariantFields::Tuple(fields) => {
|
||||||
|
for (i, field) in fields.iter().enumerate() {
|
||||||
let c_type = self.type_to_c(field)?;
|
let c_type = self.type_to_c(field)?;
|
||||||
self.writeln(&format!("{} field{};", c_type, i));
|
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.indent -= 1;
|
||||||
self.writeln(&format!("}} {}_{}_Data;", name, variant.name.name));
|
self.writeln(&format!("}} {}_{}_Data;", name, variant.name.name));
|
||||||
self.writeln("");
|
self.writeln("");
|
||||||
@@ -215,7 +227,8 @@ impl CBackend {
|
|||||||
self.writeln("union {");
|
self.writeln("union {");
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
for variant in variants {
|
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()));
|
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(&format!("}} {};", name));
|
||||||
self.writeln("");
|
self.writeln("");
|
||||||
}
|
}
|
||||||
TypeDefinition::Alias(_) => {
|
TypeDef::Alias(_) => {
|
||||||
// Type aliases are handled during type resolution
|
// Type aliases are handled during type resolution
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +283,7 @@ impl CBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let param_strs: Result<Vec<_>, _> = params.iter().map(|p| {
|
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))
|
Ok(format!("{} {}", c_type, p.name.name))
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
@@ -301,13 +314,10 @@ impl CBackend {
|
|||||||
BinaryOp::Ge => ">=",
|
BinaryOp::Ge => ">=",
|
||||||
BinaryOp::And => "&&",
|
BinaryOp::And => "&&",
|
||||||
BinaryOp::Or => "||",
|
BinaryOp::Or => "||",
|
||||||
BinaryOp::Concat => {
|
BinaryOp::Pipe => {
|
||||||
return Ok(format!("lux_string_concat({}, {})", l, r));
|
// 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))
|
Ok(format!("({} {} {})", l, op_str, r))
|
||||||
@@ -404,19 +414,8 @@ impl CBackend {
|
|||||||
Ok(format!("{}.{}", obj, field.name))
|
Ok(format!("{}.{}", obj, field.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::Match { expr, arms, .. } => {
|
Expr::Match { scrutinee, arms, .. } => {
|
||||||
self.emit_match(expr, arms)
|
self.emit_match(scrutinee, 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(", ")))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Err(CGenError {
|
_ => Err(CGenError {
|
||||||
@@ -462,9 +461,9 @@ impl CBackend {
|
|||||||
fn pattern_to_condition(&self, pattern: &Pattern, scrutinee: &str) -> Result<String, CGenError> {
|
fn pattern_to_condition(&self, pattern: &Pattern, scrutinee: &str) -> Result<String, CGenError> {
|
||||||
match pattern {
|
match pattern {
|
||||||
Pattern::Wildcard(_) => Ok("1".to_string()),
|
Pattern::Wildcard(_) => Ok("1".to_string()),
|
||||||
Pattern::Var(ident, _) => Ok(format!("(1) /* bind {} = {} */", ident.name, scrutinee)),
|
Pattern::Var(ident) => Ok(format!("(1) /* bind {} = {} */", ident.name, scrutinee)),
|
||||||
Pattern::Literal(lit, _) => {
|
Pattern::Literal(lit) => {
|
||||||
let lit_val = self.emit_literal_value(lit)?;
|
let lit_val = self.emit_literal_value(&lit.kind)?;
|
||||||
Ok(format!("{} == {}", scrutinee, lit_val))
|
Ok(format!("{} == {}", scrutinee, lit_val))
|
||||||
}
|
}
|
||||||
Pattern::Constructor { name, .. } => {
|
Pattern::Constructor { name, .. } => {
|
||||||
@@ -504,7 +503,7 @@ impl CBackend {
|
|||||||
|
|
||||||
// Check for top-level run expressions
|
// Check for top-level run expressions
|
||||||
let has_run = program.declarations.iter().any(|d| {
|
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 {
|
if has_main || has_run {
|
||||||
@@ -513,9 +512,9 @@ impl CBackend {
|
|||||||
|
|
||||||
// Execute top-level let bindings with run expressions
|
// Execute top-level let bindings with run expressions
|
||||||
for decl in &program.declarations {
|
for decl in &program.declarations {
|
||||||
if let Declaration::Let { name, value, .. } = decl {
|
if let Declaration::Let(let_decl) = decl {
|
||||||
if matches!(value.as_ref(), Expr::Run { .. }) {
|
if matches!(&let_decl.value, Expr::Run { .. }) {
|
||||||
if let Expr::Run { expr, .. } = value.as_ref() {
|
if let Expr::Run { expr, .. } = &let_decl.value {
|
||||||
if let Expr::Call { func, .. } = expr.as_ref() {
|
if let Expr::Call { func, .. } = expr.as_ref() {
|
||||||
if let Expr::Var(fn_name) = func.as_ref() {
|
if let Expr::Var(fn_name) = func.as_ref() {
|
||||||
self.writeln(&format!("{}();", fn_name.name));
|
self.writeln(&format!("{}();", fn_name.name));
|
||||||
@@ -549,18 +548,23 @@ impl CBackend {
|
|||||||
other => Ok(other.to_string()),
|
other => Ok(other.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TypeExpr::Generic { name, .. } => {
|
TypeExpr::App(base, _) => {
|
||||||
// For now, use void* for generic types
|
// For now, use void* for generic types
|
||||||
|
if let TypeExpr::Named(name) = base.as_ref() {
|
||||||
match name.name.as_str() {
|
match name.name.as_str() {
|
||||||
"List" => Ok("void*".to_string()),
|
"List" => Ok("void*".to_string()),
|
||||||
"Option" => Ok("void*".to_string()),
|
"Option" => Ok("void*".to_string()),
|
||||||
_ => 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::Function { .. } => Ok("void*".to_string()),
|
||||||
TypeExpr::Tuple(_) => Ok("void*".to_string()),
|
TypeExpr::Tuple(_) => Ok("void*".to_string()),
|
||||||
TypeExpr::Record(_) => Ok("void*".to_string()),
|
TypeExpr::Record(_) => Ok("void*".to_string()),
|
||||||
_ => Ok("void*".to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,5 @@
|
|||||||
|
|
||||||
pub mod c_backend;
|
pub mod c_backend;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub use c_backend::CBackend;
|
pub use c_backend::CBackend;
|
||||||
|
|||||||
@@ -465,12 +465,13 @@ fn compile_expr(
|
|||||||
}
|
}
|
||||||
|
|
||||||
expr => {
|
expr => {
|
||||||
|
use crate::ast::LiteralKind;
|
||||||
let expr_type = match expr {
|
let expr_type = match expr {
|
||||||
Expr::Literal(lit) => match lit {
|
Expr::Literal(lit) => match &lit.kind {
|
||||||
Literal::String(_) => "String literal",
|
LiteralKind::String(_) => "String literal",
|
||||||
Literal::Float(_) => "Float literal",
|
LiteralKind::Float(_) => "Float literal",
|
||||||
Literal::Char(_) => "Char literal",
|
LiteralKind::Char(_) => "Char literal",
|
||||||
Literal::Unit => "Unit literal",
|
LiteralKind::Unit => "Unit literal",
|
||||||
_ => "Literal",
|
_ => "Literal",
|
||||||
},
|
},
|
||||||
Expr::EffectOp { effect, operation, .. } => {
|
Expr::EffectOp { effect, operation, .. } => {
|
||||||
@@ -485,17 +486,8 @@ fn compile_expr(
|
|||||||
Expr::List { .. } => "List literal",
|
Expr::List { .. } => "List literal",
|
||||||
Expr::Record { .. } => "Record literal",
|
Expr::Record { .. } => "Record literal",
|
||||||
Expr::Tuple { .. } => "Tuple literal",
|
Expr::Tuple { .. } => "Tuple literal",
|
||||||
Expr::Index { .. } => "Index access",
|
|
||||||
Expr::Run { .. } => "Run expression (effects)",
|
Expr::Run { .. } => "Run expression (effects)",
|
||||||
Expr::Handle { .. } => "Handle expression (effects)",
|
|
||||||
Expr::Resume { .. } => "Resume expression (effects)",
|
Expr::Resume { .. } => "Resume expression (effects)",
|
||||||
Expr::Pipe { .. } => "Pipe operator",
|
|
||||||
Expr::Interpolation { .. } => "String interpolation",
|
|
||||||
Expr::Constructor { name, .. } => {
|
|
||||||
return Err(CompileError {
|
|
||||||
message: format!("ADT constructor '{}' - algebraic data types are not supported in JIT", name.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => "Unknown expression",
|
_ => "Unknown expression",
|
||||||
};
|
};
|
||||||
Err(CompileError {
|
Err(CompileError {
|
||||||
|
|||||||
@@ -177,6 +177,40 @@ impl Value {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compare two values for equality (for testing)
|
||||||
|
/// Returns true if the values are structurally equal
|
||||||
|
pub fn values_equal(a: &Value, b: &Value) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(Value::Int(x), Value::Int(y)) => x == y,
|
||||||
|
(Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON,
|
||||||
|
(Value::Bool(x), Value::Bool(y)) => x == y,
|
||||||
|
(Value::String(x), Value::String(y)) => x == y,
|
||||||
|
(Value::Char(x), Value::Char(y)) => x == y,
|
||||||
|
(Value::Unit, Value::Unit) => true,
|
||||||
|
(Value::List(xs), Value::List(ys)) => {
|
||||||
|
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
|
||||||
|
}
|
||||||
|
(Value::Tuple(xs), Value::Tuple(ys)) => {
|
||||||
|
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
|
||||||
|
}
|
||||||
|
(Value::Record(xs), Value::Record(ys)) => {
|
||||||
|
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
|
||||||
|
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => {
|
||||||
|
n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y))
|
||||||
|
}
|
||||||
|
(Value::Versioned { type_name: t1, version: v1, value: val1 },
|
||||||
|
Value::Versioned { type_name: t2, version: v2, value: val2 }) => {
|
||||||
|
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
|
||||||
|
}
|
||||||
|
(Value::Json(j1), Value::Json(j2)) => j1 == j2,
|
||||||
|
// Functions and handlers cannot be compared for equality
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for extracting typed values from Value
|
/// Trait for extracting typed values from Value
|
||||||
@@ -545,6 +579,24 @@ pub struct Interpreter {
|
|||||||
http_server: Arc<Mutex<Option<tiny_http::Server>>>,
|
http_server: Arc<Mutex<Option<tiny_http::Server>>>,
|
||||||
/// Current HTTP request being handled (stored for respond operation)
|
/// Current HTTP request being handled (stored for respond operation)
|
||||||
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
|
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
|
||||||
|
/// Test results for the Test effect
|
||||||
|
test_results: RefCell<TestResults>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Results from running tests
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TestResults {
|
||||||
|
pub passed: usize,
|
||||||
|
pub failed: usize,
|
||||||
|
pub failures: Vec<TestFailure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single test failure
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TestFailure {
|
||||||
|
pub message: String,
|
||||||
|
pub expected: Option<String>,
|
||||||
|
pub actual: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Interpreter {
|
impl Interpreter {
|
||||||
@@ -567,9 +619,20 @@ impl Interpreter {
|
|||||||
in_handler_depth: 0,
|
in_handler_depth: 0,
|
||||||
http_server: Arc::new(Mutex::new(None)),
|
http_server: Arc::new(Mutex::new(None)),
|
||||||
current_http_request: Arc::new(Mutex::new(None)),
|
current_http_request: Arc::new(Mutex::new(None)),
|
||||||
|
test_results: RefCell::new(TestResults::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the test results
|
||||||
|
pub fn get_test_results(&self) -> TestResults {
|
||||||
|
self.test_results.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset test results for a new test run
|
||||||
|
pub fn reset_test_results(&self) {
|
||||||
|
*self.test_results.borrow_mut() = TestResults::default();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the initial value for the built-in State effect
|
/// Set the initial value for the built-in State effect
|
||||||
pub fn set_state(&self, value: Value) {
|
pub fn set_state(&self, value: Value) {
|
||||||
*self.builtin_state.borrow_mut() = value;
|
*self.builtin_state.borrow_mut() = value;
|
||||||
@@ -3466,6 +3529,111 @@ impl Interpreter {
|
|||||||
Ok(Value::Unit)
|
Ok(Value::Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test effect for testing framework
|
||||||
|
("Test", "assert") => {
|
||||||
|
let condition = match request.args.first() {
|
||||||
|
Some(Value::Bool(b)) => *b,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let message = match request.args.get(1) {
|
||||||
|
Some(Value::String(s)) => s.clone(),
|
||||||
|
_ => "Assertion failed".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if condition {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message,
|
||||||
|
expected: None,
|
||||||
|
actual: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Test", "assertEqual") => {
|
||||||
|
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||||
|
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||||
|
|
||||||
|
if Value::values_equal(&expected, &actual) {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message: "Values not equal".to_string(),
|
||||||
|
expected: Some(format!("{}", expected)),
|
||||||
|
actual: Some(format!("{}", actual)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Test", "assertNotEqual") => {
|
||||||
|
let a = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||||
|
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||||
|
|
||||||
|
if !Value::values_equal(&a, &b) {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message: "Values should not be equal".to_string(),
|
||||||
|
expected: Some(format!("not {}", a)),
|
||||||
|
actual: Some(format!("{}", b)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Test", "assertTrue") => {
|
||||||
|
let condition = match request.args.first() {
|
||||||
|
Some(Value::Bool(b)) => *b,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if condition {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message: "Expected true".to_string(),
|
||||||
|
expected: Some("true".to_string()),
|
||||||
|
actual: Some("false".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Test", "assertFalse") => {
|
||||||
|
let condition = match request.args.first() {
|
||||||
|
Some(Value::Bool(b)) => *b,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !condition {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message: "Expected false".to_string(),
|
||||||
|
expected: Some("false".to_string()),
|
||||||
|
actual: Some("true".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Test", "fail") => {
|
||||||
|
let message = match request.args.first() {
|
||||||
|
Some(Value::String(s)) => s.clone(),
|
||||||
|
_ => "Test failed".to_string(),
|
||||||
|
};
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message,
|
||||||
|
expected: None,
|
||||||
|
actual: None,
|
||||||
|
});
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
|
||||||
_ => Err(RuntimeError {
|
_ => Err(RuntimeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unhandled effect operation: {}.{}",
|
"Unhandled effect operation: {}.{}",
|
||||||
|
|||||||
211
src/main.rs
211
src/main.rs
@@ -1,6 +1,7 @@
|
|||||||
//! Lux - A functional programming language with first-class effects
|
//! Lux - A functional programming language with first-class effects
|
||||||
|
|
||||||
mod ast;
|
mod ast;
|
||||||
|
mod codegen;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
mod debugger;
|
mod debugger;
|
||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
@@ -364,80 +365,208 @@ fn run_tests(args: &[String]) {
|
|||||||
// Find test files
|
// Find test files
|
||||||
let pattern = args.first().map(|s| s.as_str());
|
let pattern = args.first().map(|s| s.as_str());
|
||||||
|
|
||||||
// Look for test files in current directory and tests/ subdirectory
|
// Look for test files in multiple locations
|
||||||
let mut test_files = Vec::new();
|
let mut test_files = Vec::new();
|
||||||
|
|
||||||
for entry in fs::read_dir(".").into_iter().flatten().flatten() {
|
// Current directory
|
||||||
let path = entry.path();
|
collect_test_files(".", pattern, &mut test_files);
|
||||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
|
||||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
// tests/ subdirectory
|
||||||
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
if Path::new("tests").is_dir() {
|
||||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
collect_test_files("tests", pattern, &mut test_files);
|
||||||
test_files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if Path::new("tests").is_dir() {
|
// examples/ subdirectory (for example tests)
|
||||||
for entry in fs::read_dir("tests").into_iter().flatten().flatten() {
|
if Path::new("examples").is_dir() {
|
||||||
let path = entry.path();
|
collect_test_files("examples", pattern, &mut test_files);
|
||||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
|
||||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
|
||||||
test_files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a specific file is given, use that
|
||||||
|
if let Some(p) = pattern {
|
||||||
|
if Path::new(p).is_file() {
|
||||||
|
test_files.clear();
|
||||||
|
test_files.push(std::path::PathBuf::from(p));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if test_files.is_empty() {
|
if test_files.is_empty() {
|
||||||
println!("No test files found.");
|
println!("No test files found.");
|
||||||
println!("Test files should be named test_*.lux or *_test.lux");
|
println!("Test files should be named test_*.lux or *_test.lux");
|
||||||
|
println!("Or contain functions named test_*");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut passed = 0;
|
println!("Running tests...\n");
|
||||||
let mut failed = 0;
|
|
||||||
|
let mut total_passed = 0;
|
||||||
|
let mut total_failed = 0;
|
||||||
|
let mut all_failures = Vec::new();
|
||||||
|
|
||||||
for test_file in &test_files {
|
for test_file in &test_files {
|
||||||
let path_str = test_file.to_string_lossy().to_string();
|
let path_str = test_file.to_string_lossy().to_string();
|
||||||
print!("Testing {}... ", path_str);
|
|
||||||
|
|
||||||
// Run the test file
|
// Read and parse the file
|
||||||
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
let source = match fs::read_to_string(test_file) {
|
||||||
.arg(&path_str)
|
Ok(s) => s,
|
||||||
.output();
|
Err(e) => {
|
||||||
|
println!(" {} ... ERROR: {}", path_str, e);
|
||||||
|
total_failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match result {
|
let program = match Parser::parse_source(&source) {
|
||||||
Ok(output) if output.status.success() => {
|
Ok(p) => p,
|
||||||
println!("OK");
|
Err(e) => {
|
||||||
passed += 1;
|
println!(" {} ... PARSE ERROR: {}", path_str, e);
|
||||||
|
total_failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type check
|
||||||
|
let mut checker = typechecker::TypeChecker::new();
|
||||||
|
if let Err(errors) = checker.check_program(&program) {
|
||||||
|
println!(" {} ... TYPE ERROR", path_str);
|
||||||
|
for err in errors {
|
||||||
|
eprintln!(" {}", err);
|
||||||
|
}
|
||||||
|
total_failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find test functions (functions starting with test_)
|
||||||
|
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
|
||||||
|
if let ast::Declaration::Function(f) = d {
|
||||||
|
if f.name.name.starts_with("test_") {
|
||||||
|
return Some(f.name.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
if test_funcs.is_empty() {
|
||||||
|
// No test functions, run the whole file
|
||||||
|
let mut interp = Interpreter::new();
|
||||||
|
interp.reset_test_results();
|
||||||
|
|
||||||
|
match interp.run(&program) {
|
||||||
|
Ok(_) => {
|
||||||
|
let results = interp.get_test_results();
|
||||||
|
if results.failed == 0 && results.passed == 0 {
|
||||||
|
// No Test assertions, just check it runs
|
||||||
|
println!(" {} ... OK (no assertions)", path_str);
|
||||||
|
total_passed += 1;
|
||||||
|
} else if results.failed == 0 {
|
||||||
|
println!(" {} ... OK ({} assertions)", path_str, results.passed);
|
||||||
|
total_passed += results.passed;
|
||||||
|
} else {
|
||||||
|
println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed);
|
||||||
|
total_passed += results.passed;
|
||||||
|
total_failed += results.failed;
|
||||||
|
for failure in &results.failures {
|
||||||
|
all_failures.push((path_str.clone(), "".to_string(), failure.clone()));
|
||||||
}
|
}
|
||||||
Ok(output) => {
|
|
||||||
println!("FAILED");
|
|
||||||
if !output.stderr.is_empty() {
|
|
||||||
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
}
|
}
|
||||||
failed += 1;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("ERROR: {}", e);
|
println!(" {} ... RUNTIME ERROR: {}", path_str, e);
|
||||||
failed += 1;
|
total_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Run individual test functions
|
||||||
|
println!(" {}:", path_str);
|
||||||
|
|
||||||
|
for test_name in &test_funcs {
|
||||||
|
let mut interp = Interpreter::new();
|
||||||
|
interp.reset_test_results();
|
||||||
|
|
||||||
|
// First run the file to define all functions
|
||||||
|
if let Err(e) = interp.run(&program) {
|
||||||
|
println!(" {} ... ERROR: {}", test_name, e);
|
||||||
|
total_failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the test function
|
||||||
|
let call_source = format!("let testResult = run {}() with {{}}", test_name);
|
||||||
|
let call_program = match Parser::parse_source(&call_source) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
println!(" {} ... ERROR: {}", test_name, e);
|
||||||
|
total_failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match interp.run(&call_program) {
|
||||||
|
Ok(_) => {
|
||||||
|
let results = interp.get_test_results();
|
||||||
|
if results.failed == 0 {
|
||||||
|
println!(" {} ... OK", test_name);
|
||||||
|
total_passed += 1;
|
||||||
|
} else {
|
||||||
|
println!(" {} ... FAILED", test_name);
|
||||||
|
total_failed += 1;
|
||||||
|
for failure in &results.failures {
|
||||||
|
all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(" {} ... ERROR: {}", test_name, e);
|
||||||
|
total_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Print failure details
|
||||||
|
if !all_failures.is_empty() {
|
||||||
|
println!("\n--- Failures ---\n");
|
||||||
|
for (file, test, failure) in &all_failures {
|
||||||
|
if test.is_empty() {
|
||||||
|
println!("{}:", file);
|
||||||
|
} else {
|
||||||
|
println!("{} - {}:", file, test);
|
||||||
|
}
|
||||||
|
println!(" {}", failure.message);
|
||||||
|
if let Some(expected) = &failure.expected {
|
||||||
|
println!(" Expected: {}", expected);
|
||||||
|
}
|
||||||
|
if let Some(actual) = &failure.actual {
|
||||||
|
println!(" Actual: {}", actual);
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
println!("Results: {} passed, {} failed", passed, failed);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if failed > 0 {
|
println!("Results: {} passed, {} failed", total_passed, total_failed);
|
||||||
|
|
||||||
|
if total_failed > 0 {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
||||||
|
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn watch_file(path: &str) {
|
fn watch_file(path: &str) {
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|||||||
@@ -1532,7 +1532,7 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Built-in effects are always available
|
// Built-in effects are always available
|
||||||
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"];
|
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test"];
|
||||||
let is_builtin = builtin_effects.contains(&effect.name.as_str());
|
let is_builtin = builtin_effects.contains(&effect.name.as_str());
|
||||||
|
|
||||||
// Track this effect for inference
|
// Track this effect for inference
|
||||||
|
|||||||
50
src/types.rs
50
src/types.rs
@@ -1123,6 +1123,56 @@ impl TypeEnv {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add Test effect for test framework
|
||||||
|
env.effects.insert(
|
||||||
|
"Test".to_string(),
|
||||||
|
EffectDef {
|
||||||
|
name: "Test".to_string(),
|
||||||
|
type_params: Vec::new(),
|
||||||
|
operations: vec![
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assert".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("condition".to_string(), Type::Bool),
|
||||||
|
("message".to_string(), Type::String),
|
||||||
|
],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assertEqual".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("expected".to_string(), Type::Var(0)),
|
||||||
|
("actual".to_string(), Type::Var(0)),
|
||||||
|
],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assertNotEqual".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("a".to_string(), Type::Var(0)),
|
||||||
|
("b".to_string(), Type::Var(0)),
|
||||||
|
],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assertTrue".to_string(),
|
||||||
|
params: vec![("condition".to_string(), Type::Bool)],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assertFalse".to_string(),
|
||||||
|
params: vec![("condition".to_string(), Type::Bool)],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "fail".to_string(),
|
||||||
|
params: vec![("message".to_string(), Type::String)],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Add Some and Ok, Err constructors
|
// Add Some and Ok, Err constructors
|
||||||
// Some : fn(a) -> Option<a>
|
// Some : fn(a) -> Option<a>
|
||||||
let a = Type::var();
|
let a = Type::var();
|
||||||
|
|||||||
Reference in New Issue
Block a user