diff --git a/Cargo.lock b/Cargo.lock index bade204..e459c1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -171,6 +177,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "cipher" version = "0.4.4" @@ -312,6 +324,7 @@ dependencies = [ name = "el" version = "0.1.0" dependencies = [ + "blake3", "clap", "el-build", "el-compiler", @@ -325,7 +338,10 @@ dependencies = [ "reqwest", "serde_json", "thiserror 2.0.18", + "tiny_http", "tokio", + "uuid", + "walkdir", ] [[package]] @@ -711,6 +727,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.9.0" @@ -1425,6 +1447,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -1692,6 +1723,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.3" @@ -1970,6 +2013,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2121,6 +2174,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 22c56ac..6343cbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,6 @@ aes-gcm = "0.10" rand = "0.8" clap = { version = "4", features = ["derive", "env"] } reqwest = { version = "0.12", features = ["json", "blocking"] } +uuid = { version = "1", features = ["v4"] } +walkdir = "2" +tiny_http = "0.12" diff --git a/bin/el/Cargo.toml b/bin/el/Cargo.toml index b884002..7cf8370 100644 --- a/bin/el/Cargo.toml +++ b/bin/el/Cargo.toml @@ -24,3 +24,7 @@ thiserror = { workspace = true } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } reqwest = { workspace = true } serde_json = { workspace = true } +uuid = { workspace = true } +walkdir = { workspace = true } +tiny_http = { workspace = true } +blake3 = { workspace = true } diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index 313bf16..35ff94c 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -36,6 +36,26 @@ use el_test; use el_manifest::{BuildTarget, Manifest}; use el_seal::{seal as seal_fn, unseal as unseal_fn, SealedArtifact, DeploymentBinding, SealAlgorithm, SealConfig}; +// ── Global state (thread-local for simplicity) ──────────────────────────────── + +thread_local! { + static GLOBAL_STATE: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashMap::new()); + + /// Callback set by the interpreter before http_serve blocks. + /// When a request arrives, http_serve calls this to invoke handle_request. + static HTTP_SERVE_CALL: std::cell::RefCell>> = + std::cell::RefCell::new(None); + + /// Shared bytecode instructions for sub-interpreter calls from http_serve. + static SERVE_INSTRUCTIONS: std::cell::RefCell>>> = + std::cell::RefCell::new(None); + + /// Shared fn_table for sub-interpreter calls. + static SERVE_FN_TABLE: std::cell::RefCell>>> = + std::cell::RefCell::new(None); +} + // ── CLI definition ──────────────────────────────────────────────────────────── #[derive(Parser, Debug)] @@ -441,8 +461,8 @@ async fn run(cli: Cli) -> Result<(), Box> { // ── Low-level / single-file ─────────────────────────────────────────── Command::RunFile { file, args } => { - let source = std::fs::read_to_string(&file) - .map_err(|e| format!("cannot read {}: {e}", file.display()))?; + let source = resolve_imports(&file) + .map_err(|e| format!("cannot resolve imports for {}: {e}", file.display()))?; let opts = CompilerOptions { target: Target::Debug, @@ -689,6 +709,56 @@ fn cmd_plugin_add(plugin: &str) -> Result<(), Box> { // ── Helpers ─────────────────────────────────────────────────────────────────── +/// Resolve `import "path.el"` directives by reading and concatenating source files. +/// Imports are resolved relative to the directory of the file being imported from. +/// Circular imports are detected via a visited set. +fn resolve_imports(file: &std::path::Path) -> Result> { + let mut visited = std::collections::HashSet::new(); + resolve_imports_inner(file, &mut visited) +} + +fn resolve_imports_inner( + file: &std::path::Path, + visited: &mut std::collections::HashSet, +) -> Result> { + let canonical = file.canonicalize() + .unwrap_or_else(|_| file.to_path_buf()); + + if visited.contains(&canonical) { + return Ok(String::new()); // circular — skip + } + visited.insert(canonical.clone()); + + let dir = file.parent().unwrap_or(std::path::Path::new(".")); + let source = std::fs::read_to_string(file) + .map_err(|e| format!("cannot read {}: {e}", file.display()))?; + + let mut out = String::new(); + + for line in source.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("import ") { + // import "filename.el" + let rest = rest.trim(); + if rest.starts_with('"') && rest.ends_with('"') { + let import_path_str = &rest[1..rest.len() - 1]; + let import_path = dir.join(import_path_str); + let imported = resolve_imports_inner(&import_path, visited)?; + out.push_str(&imported); + out.push('\n'); + } else { + out.push_str(line); + out.push('\n'); + } + } else { + out.push_str(line); + out.push('\n'); + } + } + + Ok(out) +} + /// Find the nearest `el.toml` starting from the current directory. fn resolve_manifest(path: Option<&std::path::Path>) -> Result> { if let Some(p) = path { @@ -776,6 +846,201 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) { run_interpreter_with_args(instructions, &[]); } +/// Run a sub-interpreter starting at the given function entry point. +/// Returns the value left on the stack (the return value). +fn run_sub_interpreter( + instructions: &[el_compiler::Bytecode], + fn_table: &std::collections::HashMap, + entry: usize, +) -> el_compiler::Value { + use el_compiler::{Bytecode, Value}; + let mut stack: Vec = Vec::new(); + let mut locals: std::collections::HashMap = std::collections::HashMap::new(); + let mut call_stack: Vec<(usize, std::collections::HashMap)> = Vec::new(); + let mut ip = entry; + let program_args: Vec = vec![]; + + // Bind zero params (handle_request takes none) + + while ip < instructions.len() { + match &instructions[ip] { + Bytecode::Push(v) => stack.push(v.clone()), + Bytecode::Pop => { stack.pop(); } + Bytecode::Dup => { + if let Some(top) = stack.last().cloned() { stack.push(top); } + } + Bytecode::Add => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x + y), + (Value::Float(x), Value::Float(y)) => Value::Float(x + y), + (Value::Str(x), Value::Str(y)) => Value::Str(x + &y), + _ => Value::Nil, + }); + } + Bytecode::Sub => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x - y), + (Value::Float(x), Value::Float(y)) => Value::Float(x - y), + _ => Value::Nil, + }); + } + Bytecode::Mul => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Int(x * y), + (Value::Float(x), Value::Float(y)) => Value::Float(x * y), + _ => Value::Nil, + }); + } + Bytecode::Div => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) if y != 0 => Value::Int(x / y), + (Value::Float(x), Value::Float(y)) => Value::Float(x / y), + _ => Value::Nil, + }); + } + Bytecode::Eq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a == b)); + } + Bytecode::NotEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool(a != b)); + } + Bytecode::Lt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Bool(x < y), + _ => Value::Bool(false), + }); + } + Bytecode::Gt => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Bool(x > y), + _ => Value::Bool(false), + }); + } + Bytecode::LtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Bool(x <= y), + _ => Value::Bool(false), + }); + } + Bytecode::GtEq => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(match (a, b) { + (Value::Int(x), Value::Int(y)) => Value::Bool(x >= y), + _ => Value::Bool(false), + }); + } + Bytecode::And => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) && matches!(b, Value::Bool(true)) + )); + } + Bytecode::Or => { + let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil)); + stack.push(Value::Bool( + matches!(a, Value::Bool(true)) || matches!(b, Value::Bool(true)) + )); + } + Bytecode::Not => { + let v = stack.pop().unwrap_or(Value::Nil); + stack.push(Value::Bool(!matches!(v, Value::Bool(true)))); + } + Bytecode::StoreLocal(name) => { + let v = stack.pop().unwrap_or(Value::Nil); + locals.insert(name.clone(), v); + } + Bytecode::LoadLocal(name) => { + let v = locals.get(name).cloned().unwrap_or(Value::Nil); + stack.push(v); + } + Bytecode::Call { name, arity } => { + let result = dispatch_builtin(name, *arity, &mut stack, &program_args); + match result { + BuiltinResult::Handled | BuiltinResult::HttpServe => {} + BuiltinResult::Exit(code) => std::process::exit(code), + BuiltinResult::NotBuiltin => { + if let Some(&entry) = fn_table.get(name.as_str()) { + let saved = locals.clone(); + call_stack.push((ip + 1, saved)); + ip = entry; + continue; + } + stack.push(Value::Nil); + } + } + } + Bytecode::GetField(field) => { + stack.pop(); + stack.push(Value::Str(format!(""))); + } + Bytecode::GetIndex => { + let idx = stack.pop().unwrap_or(Value::Nil); + let obj = stack.pop().unwrap_or(Value::Nil); + match (obj, idx) { + (Value::List(items), Value::Int(i)) => { + let v = if i >= 0 && (i as usize) < items.len() { + items[i as usize].clone() + } else { + Value::Nil + }; + stack.push(v); + } + _ => stack.push(Value::Nil), + } + } + Bytecode::Jump(offset) => { + let new_ip = (ip as i32 + 1 + offset) as usize; + ip = new_ip; + continue; + } + Bytecode::JumpIf(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if matches!(cond, Value::Bool(true)) { + let new_ip = (ip as i32 + 1 + offset) as usize; + ip = new_ip; + continue; + } + } + Bytecode::JumpIfNot(offset) => { + let cond = stack.pop().unwrap_or(Value::Nil); + if !matches!(cond, Value::Bool(true)) { + let new_ip = (ip as i32 + 1 + offset) as usize; + ip = new_ip; + continue; + } + } + Bytecode::Return => { + if call_stack.is_empty() { + // Return from handle_request — value is on stack + break; + } + if let Some((ret_ip, saved_locals)) = call_stack.pop() { + locals = saved_locals; + ip = ret_ip; + continue; + } + } + Bytecode::Halt => break, + Bytecode::Activate { .. } => { + stack.push(Value::List(vec![])); + } + _ => {} + } + ip += 1; + } + + stack.pop().unwrap_or(Value::Nil) +} + /// Interpreter with program args — the args() builtin returns these. fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_args: &[String]) { use el_compiler::{Bytecode, Value}; @@ -811,6 +1076,45 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg } } + // Store instructions and fn_table in thread-locals so http_serve can call + // handle_request via a sub-interpreter invocation. + let arc_instructions = std::sync::Arc::new(instructions.to_vec()); + let arc_fn_table = std::sync::Arc::new(fn_table.clone()); + let arc_instructions_clone = arc_instructions.clone(); + let arc_fn_table_clone = arc_fn_table.clone(); + + SERVE_INSTRUCTIONS.with(|si| *si.borrow_mut() = Some(arc_instructions.clone())); + SERVE_FN_TABLE.with(|sf| *sf.borrow_mut() = Some(arc_fn_table.clone())); + + // Set up the http_serve callback that calls handle_request + HTTP_SERVE_CALL.with(|f| { + *f.borrow_mut() = Some(Box::new(move || { + // Run a sub-interpreter starting at handle_request + if let Some(entry) = arc_fn_table_clone.get("handle_request") { + let result = run_sub_interpreter( + &arc_instructions_clone, + &arc_fn_table_clone, + *entry, + ); + // Store the result as __response__ in global state + let response = match result { + el_compiler::Value::Str(s) => s, + other => other.to_string(), + }; + GLOBAL_STATE.with(|gs| { + gs.borrow_mut().insert("__response__".to_string(), response); + }); + } else { + GLOBAL_STATE.with(|gs| { + gs.borrow_mut().insert( + "__response__".to_string(), + r#"{"error":"handle_request function not found"}"#.to_string(), + ); + }); + } + })); + }); + // Call stack for user-defined function calls: (return_ip, saved_locals) let mut call_stack: Vec<(usize, std::collections::HashMap)> = Vec::new(); @@ -921,7 +1225,7 @@ fn run_interpreter_with_args(instructions: &[el_compiler::Bytecode], program_arg Bytecode::Call { name, arity } => { let result = dispatch_builtin(name, *arity, &mut stack, program_args); match result { - BuiltinResult::Handled => {} + BuiltinResult::Handled | BuiltinResult::HttpServe => {} BuiltinResult::Exit(code) => std::process::exit(code), BuiltinResult::NotBuiltin => { // Try user-defined function @@ -1011,6 +1315,9 @@ enum BuiltinResult { Handled, Exit(i32), NotBuiltin, + /// http_serve was called — the interpreter should treat this like Handled + /// but the actual serve loop is blocking inside dispatch_builtin. + HttpServe, } fn dispatch_builtin( @@ -1237,6 +1544,533 @@ fn dispatch_builtin( stack.push(Value::List(items)); BuiltinResult::Handled } + + // ── Filesystem builtins ─────────────────────────────────────────────── + + "fs_read" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } + }; + let result = std::fs::read_to_string(&path) + .map(Value::Str) + .unwrap_or(Value::Nil); + stack.push(result); + BuiltinResult::Handled + } + "fs_write" => { + let content = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let ok = std::fs::write(&path, &content).is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_append" => { + let content = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + use std::io::Write; + let ok = std::fs::OpenOptions::new() + .create(true).append(true).open(&path) + .and_then(|mut f| f.write_all(content.as_bytes())) + .is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_exists" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Bool(std::path::Path::new(&path).exists())); + BuiltinResult::Handled + } + "fs_mkdir" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let ok = std::fs::create_dir_all(&path).is_ok(); + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_list" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let entries = std::fs::read_dir(&path) + .map(|rd| { + rd.filter_map(|e| { + e.ok().and_then(|e| { + e.file_name().into_string().ok().map(Value::Str) + }) + }).collect::>() + }) + .unwrap_or_default(); + stack.push(Value::List(entries)); + BuiltinResult::Handled + } + "fs_remove" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let p = std::path::Path::new(&path); + let ok = if p.is_dir() { + std::fs::remove_dir(p).is_ok() + } else { + std::fs::remove_file(p).is_ok() + }; + stack.push(Value::Bool(ok)); + BuiltinResult::Handled + } + "fs_is_dir" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Bool(std::path::Path::new(&path).is_dir())); + BuiltinResult::Handled + } + "path_join" => { + let name = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let base = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let joined = std::path::Path::new(&base).join(&name); + stack.push(Value::Str(joined.to_string_lossy().to_string())); + BuiltinResult::Handled + } + "path_parent" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let parent = std::path::Path::new(&path) + .parent() + .map(|p| Value::Str(p.to_string_lossy().to_string())) + .unwrap_or(Value::Nil); + stack.push(parent); + BuiltinResult::Handled + } + + // ── Filesystem recursive list ───────────────────────────────────────── + + "fs_list_recursive" => { + let path = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let skip_dirs = ["node_modules", ".git", ".nc", "target", "__pycache__", ".el"]; + let mut files = Vec::new(); + if let Ok(walker) = walkdir::WalkDir::new(&path).into_iter().collect::, _>>() { + for entry in walker { + // Skip hidden/build dirs + let should_skip = entry.path().components().any(|c| { + let s = c.as_os_str().to_string_lossy(); + skip_dirs.iter().any(|d| s == *d) + }); + if should_skip && entry.path() != std::path::Path::new(&path) { + continue; + } + if entry.file_type().is_file() { + files.push(Value::Str(entry.path().to_string_lossy().to_string())); + } + } + } + stack.push(Value::List(files)); + BuiltinResult::Handled + } + + // ── Crypto / ID builtins ────────────────────────────────────────────── + + "blake3_hash" => { + let content = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let hash = blake3::hash(content.as_bytes()); + stack.push(Value::Str(hash.to_hex().to_string())); + BuiltinResult::Handled + } + "uuid_new" => { + let id = uuid::Uuid::new_v4().to_string(); + stack.push(Value::Str(id)); + BuiltinResult::Handled + } + "now_millis" => { + let ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0); + stack.push(Value::Int(ms)); + BuiltinResult::Handled + } + + // ── State builtins ──────────────────────────────────────────────────── + + "state_set" => { + let value = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + GLOBAL_STATE.with(|gs| gs.borrow_mut().insert(key, value)); + stack.push(Value::Bool(true)); + BuiltinResult::Handled + } + "state_get" => { + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let val = GLOBAL_STATE.with(|gs| { + gs.borrow().get(&key).cloned().map(Value::Str).unwrap_or(Value::Nil) + }); + stack.push(val); + BuiltinResult::Handled + } + "state_del" => { + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + GLOBAL_STATE.with(|gs| gs.borrow_mut().remove(&key)); + stack.push(Value::Bool(true)); + BuiltinResult::Handled + } + "state_keys" => { + let keys = GLOBAL_STATE.with(|gs| { + gs.borrow().keys().cloned().map(Value::Str).collect::>() + }); + stack.push(Value::List(keys)); + BuiltinResult::Handled + } + + // ── HTTP server builtin ─────────────────────────────────────────────── + // http_serve starts a blocking HTTP server. It calls handle_request + // for each POST /axon/message by invoking the stored callback. + + "http_serve" => { + let port = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as u16, + _ => 7890, + }; + + let addr = format!("0.0.0.0:{port}"); + let server = tiny_http::Server::http(&addr) + .unwrap_or_else(|e| panic!("cannot bind to {addr}: {e}")); + println!("Neuron Code · Engram edition · http://localhost:{port}"); + + for mut request in server.incoming_requests() { + let method = request.method().to_string(); + let url = request.url().to_string(); + + // GET /health + if method == "GET" && url == "/health" { + let _ = request.respond( + tiny_http::Response::from_string(r#"{"status":"ok","version":"0.1.0"}"#) + .with_header("Content-Type: application/json".parse::().unwrap()) + ); + continue; + } + + // POST /axon/message + if method == "POST" && (url == "/axon/message" || url.starts_with("/axon/message?")) { + // Read body + let mut body = String::new(); + { + use std::io::Read; + let _ = request.as_reader().read_to_string(&mut body); + } + + // Store request in state + GLOBAL_STATE.with(|gs| gs.borrow_mut().insert("__request__".to_string(), body)); + GLOBAL_STATE.with(|gs| gs.borrow_mut().remove("__response__")); + + // Call handle_request via the thread-local fn executor + HTTP_SERVE_CALL.with(|f| { + if let Some(ref call_fn) = *f.borrow() { + call_fn(); + } + }); + + let response_body = GLOBAL_STATE.with(|gs| { + gs.borrow().get("__response__").cloned() + .unwrap_or_else(|| r#"{"error":"no response"}"#.to_string()) + }); + + let _ = request.respond( + tiny_http::Response::from_string(response_body) + .with_header("Content-Type: application/json".parse::().unwrap()) + ); + continue; + } + + // Default 404 + let _ = request.respond( + tiny_http::Response::from_string(r#"{"error":"not found"}"#) + .with_status_code(404) + ); + } + stack.push(Value::Nil); + BuiltinResult::Handled + } + + // ── String utility builtins ─────────────────────────────────────────── + + "str_replace" => { + let to = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let from = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Str(s.replace(&from, &to))); + BuiltinResult::Handled + } + "str_to_lowercase" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Str(s.to_lowercase())); + BuiltinResult::Handled + } + "str_trim" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Str(s.trim().to_string())); + BuiltinResult::Handled + } + "str_index_of" => { + let sub = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let idx = s.find(&sub).map(|i| i as i64).unwrap_or(-1); + stack.push(Value::Int(idx)); + BuiltinResult::Handled + } + "str_last_index_of" => { + let sub = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let idx = s.rfind(&sub).map(|i| i as i64).unwrap_or(-1); + stack.push(Value::Int(idx)); + BuiltinResult::Handled + } + "str_slice" => { + let end = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as usize, + _ => 0, + }; + let start = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as usize, + _ => 0, + }; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let chars: Vec = s.chars().collect(); + let start = start.min(chars.len()); + let end = end.min(chars.len()); + let slice: String = chars[start..end].iter().collect(); + stack.push(Value::Str(slice)); + BuiltinResult::Handled + } + "str_ends_with" => { + let suffix = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + stack.push(Value::Bool(s.ends_with(suffix.as_str()))); + BuiltinResult::Handled + } + + // ── JSON utility builtins ───────────────────────────────────────────── + + "json_set" => { + let value = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let key = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "{}".to_string(), + }; + let result = serde_json::from_str::(&json_str) + .ok() + .and_then(|mut v| { + if let serde_json::Value::Object(ref mut map) = v { + // Try to parse value as JSON, otherwise store as string + let jv = serde_json::from_str(&value) + .unwrap_or(serde_json::Value::String(value.clone())); + map.insert(key.clone(), jv); + serde_json::to_string(&v).ok() + } else { + None + } + }) + .unwrap_or_else(|| format!("{{\"{key}\":\"{value}\"}}")); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + "json_keys" => { + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "{}".to_string(), + }; + let keys = serde_json::from_str::(&json_str) + .ok() + .and_then(|v| { + if let serde_json::Value::Object(map) = v { + Some(map.keys().cloned().map(Value::Str).collect::>()) + } else { + None + } + }) + .unwrap_or_default(); + stack.push(Value::List(keys)); + BuiltinResult::Handled + } + "json_array_push" => { + let item = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "null".to_string(), + }; + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "[]".to_string(), + }; + let result = serde_json::from_str::(&json_str) + .ok() + .and_then(|mut v| { + if let serde_json::Value::Array(ref mut arr) = v { + let item_val = serde_json::from_str(&item) + .unwrap_or(serde_json::Value::String(item.clone())); + arr.push(item_val); + serde_json::to_string(&v).ok() + } else { + None + } + }) + .unwrap_or_else(|| format!("[{}]", item)); + stack.push(Value::Str(result)); + BuiltinResult::Handled + } + "json_array_len" => { + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "[]".to_string(), + }; + let len = serde_json::from_str::(&json_str) + .ok() + .and_then(|v| if let serde_json::Value::Array(arr) = v { Some(arr.len() as i64) } else { None }) + .unwrap_or(0); + stack.push(Value::Int(len)); + BuiltinResult::Handled + } + "json_array_get" => { + let idx = match stack.pop().unwrap_or(Value::Nil) { + Value::Int(n) => n as usize, + _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } + }; + let json_str = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => "[]".to_string(), + }; + let item = serde_json::from_str::(&json_str) + .ok() + .and_then(|v| { + if let serde_json::Value::Array(arr) = v { + arr.get(idx).map(|item| Value::Str(item.to_string())) + } else { + None + } + }) + .unwrap_or(Value::Nil); + stack.push(item); + BuiltinResult::Handled + } + "int_parse" => { + let s = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let result = s.trim().parse::() + .map(Value::Int) + .unwrap_or(Value::Nil); + stack.push(result); + BuiltinResult::Handled + } + "bool_to_str" => { + let b = match stack.pop().unwrap_or(Value::Nil) { + Value::Bool(b) => b, + _ => false, + }; + stack.push(Value::Str(if b { "true".to_string() } else { "false".to_string() })); + BuiltinResult::Handled + } + "list_join" => { + let sep = match stack.pop().unwrap_or(Value::Nil) { + Value::Str(s) => s, + _ => String::new(), + }; + let list = match stack.pop().unwrap_or(Value::Nil) { + Value::List(l) => l, + _ => vec![], + }; + let strs: Vec = list.iter().map(|v| v.to_string()).collect(); + stack.push(Value::Str(strs.join(&sep))); + BuiltinResult::Handled + } + _ => BuiltinResult::NotBuiltin, } } @@ -1292,7 +2126,7 @@ fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut Bytecode::Call { name, arity } => { let result = dispatch_builtin(name, *arity, &mut stack, &program_args); match result { - BuiltinResult::Handled | BuiltinResult::NotBuiltin => {} + BuiltinResult::Handled | BuiltinResult::NotBuiltin | BuiltinResult::HttpServe => {} BuiltinResult::Exit(code) => std::process::exit(code), } } diff --git a/crates/el-compiler/src/codegen.rs b/crates/el-compiler/src/codegen.rs index f356088..c3985f8 100644 --- a/crates/el-compiler/src/codegen.rs +++ b/crates/el-compiler/src/codegen.rs @@ -145,6 +145,8 @@ impl Codegen { // Test-related statements — skipped during normal compilation. // The el-test crate walks the AST directly rather than running compiled bytecode. Stmt::TestDef { .. } | Stmt::Seed(..) | Stmt::Assert(..) => {} + // New statement kinds — no runtime code emitted. + _ => {} } Ok(()) } @@ -355,6 +357,10 @@ impl Codegen { self.gen_expr(index)?; self.emit(Bytecode::GetIndex); } + // New expression kinds — push Nil as placeholder + _ => { + self.emit(Bytecode::Push(Value::Nil)); + } } Ok(()) } @@ -373,6 +379,7 @@ fn stmt_span(stmt: &Stmt) -> el_lexer::Span { | Stmt::TestDef { span, .. } | Stmt::Seed(_, span) | Stmt::Assert(_, span) => *span, + _ => el_lexer::Span::new(0, 0, 0, 0), } } diff --git a/crates/el-lexer/src/lexer.rs b/crates/el-lexer/src/lexer.rs index 1e9c839..fe500b3 100644 --- a/crates/el-lexer/src/lexer.rs +++ b/crates/el-lexer/src/lexer.rs @@ -191,12 +191,11 @@ impl<'src> Lexer<'src> { if self.eat('|') { Token::Or } else { - return Err(LexError::new( - LexErrorKind::UnexpectedChar('|'), - self.span_from(start), - )); + Token::Pipe } } + '@' => Token::At, + '?' => Token::QuestionMark, ':' => { if self.eat(':') { Token::ColonColon @@ -351,6 +350,11 @@ fn keyword_or_ident(s: String) -> Token { "seed" => Token::Seed, "assert" => Token::Assert, "target" => Token::Target, + "protocol" => Token::Protocol, + "impl" => Token::Impl, + "import" => Token::Import, + "from" => Token::From, + "as" => Token::As, "true" => Token::BoolLiteral(true), "false" => Token::BoolLiteral(false), _ => Token::Ident(s), @@ -486,9 +490,10 @@ mod tests { } #[test] - fn test_unexpected_char_error() { - let result = tokenize("@"); - assert!(result.is_err()); + fn test_at_token() { + let tokens = toks("@public"); + assert_eq!(tokens[0], Token::At); + assert_eq!(tokens[1], Token::Ident("public".into())); } #[test] @@ -525,6 +530,31 @@ let msg: String = greet("Will") assert_eq!(tokens[2], Token::Ident("Active".into())); } + #[test] + fn test_new_keywords() { + let tokens = toks("protocol impl import from as"); + assert_eq!(tokens[0], Token::Protocol); + assert_eq!(tokens[1], Token::Impl); + assert_eq!(tokens[2], Token::Import); + assert_eq!(tokens[3], Token::From); + assert_eq!(tokens[4], Token::As); + } + + #[test] + fn test_pipe_token() { + let tokens = toks("|x: Int|"); + assert_eq!(tokens[0], Token::Pipe); + assert_eq!(tokens[1], Token::Ident("x".into())); + assert_eq!(tokens[4], Token::Pipe); + } + + #[test] + fn test_question_mark_token() { + let tokens = toks("x?"); + assert_eq!(tokens[0], Token::Ident("x".into())); + assert_eq!(tokens[1], Token::QuestionMark); + } + #[test] fn test_ident_with_underscore() { let tokens = toks("my_var _private __double"); diff --git a/crates/el-lexer/src/token.rs b/crates/el-lexer/src/token.rs index 1448aca..aa9e3d7 100644 --- a/crates/el-lexer/src/token.rs +++ b/crates/el-lexer/src/token.rs @@ -81,6 +81,16 @@ pub enum Token { Assert, /// `target` — test target annotation (`target: e2e`) Target, + /// `protocol` — protocol definition + Protocol, + /// `impl` — protocol implementation block + Impl, + /// `import` — import statement + Import, + /// `from` — `from package import { ... }` + From, + /// `as` — alias in import (`import X as Y`) + As, /// `true` / `false` BoolLiteral(bool), @@ -151,6 +161,14 @@ pub enum Token { /// `;` Semicolon, + // ── New single-char tokens ──────────────────────────────────────────────── + /// `@` — decorator prefix + At, + /// `|` — closure param delimiter (single pipe, not `||`) + Pipe, + /// `?` — used both for Optional types and the Try operator + QuestionMark, + // ── Special ─────────────────────────────────────────────────────────────── Eof, } @@ -175,6 +193,14 @@ impl std::fmt::Display for Token { Token::Seed => write!(f, "seed"), Token::Assert => write!(f, "assert"), Token::Target => write!(f, "target"), + Token::Protocol => write!(f, "protocol"), + Token::Impl => write!(f, "impl"), + Token::Import => write!(f, "import"), + Token::From => write!(f, "from"), + Token::As => write!(f, "as"), + Token::At => write!(f, "@"), + Token::Pipe => write!(f, "|"), + Token::QuestionMark => write!(f, "?"), Token::BoolLiteral(b) => write!(f, "{b}"), Token::IntLiteral(n) => write!(f, "{n}"), Token::FloatLiteral(n) => write!(f, "{n}"), diff --git a/crates/el-parser/src/ast.rs b/crates/el-parser/src/ast.rs index b804b0e..092578b 100644 --- a/crates/el-parser/src/ast.rs +++ b/crates/el-parser/src/ast.rs @@ -55,6 +55,10 @@ pub enum TypeExpr { Optional(Box), /// A function type: `fn(A, B) -> C` Fn { params: Vec, return_type: Box }, + /// `Result` — built-in error-propagation type + Result { ok: Box, err: Box }, + /// `Map` — built-in key-value map type + Map { key: Box, value: Box }, } // ── Patterns (for match arms) ───────────────────────────────────────────────── @@ -103,6 +107,17 @@ pub enum Expr { Path { segments: Vec }, /// Index expression: `arr[0]` Index { object: Box, index: Box }, + /// Closure: `|x: Int| x * 2` or `|x: Int| -> Int { x * 2 }` + Closure { + params: Vec, + return_type: Option, + body: Box, + span: Span, + }, + /// Try operator: `expr?` — unwraps Result, propagates error + Try(Box), + /// Map literal: `{"key": value, ...}` + MapLiteral(Vec<(Expr, Expr)>), } // ── Match arm ───────────────────────────────────────────────────────────────── @@ -114,6 +129,27 @@ pub struct MatchArm { pub span: Span, } +// ── Decorators ──────────────────────────────────────────────────────────────── + +/// A decorator applied to a function: `@name` or `@name(args)` +#[derive(Debug, Clone, PartialEq)] +pub struct Decorator { + pub name: String, + pub args: Vec, + pub span: Span, +} + +// ── Protocol ────────────────────────────────────────────────────────────────── + +/// A method signature inside a protocol definition. +#[derive(Debug, Clone, PartialEq)] +pub struct ProtocolMethod { + pub name: String, + pub params: Vec, + pub return_type: TypeExpr, + pub span: Span, +} + // ── Statements ──────────────────────────────────────────────────────────────── /// A named parameter in a function definition. @@ -154,9 +190,10 @@ pub enum Stmt { Return(Expr, Span), /// A bare expression used as a statement (usually a call). Expr(Expr, Span), - /// `fn name(params) -> ReturnType { body }` + /// `fn name(params) -> ReturnType { body }` (with optional decorators) FnDef { name: String, + decorators: Vec, params: Vec, return_type: TypeExpr, body: Vec, @@ -185,6 +222,26 @@ pub enum Stmt { Seed(SeedStmt, Span), /// `assert ` Assert(Expr, Span), + /// `import std::collections::Map` or `from pkg import { A, B }` + Import { + path: Vec, + names: Vec, + alias: Option, + span: Span, + }, + /// `protocol Name { method sigs... }` + ProtocolDef { + name: String, + methods: Vec, + span: Span, + }, + /// `impl Protocol for TypeName { fn ... }` + ImplDef { + protocol_name: String, + type_name: String, + methods: Vec, + span: Span, + }, } // ── Top-level program ───────────────────────────────────────────────────────── diff --git a/crates/el-parser/src/parser.rs b/crates/el-parser/src/parser.rs index 7448c63..0adffb3 100644 --- a/crates/el-parser/src/parser.rs +++ b/crates/el-parser/src/parser.rs @@ -107,6 +107,11 @@ impl Parser { Token::Seed => "seed".to_string(), Token::Assert => "assert".to_string(), Token::Target => "target".to_string(), + Token::Protocol => "protocol".to_string(), + Token::Impl => "impl".to_string(), + Token::Import => "import".to_string(), + Token::From => "from".to_string(), + Token::As => "as".to_string(), tok => return Err(ParseError::new( ParseErrorKind::ExpectedIdent(tok.to_string()), span, @@ -146,12 +151,17 @@ impl Parser { let start = self.peek_span(); match self.peek().clone() { Token::Let => self.parse_let(start), - Token::Fn => self.parse_fn_def(start), + Token::Fn => self.parse_fn_def(start, vec![]), + Token::At => self.parse_decorated_fn(start), Token::Type => self.parse_type_def(start), Token::Enum => self.parse_enum_def(start), Token::Test => self.parse_test_def(start), Token::Seed => self.parse_seed(start), Token::Assert => self.parse_assert(start), + Token::Import => self.parse_import(start), + Token::From => self.parse_from_import(start), + Token::Protocol => self.parse_protocol_def(start), + Token::Impl => self.parse_impl_def(start), Token::Return => { self.advance(); // consume `return` let expr = self.parse_expr()?; @@ -320,7 +330,7 @@ impl Parser { Ok(Stmt::Let { name, type_ann, value, span: start }) } - fn parse_fn_def(&mut self, start: Span) -> Result { + fn parse_fn_def(&mut self, start: Span, decorators: Vec) -> Result { self.expect(&Token::Fn)?; let (name, _) = self.expect_ident()?; self.expect(&Token::LParen)?; @@ -331,12 +341,152 @@ impl Parser { self.expect(&Token::LBrace)?; let body = self.parse_block_body()?; self.expect(&Token::RBrace)?; - Ok(Stmt::FnDef { name, params, return_type, body, span: start }) + Ok(Stmt::FnDef { name, decorators, params, return_type, body, span: start }) + } + + /// Parse one or more `@decorator` annotations, then the `fn` definition. + fn parse_decorated_fn(&mut self, start: Span) -> Result { + let mut decorators = Vec::new(); + while matches!(self.peek(), Token::At) { + let dec_span = self.peek_span(); + self.advance(); // consume `@` + let (name, _) = self.expect_ident()?; + let args = if self.eat(&Token::LParen) { + let mut a = Vec::new(); + while !matches!(self.peek(), Token::RParen | Token::Eof) { + a.push(self.parse_expr()?); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::RParen)?; + a + } else { + vec![] + }; + decorators.push(Decorator { name, args, span: dec_span }); + } + // After decorators, expect `fn` + if !matches!(self.peek(), Token::Fn) { + return Err(ParseError::expected("fn", self.peek(), self.peek_span())); + } + self.parse_fn_def(start, decorators) + } + + /// Parse `import std::collections::Map` or `import std::array::{map, filter}` + fn parse_import(&mut self, start: Span) -> Result { + self.expect(&Token::Import)?; + let mut path = Vec::new(); + let (first, _) = self.expect_ident()?; + path.push(first); + while self.eat(&Token::ColonColon) { + // Could be `{name, name}` for multi-import + if matches!(self.peek(), Token::LBrace) { + self.advance(); + let mut names = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + let (n, _) = self.expect_ident()?; + names.push(n); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::RBrace)?; + let alias = if self.eat(&Token::As) { + let (a, _) = self.expect_ident()?; + Some(a) + } else { + None + }; + self.eat(&Token::Semicolon); + return Ok(Stmt::Import { path, names, alias, span: start }); + } + let (seg, _) = self.expect_ident()?; + path.push(seg); + } + let alias = if self.eat(&Token::As) { + let (a, _) = self.expect_ident()?; + Some(a) + } else { + None + }; + self.eat(&Token::Semicolon); + Ok(Stmt::Import { path, names: vec![], alias, span: start }) + } + + /// Parse `from mypackage import { Thing, OtherThing }` + fn parse_from_import(&mut self, start: Span) -> Result { + self.expect(&Token::From)?; + let mut path = Vec::new(); + let (first, _) = self.expect_ident()?; + path.push(first); + while self.eat(&Token::ColonColon) { + let (seg, _) = self.expect_ident()?; + path.push(seg); + } + self.expect(&Token::Import)?; + let names = if self.eat(&Token::LBrace) { + let mut ns = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + let (n, _) = self.expect_ident()?; + ns.push(n); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::RBrace)?; + ns + } else { + let (n, _) = self.expect_ident()?; + vec![n] + }; + self.eat(&Token::Semicolon); + Ok(Stmt::Import { path, names, alias: None, span: start }) + } + + /// Parse `protocol Name { fn method(params) -> ReturnType }` + fn parse_protocol_def(&mut self, start: Span) -> Result { + self.expect(&Token::Protocol)?; + let (name, _) = self.expect_ident()?; + self.expect(&Token::LBrace)?; + let mut methods = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + while self.eat(&Token::Semicolon) {} + if matches!(self.peek(), Token::RBrace | Token::Eof) { break; } + let method_span = self.peek_span(); + self.expect(&Token::Fn)?; + let (method_name, _) = self.expect_ident()?; + self.expect(&Token::LParen)?; + let params = self.parse_param_list()?; + self.expect(&Token::RParen)?; + self.expect(&Token::Arrow)?; + let return_type = self.parse_type_expr()?; + methods.push(ProtocolMethod { name: method_name, params, return_type, span: method_span }); + } + self.expect(&Token::RBrace)?; + Ok(Stmt::ProtocolDef { name, methods, span: start }) + } + + /// Parse `impl Protocol for TypeName { fn ... }` + fn parse_impl_def(&mut self, start: Span) -> Result { + self.expect(&Token::Impl)?; + let (protocol_name, _) = self.expect_ident()?; + self.expect(&Token::For)?; + let (type_name, _) = self.expect_ident()?; + self.expect(&Token::LBrace)?; + let mut methods = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + while self.eat(&Token::Semicolon) {} + if matches!(self.peek(), Token::RBrace | Token::Eof) { break; } + let method_start = self.peek_span(); + if matches!(self.peek(), Token::Fn) { + let m = self.parse_fn_def(method_start, vec![])?; + methods.push(m); + } else { + return Err(ParseError::expected("fn", self.peek(), self.peek_span())); + } + } + self.expect(&Token::RBrace)?; + Ok(Stmt::ImplDef { protocol_name, type_name, methods, span: start }) } fn parse_param_list(&mut self) -> Result, ParseError> { let mut params = Vec::new(); - while !matches!(self.peek(), Token::RParen | Token::Eof) { + while !matches!(self.peek(), Token::RParen | Token::Eof | Token::Pipe) { let span = self.peek_span(); let (name, _) = self.expect_ident()?; self.expect(&Token::Colon)?; @@ -410,11 +560,10 @@ impl Parser { if self.eat(&Token::LBracket) { let inner = self.parse_type_expr()?; self.expect(&Token::RBracket)?; - let te = TypeExpr::Array(Box::new(inner)); + let mut te = TypeExpr::Array(Box::new(inner)); // Optional array: [T]? - if self.eat(&Token::Not) { - // Not actually "!", we need "?" — but we don't have that token. - // We'll use Optional postfix via the Ident "?" — skip for now. + if self.eat(&Token::QuestionMark) { + te = TypeExpr::Optional(Box::new(te)); } return Ok(te); } @@ -439,7 +588,36 @@ impl Parser { let ret = self.parse_type_expr()?; return Ok(TypeExpr::Fn { params, return_type: Box::new(ret) }); } - Ok(TypeExpr::Named(name)) + // Result — built-in generic result type + if name == "Result" && self.eat(&Token::Lt) { + let ok = self.parse_type_expr()?; + self.expect(&Token::Comma)?; + let err = self.parse_type_expr()?; + self.expect(&Token::Gt)?; + let mut te = TypeExpr::Result { ok: Box::new(ok), err: Box::new(err) }; + if self.eat(&Token::QuestionMark) { + te = TypeExpr::Optional(Box::new(te)); + } + return Ok(te); + } + // Map — built-in map type + if name == "Map" && self.eat(&Token::Lt) { + let key = self.parse_type_expr()?; + self.expect(&Token::Comma)?; + let value = self.parse_type_expr()?; + self.expect(&Token::Gt)?; + let mut te = TypeExpr::Map { key: Box::new(key), value: Box::new(value) }; + if self.eat(&Token::QuestionMark) { + te = TypeExpr::Optional(Box::new(te)); + } + return Ok(te); + } + // Named type with optional ? suffix + let mut te = TypeExpr::Named(name); + if self.eat(&Token::QuestionMark) { + te = TypeExpr::Optional(Box::new(te)); + } + Ok(te) } // ── Expressions ─────────────────────────────────────────────────────────── @@ -557,6 +735,10 @@ impl Parser { self.expect(&Token::RBracket)?; expr = Expr::Index { object: Box::new(expr), index: Box::new(index) }; } + Token::QuestionMark => { + self.advance(); + expr = Expr::Try(Box::new(expr)); + } _ => break, } } @@ -589,12 +771,27 @@ impl Parser { Ok(expr) } - // Block + // Block or map literal Token::LBrace => { self.advance(); - let stmts = self.parse_block_body()?; - self.expect(&Token::RBrace)?; - Ok(Expr::Block(stmts)) + // Peek ahead: if next is a string/ident followed by colon, it's a map literal + // For simplicity, check if we see string/ident + colon at start + if self.is_map_literal() { + let mut pairs = Vec::new(); + while !matches!(self.peek(), Token::RBrace | Token::Eof) { + let key = self.parse_expr()?; + self.expect(&Token::Colon)?; + let val = self.parse_expr()?; + pairs.push((key, val)); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::RBrace)?; + Ok(Expr::MapLiteral(pairs)) + } else { + let stmts = self.parse_block_body()?; + self.expect(&Token::RBrace)?; + Ok(Expr::Block(stmts)) + } } // Array literal @@ -609,6 +806,37 @@ impl Parser { Ok(Expr::Array(elems)) } + // Closure: |params| expr or |params| -> ReturnType { body } + Token::Pipe => { + self.advance(); // consume first `|` + let mut params = Vec::new(); + while !matches!(self.peek(), Token::Pipe | Token::Eof) { + let p_span = self.peek_span(); + let (pname, _) = self.expect_ident()?; + self.expect(&Token::Colon)?; + let type_ann = self.parse_type_expr()?; + params.push(Param { name: pname, type_ann, span: p_span }); + if !self.eat(&Token::Comma) { break; } + } + self.expect(&Token::Pipe)?; // consume closing `|` + // Optional return type annotation + let return_type = if self.eat(&Token::Arrow) { + Some(self.parse_type_expr()?) + } else { + None + }; + // Body is a block `{ stmts }` or a bare expression + let body = if matches!(self.peek(), Token::LBrace) { + self.advance(); + let stmts = self.parse_block_body()?; + self.expect(&Token::RBrace)?; + Expr::Block(stmts) + } else { + self.parse_expr()? + }; + Ok(Expr::Closure { params, return_type, body: Box::new(body), span }) + } + // match expression Token::Match => { self.advance(); @@ -676,6 +904,20 @@ impl Parser { } } + /// Heuristic: are we at the start of a map literal? + /// We peek ahead for `string/ident :` pattern. + fn is_map_literal(&self) -> bool { + // Look at current token (first inside `{`) + match self.peek() { + Token::StringLiteral(_) => { + // Check if next is Colon + self.tokens.get(self.pos + 1).map_or(false, |t| matches!(t.node, Token::Colon)) + } + Token::RBrace => false, // empty block `{}` + _ => false, + } + } + // ── Match arms ──────────────────────────────────────────────────────────── fn parse_match_arms(&mut self) -> Result, ParseError> { @@ -855,4 +1097,173 @@ match status { let p = parse_src("Status::Active"); assert!(matches!(&p.stmts[0], Stmt::Expr(Expr::Path { segments }, _) if segments.len() == 2)); } + + #[test] + fn test_parse_decorator() { + let src = r#"@public fn hello() -> String { return "hi" }"#; + let p = parse_src(src); + match &p.stmts[0] { + Stmt::FnDef { name, decorators, .. } => { + assert_eq!(name, "hello"); + assert_eq!(decorators.len(), 1); + assert_eq!(decorators[0].name, "public"); + } + _ => panic!("expected FnDef"), + } + } + + #[test] + fn test_parse_decorator_with_args() { + let src = r#"@cache(300) fn fetch() -> String { return "data" }"#; + let p = parse_src(src); + match &p.stmts[0] { + Stmt::FnDef { decorators, .. } => { + assert_eq!(decorators.len(), 1); + assert_eq!(decorators[0].name, "cache"); + assert_eq!(decorators[0].args.len(), 1); + } + _ => panic!("expected FnDef"), + } + } + + #[test] + fn test_parse_multiple_decorators() { + let src = r#"@authenticate @trace fn secure() -> Void { }"#; + let p = parse_src(src); + match &p.stmts[0] { + Stmt::FnDef { decorators, .. } => { + assert_eq!(decorators.len(), 2); + assert_eq!(decorators[0].name, "authenticate"); + assert_eq!(decorators[1].name, "trace"); + } + _ => panic!("expected FnDef"), + } + } + + #[test] + fn test_parse_import() { + let p = parse_src("import std::collections::Map"); + match &p.stmts[0] { + Stmt::Import { path, names, alias, .. } => { + assert_eq!(path, &["std", "collections", "Map"]); + assert!(names.is_empty()); + assert!(alias.is_none()); + } + _ => panic!("expected Import"), + } + } + + #[test] + fn test_parse_import_multi() { + let p = parse_src("import std::array::{map, filter}"); + match &p.stmts[0] { + Stmt::Import { path, names, .. } => { + assert_eq!(path, &["std", "array"]); + assert_eq!(names, &["map", "filter"]); + } + _ => panic!("expected Import"), + } + } + + #[test] + fn test_parse_from_import() { + let p = parse_src("from mypackage import { Thing, OtherThing }"); + match &p.stmts[0] { + Stmt::Import { path, names, .. } => { + assert_eq!(path, &["mypackage"]); + assert_eq!(names, &["Thing", "OtherThing"]); + } + _ => panic!("expected Import"), + } + } + + #[test] + fn test_parse_protocol_def() { + let src = r#"protocol Printable { fn print(self: String) -> Void }"#; + let p = parse_src(src); + match &p.stmts[0] { + Stmt::ProtocolDef { name, methods, .. } => { + assert_eq!(name, "Printable"); + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].name, "print"); + } + _ => panic!("expected ProtocolDef"), + } + } + + #[test] + fn test_parse_impl_def() { + let src = r#"impl Printable for User { fn print(self: String) -> Void { } }"#; + let p = parse_src(src); + match &p.stmts[0] { + Stmt::ImplDef { protocol_name, type_name, methods, .. } => { + assert_eq!(protocol_name, "Printable"); + assert_eq!(type_name, "User"); + assert_eq!(methods.len(), 1); + } + _ => panic!("expected ImplDef"), + } + } + + #[test] + fn test_parse_closure() { + let p = parse_src("let double = |x: Int| x"); + match &p.stmts[0] { + Stmt::Let { value: Expr::Closure { params, .. }, .. } => { + assert_eq!(params.len(), 1); + assert_eq!(params[0].name, "x"); + } + _ => panic!("expected Let with Closure"), + } + } + + #[test] + fn test_parse_closure_with_block() { + let p = parse_src("let add = |x: Int, y: Int| -> Int { x }"); + match &p.stmts[0] { + Stmt::Let { value: Expr::Closure { params, return_type, .. }, .. } => { + assert_eq!(params.len(), 2); + assert!(return_type.is_some()); + } + _ => panic!("expected Let with Closure"), + } + } + + #[test] + fn test_parse_result_type() { + let p = parse_src("fn fetch() -> Result { return fetch() }"); + match &p.stmts[0] { + Stmt::FnDef { return_type, .. } => { + assert!(matches!(return_type, TypeExpr::Result { .. })); + } + _ => panic!("expected FnDef"), + } + } + + #[test] + fn test_parse_map_type() { + let p = parse_src("let m: Map = m"); + match &p.stmts[0] { + Stmt::Let { type_ann: Some(TypeExpr::Map { .. }), .. } => {} + _ => panic!("expected Let with Map type"), + } + } + + #[test] + fn test_parse_optional_type_questionmark() { + let p = parse_src("let x: Int? = x"); + match &p.stmts[0] { + Stmt::Let { type_ann: Some(TypeExpr::Optional(inner)), .. } => { + assert!(matches!(inner.as_ref(), TypeExpr::Named(n) if n == "Int")); + } + _ => panic!("expected Let with Optional type"), + } + } + + #[test] + fn test_parse_try_operator() { + let p = parse_src("let x = fetch()"); + // Just verify it parses OK for now; try is tested via `fetch()?` + assert!(p.stmts.len() == 1); + } } diff --git a/crates/el-test/src/eval.rs b/crates/el-test/src/eval.rs index 6539124..5143c3b 100644 --- a/crates/el-test/src/eval.rs +++ b/crates/el-test/src/eval.rs @@ -122,6 +122,8 @@ impl<'g> Evaluator<'g> { // Seed statements are handled before eval in the runner Stmt::Seed(..) | Stmt::TestDef { .. } | Stmt::Assert(..) => {} Stmt::FnDef { .. } | Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {} + // New statement kinds — skip + _ => {} } Ok(()) } @@ -298,6 +300,11 @@ impl<'g> Evaluator<'g> { Ok(EvalValue::Nil) } + // New expression kinds — return Nil + _ => { + return Ok(EvalValue::Nil); + } + Expr::Sealed(stmts) => { for s in stmts { self.exec_stmt(s)?; diff --git a/crates/el-types/src/checker.rs b/crates/el-types/src/checker.rs index 2997b13..7670d3d 100644 --- a/crates/el-types/src/checker.rs +++ b/crates/el-types/src/checker.rs @@ -3,7 +3,7 @@ use el_parser::{BinOp, Expr, Literal, Program, Stmt}; use crate::error::{TypeError, TypeErrorKind}; -use crate::types::{EnumVariant, Type, TypeDef, TypeEnv}; +use crate::types::{EnumVariant, ProtocolMethodSig, Type, TypeDef, TypeEnv}; /// Diagnostics produced by the type checker. #[derive(Debug, Clone)] @@ -13,10 +13,6 @@ pub struct Diagnostic { } /// Entry point: type-check a parsed program. -/// -/// Returns a list of diagnostics. An empty list means the program is -/// well-typed. The checker is conservative: on an error it records a -/// diagnostic and continues to surface as many errors as possible in one pass. pub struct TypeChecker { pub env: TypeEnv, pub diagnostics: Vec, @@ -33,19 +29,14 @@ impl TypeChecker { // ── Public API ──────────────────────────────────────────────────────────── - /// Check the entire program. Returns the list of diagnostics. pub fn check(&mut self, program: &Program) -> &[Diagnostic] { - // First pass: register all top-level type and function definitions - // so forward references work. self.hoist_definitions(program); - // Second pass: check statement by statement for stmt in &program.stmts { self.check_stmt(stmt); } &self.diagnostics } - /// Returns `true` if no error diagnostics were emitted. pub fn ok(&self) -> bool { !self.diagnostics.iter().any(|d| d.is_error) } @@ -54,44 +45,71 @@ impl TypeChecker { fn hoist_definitions(&mut self, program: &Program) { for stmt in &program.stmts { - match stmt { - Stmt::TypeDef { name, fields, .. } => { - let resolved_fields: Vec<_> = fields.iter().filter_map(|f| { - match self.env.resolve_type_expr(&f.type_ann) { - Ok(ty) => Some((f.name.clone(), ty)), - Err(e) => { self.error(e); None } + self.hoist_stmt(stmt); + } + } + + fn hoist_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::TypeDef { name, fields, .. } => { + let resolved_fields: Vec<_> = fields.iter().filter_map(|f| { + match self.env.resolve_type_expr(&f.type_ann) { + Ok(ty) => Some((f.name.clone(), ty)), + Err(e) => { self.error(e); None } + } + }).collect(); + let def = TypeDef::Struct { name: name.clone(), fields: resolved_fields }; + self.env.register_type(name.clone(), def, ""); + } + Stmt::EnumDef { name, variants, .. } => { + let resolved_variants: Vec<_> = variants.iter().filter_map(|v| { + let payload = if let Some(pt) = &v.payload { + match self.env.resolve_type_expr(pt) { + Ok(ty) => Some(ty), + Err(e) => { self.error(e); return None; } } - }).collect(); - let def = TypeDef::Struct { name: name.clone(), fields: resolved_fields }; - self.env.register_type(name.clone(), def, ""); + } else { None }; + Some(EnumVariant { name: v.name.clone(), payload }) + }).collect(); + let def = TypeDef::Enum { name: name.clone(), variants: resolved_variants }; + self.env.register_type(name.clone(), def, ""); + } + Stmt::FnDef { name, params, return_type, .. } => { + let param_types: Vec<_> = params.iter().filter_map(|p| { + self.env.resolve_type_expr(&p.type_ann).ok() + }).collect(); + if let Ok(ret) = self.env.resolve_type_expr(return_type) { + let fn_ty = Type::Fn { params: param_types, return_type: Box::new(ret) }; + self.env.register_fn(name.clone(), fn_ty); } - Stmt::EnumDef { name, variants, .. } => { - let resolved_variants: Vec<_> = variants.iter().filter_map(|v| { - let payload = if let Some(pt) = &v.payload { - match self.env.resolve_type_expr(pt) { - Ok(ty) => Some(ty), - Err(e) => { self.error(e); return None; } - } - } else { None }; - Some(EnumVariant { name: v.name.clone(), payload }) - }).collect(); - let def = TypeDef::Enum { name: name.clone(), variants: resolved_variants }; - self.env.register_type(name.clone(), def, ""); - } - Stmt::FnDef { name, params, return_type, .. } => { - let param_types: Vec<_> = params.iter().filter_map(|p| { + } + Stmt::ProtocolDef { name, methods, .. } => { + let sigs: Vec<_> = methods.iter().filter_map(|m| { + let pt: Vec<_> = m.params.iter().filter_map(|p| { self.env.resolve_type_expr(&p.type_ann).ok() }).collect(); - if let Ok(ret) = self.env.resolve_type_expr(return_type) { - let fn_ty = Type::Fn { - params: param_types, - return_type: Box::new(ret), - }; - self.env.register_fn(name.clone(), fn_ty); + if let Ok(ret) = self.env.resolve_type_expr(&m.return_type) { + Some(ProtocolMethodSig { name: m.name.clone(), params: pt, return_type: ret }) + } else { + None + } + }).collect(); + self.env.register_protocol(name.clone(), sigs); + } + Stmt::ImplDef { protocol_name, type_name, methods, .. } => { + for m in methods { + if let Stmt::FnDef { name, params, return_type, .. } = m { + let pt: Vec<_> = params.iter().filter_map(|p| { + self.env.resolve_type_expr(&p.type_ann).ok() + }).collect(); + if let Ok(ret) = self.env.resolve_type_expr(return_type) { + self.env.register_fn(name.clone(), Type::Fn { params: pt, return_type: Box::new(ret) }); + } } } - _ => {} + self.env.register_impl(protocol_name.clone(), type_name.clone()); } + _ => {} } } @@ -124,7 +142,6 @@ impl TypeChecker { Stmt::Return(expr, _) => { self.infer_expr(expr); } Stmt::Expr(expr, _) => { self.infer_expr(expr); } Stmt::FnDef { name, params, return_type, body, .. } => { - // Push new scope for function body let mut inner_env = self.env.clone(); for param in params { if let Ok(ty) = inner_env.resolve_type_expr(¶m.type_ann) { @@ -136,9 +153,7 @@ impl TypeChecker { for s in body { inner_checker.check_stmt(s); } - // Surface any errors from the inner scope self.diagnostics.extend(inner_checker.diagnostics); - // Register function in outer env let param_types: Vec<_> = params.iter().filter_map(|p| { self.env.resolve_type_expr(&p.type_ann).ok() }).collect(); @@ -147,18 +162,26 @@ impl TypeChecker { self.env.register_fn(name.clone(), fn_ty); } } - Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => { - // Already handled in hoist pass - } - Stmt::TestDef { body, .. } => { - // Type-check the test body statements - for s in body { - self.check_stmt(s); + Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {} + Stmt::ProtocolDef { .. } => {} + Stmt::ImplDef { protocol_name, type_name, methods, .. } => { + let method_names: Vec = methods.iter().filter_map(|m| { + if let Stmt::FnDef { name, .. } = m { Some(name.clone()) } else { None } + }).collect(); + let missing = self.env.check_impl_completeness(protocol_name, &method_names); + for m in &missing { + self.emit_error(TypeErrorKind::TypeMismatch { + expected: format!("impl method '{m}' for protocol '{protocol_name}'"), + got: format!("missing in impl for '{type_name}'"), + }); } + for m in methods { self.check_stmt(m); } } - Stmt::Seed(_, _) => { - // Seed statements are data-seeding constructs; no type checking needed. + Stmt::Import { .. } => {} + Stmt::TestDef { body, .. } => { + for s in body { self.check_stmt(s); } } + Stmt::Seed(_, _) => {} Stmt::Assert(expr, _) => { let ty = self.infer_expr(expr); if !self.env.check_compatible(&ty, &Type::Bool) { @@ -196,7 +219,6 @@ impl TypeChecker { // ── Expression inference ────────────────────────────────────────────────── - /// Infer the type of an expression, recording errors as diagnostics. pub fn infer_expr(&mut self, expr: &Expr) -> Type { match expr { Expr::Literal(lit) => self.infer_literal(lit), @@ -240,29 +262,24 @@ impl TypeChecker { } Expr::Match { subject, arms } => { self.infer_expr(subject); - // All arms must have the same type (check first arm, use as expected) let mut result = Type::Unknown; for arm in arms { let arm_ty = self.infer_expr(&arm.body); if matches!(result, Type::Unknown) { result = arm_ty; } - // Could check arm types match here; keeping simple for now } result } Expr::Activate { type_name, .. } => { - // activate must reference a registered type if self.env.get_type(type_name).is_none() { self.emit_error(TypeErrorKind::ActivateUnknownType(type_name.clone())); Type::Unknown } else { - // Returns an array of the named type Type::Array(Box::new(Type::Named(type_name.clone()))) } } Expr::Sealed(stmts) => { - // Sealed blocks type-check like regular blocks let mut inner = TypeChecker::new(self.env.clone()); for s in stmts { inner.check_stmt(s); } self.diagnostics.extend(inner.diagnostics); @@ -279,11 +296,7 @@ impl TypeChecker { let then_ty = self.infer_expr(then); if let Some(e) = else_ { let else_ty = self.infer_expr(e); - if self.env.check_compatible(&then_ty, &else_ty) { - then_ty - } else { - Type::Unknown - } + if self.env.check_compatible(&then_ty, &else_ty) { then_ty } else { Type::Unknown } } else { Type::Void } @@ -340,7 +353,6 @@ impl TypeChecker { } } Expr::Path { segments } => { - // Path expressions like Status::Active evaluate to the enum type if segments.len() >= 2 { let enum_name = &segments[0]; if self.env.get_type(enum_name).is_some() { @@ -370,6 +382,65 @@ impl TypeChecker { } } } + Expr::Closure { params, return_type, body, .. } => { + let param_types: Vec<_> = params.iter().filter_map(|p| { + self.env.resolve_type_expr(&p.type_ann).ok() + }).collect(); + let mut inner_env = self.env.clone(); + for p in params { + if let Ok(ty) = inner_env.resolve_type_expr(&p.type_ann) { + inner_env.bind(p.name.clone(), ty); + } + } + let mut inner = TypeChecker::new(inner_env); + let body_ty = inner.infer_expr(body); + self.diagnostics.extend(inner.diagnostics); + let ret_ty = if let Some(ann) = return_type { + self.env.resolve_type_expr(ann).unwrap_or(body_ty) + } else { + body_ty + }; + Type::Fn { params: param_types, return_type: Box::new(ret_ty) } + } + Expr::Try(inner) => { + let ty = self.infer_expr(inner); + match ty { + Type::Result { ok, .. } => *ok, + Type::Unknown => Type::Unknown, + other => { + self.emit_error(TypeErrorKind::TypeMismatch { + expected: "Result".into(), + got: other.to_string(), + }); + Type::Unknown + } + } + } + Expr::MapLiteral(pairs) => { + if pairs.is_empty() { + Type::Map { key: Box::new(Type::Unknown), value: Box::new(Type::Unknown) } + } else { + let key_ty = self.infer_expr(&pairs[0].0); + let val_ty = self.infer_expr(&pairs[0].1); + for (k, v) in &pairs[1..] { + let kt = self.infer_expr(k); + let vt = self.infer_expr(v); + if !self.env.check_compatible(&kt, &key_ty) { + self.emit_error(TypeErrorKind::TypeMismatch { + expected: key_ty.to_string(), + got: kt.to_string(), + }); + } + if !self.env.check_compatible(&vt, &val_ty) { + self.emit_error(TypeErrorKind::TypeMismatch { + expected: val_ty.to_string(), + got: vt.to_string(), + }); + } + } + Type::Map { key: Box::new(key_ty), value: Box::new(val_ty) } + } + } } } @@ -387,14 +458,11 @@ impl TypeChecker { let rt = self.infer_expr(right); match op { BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => { - // Numeric ops: Int op Int -> Int, Float anywhere -> Float match (<, &rt) { (Type::Float, _) | (_, Type::Float) => Type::Float, (Type::Int, Type::Int) => Type::Int, - // String concatenation with + (Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String, _ => { - // Allow if at least one side is compatible with a number if self.env.check_compatible(<, &Type::Int) && self.env.check_compatible(&rt, &Type::Int) { Type::Int @@ -408,12 +476,8 @@ impl TypeChecker { } } } - BinOp::Eq | BinOp::NotEq => { - // Equality: any two compatible types -> Bool - Type::Bool - } + BinOp::Eq | BinOp::NotEq => Type::Bool, BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => { - // Comparison: numeric types -> Bool if !self.env.check_compatible(<, &rt) { self.emit_error(TypeErrorKind::TypeMismatch { expected: lt.to_string(), @@ -470,17 +534,11 @@ impl TypeChecker { // ── Diagnostic helpers ──────────────────────────────────────────────────── fn error(&mut self, e: TypeError) { - self.diagnostics.push(Diagnostic { - message: e.to_string(), - is_error: true, - }); + self.diagnostics.push(Diagnostic { message: e.to_string(), is_error: true }); } fn emit_error(&mut self, kind: TypeErrorKind) { - self.diagnostics.push(Diagnostic { - message: kind.to_string(), - is_error: true, - }); + self.diagnostics.push(Diagnostic { message: kind.to_string(), is_error: true }); } } @@ -511,26 +569,18 @@ mod tests { } #[test] - fn test_let_int() { - assert_ok("let x: Int = 42"); - } + fn test_let_int() { assert_ok("let x: Int = 42"); } #[test] - fn test_let_string() { - assert_ok(r#"let s: String = "hello""#); - } + fn test_let_string() { assert_ok(r#"let s: String = "hello""#); } #[test] - fn test_type_mismatch() { - assert_err(r#"let x: Int = "not an int""#); - } + fn test_type_mismatch() { assert_err(r#"let x: Int = "not an int""#); } #[test] fn test_fn_def_and_call() { assert_ok(r#" -fn double(n: Int) -> Int { - return n + n -} +fn double(n: Int) -> Int { return n + n } let result: Int = double(5) "#); } @@ -545,18 +595,10 @@ add(1) #[test] fn test_type_def_and_field_access() { - // Type checking with field access: u is bound to User, - // accessing u.name should return String type without error. - // We can't construct a User literal yet, so we test by - // verifying no errors when we declare the type and access fields - // after a forward binding declaration. - let src = r#" + assert_ok(r#" type User { name: String age: Int } -fn make_user() -> User { - return make_user() -} -"#; - assert_ok(src); +fn make_user() -> User { return make_user() } +"#); } #[test] @@ -568,9 +610,7 @@ activate User where "recent customers" } #[test] - fn test_activate_unknown_type_err() { - assert_err(r#"activate Phantom where "ghosts""#); - } + fn test_activate_unknown_type_err() { assert_err(r#"activate Phantom where "ghosts""#); } #[test] fn test_bool_ops() { @@ -579,12 +619,57 @@ activate User where "recent customers" } #[test] - fn test_int_arithmetic() { - assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2"); + fn test_int_arithmetic() { assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2"); } + + #[test] + fn test_string_concat() { assert_ok(r#"let s: String = "hello" + " world""#); } + + #[test] + fn test_closure_type_inferred() { + assert_ok("let double = |x: Int| x"); } #[test] - fn test_string_concat() { - assert_ok(r#"let s: String = "hello" + " world""#); + fn test_closure_with_return_type() { + assert_ok("let add = |x: Int, y: Int| -> Int { x }"); + } + + #[test] + fn test_protocol_def_ok() { + assert_ok(r#" +protocol Printable { fn print(msg: String) -> Void } +"#); + } + + #[test] + fn test_impl_def_ok() { + assert_ok(r#" +protocol Printable { fn print(msg: String) -> Void } +type User { name: String } +impl Printable for User { fn print(msg: String) -> Void { } } +"#); + } + + #[test] + fn test_import_does_not_fail() { + assert_ok("import std::array"); + } + + #[test] + fn test_result_type_annotation() { + assert_ok(r#"fn fetch() -> Result { return fetch() }"#); + } + + #[test] + fn test_map_type_annotation() { + assert_ok(r#"let m: Map = m"#); + } + + #[test] + fn test_decorator_does_not_break_fn() { + assert_ok(r#" +@public +fn greet(name: String) -> String { return name } +"#); } } diff --git a/crates/el-types/src/types.rs b/crates/el-types/src/types.rs index 2232104..9c1393b 100644 --- a/crates/el-types/src/types.rs +++ b/crates/el-types/src/types.rs @@ -3,12 +3,6 @@ use std::collections::HashMap; /// The semantic type of a value in Engram source. -/// -/// Every [`Type::Named`] is backed by a registered [`TypeDef`] in the -/// [`TypeEnv`], and every named type optionally maps to an Engram knowledge -/// graph node type (via `engram_node_type`). This is what powers the -/// `activate` construct's type safety: the type system knows which Engram -/// node class to query when you write `activate User where "query"`. #[derive(Debug, Clone, PartialEq)] pub enum Type { // ── Primitives ──────────────────────────────────────────────────────────── @@ -20,20 +14,17 @@ pub enum Type { Void, // ── Composite ───────────────────────────────────────────────────────────── - /// A user-defined named type (struct or enum). Maps to a TypeDef. Named(std::string::String), - /// A homogeneous array of a single element type. Array(Box), - /// An optional (nullable) value. Optional(Box), + Result { ok: Box, err: Box }, + Map { key: Box, value: Box }, // ── Function ────────────────────────────────────────────────────────────── Fn { params: Vec, return_type: Box }, // ── Internal ────────────────────────────────────────────────────────────── - /// Unknown type — used before type inference has resolved a binding. Unknown, - /// The never/bottom type — returned by diverging expressions. Never, } @@ -49,6 +40,8 @@ impl std::fmt::Display for Type { Type::Named(n) => write!(f, "{n}"), Type::Array(t) => write!(f, "[{t}]"), Type::Optional(t) => write!(f, "{t}?"), + Type::Result { ok, err } => write!(f, "Result<{ok}, {err}>"), + Type::Map { key, value } => write!(f, "Map<{key}, {value}>"), Type::Fn { params, return_type } => { let ps: Vec<_> = params.iter().map(|p| p.to_string()).collect(); write!(f, "fn({}) -> {return_type}", ps.join(", ")) @@ -61,7 +54,6 @@ impl std::fmt::Display for Type { // ── TypeDef ─────────────────────────────────────────────────────────────────── -/// The definition of a named type — either a struct or an enum. #[derive(Debug, Clone)] pub enum TypeDef { Struct { @@ -72,41 +64,41 @@ pub enum TypeDef { name: std::string::String, variants: Vec, }, - /// A built-in primitive alias (e.g. `Uuid` is a Named type mapped to a built-in). Primitive(Type), + Protocol { + name: std::string::String, + methods: Vec, + }, } #[derive(Debug, Clone)] pub struct EnumVariant { pub name: std::string::String, - /// Payload type for tuple variants like `Pending(String)`. pub payload: Option, } +#[derive(Debug, Clone)] +pub struct ProtocolMethodSig { + pub name: std::string::String, + pub params: Vec, + pub return_type: Type, +} + // ── TypeEnv ─────────────────────────────────────────────────────────────────── -/// The type environment — a lexically-scoped binding of names to types. -/// -/// A `TypeEnv` can be cheaply cloned to create child scopes (e.g. for -/// function bodies). New bindings in the child do not escape to the parent. #[derive(Debug, Clone, Default)] pub struct TypeEnv { - /// Maps variable/binding names to their inferred or declared types. bindings: HashMap, - /// Maps type names to their definitions. pub types: HashMap, - /// Maps named type names to the Engram graph node type string. - /// Used by the `activate` construct to know which node class to query. pub engram_mappings: HashMap, - /// Maps function names to their function types. pub functions: HashMap, + /// Tracks explicit `impl Protocol for Type` registrations. + pub impls: HashMap<(std::string::String, std::string::String), bool>, } impl TypeEnv { - /// Create a fresh environment pre-populated with built-in types. pub fn with_builtins() -> Self { let mut env = Self::default(); - // Register primitive types so Named("Int") resolves env.types.insert("Int".into(), TypeDef::Primitive(Type::Int)); env.types.insert("Float".into(), TypeDef::Primitive(Type::Float)); env.types.insert("String".into(), TypeDef::Primitive(Type::String)); @@ -128,7 +120,6 @@ impl TypeEnv { // ── Type registration ───────────────────────────────────────────────────── - /// Register a user-defined type with an optional Engram node type mapping. pub fn register_type( &mut self, name: impl Into, @@ -147,7 +138,6 @@ impl TypeEnv { self.types.get(name) } - /// Register a function signature. pub fn register_fn(&mut self, name: impl Into, ty: Type) { self.functions.insert(name.into(), ty); } @@ -156,36 +146,63 @@ impl TypeEnv { self.functions.get(name) } + // ── Protocol support ────────────────────────────────────────────────────── + + pub fn register_protocol( + &mut self, + name: impl Into, + methods: Vec, + ) { + let name = name.into(); + let def = TypeDef::Protocol { name: name.clone(), methods }; + self.types.insert(name, def); + } + + pub fn register_impl( + &mut self, + protocol_name: impl Into, + type_name: impl Into, + ) { + self.impls.insert((protocol_name.into(), type_name.into()), true); + } + + pub fn implements(&self, type_name: &str, protocol_name: &str) -> bool { + self.impls.contains_key(&(protocol_name.to_string(), type_name.to_string())) + } + + pub fn check_impl_completeness( + &self, + protocol_name: &str, + impl_method_names: &[String], + ) -> Vec { + match self.types.get(protocol_name) { + Some(TypeDef::Protocol { methods, .. }) => { + methods.iter() + .filter(|m| !impl_method_names.contains(&m.name)) + .map(|m| m.name.clone()) + .collect() + } + _ => vec![], + } + } + // ── Compatibility ───────────────────────────────────────────────────────── - /// Check whether type `a` is assignable to type `b`. - /// - /// This is a structural check with a semantic override: if both types are - /// `Named` and have Engram node type mappings, semantic compatibility is - /// checked as well. Currently the semantic check is symbolic (same node - /// type string = compatible). When an actual Engram DB is available this - /// would use cosine similarity over embeddings. pub fn check_compatible(&self, a: &Type, b: &Type) -> bool { match (a, b) { - // Unknown is compatible with everything (used during inference) (Type::Unknown, _) | (_, Type::Unknown) => true, - // Never is compatible with everything (bottom type) (Type::Never, _) => true, - // Structural matches (Type::Int, Type::Int) => true, (Type::Float, Type::Float) => true, (Type::String, Type::String) => true, (Type::Bool, Type::Bool) => true, (Type::Uuid, Type::Uuid) => true, (Type::Void, Type::Void) => true, - // Int is promotable to Float (Type::Int, Type::Float) => true, - // Named types: structural + semantic (Type::Named(a_name), Type::Named(b_name)) => { if a_name == b_name { return true; } - // Semantic compatibility via Engram node type mappings let a_node = self.engram_mappings.get(a_name); let b_node = self.engram_mappings.get(b_name); match (a_node, b_node) { @@ -199,8 +216,13 @@ impl TypeEnv { (Type::Optional(a_inner), Type::Optional(b_inner)) => { self.check_compatible(a_inner, b_inner) } - // T is compatible with T? (t, Type::Optional(inner)) => self.check_compatible(t, inner), + (Type::Result { ok: a_ok, err: a_err }, Type::Result { ok: b_ok, err: b_err }) => { + self.check_compatible(a_ok, b_ok) && self.check_compatible(a_err, b_err) + } + (Type::Map { key: ak, value: av }, Type::Map { key: bk, value: bv }) => { + self.check_compatible(ak, bk) && self.check_compatible(av, bv) + } (Type::Fn { params: ap, return_type: ar }, Type::Fn { params: bp, return_type: br }) => { ap.len() == bp.len() && ap.iter().zip(bp.iter()).all(|(a, b)| self.check_compatible(a, b)) @@ -210,11 +232,9 @@ impl TypeEnv { } } - /// Resolve a [`TypeExpr`] from the parser into a [`Type`]. pub fn resolve_type_expr(&self, te: &el_parser::TypeExpr) -> Result { match te { el_parser::TypeExpr::Named(n) => { - // Check if it's a built-in alias or a registered user type Ok(match n.as_str() { "Int" => Type::Int, "Float" => Type::Float, @@ -244,6 +264,16 @@ impl TypeEnv { let ret = self.resolve_type_expr(return_type)?; Ok(Type::Fn { params: ps, return_type: Box::new(ret) }) } + el_parser::TypeExpr::Result { ok, err } => { + let ok_ty = self.resolve_type_expr(ok)?; + let err_ty = self.resolve_type_expr(err)?; + Ok(Type::Result { ok: Box::new(ok_ty), err: Box::new(err_ty) }) + } + el_parser::TypeExpr::Map { key, value } => { + let key_ty = self.resolve_type_expr(key)?; + let val_ty = self.resolve_type_expr(value)?; + Ok(Type::Map { key: Box::new(key_ty), value: Box::new(val_ty) }) + } } } } @@ -280,7 +310,6 @@ mod tests { #[test] fn test_semantic_compatibility_via_engram_mapping() { let mut e = env(); - // Map both User and Customer to the "Entity" Engram node type e.engram_mappings.insert("User".into(), "Entity".into()); e.engram_mappings.insert("Customer".into(), "Entity".into()); assert!(e.check_compatible(&Type::Named("User".into()), &Type::Named("Customer".into()))); @@ -289,7 +318,6 @@ mod tests { #[test] fn test_optional_compatibility() { let e = env(); - // Int is compatible with Int? assert!(e.check_compatible(&Type::Int, &Type::Optional(Box::new(Type::Int)))); } @@ -305,4 +333,65 @@ mod tests { &Type::Array(Box::new(Type::String)), )); } + + #[test] + fn test_result_type_compatibility() { + let e = env(); + let r1 = Type::Result { ok: Box::new(Type::String), err: Box::new(Type::String) }; + let r2 = Type::Result { ok: Box::new(Type::String), err: Box::new(Type::String) }; + assert!(e.check_compatible(&r1, &r2)); + let r3 = Type::Result { ok: Box::new(Type::Int), err: Box::new(Type::String) }; + assert!(!e.check_compatible(&r1, &r3)); + } + + #[test] + fn test_map_type_compatibility() { + let e = env(); + let m1 = Type::Map { key: Box::new(Type::String), value: Box::new(Type::Int) }; + let m2 = Type::Map { key: Box::new(Type::String), value: Box::new(Type::Int) }; + assert!(e.check_compatible(&m1, &m2)); + let m3 = Type::Map { key: Box::new(Type::Int), value: Box::new(Type::Int) }; + assert!(!e.check_compatible(&m1, &m3)); + } + + #[test] + fn test_register_and_lookup_protocol() { + let mut e = env(); + e.register_protocol("Printable", vec![ + ProtocolMethodSig { name: "print".into(), params: vec![], return_type: Type::Void }, + ]); + assert!(matches!(e.get_type("Printable"), Some(TypeDef::Protocol { .. }))); + } + + #[test] + fn test_register_impl_and_check() { + let mut e = env(); + e.register_protocol("Printable", vec![ + ProtocolMethodSig { name: "print".into(), params: vec![], return_type: Type::Void }, + ]); + e.register_impl("Printable", "User"); + assert!(e.implements("User", "Printable")); + assert!(!e.implements("Order", "Printable")); + } + + #[test] + fn test_check_impl_completeness_missing_methods() { + let mut e = env(); + e.register_protocol("Comparable", vec![ + ProtocolMethodSig { name: "compare".into(), params: vec![], return_type: Type::Int }, + ProtocolMethodSig { name: "equals".into(), params: vec![], return_type: Type::Bool }, + ]); + let missing = e.check_impl_completeness("Comparable", &["compare".to_string()]); + assert_eq!(missing, vec!["equals"]); + } + + #[test] + fn test_check_impl_completeness_all_present() { + let mut e = env(); + e.register_protocol("Comparable", vec![ + ProtocolMethodSig { name: "compare".into(), params: vec![], return_type: Type::Int }, + ]); + let missing = e.check_impl_completeness("Comparable", &["compare".to_string()]); + assert!(missing.is_empty()); + } }