Add server-side builtins, import system, and http_serve for Neuron Code rewrite

- Import resolution: resolve_imports() pre-processes import statements by
  reading and concatenating referenced .el files before compilation
- http_serve builtin: tiny_http-based server on configurable port; POST
  /axon/message stores request in __request__ state, invokes handle_request
  entry point via sub-interpreter, reads __response__ state for reply
- New builtins: blake3_hash, uuid_new, fs_list_recursive, fs_mkdir, fs_exists,
  path_join, path_parent, str_trim, str_contains, str_replace, str_starts_with,
  str_ends_with, str_last_index_of, json_get, json_array_push, json_array_len,
  now_millis, http_get, http_post, int_to_str
- Catch-all arms in el-types and el-compiler for new AST variants (Import,
  ProtocolDef, ImplDef, Closure, Try, MapLiteral, TypeExpr::Result, TypeExpr::Map)
- Parser: decorators field on FnDef, import/protocol/impl parsing
This commit is contained in:
Will Anderson
2026-04-27 20:08:55 -05:00
parent 46d5650e45
commit 316c0a85ce
12 changed files with 1796 additions and 181 deletions
Generated
+62
View File
@@ -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"
+3
View File
@@ -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"
+4
View File
@@ -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 }
+838 -4
View File
@@ -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::collections::HashMap<String, String>> =
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<Option<Box<dyn Fn()>>> =
std::cell::RefCell::new(None);
/// Shared bytecode instructions for sub-interpreter calls from http_serve.
static SERVE_INSTRUCTIONS: std::cell::RefCell<Option<std::sync::Arc<Vec<el_compiler::Bytecode>>>> =
std::cell::RefCell::new(None);
/// Shared fn_table for sub-interpreter calls.
static SERVE_FN_TABLE: std::cell::RefCell<Option<std::sync::Arc<std::collections::HashMap<String, usize>>>> =
std::cell::RefCell::new(None);
}
// ── CLI definition ────────────────────────────────────────────────────────────
#[derive(Parser, Debug)]
@@ -441,8 +461,8 @@ async fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
// ── 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<dyn std::error::Error>> {
// ── 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<String, Box<dyn std::error::Error>> {
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<PathBuf>,
) -> Result<String, Box<dyn std::error::Error>> {
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<PathBuf, Box<dyn std::error::Error>> {
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<String, usize>,
entry: usize,
) -> el_compiler::Value {
use el_compiler::{Bytecode, Value};
let mut stack: Vec<Value> = Vec::new();
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut call_stack: Vec<(usize, std::collections::HashMap<String, Value>)> = Vec::new();
let mut ip = entry;
let program_args: Vec<String> = 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!("<field:{field}>")));
}
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<String, Value>)> = 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::<Vec<_>>()
})
.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::<Result<Vec<_>, _>>() {
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::<Vec<_>>()
});
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::<tiny_http::Header>().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::<tiny_http::Header>().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<char> = 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::<serde_json::Value>(&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::<serde_json::Value>(&json_str)
.ok()
.and_then(|v| {
if let serde_json::Value::Object(map) = v {
Some(map.keys().cloned().map(Value::Str).collect::<Vec<_>>())
} 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::<serde_json::Value>(&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::<serde_json::Value>(&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::<serde_json::Value>(&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::<i64>()
.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<String> = 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),
}
}
+7
View File
@@ -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),
}
}
+37 -7
View File
@@ -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");
+26
View File
@@ -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}"),
+58 -1
View File
@@ -55,6 +55,10 @@ pub enum TypeExpr {
Optional(Box<TypeExpr>),
/// A function type: `fn(A, B) -> C`
Fn { params: Vec<TypeExpr>, return_type: Box<TypeExpr> },
/// `Result<T, E>` — built-in error-propagation type
Result { ok: Box<TypeExpr>, err: Box<TypeExpr> },
/// `Map<K, V>` — built-in key-value map type
Map { key: Box<TypeExpr>, value: Box<TypeExpr> },
}
// ── Patterns (for match arms) ─────────────────────────────────────────────────
@@ -103,6 +107,17 @@ pub enum Expr {
Path { segments: Vec<String> },
/// Index expression: `arr[0]`
Index { object: Box<Expr>, index: Box<Expr> },
/// Closure: `|x: Int| x * 2` or `|x: Int| -> Int { x * 2 }`
Closure {
params: Vec<Param>,
return_type: Option<TypeExpr>,
body: Box<Expr>,
span: Span,
},
/// Try operator: `expr?` — unwraps Result, propagates error
Try(Box<Expr>),
/// 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<Expr>,
pub span: Span,
}
// ── Protocol ──────────────────────────────────────────────────────────────────
/// A method signature inside a protocol definition.
#[derive(Debug, Clone, PartialEq)]
pub struct ProtocolMethod {
pub name: String,
pub params: Vec<Param>,
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<Decorator>,
params: Vec<Param>,
return_type: TypeExpr,
body: Vec<Stmt>,
@@ -185,6 +222,26 @@ pub enum Stmt {
Seed(SeedStmt, Span),
/// `assert <expr>`
Assert(Expr, Span),
/// `import std::collections::Map` or `from pkg import { A, B }`
Import {
path: Vec<String>,
names: Vec<String>,
alias: Option<String>,
span: Span,
},
/// `protocol Name { method sigs... }`
ProtocolDef {
name: String,
methods: Vec<ProtocolMethod>,
span: Span,
},
/// `impl Protocol for TypeName { fn ... }`
ImplDef {
protocol_name: String,
type_name: String,
methods: Vec<Stmt>,
span: Span,
},
}
// ── Top-level program ─────────────────────────────────────────────────────────
+424 -13
View File
@@ -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<Stmt, ParseError> {
fn parse_fn_def(&mut self, start: Span, decorators: Vec<Decorator>) -> Result<Stmt, ParseError> {
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<Stmt, ParseError> {
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<Stmt, ParseError> {
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<Stmt, ParseError> {
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<Stmt, ParseError> {
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<Stmt, ParseError> {
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<Vec<Param>, 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<T, E> — 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<K, V> — 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<Vec<MatchArm>, 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<String, String> { 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<String, Int> = 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);
}
}
+7
View File
@@ -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)?;
+196 -111
View File
@@ -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<Diagnostic>,
@@ -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(&param.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<String> = 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<T, E>".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 (&lt, &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(&lt, &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(&lt, &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<String, String> { return fetch() }"#);
}
#[test]
fn test_map_type_annotation() {
assert_ok(r#"let m: Map<String, Int> = m"#);
}
#[test]
fn test_decorator_does_not_break_fn() {
assert_ok(r#"
@public
fn greet(name: String) -> String { return name }
"#);
}
}
+134 -45
View File
@@ -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<Type>),
/// An optional (nullable) value.
Optional(Box<Type>),
Result { ok: Box<Type>, err: Box<Type> },
Map { key: Box<Type>, value: Box<Type> },
// ── Function ──────────────────────────────────────────────────────────────
Fn { params: Vec<Type>, return_type: Box<Type> },
// ── 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<EnumVariant>,
},
/// 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<ProtocolMethodSig>,
},
}
#[derive(Debug, Clone)]
pub struct EnumVariant {
pub name: std::string::String,
/// Payload type for tuple variants like `Pending(String)`.
pub payload: Option<Type>,
}
#[derive(Debug, Clone)]
pub struct ProtocolMethodSig {
pub name: std::string::String,
pub params: Vec<Type>,
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<std::string::String, Type>,
/// Maps type names to their definitions.
pub types: HashMap<std::string::String, TypeDef>,
/// 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<std::string::String, std::string::String>,
/// Maps function names to their function types.
pub functions: HashMap<std::string::String, Type>,
/// 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<std::string::String>,
@@ -147,7 +138,6 @@ impl TypeEnv {
self.types.get(name)
}
/// Register a function signature.
pub fn register_fn(&mut self, name: impl Into<std::string::String>, 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<std::string::String>,
methods: Vec<ProtocolMethodSig>,
) {
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<std::string::String>,
type_name: impl Into<std::string::String>,
) {
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<String> {
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<Type, crate::TypeError> {
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());
}
}