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

- Import resolution: resolve_imports() pre-processes import statements by
  reading and concatenating referenced .el files before compilation
- http_serve builtin: tiny_http-based server on configurable port; POST
  /axon/message stores request in __request__ state, invokes handle_request
  entry point via sub-interpreter, reads __response__ state for reply
- New builtins: blake3_hash, uuid_new, fs_list_recursive, fs_mkdir, fs_exists,
  path_join, path_parent, str_trim, str_contains, str_replace, str_starts_with,
  str_ends_with, str_last_index_of, json_get, json_array_push, json_array_len,
  now_millis, http_get, http_post, int_to_str
- Catch-all arms in el-types and el-compiler for new AST variants (Import,
  ProtocolDef, ImplDef, Closure, Try, MapLiteral, TypeExpr::Result, TypeExpr::Map)
- Parser: decorators field on FnDef, import/protocol/impl parsing
This commit is contained in:
Will Anderson
2026-04-27 20:08:55 -05:00
parent 46d5650e45
commit 316c0a85ce
12 changed files with 1796 additions and 181 deletions
+196 -111
View File
@@ -3,7 +3,7 @@
use el_parser::{BinOp, Expr, Literal, Program, Stmt};
use crate::error::{TypeError, TypeErrorKind};
use crate::types::{EnumVariant, Type, TypeDef, TypeEnv};
use crate::types::{EnumVariant, ProtocolMethodSig, Type, TypeDef, TypeEnv};
/// Diagnostics produced by the type checker.
#[derive(Debug, Clone)]
@@ -13,10 +13,6 @@ pub struct Diagnostic {
}
/// Entry point: type-check a parsed program.
///
/// Returns a list of diagnostics. An empty list means the program is
/// well-typed. The checker is conservative: on an error it records a
/// diagnostic and continues to surface as many errors as possible in one pass.
pub struct TypeChecker {
pub env: TypeEnv,
pub diagnostics: Vec<Diagnostic>,
@@ -33,19 +29,14 @@ impl TypeChecker {
// ── Public API ────────────────────────────────────────────────────────────
/// Check the entire program. Returns the list of diagnostics.
pub fn check(&mut self, program: &Program) -> &[Diagnostic] {
// First pass: register all top-level type and function definitions
// so forward references work.
self.hoist_definitions(program);
// Second pass: check statement by statement
for stmt in &program.stmts {
self.check_stmt(stmt);
}
&self.diagnostics
}
/// Returns `true` if no error diagnostics were emitted.
pub fn ok(&self) -> bool {
!self.diagnostics.iter().any(|d| d.is_error)
}
@@ -54,44 +45,71 @@ impl TypeChecker {
fn hoist_definitions(&mut self, program: &Program) {
for stmt in &program.stmts {
match stmt {
Stmt::TypeDef { name, fields, .. } => {
let resolved_fields: Vec<_> = fields.iter().filter_map(|f| {
match self.env.resolve_type_expr(&f.type_ann) {
Ok(ty) => Some((f.name.clone(), ty)),
Err(e) => { self.error(e); None }
self.hoist_stmt(stmt);
}
}
fn hoist_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::TypeDef { name, fields, .. } => {
let resolved_fields: Vec<_> = fields.iter().filter_map(|f| {
match self.env.resolve_type_expr(&f.type_ann) {
Ok(ty) => Some((f.name.clone(), ty)),
Err(e) => { self.error(e); None }
}
}).collect();
let def = TypeDef::Struct { name: name.clone(), fields: resolved_fields };
self.env.register_type(name.clone(), def, "");
}
Stmt::EnumDef { name, variants, .. } => {
let resolved_variants: Vec<_> = variants.iter().filter_map(|v| {
let payload = if let Some(pt) = &v.payload {
match self.env.resolve_type_expr(pt) {
Ok(ty) => Some(ty),
Err(e) => { self.error(e); return None; }
}
}).collect();
let def = TypeDef::Struct { name: name.clone(), fields: resolved_fields };
self.env.register_type(name.clone(), def, "");
} else { None };
Some(EnumVariant { name: v.name.clone(), payload })
}).collect();
let def = TypeDef::Enum { name: name.clone(), variants: resolved_variants };
self.env.register_type(name.clone(), def, "");
}
Stmt::FnDef { name, params, return_type, .. } => {
let param_types: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
let fn_ty = Type::Fn { params: param_types, return_type: Box::new(ret) };
self.env.register_fn(name.clone(), fn_ty);
}
Stmt::EnumDef { name, variants, .. } => {
let resolved_variants: Vec<_> = variants.iter().filter_map(|v| {
let payload = if let Some(pt) = &v.payload {
match self.env.resolve_type_expr(pt) {
Ok(ty) => Some(ty),
Err(e) => { self.error(e); return None; }
}
} else { None };
Some(EnumVariant { name: v.name.clone(), payload })
}).collect();
let def = TypeDef::Enum { name: name.clone(), variants: resolved_variants };
self.env.register_type(name.clone(), def, "");
}
Stmt::FnDef { name, params, return_type, .. } => {
let param_types: Vec<_> = params.iter().filter_map(|p| {
}
Stmt::ProtocolDef { name, methods, .. } => {
let sigs: Vec<_> = methods.iter().filter_map(|m| {
let pt: Vec<_> = m.params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
let fn_ty = Type::Fn {
params: param_types,
return_type: Box::new(ret),
};
self.env.register_fn(name.clone(), fn_ty);
if let Ok(ret) = self.env.resolve_type_expr(&m.return_type) {
Some(ProtocolMethodSig { name: m.name.clone(), params: pt, return_type: ret })
} else {
None
}
}).collect();
self.env.register_protocol(name.clone(), sigs);
}
Stmt::ImplDef { protocol_name, type_name, methods, .. } => {
for m in methods {
if let Stmt::FnDef { name, params, return_type, .. } = m {
let pt: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
if let Ok(ret) = self.env.resolve_type_expr(return_type) {
self.env.register_fn(name.clone(), Type::Fn { params: pt, return_type: Box::new(ret) });
}
}
}
_ => {}
self.env.register_impl(protocol_name.clone(), type_name.clone());
}
_ => {}
}
}
@@ -124,7 +142,6 @@ impl TypeChecker {
Stmt::Return(expr, _) => { self.infer_expr(expr); }
Stmt::Expr(expr, _) => { self.infer_expr(expr); }
Stmt::FnDef { name, params, return_type, body, .. } => {
// Push new scope for function body
let mut inner_env = self.env.clone();
for param in params {
if let Ok(ty) = inner_env.resolve_type_expr(&param.type_ann) {
@@ -136,9 +153,7 @@ impl TypeChecker {
for s in body {
inner_checker.check_stmt(s);
}
// Surface any errors from the inner scope
self.diagnostics.extend(inner_checker.diagnostics);
// Register function in outer env
let param_types: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
@@ -147,18 +162,26 @@ impl TypeChecker {
self.env.register_fn(name.clone(), fn_ty);
}
}
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {
// Already handled in hoist pass
}
Stmt::TestDef { body, .. } => {
// Type-check the test body statements
for s in body {
self.check_stmt(s);
Stmt::TypeDef { .. } | Stmt::EnumDef { .. } => {}
Stmt::ProtocolDef { .. } => {}
Stmt::ImplDef { protocol_name, type_name, methods, .. } => {
let method_names: Vec<String> = methods.iter().filter_map(|m| {
if let Stmt::FnDef { name, .. } = m { Some(name.clone()) } else { None }
}).collect();
let missing = self.env.check_impl_completeness(protocol_name, &method_names);
for m in &missing {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: format!("impl method '{m}' for protocol '{protocol_name}'"),
got: format!("missing in impl for '{type_name}'"),
});
}
for m in methods { self.check_stmt(m); }
}
Stmt::Seed(_, _) => {
// Seed statements are data-seeding constructs; no type checking needed.
Stmt::Import { .. } => {}
Stmt::TestDef { body, .. } => {
for s in body { self.check_stmt(s); }
}
Stmt::Seed(_, _) => {}
Stmt::Assert(expr, _) => {
let ty = self.infer_expr(expr);
if !self.env.check_compatible(&ty, &Type::Bool) {
@@ -196,7 +219,6 @@ impl TypeChecker {
// ── Expression inference ──────────────────────────────────────────────────
/// Infer the type of an expression, recording errors as diagnostics.
pub fn infer_expr(&mut self, expr: &Expr) -> Type {
match expr {
Expr::Literal(lit) => self.infer_literal(lit),
@@ -240,29 +262,24 @@ impl TypeChecker {
}
Expr::Match { subject, arms } => {
self.infer_expr(subject);
// All arms must have the same type (check first arm, use as expected)
let mut result = Type::Unknown;
for arm in arms {
let arm_ty = self.infer_expr(&arm.body);
if matches!(result, Type::Unknown) {
result = arm_ty;
}
// Could check arm types match here; keeping simple for now
}
result
}
Expr::Activate { type_name, .. } => {
// activate must reference a registered type
if self.env.get_type(type_name).is_none() {
self.emit_error(TypeErrorKind::ActivateUnknownType(type_name.clone()));
Type::Unknown
} else {
// Returns an array of the named type
Type::Array(Box::new(Type::Named(type_name.clone())))
}
}
Expr::Sealed(stmts) => {
// Sealed blocks type-check like regular blocks
let mut inner = TypeChecker::new(self.env.clone());
for s in stmts { inner.check_stmt(s); }
self.diagnostics.extend(inner.diagnostics);
@@ -279,11 +296,7 @@ impl TypeChecker {
let then_ty = self.infer_expr(then);
if let Some(e) = else_ {
let else_ty = self.infer_expr(e);
if self.env.check_compatible(&then_ty, &else_ty) {
then_ty
} else {
Type::Unknown
}
if self.env.check_compatible(&then_ty, &else_ty) { then_ty } else { Type::Unknown }
} else {
Type::Void
}
@@ -340,7 +353,6 @@ impl TypeChecker {
}
}
Expr::Path { segments } => {
// Path expressions like Status::Active evaluate to the enum type
if segments.len() >= 2 {
let enum_name = &segments[0];
if self.env.get_type(enum_name).is_some() {
@@ -370,6 +382,65 @@ impl TypeChecker {
}
}
}
Expr::Closure { params, return_type, body, .. } => {
let param_types: Vec<_> = params.iter().filter_map(|p| {
self.env.resolve_type_expr(&p.type_ann).ok()
}).collect();
let mut inner_env = self.env.clone();
for p in params {
if let Ok(ty) = inner_env.resolve_type_expr(&p.type_ann) {
inner_env.bind(p.name.clone(), ty);
}
}
let mut inner = TypeChecker::new(inner_env);
let body_ty = inner.infer_expr(body);
self.diagnostics.extend(inner.diagnostics);
let ret_ty = if let Some(ann) = return_type {
self.env.resolve_type_expr(ann).unwrap_or(body_ty)
} else {
body_ty
};
Type::Fn { params: param_types, return_type: Box::new(ret_ty) }
}
Expr::Try(inner) => {
let ty = self.infer_expr(inner);
match ty {
Type::Result { ok, .. } => *ok,
Type::Unknown => Type::Unknown,
other => {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Result<T, E>".into(),
got: other.to_string(),
});
Type::Unknown
}
}
}
Expr::MapLiteral(pairs) => {
if pairs.is_empty() {
Type::Map { key: Box::new(Type::Unknown), value: Box::new(Type::Unknown) }
} else {
let key_ty = self.infer_expr(&pairs[0].0);
let val_ty = self.infer_expr(&pairs[0].1);
for (k, v) in &pairs[1..] {
let kt = self.infer_expr(k);
let vt = self.infer_expr(v);
if !self.env.check_compatible(&kt, &key_ty) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: key_ty.to_string(),
got: kt.to_string(),
});
}
if !self.env.check_compatible(&vt, &val_ty) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: val_ty.to_string(),
got: vt.to_string(),
});
}
}
Type::Map { key: Box::new(key_ty), value: Box::new(val_ty) }
}
}
}
}
@@ -387,14 +458,11 @@ impl TypeChecker {
let rt = self.infer_expr(right);
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => {
// Numeric ops: Int op Int -> Int, Float anywhere -> Float
match (&lt, &rt) {
(Type::Float, _) | (_, Type::Float) => Type::Float,
(Type::Int, Type::Int) => Type::Int,
// String concatenation with +
(Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String,
_ => {
// Allow if at least one side is compatible with a number
if self.env.check_compatible(&lt, &Type::Int)
&& self.env.check_compatible(&rt, &Type::Int) {
Type::Int
@@ -408,12 +476,8 @@ impl TypeChecker {
}
}
}
BinOp::Eq | BinOp::NotEq => {
// Equality: any two compatible types -> Bool
Type::Bool
}
BinOp::Eq | BinOp::NotEq => Type::Bool,
BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
// Comparison: numeric types -> Bool
if !self.env.check_compatible(&lt, &rt) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: lt.to_string(),
@@ -470,17 +534,11 @@ impl TypeChecker {
// ── Diagnostic helpers ────────────────────────────────────────────────────
fn error(&mut self, e: TypeError) {
self.diagnostics.push(Diagnostic {
message: e.to_string(),
is_error: true,
});
self.diagnostics.push(Diagnostic { message: e.to_string(), is_error: true });
}
fn emit_error(&mut self, kind: TypeErrorKind) {
self.diagnostics.push(Diagnostic {
message: kind.to_string(),
is_error: true,
});
self.diagnostics.push(Diagnostic { message: kind.to_string(), is_error: true });
}
}
@@ -511,26 +569,18 @@ mod tests {
}
#[test]
fn test_let_int() {
assert_ok("let x: Int = 42");
}
fn test_let_int() { assert_ok("let x: Int = 42"); }
#[test]
fn test_let_string() {
assert_ok(r#"let s: String = "hello""#);
}
fn test_let_string() { assert_ok(r#"let s: String = "hello""#); }
#[test]
fn test_type_mismatch() {
assert_err(r#"let x: Int = "not an int""#);
}
fn test_type_mismatch() { assert_err(r#"let x: Int = "not an int""#); }
#[test]
fn test_fn_def_and_call() {
assert_ok(r#"
fn double(n: Int) -> Int {
return n + n
}
fn double(n: Int) -> Int { return n + n }
let result: Int = double(5)
"#);
}
@@ -545,18 +595,10 @@ add(1)
#[test]
fn test_type_def_and_field_access() {
// Type checking with field access: u is bound to User,
// accessing u.name should return String type without error.
// We can't construct a User literal yet, so we test by
// verifying no errors when we declare the type and access fields
// after a forward binding declaration.
let src = r#"
assert_ok(r#"
type User { name: String age: Int }
fn make_user() -> User {
return make_user()
}
"#;
assert_ok(src);
fn make_user() -> User { return make_user() }
"#);
}
#[test]
@@ -568,9 +610,7 @@ activate User where "recent customers"
}
#[test]
fn test_activate_unknown_type_err() {
assert_err(r#"activate Phantom where "ghosts""#);
}
fn test_activate_unknown_type_err() { assert_err(r#"activate Phantom where "ghosts""#); }
#[test]
fn test_bool_ops() {
@@ -579,12 +619,57 @@ activate User where "recent customers"
}
#[test]
fn test_int_arithmetic() {
assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2");
fn test_int_arithmetic() { assert_ok("let x: Int = 1 + 2 * 3 - 4 / 2"); }
#[test]
fn test_string_concat() { assert_ok(r#"let s: String = "hello" + " world""#); }
#[test]
fn test_closure_type_inferred() {
assert_ok("let double = |x: Int| x");
}
#[test]
fn test_string_concat() {
assert_ok(r#"let s: String = "hello" + " world""#);
fn test_closure_with_return_type() {
assert_ok("let add = |x: Int, y: Int| -> Int { x }");
}
#[test]
fn test_protocol_def_ok() {
assert_ok(r#"
protocol Printable { fn print(msg: String) -> Void }
"#);
}
#[test]
fn test_impl_def_ok() {
assert_ok(r#"
protocol Printable { fn print(msg: String) -> Void }
type User { name: String }
impl Printable for User { fn print(msg: String) -> Void { } }
"#);
}
#[test]
fn test_import_does_not_fail() {
assert_ok("import std::array");
}
#[test]
fn test_result_type_annotation() {
assert_ok(r#"fn fetch() -> Result<String, String> { return fetch() }"#);
}
#[test]
fn test_map_type_annotation() {
assert_ok(r#"let m: Map<String, Int> = m"#);
}
#[test]
fn test_decorator_does_not_break_fn() {
assert_ok(r#"
@public
fn greet(name: String) -> String { return name }
"#);
}
}
+134 -45
View File
@@ -3,12 +3,6 @@
use std::collections::HashMap;
/// The semantic type of a value in Engram source.
///
/// Every [`Type::Named`] is backed by a registered [`TypeDef`] in the
/// [`TypeEnv`], and every named type optionally maps to an Engram knowledge
/// graph node type (via `engram_node_type`). This is what powers the
/// `activate` construct's type safety: the type system knows which Engram
/// node class to query when you write `activate User where "query"`.
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
// ── Primitives ────────────────────────────────────────────────────────────
@@ -20,20 +14,17 @@ pub enum Type {
Void,
// ── Composite ─────────────────────────────────────────────────────────────
/// A user-defined named type (struct or enum). Maps to a TypeDef.
Named(std::string::String),
/// A homogeneous array of a single element type.
Array(Box<Type>),
/// An optional (nullable) value.
Optional(Box<Type>),
Result { ok: Box<Type>, err: Box<Type> },
Map { key: Box<Type>, value: Box<Type> },
// ── Function ──────────────────────────────────────────────────────────────
Fn { params: Vec<Type>, return_type: Box<Type> },
// ── Internal ──────────────────────────────────────────────────────────────
/// Unknown type — used before type inference has resolved a binding.
Unknown,
/// The never/bottom type — returned by diverging expressions.
Never,
}
@@ -49,6 +40,8 @@ impl std::fmt::Display for Type {
Type::Named(n) => write!(f, "{n}"),
Type::Array(t) => write!(f, "[{t}]"),
Type::Optional(t) => write!(f, "{t}?"),
Type::Result { ok, err } => write!(f, "Result<{ok}, {err}>"),
Type::Map { key, value } => write!(f, "Map<{key}, {value}>"),
Type::Fn { params, return_type } => {
let ps: Vec<_> = params.iter().map(|p| p.to_string()).collect();
write!(f, "fn({}) -> {return_type}", ps.join(", "))
@@ -61,7 +54,6 @@ impl std::fmt::Display for Type {
// ── TypeDef ───────────────────────────────────────────────────────────────────
/// The definition of a named type — either a struct or an enum.
#[derive(Debug, Clone)]
pub enum TypeDef {
Struct {
@@ -72,41 +64,41 @@ pub enum TypeDef {
name: std::string::String,
variants: Vec<EnumVariant>,
},
/// A built-in primitive alias (e.g. `Uuid` is a Named type mapped to a built-in).
Primitive(Type),
Protocol {
name: std::string::String,
methods: Vec<ProtocolMethodSig>,
},
}
#[derive(Debug, Clone)]
pub struct EnumVariant {
pub name: std::string::String,
/// Payload type for tuple variants like `Pending(String)`.
pub payload: Option<Type>,
}
#[derive(Debug, Clone)]
pub struct ProtocolMethodSig {
pub name: std::string::String,
pub params: Vec<Type>,
pub return_type: Type,
}
// ── TypeEnv ───────────────────────────────────────────────────────────────────
/// The type environment — a lexically-scoped binding of names to types.
///
/// A `TypeEnv` can be cheaply cloned to create child scopes (e.g. for
/// function bodies). New bindings in the child do not escape to the parent.
#[derive(Debug, Clone, Default)]
pub struct TypeEnv {
/// Maps variable/binding names to their inferred or declared types.
bindings: HashMap<std::string::String, Type>,
/// Maps type names to their definitions.
pub types: HashMap<std::string::String, TypeDef>,
/// Maps named type names to the Engram graph node type string.
/// Used by the `activate` construct to know which node class to query.
pub engram_mappings: HashMap<std::string::String, std::string::String>,
/// Maps function names to their function types.
pub functions: HashMap<std::string::String, Type>,
/// Tracks explicit `impl Protocol for Type` registrations.
pub impls: HashMap<(std::string::String, std::string::String), bool>,
}
impl TypeEnv {
/// Create a fresh environment pre-populated with built-in types.
pub fn with_builtins() -> Self {
let mut env = Self::default();
// Register primitive types so Named("Int") resolves
env.types.insert("Int".into(), TypeDef::Primitive(Type::Int));
env.types.insert("Float".into(), TypeDef::Primitive(Type::Float));
env.types.insert("String".into(), TypeDef::Primitive(Type::String));
@@ -128,7 +120,6 @@ impl TypeEnv {
// ── Type registration ─────────────────────────────────────────────────────
/// Register a user-defined type with an optional Engram node type mapping.
pub fn register_type(
&mut self,
name: impl Into<std::string::String>,
@@ -147,7 +138,6 @@ impl TypeEnv {
self.types.get(name)
}
/// Register a function signature.
pub fn register_fn(&mut self, name: impl Into<std::string::String>, ty: Type) {
self.functions.insert(name.into(), ty);
}
@@ -156,36 +146,63 @@ impl TypeEnv {
self.functions.get(name)
}
// ── Protocol support ──────────────────────────────────────────────────────
pub fn register_protocol(
&mut self,
name: impl Into<std::string::String>,
methods: Vec<ProtocolMethodSig>,
) {
let name = name.into();
let def = TypeDef::Protocol { name: name.clone(), methods };
self.types.insert(name, def);
}
pub fn register_impl(
&mut self,
protocol_name: impl Into<std::string::String>,
type_name: impl Into<std::string::String>,
) {
self.impls.insert((protocol_name.into(), type_name.into()), true);
}
pub fn implements(&self, type_name: &str, protocol_name: &str) -> bool {
self.impls.contains_key(&(protocol_name.to_string(), type_name.to_string()))
}
pub fn check_impl_completeness(
&self,
protocol_name: &str,
impl_method_names: &[String],
) -> Vec<String> {
match self.types.get(protocol_name) {
Some(TypeDef::Protocol { methods, .. }) => {
methods.iter()
.filter(|m| !impl_method_names.contains(&m.name))
.map(|m| m.name.clone())
.collect()
}
_ => vec![],
}
}
// ── Compatibility ─────────────────────────────────────────────────────────
/// Check whether type `a` is assignable to type `b`.
///
/// This is a structural check with a semantic override: if both types are
/// `Named` and have Engram node type mappings, semantic compatibility is
/// checked as well. Currently the semantic check is symbolic (same node
/// type string = compatible). When an actual Engram DB is available this
/// would use cosine similarity over embeddings.
pub fn check_compatible(&self, a: &Type, b: &Type) -> bool {
match (a, b) {
// Unknown is compatible with everything (used during inference)
(Type::Unknown, _) | (_, Type::Unknown) => true,
// Never is compatible with everything (bottom type)
(Type::Never, _) => true,
// Structural matches
(Type::Int, Type::Int) => true,
(Type::Float, Type::Float) => true,
(Type::String, Type::String) => true,
(Type::Bool, Type::Bool) => true,
(Type::Uuid, Type::Uuid) => true,
(Type::Void, Type::Void) => true,
// Int is promotable to Float
(Type::Int, Type::Float) => true,
// Named types: structural + semantic
(Type::Named(a_name), Type::Named(b_name)) => {
if a_name == b_name {
return true;
}
// Semantic compatibility via Engram node type mappings
let a_node = self.engram_mappings.get(a_name);
let b_node = self.engram_mappings.get(b_name);
match (a_node, b_node) {
@@ -199,8 +216,13 @@ impl TypeEnv {
(Type::Optional(a_inner), Type::Optional(b_inner)) => {
self.check_compatible(a_inner, b_inner)
}
// T is compatible with T?
(t, Type::Optional(inner)) => self.check_compatible(t, inner),
(Type::Result { ok: a_ok, err: a_err }, Type::Result { ok: b_ok, err: b_err }) => {
self.check_compatible(a_ok, b_ok) && self.check_compatible(a_err, b_err)
}
(Type::Map { key: ak, value: av }, Type::Map { key: bk, value: bv }) => {
self.check_compatible(ak, bk) && self.check_compatible(av, bv)
}
(Type::Fn { params: ap, return_type: ar }, Type::Fn { params: bp, return_type: br }) => {
ap.len() == bp.len()
&& ap.iter().zip(bp.iter()).all(|(a, b)| self.check_compatible(a, b))
@@ -210,11 +232,9 @@ impl TypeEnv {
}
}
/// Resolve a [`TypeExpr`] from the parser into a [`Type`].
pub fn resolve_type_expr(&self, te: &el_parser::TypeExpr) -> Result<Type, crate::TypeError> {
match te {
el_parser::TypeExpr::Named(n) => {
// Check if it's a built-in alias or a registered user type
Ok(match n.as_str() {
"Int" => Type::Int,
"Float" => Type::Float,
@@ -244,6 +264,16 @@ impl TypeEnv {
let ret = self.resolve_type_expr(return_type)?;
Ok(Type::Fn { params: ps, return_type: Box::new(ret) })
}
el_parser::TypeExpr::Result { ok, err } => {
let ok_ty = self.resolve_type_expr(ok)?;
let err_ty = self.resolve_type_expr(err)?;
Ok(Type::Result { ok: Box::new(ok_ty), err: Box::new(err_ty) })
}
el_parser::TypeExpr::Map { key, value } => {
let key_ty = self.resolve_type_expr(key)?;
let val_ty = self.resolve_type_expr(value)?;
Ok(Type::Map { key: Box::new(key_ty), value: Box::new(val_ty) })
}
}
}
}
@@ -280,7 +310,6 @@ mod tests {
#[test]
fn test_semantic_compatibility_via_engram_mapping() {
let mut e = env();
// Map both User and Customer to the "Entity" Engram node type
e.engram_mappings.insert("User".into(), "Entity".into());
e.engram_mappings.insert("Customer".into(), "Entity".into());
assert!(e.check_compatible(&Type::Named("User".into()), &Type::Named("Customer".into())));
@@ -289,7 +318,6 @@ mod tests {
#[test]
fn test_optional_compatibility() {
let e = env();
// Int is compatible with Int?
assert!(e.check_compatible(&Type::Int, &Type::Optional(Box::new(Type::Int))));
}
@@ -305,4 +333,65 @@ mod tests {
&Type::Array(Box::new(Type::String)),
));
}
#[test]
fn test_result_type_compatibility() {
let e = env();
let r1 = Type::Result { ok: Box::new(Type::String), err: Box::new(Type::String) };
let r2 = Type::Result { ok: Box::new(Type::String), err: Box::new(Type::String) };
assert!(e.check_compatible(&r1, &r2));
let r3 = Type::Result { ok: Box::new(Type::Int), err: Box::new(Type::String) };
assert!(!e.check_compatible(&r1, &r3));
}
#[test]
fn test_map_type_compatibility() {
let e = env();
let m1 = Type::Map { key: Box::new(Type::String), value: Box::new(Type::Int) };
let m2 = Type::Map { key: Box::new(Type::String), value: Box::new(Type::Int) };
assert!(e.check_compatible(&m1, &m2));
let m3 = Type::Map { key: Box::new(Type::Int), value: Box::new(Type::Int) };
assert!(!e.check_compatible(&m1, &m3));
}
#[test]
fn test_register_and_lookup_protocol() {
let mut e = env();
e.register_protocol("Printable", vec![
ProtocolMethodSig { name: "print".into(), params: vec![], return_type: Type::Void },
]);
assert!(matches!(e.get_type("Printable"), Some(TypeDef::Protocol { .. })));
}
#[test]
fn test_register_impl_and_check() {
let mut e = env();
e.register_protocol("Printable", vec![
ProtocolMethodSig { name: "print".into(), params: vec![], return_type: Type::Void },
]);
e.register_impl("Printable", "User");
assert!(e.implements("User", "Printable"));
assert!(!e.implements("Order", "Printable"));
}
#[test]
fn test_check_impl_completeness_missing_methods() {
let mut e = env();
e.register_protocol("Comparable", vec![
ProtocolMethodSig { name: "compare".into(), params: vec![], return_type: Type::Int },
ProtocolMethodSig { name: "equals".into(), params: vec![], return_type: Type::Bool },
]);
let missing = e.check_impl_completeness("Comparable", &["compare".to_string()]);
assert_eq!(missing, vec!["equals"]);
}
#[test]
fn test_check_impl_completeness_all_present() {
let mut e = env();
e.register_protocol("Comparable", vec![
ProtocolMethodSig { name: "compare".into(), params: vec![], return_type: Type::Int },
]);
let missing = e.check_impl_completeness("Comparable", &["compare".to_string()]);
assert!(missing.is_empty());
}
}