feat: add schema evolution type system integration and HTTP server effect

Schema Evolution:
- Preserve version info in type resolution (Type::Versioned)
- Track versioned type declarations in typechecker
- Detect version mismatches at compile time (@v1 vs @v2 errors)
- Support @v2+ (at least) and @latest version constraints
- Store migrations for future auto-migration support
- Fix let bindings to preserve declared type annotations

HTTP Server Effect:
- Add HttpServer effect with listen, accept, respond, respondWithHeaders, stop
- Implement blocking request handling via tiny_http
- Request record includes method, path, body, headers
- Add http_server.lux example with routing via pattern matching
- Add type-checking test for HttpServer effect

Tests: 222 passing (up from 217)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 22:06:31 -05:00
parent 554a1e7c3e
commit 086552b7a4
9 changed files with 1153 additions and 21 deletions

View File

@@ -1796,6 +1796,215 @@ c")"#;
assert!(result.unwrap_err().contains("pure but has effects"));
}
#[test]
fn test_behavioral_deterministic_with_random_error() {
// A deterministic function cannot use Random effect
let source = r#"
fn bad(): Int with {Random} is deterministic = Random.int(1, 100)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects"));
}
#[test]
fn test_behavioral_deterministic_with_time_error() {
// A deterministic function cannot use Time effect
let source = r#"
fn bad(): Int with {Time} is deterministic = Time.now()
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("deterministic but uses non-deterministic effects"));
}
#[test]
fn test_behavioral_commutative_add() {
// Addition is commutative
let source = r#"
fn add(a: Int, b: Int): Int is commutative = a + b
let result = add(3, 5)
"#;
assert_eq!(eval(source).unwrap(), "8");
}
#[test]
fn test_behavioral_commutative_mul() {
// Multiplication is commutative
let source = r#"
fn mul(a: Int, b: Int): Int is commutative = a * b
let result = mul(3, 5)
"#;
assert_eq!(eval(source).unwrap(), "15");
}
#[test]
fn test_behavioral_commutative_subtract_error() {
// Subtraction is NOT commutative
let source = r#"
fn sub(a: Int, b: Int): Int is commutative = a - b
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("commutative but its body is not"));
}
#[test]
fn test_behavioral_commutative_wrong_params_error() {
// Commutative requires exactly 2 params
let source = r#"
fn bad(a: Int): Int is commutative = a
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("has 1 parameters (expected 2)"));
}
#[test]
fn test_behavioral_idempotent_identity() {
// Identity function is idempotent
let source = r#"
fn identity(x: Int): Int is idempotent = x
let result = identity(42)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_behavioral_idempotent_constant() {
// Constant function is idempotent
let source = r#"
fn always42(x: Int): Int is idempotent = 42
let result = always42(100)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_behavioral_idempotent_clamping() {
// Clamping pattern is idempotent
let source = r#"
fn clampPositive(x: Int): Int is idempotent =
if x < 0 then 0 else x
let result = clampPositive(-5)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_behavioral_idempotent_increment_error() {
// Increment is NOT idempotent
let source = r#"
fn increment(x: Int): Int is idempotent = x + 1
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("idempotent but could not be verified"));
}
#[test]
fn test_behavioral_total_non_recursive() {
// Non-recursive function is total
let source = r#"
fn double(x: Int): Int is total = x * 2
let result = double(21)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_behavioral_total_structural_recursion() {
// Structural recursion (n - 1) is total
let source = r#"
fn factorial(n: Int): Int is total =
if n <= 1 then 1 else n * factorial(n - 1)
let result = factorial(5)
"#;
assert_eq!(eval(source).unwrap(), "120");
}
#[test]
fn test_behavioral_total_infinite_loop_error() {
// Infinite loop is NOT total
let source = r#"
fn loop(x: Int): Int is total = loop(x)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("may not terminate"));
}
#[test]
fn test_behavioral_total_with_fail_error() {
// Total function cannot use Fail effect
let source = r#"
effect Fail { fn fail(msg: String): Unit }
fn bad(x: Int): Int with {Fail} is total =
if x < 0 then { Fail.fail("negative"); 0 } else x
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("total but uses the Fail effect"));
}
#[test]
fn test_where_clause_parsing() {
// Where clause property constraints are parsed correctly
let source = r#"
fn retry<F>(action: F, times: Int): Int where F is idempotent = times
fn idempotentFn(): Int is idempotent = 42
let x = 1
"#;
// Just verify it parses and type-checks without errors
assert!(eval(source).is_ok());
}
#[test]
fn test_schema_version_preserved() {
// Version annotations are preserved in type annotations
let source = r#"
fn processV1(x: Int @v1): Int = x
let value: Int @v1 = 42
let result = processV1(value)
"#;
assert!(eval(source).is_ok());
}
#[test]
fn test_schema_version_mismatch_error() {
// Version mismatch should produce an error when versions don't match
let source = r#"
fn expectV2(x: Int @v2): Int = x
let oldValue: Int @v1 = 42
let result = expectV2(oldValue)
"#;
let result = eval(source);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Version mismatch"));
}
#[test]
fn test_schema_version_atleast() {
// @v2+ should accept v2 or higher
let source = r#"
fn processAtLeastV2(x: Int @v2+): Int = x
let value: Int @v2 = 42
let result = processAtLeastV2(value)
"#;
assert!(eval(source).is_ok());
}
#[test]
fn test_schema_version_latest() {
// @latest should be compatible with any version
let source = r#"
fn processLatest(x: Int @latest): Int = x
let value: Int @v1 = 42
let result = processLatest(value)
"#;
assert!(eval(source).is_ok());
}
// Built-in effect tests
mod effect_tests {
use crate::interpreter::{Interpreter, Value};
@@ -2018,6 +2227,33 @@ c")"#;
assert!(result.unwrap_err().contains("downgrade"));
}
// HttpServer effect type-checking tests
#[test]
fn test_http_server_typecheck() {
// Verify HttpServer effect operations type-check correctly
// We can't actually run the server in tests, but we can verify types
use crate::parser::Parser;
use crate::typechecker::TypeChecker;
let source = r#"
// Function that uses HttpServer effect
fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {HttpServer} =
HttpServer.respond(200, "Hello!")
fn serveOne(port: Int): Unit with {HttpServer} = {
HttpServer.listen(port)
let req = HttpServer.accept()
handleRequest(req)
HttpServer.stop()
}
"#;
let program = Parser::parse_source(source).expect("parse failed");
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_ok(), "HttpServer type checking failed: {:?}", result);
}
// Math module tests
#[test]
fn test_math_abs() {