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:
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
48
examples/http_server.lux
Normal file
48
examples/http_server.lux
Normal file
@@ -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()
|
||||
@@ -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: {}.{}",
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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, ¶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<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 { .. } = ¶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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/types.rs
49
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<a>
|
||||
let a = Type::var();
|
||||
|
||||
Reference in New Issue
Block a user