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:
@@ -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: {}.{}",
|
||||
|
||||
Reference in New Issue
Block a user