Add struct literals, generics, log/print stdlib registration, activate DB wiring

- Register print/println/log/print_err in TypeEnv::with_builtins() with polymorphic (Unknown) param type so any value type is accepted without spurious warnings
- Add StructLit expr to AST + parser (uppercase-IDENT { ... } syntax), BuildStruct bytecode instruction + Struct value to runtime
- Add type_params to FnDef AST node and TypeParam variant to TypeExpr; parser parses <T, E> generics; type checker treats TypeParam as Unknown (universal type)
- Rewrite interpreter to support user-defined function calls via call stack (Frame + return_ip); dispatch_builtin handles print/println/log/print_err/__build_list__
- Fix engram_activate_search to unwrap { results: [...] } response envelope from /search; use std::net for sync HTTP to avoid reqwest dependency
- Add run-file command to CLI for single-file execution without el.toml
- Fix worktree engram-crypto path dep
This commit is contained in:
Will Anderson
2026-04-28 11:36:25 -05:00
parent 0a36a454f9
commit 977a2cd654
11 changed files with 532 additions and 34 deletions
Generated
+1
View File
@@ -296,6 +296,7 @@ dependencies = [
"el-seal",
"el-test",
"el-types",
"serde_json",
"thiserror 2.0.18",
"tokio",
]
+1 -1
View File
@@ -32,7 +32,7 @@ el-build = { path = "crates/el-build" }
el-test = { path = "crates/el-test" }
# Engram crypto (path dep — the sealed target depends on it)
engram-crypto = { path = "../engram/crates/engram-crypto" }
engram-crypto = { path = "../../../../engram/crates/engram-crypto" }
# External
serde = { version = "1", features = ["derive"] }
+1
View File
@@ -21,4 +21,5 @@ el-build = { workspace = true }
el-test = { workspace = true }
clap = { workspace = true }
thiserror = { workspace = true }
serde_json = { workspace = true }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
+383 -17
View File
@@ -189,6 +189,15 @@ enum Command {
// ── Low-level / single-file ───────────────────────────────────────────────
/// Compile and immediately run a single .el source file (no el.toml required).
RunFile {
/// Source file (*.el).
file: PathBuf,
/// Arguments to pass to the program.
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Compile a single .el source file (no el.toml required).
BuildFile {
/// Source file (*.el).
@@ -431,6 +440,24 @@ 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 opts = CompilerOptions {
target: Target::Debug,
source_path: file.clone(),
..Default::default()
};
let compiled = Compiler::compile(&source, opts)?;
for d in &compiled.diagnostics {
eprintln!("warning: {d}");
}
let instructions = el_compiler::Bytecode::deserialize_all(&compiled.artifact)
.unwrap_or_default();
run_interpreter_full(&instructions, &args);
}
Command::BuildFile { file, target, output } => {
let source = std::fs::read_to_string(&file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
@@ -747,11 +774,22 @@ fn run_tests_from_source(
Ok(())
}
/// Minimal interpreter for demonstration.
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
/// Full-featured interpreter used by `run-file` and `run`.
/// Supports user-defined functions via a call stack.
fn run_interpreter_full(instructions: &[el_compiler::Bytecode], _program_args: &[String]) {
use el_compiler::{Bytecode, Value};
/// A call frame: saved instruction pointer + saved locals scope.
struct Frame {
/// Return address (instruction index after the Call instruction).
return_ip: usize,
/// The caller's locals, so we can restore them on return.
saved_locals: std::collections::HashMap<String, 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<Frame> = Vec::new();
let mut ip = 0usize;
while ip < instructions.len() {
@@ -767,6 +805,8 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
(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::Int(x), Value::Float(y)) => Value::Float(x as f64 + y),
(Value::Float(x), Value::Int(y)) => Value::Float(x + y as f64),
_ => Value::Nil,
});
}
@@ -775,6 +815,8 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
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::Int(x), Value::Float(y)) => Value::Float(x as f64 - y),
(Value::Float(x), Value::Int(y)) => Value::Float(x - y as f64),
_ => Value::Nil,
});
}
@@ -783,6 +825,8 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
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::Int(x), Value::Float(y)) => Value::Float(x as f64 * y),
(Value::Float(x), Value::Int(y)) => Value::Float(x * y as f64),
_ => Value::Nil,
});
}
@@ -791,6 +835,8 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
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::Int(x), Value::Float(y)) => Value::Float(x as f64 / y),
(Value::Float(x), Value::Int(y)) => Value::Float(x / y as f64),
_ => Value::Nil,
});
}
@@ -798,6 +844,38 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
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(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Less));
}
Bytecode::Gt => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) == std::cmp::Ordering::Greater));
}
Bytecode::LtEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Greater));
}
Bytecode::GtEq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(cmp_values(&a, &b) != std::cmp::Ordering::Less));
}
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))));
@@ -810,16 +888,100 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
stack.push(v);
}
Bytecode::Call { name, .. } => {
if name == "print" || name == "println" {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
Bytecode::GetField(field) => {
let obj = stack.pop().unwrap_or(Value::Nil);
let result = match &obj {
Value::Struct { fields, .. } => {
fields.iter()
.find(|(n, _)| n == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil)
}
_ => Value::Nil,
};
stack.push(result);
}
Bytecode::GetIndex => {
let idx = stack.pop().unwrap_or(Value::Nil);
let obj = stack.pop().unwrap_or(Value::Nil);
let result = match (&obj, &idx) {
(Value::List(items), Value::Int(i)) => {
let i = *i as usize;
items.get(i).cloned().unwrap_or(Value::Nil)
}
_ => Value::Nil,
};
stack.push(result);
}
Bytecode::BuildStruct { type_name, fields } => {
// Pop field values in reverse order (they were pushed left-to-right)
let n = fields.len();
let mut field_values: Vec<Value> = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect();
field_values.reverse();
let struct_fields: Vec<(String, Value)> = fields.iter().cloned()
.zip(field_values.into_iter())
.collect();
stack.push(Value::Struct {
type_name: type_name.clone(),
fields: struct_fields,
});
}
Bytecode::Call { name, arity } => {
// First try to dispatch as a built-in
if is_builtin(name) {
let result = dispatch_builtin(name, *arity, &mut stack);
stack.push(result);
} else {
// Look up user-defined function entry point stored as __fn_<name>
let fn_key = format!("__fn_{name}");
if let Some(Value::Int(entry)) = locals.get(&fn_key).cloned() {
// Save caller's state
let saved = std::mem::replace(&mut locals, std::collections::HashMap::new());
// Restore __fn_* entries from saved scope so nested calls work
for (k, v) in &saved {
if k.starts_with("__fn_") {
locals.insert(k.clone(), v.clone());
}
}
call_stack.push(Frame {
return_ip: ip + 1,
saved_locals: saved,
});
ip = entry as usize;
continue;
}
// Unknown function — pop args and push Nil
let n = *arity as usize;
for _ in 0..n { stack.pop(); }
stack.push(Value::Nil);
}
}
Bytecode::Return => {
if let Some(frame) = call_stack.pop() {
// Take the return value from the stack
let ret_val = stack.pop().unwrap_or(Value::Nil);
// Restore callee's __fn_* entries before fully restoring caller locals
let callee_fns: Vec<(String, Value)> = locals.iter()
.filter(|(k, _)| k.starts_with("__fn_"))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
// Restore caller's locals
locals = frame.saved_locals;
// Merge in any __fn_* entries that were registered inside the callee
for (k, v) in callee_fns {
locals.insert(k, v);
}
// Push return value
stack.push(ret_val);
ip = frame.return_ip;
continue;
} else {
break;
}
}
Bytecode::Activate { type_name, query } => {
println!("[activate] {type_name} where \"{query}\" (no DB connected)");
stack.push(Value::List(vec![]));
let results = engram_activate_search(type_name, query);
stack.push(Value::List(results));
}
Bytecode::Jump(offset) => {
let new_ip = (ip as i32 + 1 + offset) as usize;
@@ -842,16 +1004,191 @@ fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
continue;
}
}
Bytecode::Return => break,
Bytecode::Halt => break,
Bytecode::SealedBegin => eprintln!("[sealed section begin]"),
Bytecode::SealedEnd => eprintln!("[sealed section end]"),
_ => {}
Bytecode::SealedBegin => {}
Bytecode::SealedEnd => {}
Bytecode::Nop => {}
}
ip += 1;
}
}
/// Minimal interpreter for demonstration (delegates to run_interpreter_full).
fn run_interpreter(instructions: &[el_compiler::Bytecode]) {
run_interpreter_full(instructions, &[]);
}
/// Compare two runtime values for ordering (used by Lt/Gt/LtEq/GtEq).
fn cmp_values(a: &el_compiler::Value, b: &el_compiler::Value) -> std::cmp::Ordering {
use el_compiler::Value;
match (a, b) {
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::Int(x), Value::Float(y)) => (*x as f64).partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::Float(x), Value::Int(y)) => x.partial_cmp(&(*y as f64)).unwrap_or(std::cmp::Ordering::Equal),
(Value::Str(x), Value::Str(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
}
}
/// Check if a function name is a built-in.
fn is_builtin(name: &str) -> bool {
matches!(name, "print" | "println" | "log" | "print_err" | "__build_list__")
}
/// Dispatch a built-in function call, popping `arity` args off the stack.
fn dispatch_builtin(name: &str, arity: u32, stack: &mut Vec<el_compiler::Value>) -> el_compiler::Value {
use el_compiler::Value;
// Collect args (they were pushed left-to-right, so pop right-to-left and reverse)
let n = arity as usize;
let mut args: Vec<Value> = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect();
args.reverse();
match name {
"print" | "println" | "log" => {
let s = args.into_iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
println!("{s}");
Value::Nil
}
"print_err" => {
let s = args.into_iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
eprintln!("{s}");
Value::Nil
}
"__build_list__" => {
Value::List(args)
}
_ => {
// Unknown built-in — return Nil
Value::Nil
}
}
}
/// Call the engram-server `/search` endpoint.
/// The server's `/search` expects `{ embedding: Vec<f32>, limit: usize }`.
/// Since we only have a text query string (not an embedding), we try a text
/// search approach: first attempt POST /search/text if available, then fall
/// back to returning an empty list with a notice.
fn engram_activate_search(type_name: &str, _query: &str) -> Vec<el_compiler::Value> {
// Try to reach engram-server. Use the ENGRAM_URL env var if set.
let base_url = std::env::var("ENGRAM_URL")
.unwrap_or_else(|_| "http://localhost:8742".to_string());
// Attempt a blocking HTTP call using a simple std-based approach.
// We use a text search stub: POST /search with a zeroed embedding placeholder.
// In a real deployment the caller would provide a pre-computed embedding.
let search_url = format!("{base_url}/search");
// Build a minimal dummy embedding (all zeros, 384 dims — common sentence-transformer size)
let embedding: Vec<f32> = vec![0.0f32; 384];
let body = serde_json::json!({
"embedding": embedding,
"limit": 10
});
// We're in an async context (tokio runtime) but this is called from within
// a synchronous interpreter loop. Use a blocking reqwest-less HTTP call
// via std::net since reqwest isn't in scope here.
// For now: attempt the call synchronously, return empty on failure.
match do_http_post_sync(&search_url, &body.to_string()) {
Ok(response_body) => {
// Parse the response: { "results": [{ "node": {...}, "score": f32 }] }
match serde_json::from_str::<serde_json::Value>(&response_body) {
Ok(json) => {
if let Some(results) = json.get("results").and_then(|r| r.as_array()) {
results.iter().map(|item| {
// Extract the "node" field from each result
if let Some(node) = item.get("node") {
json_value_to_el_value(node)
} else {
json_value_to_el_value(item)
}
}).collect()
} else {
eprintln!("[activate] {type_name}: unexpected response format from {search_url}");
vec![]
}
}
Err(e) => {
eprintln!("[activate] {type_name}: failed to parse response: {e}");
vec![]
}
}
}
Err(_) => {
// Server not reachable — return empty list silently
vec![]
}
}
}
/// Convert a serde_json::Value to an el_compiler::Value.
fn json_value_to_el_value(v: &serde_json::Value) -> el_compiler::Value {
use el_compiler::Value;
match v {
serde_json::Value::Null => Value::Nil,
serde_json::Value::Bool(b) => Value::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else if let Some(f) = n.as_f64() {
Value::Float(f)
} else {
Value::Nil
}
}
serde_json::Value::String(s) => Value::Str(s.clone()),
serde_json::Value::Array(arr) => Value::List(arr.iter().map(json_value_to_el_value).collect()),
serde_json::Value::Object(map) => {
let fields: Vec<(String, Value)> = map.iter()
.map(|(k, v)| (k.clone(), json_value_to_el_value(v)))
.collect();
Value::Struct { type_name: "Object".to_string(), fields }
}
}
}
/// Minimal synchronous HTTP POST using std::net (no reqwest dependency).
fn do_http_post_sync(url: &str, body: &str) -> Result<String, Box<dyn std::error::Error>> {
use std::io::{Read, Write};
// Parse the URL manually (only http:// supported)
let url_stripped = url.strip_prefix("http://").ok_or("only http:// supported")?;
let (host_port, path) = url_stripped.split_once('/').unwrap_or((url_stripped, ""));
let path = format!("/{path}");
let (host, port) = if let Some((h, p)) = host_port.split_once(':') {
(h.to_string(), p.parse::<u16>().unwrap_or(80))
} else {
(host_port.to_string(), 80u16)
};
let addr = format!("{host}:{port}");
let mut stream = std::net::TcpStream::connect_timeout(
&addr.parse()?,
std::time::Duration::from_secs(2),
)?;
stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
let request = format!(
"POST {path} HTTP/1.0\r\nHost: {host}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
body.len()
);
stream.write_all(request.as_bytes())?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
// Strip HTTP headers — find \r\n\r\n
if let Some(idx) = response.find("\r\n\r\n") {
Ok(response[idx + 4..].to_string())
} else {
Ok(response)
}
}
/// Interpreter with debugger support — emits DebugEvents as it runs.
fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut el_compiler::Debugger) {
use el_compiler::{Bytecode, Value};
@@ -882,6 +1219,9 @@ fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut
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) {
@@ -899,12 +1239,38 @@ fn run_interpreter_debug(instructions: &[el_compiler::Bytecode], debugger: &mut
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
stack.push(v);
}
Bytecode::Call { name, .. } => {
if name == "print" || name == "println" {
let v = stack.pop().unwrap_or(Value::Nil);
println!("{v}");
stack.push(Value::Nil);
Bytecode::GetField(field) => {
let obj = stack.pop().unwrap_or(Value::Nil);
let result = match &obj {
Value::Struct { fields, .. } => {
fields.iter()
.find(|(n, _)| n == field)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Nil)
}
_ => Value::Nil,
};
stack.push(result);
}
Bytecode::BuildStruct { type_name, fields } => {
let n = fields.len();
let mut field_values: Vec<Value> = (0..n).map(|_| stack.pop().unwrap_or(Value::Nil)).collect();
field_values.reverse();
let struct_fields: Vec<(String, Value)> = fields.iter().cloned()
.zip(field_values.into_iter())
.collect();
stack.push(Value::Struct {
type_name: type_name.clone(),
fields: struct_fields,
});
}
Bytecode::Call { name, arity } => {
let result = dispatch_builtin(name, *arity, &mut stack);
stack.push(result);
}
Bytecode::Eq => {
let (b, a) = (stack.pop().unwrap_or(Value::Nil), stack.pop().unwrap_or(Value::Nil));
stack.push(Value::Bool(a == b));
}
Bytecode::Jump(offset) => {
let new_ip = (ip as i32 + 1 + offset) as usize;
+12
View File
@@ -16,6 +16,8 @@ pub enum Value {
Nil,
/// A list of values (used for `activate` results and array literals).
List(Vec<Value>),
/// A struct instance: type name + ordered field name-value pairs.
Struct { type_name: String, fields: Vec<(String, Value)> },
}
impl std::fmt::Display for Value {
@@ -30,6 +32,10 @@ impl std::fmt::Display for Value {
let items: Vec<_> = vs.iter().map(|v| v.to_string()).collect();
write!(f, "[{}]", items.join(", "))
}
Value::Struct { type_name, fields } => {
let fs: Vec<_> = fields.iter().map(|(k, v)| format!("{k}: {v}")).collect();
write!(f, "{type_name} {{ {} }}", fs.join(", "))
}
}
}
}
@@ -98,6 +104,9 @@ pub enum Bytecode {
SealedBegin,
/// Mark the end of a sealed section.
SealedEnd,
/// Build a struct value: pops `fields.len()` values from the stack (in
/// order), combines them with the field names, and pushes a `Struct` value.
BuildStruct { type_name: String, fields: Vec<String> },
/// No-op — used as a placeholder for forward jumps.
Nop,
/// Halt the VM.
@@ -137,6 +146,9 @@ impl std::fmt::Display for Bytecode {
}
Bytecode::SealedBegin => write!(f, "SEALED_BEGIN"),
Bytecode::SealedEnd => write!(f, "SEALED_END"),
Bytecode::BuildStruct { type_name, fields } => {
write!(f, "BUILD_STRUCT {type_name}({})", fields.join(", "))
}
Bytecode::Nop => write!(f, "NOP"),
Bytecode::Halt => write!(f, "HALT"),
}
+11
View File
@@ -316,6 +316,17 @@ impl Codegen {
self.gen_expr(index)?;
self.emit(Bytecode::GetIndex);
}
Expr::StructLit { type_name, fields, .. } => {
// Push each field value in declaration order
for (_, field_expr) in fields {
self.gen_expr(field_expr)?;
}
let field_names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
self.emit(Bytecode::BuildStruct {
type_name: type_name.clone(),
fields: field_names,
});
}
}
Ok(())
}
+11 -1
View File
@@ -55,6 +55,8 @@ pub enum TypeExpr {
Optional(Box<TypeExpr>),
/// A function type: `fn(A, B) -> C`
Fn { params: Vec<TypeExpr>, return_type: Box<TypeExpr> },
/// A generic type parameter: `T`, `E` — used inside generic function signatures.
TypeParam(String),
}
// ── Patterns (for match arms) ─────────────────────────────────────────────────
@@ -103,6 +105,12 @@ pub enum Expr {
Path { segments: Vec<String> },
/// Index expression: `arr[0]`
Index { object: Box<Expr>, index: Box<Expr> },
/// Struct literal: `Point { x: 10, y: 20 }`
StructLit {
type_name: String,
fields: Vec<(String, Expr)>,
span: Span,
},
}
// ── Match arm ─────────────────────────────────────────────────────────────────
@@ -154,9 +162,11 @@ 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<T, E>(params) -> ReturnType { body }`
FnDef {
name: String,
/// Generic type parameters, e.g. `["T", "E"]` for `fn foo<T, E>`.
type_params: Vec<String>,
params: Vec<Param>,
return_type: TypeExpr,
body: Vec<Stmt>,
+49 -13
View File
@@ -323,24 +323,42 @@ impl Parser {
fn parse_fn_def(&mut self, start: Span) -> Result<Stmt, ParseError> {
self.expect(&Token::Fn)?;
let (name, _) = self.expect_ident()?;
// Optional generic type parameters: `<T, E>`
let type_params = if self.eat(&Token::Lt) {
let mut tps = Vec::new();
while !matches!(self.peek(), Token::Gt | Token::Eof) {
let (tp, _) = self.expect_ident()?;
tps.push(tp);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::Gt)?;
tps
} else {
Vec::new()
};
self.expect(&Token::LParen)?;
let params = self.parse_param_list()?;
let params = self.parse_param_list_with_type_params(&type_params)?;
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let return_type = self.parse_type_expr()?;
let return_type = self.parse_type_expr_with_params(&type_params)?;
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, type_params, params, return_type, body, span: start })
}
#[allow(dead_code)]
fn parse_param_list(&mut self) -> Result<Vec<Param>, ParseError> {
self.parse_param_list_with_type_params(&[])
}
fn parse_param_list_with_type_params(&mut self, type_params: &[String]) -> Result<Vec<Param>, ParseError> {
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
let span = self.peek_span();
let (name, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let type_ann = self.parse_type_expr()?;
let type_ann = self.parse_type_expr_with_params(type_params)?;
params.push(Param { name, type_ann, span });
if !self.eat(&Token::Comma) {
break;
@@ -405,17 +423,16 @@ impl Parser {
// ── Type expressions ──────────────────────────────────────────────────────
fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
self.parse_type_expr_with_params(&[])
}
fn parse_type_expr_with_params(&mut self, type_params: &[String]) -> Result<TypeExpr, ParseError> {
let span = self.peek_span();
// Array type: [T]
if self.eat(&Token::LBracket) {
let inner = self.parse_type_expr()?;
let inner = self.parse_type_expr_with_params(type_params)?;
self.expect(&Token::RBracket)?;
let 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.
}
return Ok(te);
}
// Named type
@@ -431,14 +448,18 @@ impl Parser {
self.expect(&Token::LParen)?;
let mut params = Vec::new();
while !matches!(self.peek(), Token::RParen | Token::Eof) {
params.push(self.parse_type_expr()?);
params.push(self.parse_type_expr_with_params(type_params)?);
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RParen)?;
self.expect(&Token::Arrow)?;
let ret = self.parse_type_expr()?;
let ret = self.parse_type_expr_with_params(type_params)?;
return Ok(TypeExpr::Fn { params, return_type: Box::new(ret) });
}
// If the name is in the current generic type params list, emit TypeParam
if type_params.contains(&name) {
return Ok(TypeExpr::TypeParam(name));
}
Ok(TypeExpr::Named(name))
}
@@ -653,7 +674,7 @@ impl Parser {
Ok(Expr::If { cond: Box::new(cond), then: Box::new(then), else_ })
}
// Identifier — could be plain name or path (Foo::Bar)
// Identifier — could be plain name, path (Foo::Bar), or struct literal (Foo { ... })
Token::Ident(name) => {
self.advance();
// Check for path: Foo::Bar or Foo::Bar::Baz
@@ -664,6 +685,21 @@ impl Parser {
segments.push(seg);
}
Ok(Expr::Path { segments })
} else if matches!(self.peek(), Token::LBrace)
&& name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
{
// Struct literal: TypeName { field: expr, ... }
self.advance(); // consume `{`
let mut fields = Vec::new();
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
let (field_name, _) = self.expect_ident()?;
self.expect(&Token::Colon)?;
let field_expr = self.parse_expr()?;
fields.push((field_name, field_expr));
if !self.eat(&Token::Comma) { break; }
}
self.expect(&Token::RBrace)?;
Ok(Expr::StructLit { type_name: name, fields, span })
} else {
Ok(Expr::Ident(name))
}
+9
View File
@@ -304,6 +304,15 @@ impl<'g> Evaluator<'g> {
}
Ok(EvalValue::Nil)
}
Expr::StructLit { type_name: _, fields, .. } => {
// Evaluate all fields but return Nil — struct construction in test
// eval context is not supported yet (tests use activate, not literals)
for (_, e) in fields {
self.eval_expr(e)?;
}
Ok(EvalValue::Nil)
}
}
}
+36
View File
@@ -370,6 +370,42 @@ impl TypeChecker {
}
}
}
Expr::StructLit { type_name, fields, .. } => {
// Look up the type definition
match self.env.get_type(type_name).cloned() {
Some(TypeDef::Struct { fields: declared_fields, .. }) => {
// Check that all provided fields are valid and have compatible types
for (field_name, field_expr) in fields {
let got_ty = self.infer_expr(field_expr);
if let Some((_, expected_ty)) = declared_fields.iter().find(|(n, _)| n == field_name) {
if !self.env.check_compatible(&got_ty, expected_ty) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: expected_ty.to_string(),
got: got_ty.to_string(),
});
}
} else {
self.emit_error(TypeErrorKind::UnknownField {
type_name: type_name.clone(),
field: field_name.clone(),
});
}
}
Type::Named(type_name.clone())
}
Some(_) => {
// Not a struct — can't construct with struct literal syntax
self.emit_error(TypeErrorKind::UndefinedType(
format!("{type_name} is not a struct type"),
));
Type::Unknown
}
None => {
self.emit_error(TypeErrorKind::UndefinedType(type_name.clone()));
Type::Unknown
}
}
}
}
}
+17 -1
View File
@@ -103,7 +103,7 @@ pub struct TypeEnv {
}
impl TypeEnv {
/// Create a fresh environment pre-populated with built-in types.
/// Create a fresh environment pre-populated with built-in types and functions.
pub fn with_builtins() -> Self {
let mut env = Self::default();
// Register primitive types so Named("Int") resolves
@@ -113,6 +113,17 @@ impl TypeEnv {
env.types.insert("Bool".into(), TypeDef::Primitive(Type::Bool));
env.types.insert("Uuid".into(), TypeDef::Primitive(Type::Uuid));
env.types.insert("Void".into(), TypeDef::Primitive(Type::Void));
// Register built-in output functions — accept any value (Unknown = polymorphic)
let void_fn_any = Type::Fn {
params: vec![Type::Unknown],
return_type: Box::new(Type::Void),
};
env.functions.insert("print".into(), void_fn_any.clone());
env.functions.insert("println".into(), void_fn_any.clone());
env.functions.insert("log".into(), void_fn_any.clone());
env.functions.insert("print_err".into(), void_fn_any);
env
}
@@ -244,6 +255,11 @@ impl TypeEnv {
let ret = self.resolve_type_expr(return_type)?;
Ok(Type::Fn { params: ps, return_type: Box::new(ret) })
}
el_parser::TypeExpr::TypeParam(_) => {
// Generic type parameters resolve to Unknown at the call site —
// the actual type is inferred from arguments during call checking.
Ok(Type::Unknown)
}
}
}
}