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