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:
236
src/main.rs
236
src/main.rs
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user