feat: Add type checking to the compiler

Introduces the `TypeChecker` struct and its associated `TypeContext` for
performing static type analysis on the abstract syntax tree.

This change includes:
- A new `type_checker.rs` module.
- Integration of `TypeChecker` into the `Environment::compile` method.
- Updates to `Environment` to manage global types.
- Renaming `bound_nodes.rs`'s `BoundNode` to `TypedNode` to reflect its
  type-checked nature.
- Refinements in binder and macro expansion to accommodate type
  information.
This commit is contained in:
Michael Schimmel
2026-02-19 12:22:39 +01:00
parent 4673ea78b9
commit d93727e198
8 changed files with 270 additions and 20 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ struct LocalInfo {
// Note: Binder doesn't strictly need the type anymore, // Note: Binder doesn't strictly need the type anymore,
// but it might be useful for built-ins during resolution. // but it might be useful for built-ins during resolution.
// For now we keep it as Any or Unknown. // For now we keep it as Any or Unknown.
ty: StaticType, _ty: StaticType,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -33,7 +33,7 @@ impl CompilerScope {
return Err(format!("Variable '{}' is already defined in this scope level.", sym.name)); return Err(format!("Variable '{}' is already defined in this scope level.", sym.name));
} }
let slot = self.slot_count; let slot = self.slot_count;
self.locals.insert(sym.clone(), LocalInfo { slot, ty: StaticType::Any }); self.locals.insert(sym.clone(), LocalInfo { slot, _ty: StaticType::Any });
self.slot_count += 1; self.slot_count += 1;
Ok(slot) Ok(slot)
} }
+4 -1
View File
@@ -80,7 +80,7 @@ pub enum BoundKind<T = ()> {
}, },
Map { Map {
entries: Vec<(Node<BoundKind<T>, T>, Node<BoundKind<T>, T>)>, entries: Vec<MapEntry<T>>,
}, },
/// An expanded macro call, preserving the original call for debugging and UI. /// An expanded macro call, preserving the original call for debugging and UI.
@@ -95,6 +95,9 @@ pub enum BoundKind<T = ()> {
Extension(Box<dyn BoundExtension<T>>), Extension(Box<dyn BoundExtension<T>>),
} }
/// A single entry in a Map literal (Key-Value pair)
pub type MapEntry<T> = (Node<BoundKind<T>, T>, Node<BoundKind<T>, T>);
/// Type alias for a node that has been bound (addresses resolved) but not yet type-checked. /// Type alias for a node that has been bound (addresses resolved) but not yet type-checked.
pub type BoundNode = Node<BoundKind<()>, ()>; pub type BoundNode = Node<BoundKind<()>, ()>;
+1 -1
View File
@@ -440,7 +440,7 @@ mod tests {
use super::*; use super::*;
use std::cell::RefCell; use std::cell::RefCell;
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
use crate::ast::types::{Value, StaticType, Object}; use crate::ast::types::{Value, Object};
use crate::ast::compiler::Binder; use crate::ast::compiler::Binder;
struct SimpleEvaluator; struct SimpleEvaluator;
+2
View File
@@ -4,6 +4,7 @@ pub mod tco;
pub mod upvalues; pub mod upvalues;
pub mod dumper; pub mod dumper;
pub mod macros; pub mod macros;
pub mod type_checker;
pub use binder::*; pub use binder::*;
pub use bound_nodes::*; pub use bound_nodes::*;
@@ -11,3 +12,4 @@ pub use tco::*;
pub use upvalues::*; pub use upvalues::*;
pub use dumper::*; pub use dumper::*;
pub use macros::*; pub use macros::*;
pub use type_checker::*;
+227
View File
@@ -0,0 +1,227 @@
use std::collections::HashMap;
use std::rc::Rc;
use crate::ast::compiler::bound_nodes::{BoundKind, Address, BoundNode, TypedNode};
use crate::ast::nodes::Node;
use crate::ast::types::StaticType;
/// Manages the types of locals and upvalues during a single type-checking pass.
struct TypeContext<'a> {
_parent: Option<&'a TypeContext<'a>>,
/// Maps slot index -> Inferred Type
slots: Vec<StaticType>,
/// Types of captured variables (passed from outer scope)
upvalue_types: Vec<StaticType>,
}
impl<'a> TypeContext<'a> {
fn new(slot_count: u32, upvalue_types: Vec<StaticType>, parent: Option<&'a TypeContext<'a>>) -> Self {
Self {
_parent: parent,
slots: vec![StaticType::Any; slot_count as usize],
upvalue_types,
}
}
fn get_type(&self, addr: Address) -> StaticType {
match addr {
Address::Local(idx) => self.slots.get(idx as usize).cloned().unwrap_or(StaticType::Any),
Address::Global(_) => StaticType::Any, // Globals are dynamic for now
Address::Upvalue(idx) => self.upvalue_types.get(idx as usize).cloned().unwrap_or(StaticType::Any),
}
}
fn set_local_type(&mut self, idx: u32, ty: StaticType) {
if let Some(slot) = self.slots.get_mut(idx as usize) {
*slot = ty;
}
}
}
pub struct TypeChecker {
global_types: Rc<std::cell::RefCell<HashMap<u32, StaticType>>>,
}
impl TypeChecker {
pub fn new(global_types: Rc<std::cell::RefCell<HashMap<u32, StaticType>>>) -> Self {
Self { global_types }
}
pub fn check(&self, node: BoundNode) -> Result<TypedNode, String> {
// Start with a root context. Root scope has no upvalues.
// We assume 1000 slots for global script level if needed, but Binder handles it.
// Actually, Binder already assigned slot indices. We need to know the max slot.
let mut ctx = TypeContext::new(256, vec![], None);
self.check_node(node, &mut ctx)
}
fn check_node(&self, node: BoundNode, ctx: &mut TypeContext) -> Result<TypedNode, String> {
let (kind, ty) = match node.kind {
BoundKind::Nop => (BoundKind::Nop, StaticType::Void),
BoundKind::Constant(v) => {
let ty = v.static_type();
(BoundKind::Constant(v), ty)
},
BoundKind::Get(addr) => {
let ty = if let Address::Global(idx) = addr {
self.global_types.borrow().get(&idx).cloned().unwrap_or(StaticType::Any)
} else {
ctx.get_type(addr)
};
(BoundKind::Get(addr), ty)
},
BoundKind::Set { addr, value } => {
let val_typed = self.check_node(*value, ctx)?;
let ty = val_typed.ty.clone();
if let Address::Local(idx) = addr {
ctx.set_local_type(idx, ty.clone());
} else if let Address::Global(idx) = addr {
self.global_types.borrow_mut().insert(idx, ty.clone());
}
(BoundKind::Set { addr, value: Box::new(val_typed) }, ty)
},
BoundKind::DefLocal { slot, value, captured_by } => {
let val_typed = self.check_node(*value, ctx)?;
let ty = val_typed.ty.clone();
ctx.set_local_type(slot, ty.clone());
(BoundKind::DefLocal { slot, value: Box::new(val_typed), captured_by }, ty)
},
BoundKind::DefGlobal { global_index, value } => {
let val_typed = self.check_node(*value, ctx)?;
let ty = val_typed.ty.clone();
self.global_types.borrow_mut().insert(global_index, ty.clone());
(BoundKind::DefGlobal { global_index, value: Box::new(val_typed) }, ty)
},
BoundKind::If { cond, then_br, else_br } => {
let cond_typed = self.check_node(*cond, ctx)?;
let then_typed = self.check_node(*then_br, ctx)?;
let mut else_typed = None;
let mut final_ty = then_typed.ty.clone();
if let Some(e) = else_br {
let et = self.check_node(*e, ctx)?;
// Basic type promotion: if types differ, fall back to Any
if et.ty != final_ty {
final_ty = StaticType::Any;
}
else_typed = Some(Box::new(et));
} else {
// If without else returns Void or Optional(Then)
final_ty = StaticType::Any; // Delphi: MakeOptional(Then)
}
(BoundKind::If {
cond: Box::new(cond_typed),
then_br: Box::new(then_typed),
else_br: else_typed
}, final_ty)
},
BoundKind::Block { exprs } => {
let mut typed_exprs = Vec::new();
let mut last_ty = StaticType::Void;
for e in exprs {
let t = self.check_node(e, ctx)?;
last_ty = t.ty.clone();
typed_exprs.push(t);
}
(BoundKind::Block { exprs: typed_exprs }, last_ty)
},
BoundKind::Lambda { param_count, upvalues, body } => {
// 1. Determine types of captured variables
let mut upvalue_types = Vec::with_capacity(upvalues.len());
for &addr in &upvalues {
upvalue_types.push(ctx.get_type(addr));
}
// 2. Create nested context for lambda body
let mut lambda_ctx = TypeContext::new(param_count + 64, upvalue_types, Some(ctx));
// 3. Check body
let body_typed = self.check_node((*body).clone(), &mut lambda_ctx)?;
let ret_ty = body_typed.ty.clone();
// 4. Construct function type: fn(any ... any) -> Ret
let fn_ty = StaticType::Function {
params: vec![StaticType::Any; param_count as usize],
ret: Box::new(ret_ty),
};
(BoundKind::Lambda {
param_count,
upvalues,
body: Rc::new(body_typed)
}, fn_ty)
},
BoundKind::Call { callee, args } => {
let callee_typed = self.check_node(*callee, ctx)?;
let mut typed_args = Vec::new();
for arg in args {
typed_args.push(self.check_node(arg, ctx)?);
}
let ret_ty = match &callee_typed.ty {
StaticType::Function { ret, .. } => *ret.clone(),
_ => StaticType::Any,
};
(BoundKind::Call {
callee: Box::new(callee_typed),
args: typed_args
}, ret_ty)
},
BoundKind::Tuple { elements } => {
let mut typed_elements = Vec::new();
for e in elements {
typed_elements.push(self.check_node(e, ctx)?);
}
// Stability: Just List(Any) for now
(BoundKind::Tuple { elements: typed_elements }, StaticType::List(Box::new(StaticType::Any)))
},
BoundKind::Map { entries } => {
let mut typed_entries = Vec::new();
for (k, v) in entries {
typed_entries.push((self.check_node(k, ctx)?, self.check_node(v, ctx)?));
}
(BoundKind::Map { entries: typed_entries }, StaticType::Record(Rc::new(std::collections::BTreeMap::new())))
},
BoundKind::Expansion { original_call, bound_expanded } => {
let expanded_typed = self.check_node(*bound_expanded, ctx)?;
let ty = expanded_typed.ty.clone();
(BoundKind::Expansion {
original_call,
bound_expanded: Box::new(expanded_typed)
}, ty)
},
BoundKind::Extension(_ext) => {
return Err(format!("TypeChecking for extension '{}' not implemented", _ext.display_name()));
},
BoundKind::TailCall { .. } => unreachable!("TailCall generated before TypeChecker"),
};
Ok(Node {
identity: node.identity,
kind,
ty,
})
}
}
+27 -11
View File
@@ -5,7 +5,7 @@ use crate::ast::types::{Value, StaticType, Object};
use crate::ast::nodes::{Node, UntypedKind, Symbol}; use crate::ast::nodes::{Node, UntypedKind, Symbol};
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
use crate::ast::compiler::binder::Binder; use crate::ast::compiler::binder::Binder;
use crate::ast::compiler::BoundKind; use crate::ast::compiler::{TypedNode, TypeChecker};
use crate::ast::vm::{VM, TracingObserver}; use crate::ast::vm::{VM, TracingObserver};
use crate::ast::compiler::tco::TCO; use crate::ast::compiler::tco::TCO;
@@ -13,14 +13,16 @@ use crate::ast::compiler::dumper::Dumper;
use crate::ast::compiler::macros::{MacroExpander, MacroRegistry, MacroEvaluator}; use crate::ast::compiler::macros::{MacroExpander, MacroRegistry, MacroEvaluator};
pub struct Environment { pub struct Environment {
pub global_names: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, pub global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
pub global_types: Rc<RefCell<HashMap<u32, StaticType>>>,
pub global_values: Rc<RefCell<Vec<Value>>>, pub global_values: Rc<RefCell<Vec<Value>>>,
pub debug_mode: bool, pub debug_mode: bool,
} }
/// Evaluator used during macro expansion to allow compile-time logic. /// Evaluator used during macro expansion to allow compile-time logic.
struct RuntimeMacroEvaluator { struct RuntimeMacroEvaluator {
global_names: Rc<RefCell<HashMap<Symbol, (u32, StaticType)>>>, global_names: Rc<RefCell<HashMap<Symbol, u32>>>,
global_types: Rc<RefCell<HashMap<u32, StaticType>>>,
global_values: Rc<RefCell<Vec<Value>>>, global_values: Rc<RefCell<Vec<Value>>>,
} }
@@ -34,8 +36,12 @@ impl MacroEvaluator for RuntimeMacroEvaluator {
// 2. Full evaluation for complex compile-time expressions // 2. Full evaluation for complex compile-time expressions
let bound_ast = Binder::bind_root(self.global_names.clone(), node)?; let bound_ast = Binder::bind_root(self.global_names.clone(), node)?;
let checker = TypeChecker::new(self.global_types.clone());
let typed_ast = checker.check(bound_ast)?;
let mut vm = VM::new(self.global_values.clone()); let mut vm = VM::new(self.global_values.clone());
vm.run(&bound_ast) vm.run(&typed_ast)
} }
} }
@@ -49,6 +55,7 @@ impl Environment {
pub fn new() -> Self { pub fn new() -> Self {
let env = Self { let env = Self {
global_names: Rc::new(RefCell::new(HashMap::new())), global_names: Rc::new(RefCell::new(HashMap::new())),
global_types: Rc::new(RefCell::new(HashMap::new())),
global_values: Rc::new(RefCell::new(Vec::new())), global_values: Rc::new(RefCell::new(Vec::new())),
debug_mode: false, debug_mode: false,
}; };
@@ -63,6 +70,7 @@ impl Environment {
fn get_expander(&self) -> MacroExpander<RuntimeMacroEvaluator> { fn get_expander(&self) -> MacroExpander<RuntimeMacroEvaluator> {
let evaluator = RuntimeMacroEvaluator { let evaluator = RuntimeMacroEvaluator {
global_names: self.global_names.clone(), global_names: self.global_names.clone(),
global_types: self.global_types.clone(),
global_values: self.global_values.clone(), global_values: self.global_values.clone(),
}; };
MacroExpander::new(MacroRegistry::new(), evaluator) MacroExpander::new(MacroRegistry::new(), evaluator)
@@ -70,10 +78,12 @@ impl Environment {
pub fn register_native(&self, name: &str, ty: StaticType, func: impl Fn(Vec<Value>) -> Value + 'static) { pub fn register_native(&self, name: &str, ty: StaticType, func: impl Fn(Vec<Value>) -> Value + 'static) {
let mut names = self.global_names.borrow_mut(); let mut names = self.global_names.borrow_mut();
let mut types = self.global_types.borrow_mut();
let mut values = self.global_values.borrow_mut(); let mut values = self.global_values.borrow_mut();
let idx = values.len() as u32; let idx = values.len() as u32;
names.insert(Symbol::from(name), (idx, ty)); names.insert(Symbol::from(name), idx);
types.insert(idx, ty);
values.push(Value::Function(Rc::new(func))); values.push(Value::Function(Rc::new(func)));
} }
@@ -150,8 +160,8 @@ impl Environment {
Ok(Dumper::dump(&linked)) Ok(Dumper::dump(&linked))
} }
/// Frontend: Parse -> Expand Macros -> Bind & Type Check /// Frontend: Parse -> Expand Macros -> Bind -> Type Check
pub fn compile(&self, source: &str) -> Result<Node<BoundKind, StaticType>, String> { pub fn compile(&self, source: &str) -> Result<TypedNode, String> {
// 1. Parse // 1. Parse
let mut parser = Parser::new(source)?; let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?; let untyped_ast = parser.parse_expression()?;
@@ -164,17 +174,23 @@ impl Environment {
// 3. Expand Macros // 3. Expand Macros
let expanded_ast = self.get_expander().expand(untyped_ast)?; let expanded_ast = self.get_expander().expand(untyped_ast)?;
// 4. Bind & Type Check // 4. Bind
Binder::bind_root(self.global_names.clone(), &expanded_ast) let bound_ast = Binder::bind_root(self.global_names.clone(), &expanded_ast)?;
// 5. Type Check
let checker = TypeChecker::new(self.global_types.clone());
let typed_ast = checker.check(bound_ast)?;
Ok(typed_ast)
} }
/// Backend: Optimization (TCO, etc.) /// Backend: Optimization (TCO, etc.)
pub fn link(&self, node: Node<BoundKind, StaticType>) -> Node<BoundKind, StaticType> { pub fn link(&self, node: TypedNode) -> TypedNode {
TCO::optimize(node) TCO::optimize(node)
} }
/// Runtime: Execute the linked AST in the VM /// Runtime: Execute the linked AST in the VM
pub fn run(&self, node: &Node<BoundKind, StaticType>) -> Result<Value, String> { pub fn run(&self, node: &TypedNode) -> Result<Value, String> {
let mut vm = VM::new(self.global_values.clone()); let mut vm = VM::new(self.global_values.clone());
vm.run(node) vm.run(node)
} }
+3 -3
View File
@@ -3,8 +3,7 @@ use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::any::Any; use std::any::Any;
use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode}; use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode};
use crate::ast::nodes::Node; use crate::ast::types::{Value, Object};
use crate::ast::types::{Value, Object, StaticType};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Closure { pub struct Closure {
@@ -455,7 +454,8 @@ impl VM {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::ast::types::{SourceLocation, NodeIdentity}; use crate::ast::nodes::Node;
use crate::ast::types::{SourceLocation, NodeIdentity, StaticType};
fn make_dummy_identity() -> Rc<NodeIdentity> { fn make_dummy_identity() -> Rc<NodeIdentity> {
Rc::new(NodeIdentity { Rc::new(NodeIdentity {
+4 -2
View File
@@ -80,8 +80,10 @@ mod tests {
let mut binder = Binder::new(globals.clone()); let mut binder = Binder::new(globals.clone());
let bound_ast = binder.bind(&ast).expect("Failed to bind"); let bound_ast = binder.bind(&ast).expect("Failed to bind");
// BRIDGE: Binder -> VM requires a TypedNode // Use the real TypeChecker instead of the dummy placeholder
let typed_ast = crate::ast::environment::dummy_type_check(bound_ast); let global_types = Rc::new(RefCell::new(std::collections::HashMap::new()));
let checker = crate::ast::compiler::TypeChecker::new(global_types);
let typed_ast = checker.check(bound_ast).expect("Type check failed");
let vm_globals = Rc::new(RefCell::new(Vec::new())); let vm_globals = Rc::new(RefCell::new(Vec::new()));
let mut vm = VM::new(vm_globals); let mut vm = VM::new(vm_globals);