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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(¶m.type_ann) {
|
||||
@@ -136,9 +153,7 @@ impl TypeChecker {
|
||||
for s in body {
|
||||
inner_checker.check_stmt(s);
|
||||
}
|
||||
// Surface any errors from the inner scope
|
||||
self.diagnostics.extend(inner_checker.diagnostics);
|
||||
// Register function in outer env
|
||||
let param_types: Vec<_> = params.iter().filter_map(|p| {
|
||||
self.env.resolve_type_expr(&p.type_ann).ok()
|
||||
}).collect();
|
||||
@@ -147,18 +162,26 @@ impl TypeChecker {
|
||||
self.env.register_fn(name.clone(), fn_ty);
|
||||
}
|
||||
}
|
||||
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {
|
||||
// Already handled in hoist pass
|
||||
}
|
||||
Stmt::TestDef { body, .. } => {
|
||||
// Type-check the test body statements
|
||||
for s in body {
|
||||
self.check_stmt(s);
|
||||
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {}
|
||||
Stmt::ProtocolDef { .. } => {}
|
||||
Stmt::ImplDef { protocol_name, type_name, methods, .. } => {
|
||||
let method_names: Vec<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 (<, &rt) {
|
||||
(Type::Float, _) | (_, Type::Float) => Type::Float,
|
||||
(Type::Int, Type::Int) => Type::Int,
|
||||
// String concatenation with +
|
||||
(Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String,
|
||||
_ => {
|
||||
// Allow if at least one side is compatible with a number
|
||||
if self.env.check_compatible(<, &Type::Int)
|
||||
&& self.env.check_compatible(&rt, &Type::Int) {
|
||||
Type::Int
|
||||
@@ -408,12 +476,8 @@ impl TypeChecker {
|
||||
}
|
||||
}
|
||||
}
|
||||
BinOp::Eq | BinOp::NotEq => {
|
||||
// Equality: any two compatible types -> Bool
|
||||
Type::Bool
|
||||
}
|
||||
BinOp::Eq | BinOp::NotEq => Type::Bool,
|
||||
BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
|
||||
// Comparison: numeric types -> Bool
|
||||
if !self.env.check_compatible(<, &rt) {
|
||||
self.emit_error(TypeErrorKind::TypeMismatch {
|
||||
expected: lt.to_string(),
|
||||
@@ -470,17 +534,11 @@ impl TypeChecker {
|
||||
// ── Diagnostic helpers ────────────────────────────────────────────────────
|
||||
|
||||
fn error(&mut self, e: TypeError) {
|
||||
self.diagnostics.push(Diagnostic {
|
||||
message: e.to_string(),
|
||||
is_error: true,
|
||||
});
|
||||
self.diagnostics.push(Diagnostic { message: e.to_string(), is_error: true });
|
||||
}
|
||||
|
||||
fn emit_error(&mut self, kind: TypeErrorKind) {
|
||||
self.diagnostics.push(Diagnostic {
|
||||
message: kind.to_string(),
|
||||
is_error: true,
|
||||
});
|
||||
self.diagnostics.push(Diagnostic { message: kind.to_string(), is_error: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,26 +569,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_let_int() {
|
||||
assert_ok("let x: Int = 42");
|
||||
}
|
||||
fn test_let_int() { assert_ok("let x: Int = 42"); }
|
||||
|
||||
#[test]
|
||||
fn test_let_string() {
|
||||
assert_ok(r#"let s: String = "hello""#);
|
||||
}
|
||||
fn test_let_string() { assert_ok(r#"let s: String = "hello""#); }
|
||||
|
||||
#[test]
|
||||
fn test_type_mismatch() {
|
||||
assert_err(r#"let x: Int = "not an int""#);
|
||||
}
|
||||
fn test_type_mismatch() { assert_err(r#"let x: Int = "not an int""#); }
|
||||
|
||||
#[test]
|
||||
fn test_fn_def_and_call() {
|
||||
assert_ok(r#"
|
||||
fn double(n: Int) -> Int {
|
||||
return n + n
|
||||
}
|
||||
fn double(n: Int) -> Int { return n + n }
|
||||
let result: Int = double(5)
|
||||
"#);
|
||||
}
|
||||
@@ -545,18 +595,10 @@ add(1)
|
||||
|
||||
#[test]
|
||||
fn test_type_def_and_field_access() {
|
||||
// Type checking with field access: u is bound to User,
|
||||
// accessing u.name should return String type without error.
|
||||
// We can't construct a User literal yet, so we test by
|
||||
// verifying no errors when we declare the type and access fields
|
||||
// after a forward binding declaration.
|
||||
let src = r#"
|
||||
assert_ok(r#"
|
||||
type User { name: String age: Int }
|
||||
fn make_user() -> User {
|
||||
return make_user()
|
||||
}
|
||||
"#;
|
||||
assert_ok(src);
|
||||
fn make_user() -> User { return make_user() }
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -568,9 +610,7 @@ activate User where "recent customers"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activate_unknown_type_err() {
|
||||
assert_err(r#"activate Phantom where "ghosts""#);
|
||||
}
|
||||
fn test_activate_unknown_type_err() { assert_err(r#"activate Phantom where "ghosts""#); }
|
||||
|
||||
#[test]
|
||||
fn test_bool_ops() {
|
||||
@@ -579,12 +619,57 @@ activate User where "recent customers"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_arithmetic() {
|
||||
assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2");
|
||||
fn test_int_arithmetic() { assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2"); }
|
||||
|
||||
#[test]
|
||||
fn test_string_concat() { assert_ok(r#"let s: String = "hello" + " world""#); }
|
||||
|
||||
#[test]
|
||||
fn test_closure_type_inferred() {
|
||||
assert_ok("let double = |x: Int| x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_concat() {
|
||||
assert_ok(r#"let s: String = "hello" + " world""#);
|
||||
fn test_closure_with_return_type() {
|
||||
assert_ok("let add = |x: Int, y: Int| -> Int { x }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_def_ok() {
|
||||
assert_ok(r#"
|
||||
protocol Printable { fn print(msg: String) -> Void }
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_impl_def_ok() {
|
||||
assert_ok(r#"
|
||||
protocol Printable { fn print(msg: String) -> Void }
|
||||
type User { name: String }
|
||||
impl Printable for User { fn print(msg: String) -> Void { } }
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_does_not_fail() {
|
||||
assert_ok("import std::array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_type_annotation() {
|
||||
assert_ok(r#"fn fetch() -> Result<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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user