rename crates/ to engrams/; add el-compiler el package with bootstrap artifact

- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
  - src/{compiler,lexer,parser,codegen}.el
  - bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
This commit is contained in:
Will Anderson
2026-04-29 03:27:32 -05:00
parent 19ed2721ee
commit a42429012e
120 changed files with 3836 additions and 64 deletions
+729
View File
@@ -0,0 +1,729 @@
//! Type checker — walks the AST and infers / verifies types.
use el_parser::{BinOp, Expr, Literal, Program, Stmt};
use crate::error::{TypeError, TypeErrorKind};
use crate::types::{EnumVariant, ProtocolMethodSig, Type, TypeDef, TypeEnv};
/// Diagnostics produced by the type checker.
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub message: std::string::String,
pub is_error: bool,
}
/// Entry point: type-check a parsed program.
pub struct TypeChecker {
pub env: TypeEnv,
pub diagnostics: Vec<Diagnostic>,
}
impl TypeChecker {
pub fn new(env: TypeEnv) -> Self {
Self { env, diagnostics: Vec::new() }
}
pub fn with_builtins() -> Self {
Self::new(TypeEnv::with_builtins())
}
// ── Public API ────────────────────────────────────────────────────────────
pub fn check(&mut self, program: &Program) -> &[Diagnostic] {
self.hoist_definitions(program);
for stmt in &program.stmts {
self.check_stmt(stmt);
}
&self.diagnostics
}
pub fn ok(&self) -> bool {
!self.diagnostics.iter().any(|d| d.is_error)
}
// ── Definition hoisting ───────────────────────────────────────────────────
fn hoist_definitions(&mut self, program: &Program) {
for stmt in &program.stmts {
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; }
}
} 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::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(&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());
}
_ => {}
}
}
// ── Statement checking ────────────────────────────────────────────────────
fn check_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Let { name, type_ann, value, .. } => {
let inferred = self.infer_expr(value);
if let Some(ann) = type_ann {
match self.env.resolve_type_expr(ann) {
Ok(declared) => {
if !self.env.check_compatible(&inferred, &declared) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: declared.to_string(),
got: inferred.to_string(),
});
}
self.env.bind(name.clone(), declared);
}
Err(e) => {
self.error(e);
self.env.bind(name.clone(), inferred);
}
}
} else {
self.env.bind(name.clone(), inferred);
}
}
Stmt::Return(expr, _) => { self.infer_expr(expr); }
Stmt::Expr(expr, _) => { self.infer_expr(expr); }
Stmt::FnDef { name, params, return_type, body, .. } => {
let mut inner_env = self.env.clone();
for param in params {
if let Ok(ty) = inner_env.resolve_type_expr(&param.type_ann) {
inner_env.bind(param.name.clone(), ty);
}
}
let mut inner_checker = TypeChecker::new(inner_env);
inner_checker.hoist_definitions_stmts(body);
for s in body {
inner_checker.check_stmt(s);
}
self.diagnostics.extend(inner_checker.diagnostics);
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::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::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) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: ty.to_string(),
});
}
}
Stmt::Retry { body, fallback, .. } => {
for s in body { self.check_stmt(s); }
if let Some(fb) = fallback {
for s in fb { self.check_stmt(s); }
}
}
Stmt::Deploy { .. } => {}
Stmt::While { condition, body, .. } => {
self.infer_expr(condition);
for s in body { self.check_stmt(s); }
}
}
}
fn hoist_definitions_stmts(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
match stmt {
Stmt::TypeDef { name, fields, .. } => {
let resolved: Vec<_> = fields.iter().filter_map(|f| {
self.env.resolve_type_expr(&f.type_ann).ok().map(|ty| (f.name.clone(), ty))
}).collect();
let def = TypeDef::Struct { name: name.clone(), fields: resolved };
self.env.register_type(name.clone(), def, "");
}
Stmt::FnDef { name, params, return_type, .. } => {
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) });
}
}
_ => {}
}
}
}
// ── Expression inference ──────────────────────────────────────────────────
pub fn infer_expr(&mut self, expr: &Expr) -> Type {
match expr {
Expr::Literal(lit) => self.infer_literal(lit),
Expr::Ident(name) => {
if let Some(ty) = self.env.lookup(name) {
ty.clone()
} else if let Some(ty) = self.env.lookup_fn(name) {
ty.clone()
} else {
self.emit_error(TypeErrorKind::UndefinedVariable(name.clone()));
Type::Unknown
}
}
Expr::BinOp { op, left, right } => self.infer_binop(op, left, right),
Expr::UnaryNot(inner) => {
let ty = self.infer_expr(inner);
if !self.env.check_compatible(&ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: ty.to_string(),
});
}
Type::Bool
}
Expr::Call { func, args } => self.infer_call(func, args),
Expr::Block(stmts) => {
let mut last = Type::Void;
let mut inner = TypeChecker::new(self.env.clone());
inner.hoist_definitions_stmts(stmts);
for (i, s) in stmts.iter().enumerate() {
if i == stmts.len() - 1 {
if let Stmt::Expr(e, _) = s {
last = inner.infer_expr(e);
continue;
}
}
inner.check_stmt(s);
}
self.diagnostics.extend(inner.diagnostics);
last
}
Expr::Match { subject, arms } => {
self.infer_expr(subject);
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;
}
}
result
}
Expr::Activate { type_name, .. } => {
if self.env.get_type(type_name).is_none() {
self.emit_error(TypeErrorKind::ActivateUnknownType(type_name.clone()));
Type::Unknown
} else {
Type::Array(Box::new(Type::Named(type_name.clone())))
}
}
Expr::Sealed(stmts) => {
let mut inner = TypeChecker::new(self.env.clone());
for s in stmts { inner.check_stmt(s); }
self.diagnostics.extend(inner.diagnostics);
Type::Void
}
Expr::If { cond, then, else_ } => {
let cond_ty = self.infer_expr(cond);
if !self.env.check_compatible(&cond_ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: cond_ty.to_string(),
});
}
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 }
} else {
Type::Void
}
}
Expr::Field { object, field } => {
let obj_ty = self.infer_expr(object);
match &obj_ty {
Type::Named(type_name) => {
match self.env.get_type(type_name) {
Some(TypeDef::Struct { fields, .. }) => {
if let Some((_, fty)) = fields.iter().find(|(n, _)| n == field) {
fty.clone()
} else {
self.emit_error(TypeErrorKind::UnknownField {
type_name: type_name.clone(),
field: field.clone(),
});
Type::Unknown
}
}
_ => {
self.emit_error(TypeErrorKind::UnknownField {
type_name: obj_ty.to_string(),
field: field.clone(),
});
Type::Unknown
}
}
}
_ => {
self.emit_error(TypeErrorKind::UnknownField {
type_name: obj_ty.to_string(),
field: field.clone(),
});
Type::Unknown
}
}
}
Expr::Array(elems) => {
if elems.is_empty() {
Type::Array(Box::new(Type::Unknown))
} else {
let elem_ty = self.infer_expr(&elems[0]);
for e in &elems[1..] {
let ty = self.infer_expr(e);
if !self.env.check_compatible(&ty, &elem_ty) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: elem_ty.to_string(),
got: ty.to_string(),
});
}
}
Type::Array(Box::new(elem_ty))
}
}
Expr::Path { segments } => {
if segments.len() >= 2 {
let enum_name = &segments[0];
if self.env.get_type(enum_name).is_some() {
Type::Named(enum_name.clone())
} else {
self.emit_error(TypeErrorKind::UndefinedType(enum_name.clone()));
Type::Unknown
}
} else {
Type::Unknown
}
}
Expr::Index { object, index } => {
let obj_ty = self.infer_expr(object);
let idx_ty = self.infer_expr(index);
if !self.env.check_compatible(&idx_ty, &Type::Int) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Int".into(),
got: idx_ty.to_string(),
});
}
match obj_ty {
Type::Array(inner) => *inner,
other => {
self.emit_error(TypeErrorKind::NotIndexable(other.to_string()));
Type::Unknown
}
}
}
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) }
}
}
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(_) => {
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
}
}
}
// Engram-specific expressions
Expr::With { base, .. } => self.infer_expr(base),
Expr::Reason { .. } => Type::String,
Expr::Parallel { .. } => Type::Unknown,
Expr::Trace { .. } => Type::Unknown,
}
}
fn infer_literal(&self, lit: &Literal) -> Type {
match lit {
Literal::Int(_) => Type::Int,
Literal::Float(_) => Type::Float,
Literal::Str(_) => Type::String,
Literal::Bool(_) => Type::Bool,
}
}
fn infer_binop(&mut self, op: &BinOp, left: &Expr, right: &Expr) -> Type {
let lt = self.infer_expr(left);
let rt = self.infer_expr(right);
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => {
match (&lt, &rt) {
(Type::Float, _) | (_, Type::Float) => Type::Float,
(Type::Int, Type::Int) => Type::Int,
(Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String,
_ => {
if self.env.check_compatible(&lt, &Type::Int)
&& self.env.check_compatible(&rt, &Type::Int) {
Type::Int
} else {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "numeric or String".into(),
got: format!("{lt} and {rt}"),
});
Type::Unknown
}
}
}
}
BinOp::Eq | BinOp::NotEq => Type::Bool,
BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
if !self.env.check_compatible(&lt, &rt) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: lt.to_string(),
got: rt.to_string(),
});
}
Type::Bool
}
BinOp::And | BinOp::Or => {
for ty in [&lt, &rt] {
if !self.env.check_compatible(ty, &Type::Bool) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: "Bool".into(),
got: ty.to_string(),
});
}
}
Type::Bool
}
}
}
fn infer_call(&mut self, func: &Expr, args: &[Expr]) -> Type {
let func_ty = self.infer_expr(func);
let arg_types: Vec<_> = args.iter().map(|a| self.infer_expr(a)).collect();
match func_ty {
Type::Fn { params, return_type } => {
if params.len() != arg_types.len() {
self.emit_error(TypeErrorKind::ArgCountMismatch {
expected: params.len(),
got: arg_types.len(),
});
} else {
for (expected, got) in params.iter().zip(arg_types.iter()) {
if !self.env.check_compatible(got, expected) {
self.emit_error(TypeErrorKind::TypeMismatch {
expected: expected.to_string(),
got: got.to_string(),
});
}
}
}
*return_type
}
Type::Unknown => Type::Unknown,
other => {
self.emit_error(TypeErrorKind::NotCallable(other.to_string()));
Type::Unknown
}
}
}
// ── Diagnostic helpers ────────────────────────────────────────────────────
fn error(&mut self, e: TypeError) {
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 });
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use el_lexer::tokenize;
use el_parser::parse;
use super::*;
fn check(src: &str) -> TypeChecker {
let tokens = tokenize(src).expect("lex");
let prog = parse(tokens, src.to_string()).expect("parse");
let mut checker = TypeChecker::with_builtins();
checker.check(&prog);
checker
}
fn assert_ok(src: &str) {
let c = check(src);
assert!(c.ok(), "Expected no errors, got: {:?}", c.diagnostics);
}
fn assert_err(src: &str) {
let c = check(src);
assert!(!c.ok(), "Expected errors but got none");
}
#[test]
fn test_let_int() { assert_ok("let x: Int = 42"); }
#[test]
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""#); }
#[test]
fn test_fn_def_and_call() {
assert_ok(r#"
fn double(n: Int) -> Int { return n + n }
let result: Int = double(5)
"#);
}
#[test]
fn test_fn_arg_count_mismatch() {
assert_err(r#"
fn add(a: Int, b: Int) -> Int { return a + b }
add(1)
"#);
}
#[test]
fn test_type_def_and_field_access() {
assert_ok(r#"
type User { name: String age: Int }
fn make_user() -> User { return make_user() }
"#);
}
#[test]
fn test_activate_known_type_ok() {
assert_ok(r#"
type User { id: Uuid name: String }
activate User where "recent customers"
"#);
}
#[test]
fn test_activate_unknown_type_err() { assert_err(r#"activate Phantom where "ghosts""#); }
#[test]
fn test_bool_ops() {
assert_ok("let a: Bool = true && false");
assert_ok("let b: Bool = true || false");
}
#[test]
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_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() {
// Just test that Map<K,V> type annotation parses and resolves without crashing
// Use a function body where a self-reference is valid
assert_ok(r#"fn get_map() -> Map<String, Int> { return get_map() }"#);
}
#[test]
fn test_decorator_does_not_break_fn() {
assert_ok(r#"
@public
fn greet(name: String) -> String { return name }
"#);
}
}
+45
View File
@@ -0,0 +1,45 @@
//! Type system errors.
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("{kind}")]
pub struct TypeError {
pub kind: TypeErrorKind,
}
impl TypeError {
pub fn new(kind: TypeErrorKind) -> Self {
Self { kind }
}
}
#[derive(Debug, Clone, Error)]
pub enum TypeErrorKind {
#[error("type mismatch: expected {expected}, got {got}")]
TypeMismatch { expected: String, got: String },
#[error("undefined variable '{0}'")]
UndefinedVariable(String),
#[error("undefined type '{0}'")]
UndefinedType(String),
#[error("undefined function '{0}'")]
UndefinedFunction(String),
#[error("wrong number of arguments: expected {expected}, got {got}")]
ArgCountMismatch { expected: usize, got: usize },
#[error("field '{field}' not found on type '{type_name}'")]
UnknownField { type_name: String, field: String },
#[error("cannot call non-function type {0}")]
NotCallable(String),
#[error("activate expression requires a registered type name, got '{0}'")]
ActivateUnknownType(String),
#[error("index operator requires Array type, got {0}")]
NotIndexable(String),
}
+25
View File
@@ -0,0 +1,25 @@
//! el-types — Engram language type system.
//!
//! Types in the Engram language are more than structural contracts — every
//! named type is a node in a knowledge graph. Compatibility checking is
//! therefore two-dimensional:
//!
//! 1. **Structural compatibility** — the traditional "does this type's layout
//! match?" check.
//! 2. **Semantic compatibility** — are the Engram node embeddings for these
//! two types close enough in meaning-space? This enables the `activate`
//! construct to return a statically-typed result even though the query is
//! a free-form natural language string.
//!
//! In the current implementation, semantic compatibility falls back to a
//! symbolic check (are the Engram node type strings the same?). When an
//! actual Engram database is connected via `CompilerOptions::engram_db_path`,
//! the checker can delegate to real cosine-similarity over embeddings.
mod error;
mod types;
mod checker;
pub use error::{TypeError, TypeErrorKind};
pub use types::{Type, TypeDef, TypeEnv};
pub use checker::TypeChecker;
+596
View File
@@ -0,0 +1,596 @@
//! Core type definitions and the type environment.
use std::collections::HashMap;
/// The semantic type of a value in Engram source.
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
// ── Primitives ────────────────────────────────────────────────────────────
Int,
Float,
String,
Bool,
Uuid,
Void,
// ── Composite ─────────────────────────────────────────────────────────────
Named(std::string::String),
Array(Box<Type>),
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,
Never,
}
impl std::fmt::Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Int => write!(f, "Int"),
Type::Float => write!(f, "Float"),
Type::String => write!(f, "String"),
Type::Bool => write!(f, "Bool"),
Type::Uuid => write!(f, "Uuid"),
Type::Void => write!(f, "Void"),
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(", "))
}
Type::Unknown => write!(f, "<unknown>"),
Type::Never => write!(f, "!"),
}
}
}
// ── TypeDef ───────────────────────────────────────────────────────────────────
#[derive(Debug, Clone)]
pub enum TypeDef {
Struct {
name: std::string::String,
fields: Vec<(std::string::String, Type)>,
},
Enum {
name: std::string::String,
variants: Vec<EnumVariant>,
},
Primitive(Type),
Protocol {
name: std::string::String,
methods: Vec<ProtocolMethodSig>,
},
}
#[derive(Debug, Clone)]
pub struct EnumVariant {
pub name: std::string::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 ───────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Default)]
pub struct TypeEnv {
bindings: HashMap<std::string::String, Type>,
pub types: HashMap<std::string::String, TypeDef>,
pub engram_mappings: HashMap<std::string::String, std::string::String>,
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 and functions.
pub fn with_builtins() -> Self {
let mut env = Self::default();
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));
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);
// ── String builtins ───────────────────────────────────────────────────
let str_fn = |params: Vec<Type>, ret: Type| Type::Fn { params, return_type: Box::new(ret) };
let s = Type::String;
let i = Type::Int;
let b = Type::Bool;
let u = Type::Unknown;
// String operations
for name in &["str_contains","str_starts_with","string_starts_with","string_contains","str_ends_with","str_eq","string_ends_with","string_index_of","str_index_of","str_last_index_of"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone()], b.clone()));
}
for name in &["str_to_lowercase","str_trim","string_trim","string_to_upper","str_upper","string_to_lower","str_lower","to_string","int_to_str","bool_to_str"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], s.clone()));
}
for name in &["str_len","string_len","list_len","array_length","array_len","map_len","json_array_len"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], i.clone()));
}
for name in &["str_replace","string_replace","string_concat","string_substring"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
}
// str_slice(s: String, start: Int, end: Int) -> String
env.functions.insert("str_slice".into(), str_fn(vec![s.clone(), i.clone(), i.clone()], s.clone()));
env.functions.insert("str_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
env.functions.insert("string_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
env.functions.insert("string_split_last".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
env.functions.insert("array_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
env.functions.insert("list_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
// Parse / convert
for name in &["str_to_int","parse_int","int_parse"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], i.clone()));
}
for name in &["str_to_float","parse_float"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], Type::Float));
}
// JSON
env.functions.insert("json_get".into(), str_fn(vec![s.clone(), s.clone()], u.clone()));
env.functions.insert("json_set".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("json_keys".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("json_stringify".into(), str_fn(vec![u.clone()], s.clone()));
env.functions.insert("json_parse".into(), str_fn(vec![s.clone()], u.clone()));
env.functions.insert("json_encode".into(), str_fn(vec![u.clone()], s.clone()));
env.functions.insert("json_decode".into(), str_fn(vec![s.clone()], u.clone()));
env.functions.insert("json_get_string".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
env.functions.insert("json_get_int".into(), str_fn(vec![u.clone(), s.clone()], i.clone()));
env.functions.insert("json_get_array".into(), str_fn(vec![u.clone(), s.clone()], Type::Unknown));
env.functions.insert("json_array_get".into(), str_fn(vec![s.clone(), i.clone()], u.clone()));
env.functions.insert("json_array_push".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("json_array_len".into(), str_fn(vec![s.clone()], i.clone()));
// Array
for name in &["array_push","array_pop","array_reverse","array_sort","array_first","array_last"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
env.functions.insert("array_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone()));
env.functions.insert("list_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone()));
env.functions.insert("array_concat".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("array_contains".into(), str_fn(vec![u.clone(), u.clone()], b.clone()));
env.functions.insert("array_slice".into(), str_fn(vec![u.clone(), i.clone(), i.clone()], u.clone()));
env.functions.insert("array_zip".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("array_enumerate".into(), str_fn(vec![u.clone()], u.clone()));
// Map
env.functions.insert("map_new".into(), str_fn(vec![], u.clone()));
for name in &["map_get","map_remove","map_contains","map_keys","map_values"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone(), s.clone()], u.clone()));
}
env.functions.insert("map_set".into(), str_fn(vec![u.clone(), s.clone(), u.clone()], u.clone()));
env.functions.insert("map_len".into(), str_fn(vec![u.clone()], i.clone()));
// Filesystem
for name in &["fs_read"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], s.clone()));
}
for name in &["fs_exists","fs_mkdir","fs_remove","fs_is_dir"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone()));
}
// fs_write and fs_append take (path, content) — two arguments
for name in &["fs_write","fs_append"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone()], b.clone()));
}
env.functions.insert("fs_list".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("fs_list_recursive".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("path_join".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("path_parent".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("cwd".into(), str_fn(vec![], s.clone()));
// Crypto / UUID
env.functions.insert("blake3_hash".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("uuid_new".into(), str_fn(vec![], s.clone()));
env.functions.insert("uuid_v4".into(), str_fn(vec![], s.clone()));
env.functions.insert("hmac_sha256".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("base64_url_encode".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("base64_url_decode".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("unix_timestamp".into(), str_fn(vec![], i.clone()));
env.functions.insert("now_millis".into(), str_fn(vec![], i.clone()));
// HTTP
env.functions.insert("http_get".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("http_post".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_put".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_delete".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("http_patch".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_get_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_post_auth".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("http_put_auth".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("http_delete_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_serve".into(), str_fn(vec![u.clone()], Type::Void));
// State
env.functions.insert("state_get".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("state_set".into(), str_fn(vec![s.clone(), s.clone()], b.clone()));
env.functions.insert("state_del".into(), str_fn(vec![s.clone()], b.clone()));
env.functions.insert("state_keys".into(), str_fn(vec![], Type::Unknown));
// System
env.functions.insert("env".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("args".into(), str_fn(vec![], Type::Unknown));
env.functions.insert("exit".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("sleep_ms".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("sleep_secs".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("timestamp".into(), str_fn(vec![], s.clone()));
env.functions.insert("readline".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("getpid".into(), str_fn(vec![], i.clone()));
env.functions.insert("exec_bg".into(), str_fn(vec![s.clone()], i.clone()));
env.functions.insert("spawn_thread".into(), str_fn(vec![s.clone()], Type::Void));
// ANSI color builtins
for name in &["color_cyan","color_green","color_red","color_yellow","color_bold","color_dim"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], s.clone()));
}
// Terminal control builtins
for name in &["term_clear", "term_save_cursor", "term_restore_cursor", "term_clear_line"] {
env.functions.insert(name.to_string(), str_fn(vec![], Type::Void));
}
env.functions.insert("print_inline".into(), str_fn(vec![u.clone()], Type::Void));
env.functions.insert("term_size".into(), str_fn(vec![], Type::Unknown));
env.functions.insert("cursor_to".into(), str_fn(vec![i.clone(), i.clone()], Type::Void));
env.functions.insert("cursor_up".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("cursor_down".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("cursor_col".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("http_sse_post".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
// Canvas / native window builtins
env.functions.insert("canvas_open".into(), str_fn(vec![s.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("canvas_clear".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("canvas_fill_rect".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone()], Type::Void));
env.functions.insert("canvas_stroke_rect".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("canvas_line".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone(), s.clone(), i.clone()], Type::Void));
env.functions.insert("canvas_text".into(), str_fn(vec![i.clone(), i.clone(), s.clone(), i.clone(), s.clone()], Type::Void));
env.functions.insert("canvas_text_width".into(), str_fn(vec![s.clone(), i.clone()], i.clone()));
env.functions.insert("canvas_text_height".into(), str_fn(vec![i.clone()], i.clone()));
env.functions.insert("canvas_clip".into(), str_fn(vec![i.clone(), i.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("canvas_unclip".into(), str_fn(vec![], Type::Void));
env.functions.insert("canvas_size".into(), str_fn(vec![], Type::Unknown));
env.functions.insert("canvas_mouse_pos".into(), str_fn(vec![], Type::Unknown));
env.functions.insert("canvas_events".into(), str_fn(vec![], s.clone()));
env.functions.insert("canvas_swap".into(), str_fn(vec![], Type::Void));
env.functions.insert("canvas_run_loop".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("canvas_image".into(), str_fn(vec![s.clone(), i.clone(), i.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("state_set".into(), str_fn(vec![s.clone(), s.clone()], Type::Void));
env.functions.insert("state_get".into(), str_fn(vec![s.clone()], s.clone()));
// Math
for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
env.functions.insert("math_max".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("math_min".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("math_pow".into(), str_fn(vec![u.clone(), u.clone()], Type::Float));
// Result / Optional
for name in &["result_ok","result_err","result_unwrap","result_unwrap_or","optional_some","optional_unwrap","optional_unwrap_or"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
for name in &["result_is_ok","result_is_err","optional_is_some","optional_is_none"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone()));
}
env.functions.insert("optional_none".into(), str_fn(vec![], Type::Void));
env
}
// ── Bindings ──────────────────────────────────────────────────────────────
pub fn bind(&mut self, name: impl Into<std::string::String>, ty: Type) {
self.bindings.insert(name.into(), ty);
}
pub fn lookup(&self, name: &str) -> Option<&Type> {
self.bindings.get(name)
}
// ── Type registration ─────────────────────────────────────────────────────
pub fn register_type(
&mut self,
name: impl Into<std::string::String>,
def: TypeDef,
engram_node_type: impl Into<std::string::String>,
) {
let name = name.into();
let engram = engram_node_type.into();
if !engram.is_empty() {
self.engram_mappings.insert(name.clone(), engram);
}
self.types.insert(name, def);
}
pub fn get_type(&self, name: &str) -> Option<&TypeDef> {
self.types.get(name)
}
pub fn register_fn(&mut self, name: impl Into<std::string::String>, ty: Type) {
self.functions.insert(name.into(), ty);
}
pub fn lookup_fn(&self, name: &str) -> Option<&Type> {
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 ─────────────────────────────────────────────────────────
pub fn check_compatible(&self, a: &Type, b: &Type) -> bool {
match (a, b) {
(Type::Unknown, _) | (_, Type::Unknown) => true,
(Type::Never, _) => true,
(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,
(Type::Int, Type::Float) => true,
(Type::Named(a_name), Type::Named(b_name)) => {
if a_name == b_name {
return true;
}
let a_node = self.engram_mappings.get(a_name);
let b_node = self.engram_mappings.get(b_name);
match (a_node, b_node) {
(Some(a_n), Some(b_n)) => a_n == b_n,
_ => false,
}
}
(Type::Array(a_inner), Type::Array(b_inner)) => {
self.check_compatible(a_inner, b_inner)
}
(Type::Optional(a_inner), Type::Optional(b_inner)) => {
self.check_compatible(a_inner, b_inner)
}
(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))
&& self.check_compatible(ar, br)
}
_ => false,
}
}
pub fn resolve_type_expr(&self, te: &el_parser::TypeExpr) -> Result<Type, crate::TypeError> {
match te {
el_parser::TypeExpr::Named(n) => {
Ok(match n.as_str() {
"Int" => Type::Int,
"Float" => Type::Float,
"String" => Type::String,
"Bool" => Type::Bool,
"Uuid" => Type::Uuid,
"Void" => Type::Void,
other => {
if self.types.contains_key(other) {
Type::Named(other.to_string())
} else {
return Err(crate::TypeError::new(
crate::TypeErrorKind::UndefinedType(other.to_string()),
));
}
}
})
}
el_parser::TypeExpr::Array(inner) => {
Ok(Type::Array(Box::new(self.resolve_type_expr(inner)?)))
}
el_parser::TypeExpr::Optional(inner) => {
Ok(Type::Optional(Box::new(self.resolve_type_expr(inner)?)))
}
el_parser::TypeExpr::Fn { params, return_type } => {
let ps = params.iter().map(|p| self.resolve_type_expr(p)).collect::<Result<Vec<_>, _>>()?;
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) })
}
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)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> TypeEnv {
TypeEnv::with_builtins()
}
#[test]
fn test_primitive_compatibility() {
let e = env();
assert!(e.check_compatible(&Type::Int, &Type::Int));
assert!(e.check_compatible(&Type::String, &Type::String));
assert!(!e.check_compatible(&Type::Int, &Type::String));
}
#[test]
fn test_int_promotes_to_float() {
let e = env();
assert!(e.check_compatible(&Type::Int, &Type::Float));
}
#[test]
fn test_named_same_is_compatible() {
let e = env();
assert!(e.check_compatible(&Type::Named("User".into()), &Type::Named("User".into())));
assert!(!e.check_compatible(&Type::Named("User".into()), &Type::Named("Order".into())));
}
#[test]
fn test_semantic_compatibility_via_engram_mapping() {
let mut e = env();
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())));
}
#[test]
fn test_optional_compatibility() {
let e = env();
assert!(e.check_compatible(&Type::Int, &Type::Optional(Box::new(Type::Int))));
}
#[test]
fn test_array_compatibility() {
let e = env();
assert!(e.check_compatible(
&Type::Array(Box::new(Type::Int)),
&Type::Array(Box::new(Type::Int)),
));
assert!(!e.check_compatible(
&Type::Array(Box::new(Type::Int)),
&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());
}
}