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

@@ -9,6 +9,7 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
/// Built-in function identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -540,6 +541,10 @@ pub struct Interpreter {
builtin_reader: RefCell<Value>,
/// Depth of handler context (> 0 means we're inside a handler body where resume is valid)
in_handler_depth: usize,
/// HTTP server state (using Arc<Mutex> for thread-safety with tiny_http)
http_server: Arc<Mutex<Option<tiny_http::Server>>>,
/// Current HTTP request being handled (stored for respond operation)
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
}
impl Interpreter {
@@ -560,6 +565,8 @@ impl Interpreter {
builtin_state: RefCell::new(Value::Unit),
builtin_reader: RefCell::new(Value::Unit),
in_handler_depth: 0,
http_server: Arc::new(Mutex::new(None)),
current_http_request: Arc::new(Mutex::new(None)),
}
}
@@ -3301,6 +3308,164 @@ impl Interpreter {
Ok(Value::Unit)
}
// ===== HttpServer Effect =====
("HttpServer", "listen") => {
let port = match request.args.first() {
Some(Value::Int(p)) => *p as u16,
_ => return Err(RuntimeError {
message: "HttpServer.listen requires an integer port".to_string(),
span: None,
}),
};
let addr = format!("0.0.0.0:{}", port);
match tiny_http::Server::http(&addr) {
Ok(server) => {
*self.http_server.lock().unwrap() = Some(server);
Ok(Value::Unit)
}
Err(e) => Err(RuntimeError {
message: format!("Failed to start HTTP server on port {}: {}", port, e),
span: None,
}),
}
}
("HttpServer", "accept") => {
let server_guard = self.http_server.lock().unwrap();
let server = match server_guard.as_ref() {
Some(s) => s,
None => return Err(RuntimeError {
message: "HttpServer.accept: No server is listening. Call HttpServer.listen first.".to_string(),
span: None,
}),
};
// Block until a request arrives
match server.recv() {
Ok(mut request) => {
// Extract request info
let method = request.method().to_string();
let path = request.url().to_string();
// Read body
let mut body = String::new();
{
use std::io::Read;
let reader = request.as_reader();
let _ = reader.read_to_string(&mut body);
}
// Extract headers
let headers: Vec<Value> = request.headers()
.iter()
.map(|h| Value::Tuple(vec![
Value::String(h.field.as_str().to_string()),
Value::String(h.value.as_str().to_string()),
]))
.collect();
// Store the request for respond operation
drop(server_guard); // Release lock before storing request
*self.current_http_request.lock().unwrap() = Some(request);
// Return request as a record
Ok(Value::Record(HashMap::from([
("method".to_string(), Value::String(method)),
("path".to_string(), Value::String(path)),
("body".to_string(), Value::String(body)),
("headers".to_string(), Value::List(headers)),
])))
}
Err(e) => Err(RuntimeError {
message: format!("HttpServer.accept failed: {}", e),
span: None,
}),
}
}
("HttpServer", "respond") => {
let (status, body) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(s)), Some(Value::String(b))) => (*s as u16, b.clone()),
_ => return Err(RuntimeError {
message: "HttpServer.respond requires status (Int) and body (String)".to_string(),
span: None,
}),
};
let mut req_guard = self.current_http_request.lock().unwrap();
match req_guard.take() {
Some(http_request) => {
let status_code = tiny_http::StatusCode(status);
let response = tiny_http::Response::from_string(body)
.with_status_code(status_code);
if let Err(e) = http_request.respond(response) {
return Err(RuntimeError {
message: format!("Failed to send HTTP response: {}", e),
span: None,
});
}
Ok(Value::Unit)
}
None => Err(RuntimeError {
message: "HttpServer.respond: No pending request to respond to".to_string(),
span: None,
}),
}
}
("HttpServer", "respondWithHeaders") => {
let (status, body, headers) = match (request.args.get(0), request.args.get(1), request.args.get(2)) {
(Some(Value::Int(s)), Some(Value::String(b)), Some(Value::List(h))) => {
(*s as u16, b.clone(), h.clone())
}
_ => return Err(RuntimeError {
message: "HttpServer.respondWithHeaders requires status (Int), body (String), and headers (List)".to_string(),
span: None,
}),
};
let mut req_guard = self.current_http_request.lock().unwrap();
match req_guard.take() {
Some(http_request) => {
let status_code = tiny_http::StatusCode(status);
let mut response = tiny_http::Response::from_string(body)
.with_status_code(status_code);
// Add custom headers
for header_val in headers {
if let Value::Tuple(pair) = header_val {
if let (Some(Value::String(name)), Some(Value::String(value))) =
(pair.get(0), pair.get(1))
{
if let Ok(header) = tiny_http::Header::from_bytes(
name.as_bytes(),
value.as_bytes(),
) {
response = response.with_header(header);
}
}
}
}
if let Err(e) = http_request.respond(response) {
return Err(RuntimeError {
message: format!("Failed to send HTTP response: {}", e),
span: None,
});
}
Ok(Value::Unit)
}
None => Err(RuntimeError {
message: "HttpServer.respondWithHeaders: No pending request to respond to".to_string(),
span: None,
}),
}
}
("HttpServer", "stop") => {
// Drop the server to stop listening
*self.http_server.lock().unwrap() = None;
*self.current_http_request.lock().unwrap() = None;
Ok(Value::Unit)
}
_ => Err(RuntimeError {
message: format!(
"Unhandled effect operation: {}.{}",

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() {

View File

@@ -13,8 +13,9 @@ use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Se
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
use crate::modules::ModuleLoader;
use crate::types::{
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, PropertySet, TraitBoundDef,
TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef, VariantFieldsDef,
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
TraitBoundDef, TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef,
VariantFieldsDef, VersionInfo,
};
/// Type checking error
@@ -100,6 +101,388 @@ fn categorize_type_error(message: &str) -> (String, Vec<String>) {
}
}
/// Check if an operator is commutative
fn is_commutative_op(op: BinaryOp) -> bool {
matches!(
op,
BinaryOp::Add | BinaryOp::Mul | BinaryOp::Eq | BinaryOp::Ne | BinaryOp::And | BinaryOp::Or
)
}
/// Check if a function body represents a commutative operation on its two parameters
fn is_commutative_body(body: &Expr, param1: &str, param2: &str) -> bool {
match body {
Expr::BinaryOp { op, left, right, .. } => {
if !is_commutative_op(*op) {
return false;
}
// Check if it's (param1 op param2) or (param2 op param1)
let left_is_p1 = matches!(left.as_ref(), Expr::Var(id) if id.name == param1);
let left_is_p2 = matches!(left.as_ref(), Expr::Var(id) if id.name == param2);
let right_is_p1 = matches!(right.as_ref(), Expr::Var(id) if id.name == param1);
let right_is_p2 = matches!(right.as_ref(), Expr::Var(id) if id.name == param2);
(left_is_p1 && right_is_p2) || (left_is_p2 && right_is_p1)
}
// Handle block expressions - check last expression
Expr::Block { statements, result, .. } => {
if !statements.is_empty() {
return false;
}
is_commutative_body(result, param1, param2)
}
_ => false,
}
}
/// Check if an expression references any of the given parameter names
fn references_params(expr: &Expr, params: &[&str]) -> bool {
match expr {
Expr::Var(id) => params.contains(&id.name.as_str()),
Expr::BinaryOp { left, right, .. } => {
references_params(left, params) || references_params(right, params)
}
Expr::UnaryOp { operand, .. } => references_params(operand, params),
Expr::If {
condition,
then_branch,
else_branch,
..
} => {
references_params(condition, params)
|| references_params(then_branch, params)
|| references_params(else_branch, params)
}
Expr::Call { func, args, .. } => {
references_params(func, params) || args.iter().any(|a| references_params(a, params))
}
Expr::Block { statements, result, .. } => {
statements.iter().any(|s| match s {
Statement::Let { value, .. } => references_params(value, params),
Statement::Expr(e) => references_params(e, params),
}) || references_params(result, params)
}
Expr::Field { object, .. } => references_params(object, params),
Expr::Lambda { body, .. } => references_params(body, params),
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| references_params(e, params)),
Expr::Match { scrutinee, arms, .. } => {
references_params(scrutinee, params)
|| arms.iter().any(|a| references_params(&a.body, params))
}
_ => false,
}
}
/// Check if a function body is idempotent (f(f(x)) == f(x))
/// Uses conservative pattern recognition
fn is_idempotent_body(body: &Expr, params: &[Parameter]) -> bool {
let param_names: Vec<&str> = params.iter().map(|p| p.name.name.as_str()).collect();
// Pattern 1: Constant - body doesn't reference any parameters
if !references_params(body, &param_names) {
return true;
}
// Pattern 2: Identity function (single param, body is just that param)
if params.len() == 1 {
if let Expr::Var(id) = body {
if id.name == params[0].name.name {
return true;
}
}
}
// Pattern 3: Field projection on single param (e.g., person.name)
if params.len() == 1 {
if let Expr::Field { object, .. } = body {
if let Expr::Var(id) = object.as_ref() {
if id.name == params[0].name.name {
return true;
}
}
}
}
// Pattern 4: Clamping pattern - if x < min then min else if x > max then max else x
// or simpler: if x < 0 then 0 else x
if params.len() == 1 && is_clamping_pattern(body, &params[0].name.name) {
return true;
}
// Pattern 5: Absolute value - if x < 0 then -x else x
if params.len() == 1 && is_abs_pattern(body, &params[0].name.name) {
return true;
}
// Handle block wrapper
if let Expr::Block { statements, result, .. } = body {
if statements.is_empty() {
return is_idempotent_body(result, params);
}
}
false
}
/// Check if body matches: if x < bound then bound else x (or similar clamping patterns)
fn is_clamping_pattern(body: &Expr, param: &str) -> bool {
if let Expr::If {
condition,
then_branch,
else_branch,
..
} = body
{
// Check if then_branch is a constant and else_branch is the param (or recursive)
let else_is_param = matches!(else_branch.as_ref(), Expr::Var(id) if id.name == param);
let else_is_clamp = is_clamping_pattern(else_branch, param);
if else_is_param || else_is_clamp {
// Check if condition is a comparison involving the param
if let Expr::BinaryOp { left, right, op, .. } = condition.as_ref() {
let left_is_param = matches!(left.as_ref(), Expr::Var(id) if id.name == param);
let right_is_param = matches!(right.as_ref(), Expr::Var(id) if id.name == param);
if (left_is_param || right_is_param)
&& matches!(
op,
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge
)
{
return true;
}
}
}
}
false
}
/// Check if body matches: if x < 0 then -x else x
fn is_abs_pattern(body: &Expr, param: &str) -> bool {
if let Expr::If {
condition,
then_branch,
else_branch,
..
} = body
{
let else_is_param = matches!(else_branch.as_ref(), Expr::Var(id) if id.name == param);
if !else_is_param {
return false;
}
// Check if then_branch is -param
if let Expr::UnaryOp {
op: ast::UnaryOp::Neg,
operand,
..
} = then_branch.as_ref()
{
if matches!(operand.as_ref(), Expr::Var(id) if id.name == param) {
// Check condition is param < 0
if let Expr::BinaryOp {
op: BinaryOp::Lt,
left,
right,
..
} = condition.as_ref()
{
let left_is_param = matches!(left.as_ref(), Expr::Var(id) if id.name == param);
let right_is_zero =
matches!(right.as_ref(), Expr::Literal(lit) if matches!(lit.kind, LiteralKind::Int(0)));
if left_is_param && right_is_zero {
return true;
}
}
}
}
}
false
}
/// Check if a function body contains recursive calls to the function
fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
match body {
Expr::Call { func, args, .. } => {
// Check if this call is to the function itself
if let Expr::Var(id) = func.as_ref() {
if id.name == func_name {
return true;
}
}
// Check in function expression and arguments
has_recursive_calls(func_name, func)
|| args.iter().any(|a| has_recursive_calls(func_name, a))
}
Expr::BinaryOp { left, right, .. } => {
has_recursive_calls(func_name, left) || has_recursive_calls(func_name, right)
}
Expr::UnaryOp { operand, .. } => has_recursive_calls(func_name, operand),
Expr::If {
condition,
then_branch,
else_branch,
..
} => {
has_recursive_calls(func_name, condition)
|| has_recursive_calls(func_name, then_branch)
|| has_recursive_calls(func_name, else_branch)
}
Expr::Block { statements, result, .. } => {
statements.iter().any(|s| match s {
Statement::Let { value, .. } => has_recursive_calls(func_name, value),
Statement::Expr(e) => has_recursive_calls(func_name, e),
}) || has_recursive_calls(func_name, result)
}
Expr::Match { scrutinee, arms, .. } => {
has_recursive_calls(func_name, scrutinee)
|| arms.iter().any(|a| has_recursive_calls(func_name, &a.body))
}
Expr::Lambda { body, .. } => has_recursive_calls(func_name, body),
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
elements.iter().any(|e| has_recursive_calls(func_name, e))
}
Expr::Record { fields, .. } => {
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
}
Expr::Field { object, .. } => has_recursive_calls(func_name, object),
Expr::Let { value, body, .. } => {
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
}
Expr::Run { expr, handlers, .. } => {
has_recursive_calls(func_name, expr)
|| handlers.iter().any(|(_, e)| has_recursive_calls(func_name, e))
}
_ => false,
}
}
/// Find all recursive call arguments: returns Vec of argument lists
fn find_recursive_call_args<'a>(func_name: &str, body: &'a Expr) -> Vec<&'a [Expr]> {
let mut result = Vec::new();
collect_recursive_call_args(func_name, body, &mut result);
result
}
fn collect_recursive_call_args<'a>(func_name: &str, body: &'a Expr, result: &mut Vec<&'a [Expr]>) {
match body {
Expr::Call { func, args, .. } => {
if let Expr::Var(id) = func.as_ref() {
if id.name == func_name {
result.push(args.as_slice());
}
}
collect_recursive_call_args(func_name, func, result);
for arg in args {
collect_recursive_call_args(func_name, arg, result);
}
}
Expr::BinaryOp { left, right, .. } => {
collect_recursive_call_args(func_name, left, result);
collect_recursive_call_args(func_name, right, result);
}
Expr::UnaryOp { operand, .. } => {
collect_recursive_call_args(func_name, operand, result);
}
Expr::If {
condition,
then_branch,
else_branch,
..
} => {
collect_recursive_call_args(func_name, condition, result);
collect_recursive_call_args(func_name, then_branch, result);
collect_recursive_call_args(func_name, else_branch, result);
}
Expr::Block { statements, result: res, .. } => {
for s in statements {
match s {
Statement::Let { value, .. } => {
collect_recursive_call_args(func_name, value, result)
}
Statement::Expr(e) => collect_recursive_call_args(func_name, e, result),
}
}
collect_recursive_call_args(func_name, res, result);
}
Expr::Match { scrutinee, arms, .. } => {
collect_recursive_call_args(func_name, scrutinee, result);
for arm in arms {
collect_recursive_call_args(func_name, &arm.body, result);
}
}
Expr::Lambda { body, .. } => collect_recursive_call_args(func_name, body, result),
Expr::Let { value, body, .. } => {
collect_recursive_call_args(func_name, value, result);
collect_recursive_call_args(func_name, body, result);
}
_ => {}
}
}
/// Check if an argument is structurally decreasing from a parameter
/// Recognizes patterns like: n - 1, n - k (for positive k)
fn is_structurally_decreasing(arg: &Expr, param_name: &str) -> bool {
match arg {
// Pattern: n - 1, n - k for positive k
Expr::BinaryOp {
op: BinaryOp::Sub,
left,
right,
..
} => {
if let Expr::Var(id) = left.as_ref() {
if id.name == param_name {
// Check if right side is a positive integer literal
if let Expr::Literal(lit) = right.as_ref() {
if let LiteralKind::Int(n) = lit.kind {
return n > 0;
}
}
}
}
false
}
_ => false,
}
}
/// Check if a function terminates (structural recursion check)
fn check_termination(func: &FunctionDecl) -> Result<(), String> {
// Non-recursive functions always terminate
if !has_recursive_calls(&func.name.name, &func.body) {
return Ok(());
}
// For recursive functions, check structural recursion
let param_names: Vec<&str> = func.params.iter().map(|p| p.name.name.as_str()).collect();
let recursive_calls = find_recursive_call_args(&func.name.name, &func.body);
for call_args in recursive_calls {
// At least one argument must be structurally decreasing
let has_decreasing = call_args.iter().enumerate().any(|(i, arg)| {
if i < param_names.len() {
is_structurally_decreasing(arg, param_names[i])
} else {
false
}
});
if !has_decreasing {
return Err("Recursive call has no provably decreasing argument".to_string());
}
}
Ok(())
}
/// Property constraint on a function parameter
#[derive(Debug, Clone)]
pub struct ParamPropertyConstraint {
pub param_name: String,
pub required_properties: PropertySet,
}
/// Type checker
pub struct TypeChecker {
env: TypeEnv,
@@ -111,6 +494,14 @@ pub struct TypeChecker {
errors: Vec<TypeError>,
/// Type parameters in scope (maps "T" -> Type::Var(n) for generics)
type_params: HashMap<String, Type>,
/// Property constraints from where clauses: func_name -> Vec<(param_name, properties)>
property_constraints: HashMap<String, Vec<ParamPropertyConstraint>>,
/// Versioned type definitions: type_name -> version -> TypeDef
versioned_types: HashMap<String, HashMap<u32, types::TypeDef>>,
/// Latest version for each versioned type: type_name -> highest_version
latest_versions: HashMap<String, u32>,
/// Migrations: type_name -> source_version -> migration_body
migrations: HashMap<String, HashMap<u32, Expr>>,
}
impl TypeChecker {
@@ -122,6 +513,10 @@ impl TypeChecker {
inferring_effects: false,
errors: Vec::new(),
type_params: HashMap::new(),
property_constraints: HashMap::new(),
versioned_types: HashMap::new(),
latest_versions: HashMap::new(),
migrations: HashMap::new(),
}
}
@@ -280,6 +675,32 @@ impl TypeChecker {
let type_def = self.type_def(type_decl);
self.env.types.insert(type_decl.name.name.clone(), type_def.clone());
// Track versioned types for schema evolution
if let Some(version) = &type_decl.version {
let type_name = type_decl.name.name.clone();
let version_num = version.number;
// Store the type definition for this version
self.versioned_types
.entry(type_name.clone())
.or_default()
.insert(version_num, type_def.clone());
// Update latest version if this is higher
let current_latest = self.latest_versions.get(&type_name).copied().unwrap_or(0);
if version_num > current_latest {
self.latest_versions.insert(type_name.clone(), version_num);
}
// Register migrations (Phase 4)
for migration in &type_decl.migrations {
self.migrations
.entry(type_name.clone())
.or_default()
.insert(migration.from_version.number, migration.body.clone());
}
}
// Register ADT constructors as values with polymorphic types
if let ast::TypeDef::Enum(variants) = &type_decl.definition {
for variant in variants {
@@ -438,6 +859,89 @@ impl TypeChecker {
});
}
// Deterministic functions cannot use non-deterministic effects (Random, Time)
if properties.contains(Property::Deterministic) {
let non_det_effects: Vec<_> = effective_effects
.effects
.iter()
.filter(|e| matches!(e.as_str(), "Random" | "Time"))
.cloned()
.collect();
if !non_det_effects.is_empty() {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as deterministic but uses non-deterministic effects: {{{}}}",
func.name.name,
non_det_effects.join(", ")
),
span: func.span,
});
}
}
// Commutative functions must have 2 params and use a commutative operation
if properties.contains(Property::Commutative) {
if func.params.len() != 2 {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as commutative but has {} parameters (expected 2)",
func.name.name,
func.params.len()
),
span: func.span,
});
} else if !is_commutative_body(&func.body, &func.params[0].name.name, &func.params[1].name.name) {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as commutative but its body is not a commutative operation on its parameters",
func.name.name
),
span: func.span,
});
}
}
// Idempotent functions must satisfy f(f(x)) == f(x)
// We verify this through pattern recognition
if properties.contains(Property::Idempotent) {
if !is_idempotent_body(&func.body, &func.params) {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as idempotent but could not be verified. \
Recognized patterns: identity, constants, clamping, projections, abs. \
Use 'assume is idempotent' if you're certain it is idempotent.",
func.name.name
),
span: func.span,
});
}
}
// Total functions must terminate and cannot fail
if properties.is_total() {
// Check 1: Cannot use Fail effect
if effective_effects.contains("Fail") {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as total but uses the Fail effect",
func.name.name
),
span: func.span,
});
}
// Check 2: Must terminate (structural recursion)
if let Err(reason) = check_termination(func) {
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as total but may not terminate: {}",
func.name.name, reason
),
span: func.span,
});
}
}
// If effects were declared, verify that inferred effects are a subset
if explicit_effects && !inferred.is_subset(&declared_effects) {
let missing: Vec<_> = inferred
@@ -465,9 +969,35 @@ impl TypeChecker {
property,
span,
} => {
// Record the constraint for later checking when the function is called
// For now, we just validate that the type parameter exists
if !func.type_params.iter().any(|p| p.name == type_param.name)
// Find which parameter has this type and record the constraint
let param_with_type = func.params.iter().find(|p| {
// Check if param's type is the type parameter
if let TypeExpr::Named(name) = &p.typ {
name.name == type_param.name
} else {
false
}
});
if let Some(param) = param_with_type {
// Record the constraint for checking at call sites
let constraints = self
.property_constraints
.entry(func.name.name.clone())
.or_insert_with(Vec::new);
// Check if we already have a constraint for this param
if let Some(existing) = constraints.iter_mut().find(|c| c.param_name == param.name.name) {
existing.required_properties.insert(Property::from(*property));
} else {
let mut props = PropertySet::empty();
props.insert(Property::from(*property));
constraints.push(ParamPropertyConstraint {
param_name: param.name.name.clone(),
required_properties: props,
});
}
} else if !func.type_params.iter().any(|p| p.name == type_param.name)
&& !func.params.iter().any(|p| p.name.name == type_param.name)
{
self.errors.push(TypeError {
@@ -525,7 +1055,8 @@ impl TypeChecker {
fn check_let_decl(&mut self, let_decl: &LetDecl) {
let inferred = self.infer_expr(&let_decl.value);
if let Some(ref type_expr) = let_decl.typ {
// Use the declared type if present, otherwise use inferred
let final_type = if let Some(ref type_expr) = let_decl.typ {
let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&inferred, &declared) {
self.errors.push(TypeError {
@@ -536,10 +1067,14 @@ impl TypeChecker {
span: let_decl.span,
});
}
}
// Use declared type (preserves version annotations)
declared
} else {
inferred
};
// Update the binding with the inferred type
let scheme = self.env.generalize(&inferred);
// Update the binding with the final type
let scheme = self.env.generalize(&final_type);
self.env.bind(&let_decl.name.name, scheme);
}
@@ -870,6 +1405,48 @@ impl TypeChecker {
let func_type = self.infer_expr(func);
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
// Check property constraints from where clauses
if let Expr::Var(func_id) = func {
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
// Get parameter names from the function declaration (if available in env)
// We'll match by position since we have the constraints by param name
if let Some(scheme) = self.env.lookup(&func_id.name) {
let func_typ = scheme.instantiate();
if let Type::Function { params: param_types, .. } = &func_typ {
for constraint in &constraints {
// Find which argument position corresponds to this param
// For now, match by position based on stored param names
for (i, arg) in args.iter().enumerate() {
// Get the properties of the argument
let arg_props = self.get_expr_properties(arg);
// Check if this argument corresponds to a constrained param
// We check all constraints and verify the arg satisfies them
if !arg_props.satisfies(&constraint.required_properties) {
// Only report if this argument could be the constrained one
// (simple heuristic: function type argument)
if i < param_types.len() {
if let Type::Function { .. } = &param_types[i] {
self.errors.push(TypeError {
message: format!(
"Argument to '{}' does not satisfy property constraint: \
expected {:?}, but argument has {:?}",
func_id.name,
constraint.required_properties,
arg_props
),
span: arg.span(),
});
}
}
}
}
}
}
}
}
}
let result_type = Type::var();
// Include current effects in the expected function type
// This allows calling functions that require effects when those effects are available
@@ -891,6 +1468,26 @@ impl TypeChecker {
}
}
/// Get the behavioral properties of an expression (conservative)
fn get_expr_properties(&self, expr: &Expr) -> PropertySet {
match expr {
Expr::Var(id) => {
// Look up the function and get its properties
if let Some(scheme) = self.env.lookup(&id.name) {
let typ = scheme.instantiate();
if let Type::Function { properties, .. } = typ {
return properties;
}
}
PropertySet::empty()
}
// Lambdas: could analyze but for now be conservative
Expr::Lambda { .. } => PropertySet::empty(),
// Other expressions: no properties
_ => PropertySet::empty(),
}
}
fn infer_effect_op(
&mut self,
effect: &Ident,
@@ -935,7 +1532,7 @@ impl TypeChecker {
}
// Built-in effects are always available
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http"];
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"];
let is_builtin = builtin_effects.contains(&effect.name.as_str());
// Track this effect for inference
@@ -1440,7 +2037,7 @@ impl TypeChecker {
// Built-in effects are always available in run blocks (they have runtime implementations)
let builtin_effects: EffectSet =
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http"].iter().map(|s| s.to_string()));
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"].iter().map(|s| s.to_string()));
// Extend current effects with handled ones and built-in effects
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
@@ -1796,13 +2393,18 @@ impl TypeChecker {
.collect(),
),
TypeExpr::Unit => Type::Unit,
TypeExpr::Versioned {
base,
constraint: _,
} => {
// For now, resolve the base type and ignore versioning
// Full version tracking will be added in the type system
self.resolve_type(base)
TypeExpr::Versioned { base, constraint } => {
// Resolve the base type and preserve version information
let base_type = self.resolve_type(base);
let version_info = match constraint {
ast::VersionConstraint::Exact(v) => VersionInfo::Exact(v.number),
ast::VersionConstraint::AtLeast(v) => VersionInfo::AtLeast(v.number),
ast::VersionConstraint::Latest(_) => VersionInfo::Latest,
};
Type::Versioned {
base: Box::new(base_type),
version: version_info,
}
}
}
}

View File

@@ -1074,6 +1074,55 @@ impl TypeEnv {
},
);
// Add HttpServer effect
let http_request_type = Type::Record(vec![
("method".to_string(), Type::String),
("path".to_string(), Type::String),
("body".to_string(), Type::String),
("headers".to_string(), Type::List(Box::new(Type::Tuple(vec![Type::String, Type::String])))),
]);
env.effects.insert(
"HttpServer".to_string(),
EffectDef {
name: "HttpServer".to_string(),
type_params: Vec::new(),
operations: vec![
EffectOpDef {
name: "listen".to_string(),
params: vec![("port".to_string(), Type::Int)],
return_type: Type::Unit,
},
EffectOpDef {
name: "accept".to_string(),
params: vec![],
return_type: http_request_type.clone(),
},
EffectOpDef {
name: "respond".to_string(),
params: vec![
("status".to_string(), Type::Int),
("body".to_string(), Type::String),
],
return_type: Type::Unit,
},
EffectOpDef {
name: "respondWithHeaders".to_string(),
params: vec![
("status".to_string(), Type::Int),
("body".to_string(), Type::String),
("headers".to_string(), Type::List(Box::new(Type::Tuple(vec![Type::String, Type::String])))),
],
return_type: Type::Unit,
},
EffectOpDef {
name: "stop".to_string(),
params: vec![],
return_type: Type::Unit,
},
],
},
);
// Add Some and Ok, Err constructors
// Some : fn(a) -> Option<a>
let a = Type::var();