diff --git a/Cargo.lock b/Cargo.lock index a81fc24..8d888ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "autocfg" version = "1.5.0" @@ -84,6 +90,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -808,6 +820,7 @@ dependencies = [ "target-lexicon", "tempfile", "thiserror", + "tiny_http", ] [[package]] @@ -1408,6 +1421,18 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinystr" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 853f2b0..90f35fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" rand = "0.8" reqwest = { version = "0.11", features = ["blocking", "json"] } +tiny_http = "0.12" # Cranelift for native compilation cranelift-codegen = "0.95" diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index 5390158..0ad7842 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -128,10 +128,16 @@ let content = File.read("config.txt") File.write("output.txt", "Hello!") let exists = File.exists("file.lux") -// HTTP effects +// HTTP client effects let response = Http.get("https://api.example.com/data") Http.post("https://api.example.com/submit", "{\"key\": \"value\"}") +// HTTP server effects +HttpServer.listen(8080) +let req = HttpServer.accept() // Returns { method, path, body, headers } +HttpServer.respond(200, "Hello, World!") +HttpServer.stop() + // Random effects let n = Random.int(1, 100) let coin = Random.bool() diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d869182..fc9fd8a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -57,10 +57,10 @@ | Task | Priority | Effort | Status | |------|----------|--------|--------| -| HTTP server effect | P1 | 2 weeks | ❌ Missing | +| HTTP server effect | P1 | 2 weeks | ✅ Complete | | Routing DSL | P2 | 1 week | ❌ Missing | | Middleware pattern | P2 | 1 week | ❌ Missing | -| Request/Response types | P1 | 3 days | ❌ Missing | +| Request/Response types | P1 | 3 days | ✅ Complete (via HttpServer effect) | ### Phase 1.4: Developer Experience diff --git a/examples/http_server.lux b/examples/http_server.lux new file mode 100644 index 0000000..f9bca6c --- /dev/null +++ b/examples/http_server.lux @@ -0,0 +1,48 @@ +// HTTP Server Example +// Run with: cargo run -- examples/http_server.lux +// Then visit: http://localhost:8080/ + +// Simple request handler +fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {Console, HttpServer} = { + Console.print("Received: " + req.method + " " + req.path) + + // Route based on path + match req.path { + "/" => HttpServer.respond(200, "Welcome to Lux HTTP Server!"), + "/hello" => HttpServer.respond(200, "Hello, World!"), + "/json" => HttpServer.respondWithHeaders( + 200, + "{\"message\": \"Hello from Lux!\"}", + [("Content-Type", "application/json")] + ), + "/echo" => HttpServer.respond(200, "You sent: " + req.body), + _ => HttpServer.respond(404, "Not Found: " + req.path) + } +} + +// Handle N requests then stop +fn serveN(n: Int): Unit with {Console, HttpServer} = + if n <= 0 then { + Console.print("Server stopping...") + HttpServer.stop() + } else { + let req = HttpServer.accept() + handleRequest(req) + serveN(n - 1) + } + +// Main entry point +fn main(): Unit with {Console, HttpServer} = { + let port = 8080 + Console.print("Starting HTTP server on port " + toString(port) + "...") + HttpServer.listen(port) + Console.print("Server listening! Will handle 5 requests then stop.") + Console.print("Try: curl http://localhost:8080/") + Console.print(" curl http://localhost:8080/hello") + Console.print(" curl http://localhost:8080/json") + Console.print(" curl -X POST -d 'test data' http://localhost:8080/echo") + serveN(5) +} + +// Run main +main() diff --git a/src/interpreter.rs b/src/interpreter.rs index 7de51b2..c0ea8e4 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -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, /// 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 for thread-safety with tiny_http) + http_server: Arc>>, + /// Current HTTP request being handled (stored for respond operation) + current_http_request: Arc>>, } 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 = 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: {}.{}", diff --git a/src/main.rs b/src/main.rs index 0a6cd86..9750129 100644 --- a/src/main.rs +++ b/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(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() { diff --git a/src/typechecker.rs b/src/typechecker.rs index 9b9c4ee..11ff365 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -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) { } } +/// 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, ¶m_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, ¶ms[0].name.name) { + return true; + } + + // Pattern 5: Absolute value - if x < 0 then -x else x + if params.len() == 1 && is_abs_pattern(body, ¶ms[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, /// Type parameters in scope (maps "T" -> Type::Var(n) for generics) type_params: HashMap, + /// Property constraints from where clauses: func_name -> Vec<(param_name, properties)> + property_constraints: HashMap>, + /// Versioned type definitions: type_name -> version -> TypeDef + versioned_types: HashMap>, + /// Latest version for each versioned type: type_name -> highest_version + latest_versions: HashMap, + /// Migrations: type_name -> source_version -> migration_body + migrations: HashMap>, } 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 = 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 { .. } = ¶m_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, + } } } } diff --git a/src/types.rs b/src/types.rs index be4d90a..0a98d74 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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 let a = Type::var();