From d93727e1989d86e46612d0261508505ed18bdf7a Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Thu, 19 Feb 2026 12:22:39 +0100 Subject: [PATCH] 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. --- src/ast/compiler/binder.rs | 4 +- src/ast/compiler/bound_nodes.rs | 5 +- src/ast/compiler/macros.rs | 2 +- src/ast/compiler/mod.rs | 2 + src/ast/compiler/type_checker.rs | 227 +++++++++++++++++++++++++++++++ src/ast/environment.rs | 38 ++++-- src/ast/vm.rs | 6 +- src/integration_test.rs | 6 +- 8 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 src/ast/compiler/type_checker.rs diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index 3cc44c0..eff69cc 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -11,7 +11,7 @@ struct LocalInfo { // Note: Binder doesn't strictly need the type anymore, // but it might be useful for built-ins during resolution. // For now we keep it as Any or Unknown. - ty: StaticType, + _ty: StaticType, } #[derive(Debug, Clone)] @@ -33,7 +33,7 @@ impl CompilerScope { return Err(format!("Variable '{}' is already defined in this scope level.", sym.name)); } 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; Ok(slot) } diff --git a/src/ast/compiler/bound_nodes.rs b/src/ast/compiler/bound_nodes.rs index bc418b5..b1c83d8 100644 --- a/src/ast/compiler/bound_nodes.rs +++ b/src/ast/compiler/bound_nodes.rs @@ -80,7 +80,7 @@ pub enum BoundKind { }, Map { - entries: Vec<(Node, T>, Node, T>)>, + entries: Vec>, }, /// An expanded macro call, preserving the original call for debugging and UI. @@ -95,6 +95,9 @@ pub enum BoundKind { Extension(Box>), } +/// A single entry in a Map literal (Key-Value pair) +pub type MapEntry = (Node, T>, Node, T>); + /// Type alias for a node that has been bound (addresses resolved) but not yet type-checked. pub type BoundNode = Node, ()>; diff --git a/src/ast/compiler/macros.rs b/src/ast/compiler/macros.rs index 23ab03b..594713a 100644 --- a/src/ast/compiler/macros.rs +++ b/src/ast/compiler/macros.rs @@ -440,7 +440,7 @@ mod tests { use super::*; use std::cell::RefCell; use crate::ast::parser::Parser; - use crate::ast::types::{Value, StaticType, Object}; + use crate::ast::types::{Value, Object}; use crate::ast::compiler::Binder; struct SimpleEvaluator; diff --git a/src/ast/compiler/mod.rs b/src/ast/compiler/mod.rs index f74ee58..b02d2fb 100644 --- a/src/ast/compiler/mod.rs +++ b/src/ast/compiler/mod.rs @@ -4,6 +4,7 @@ pub mod tco; pub mod upvalues; pub mod dumper; pub mod macros; +pub mod type_checker; pub use binder::*; pub use bound_nodes::*; @@ -11,3 +12,4 @@ pub use tco::*; pub use upvalues::*; pub use dumper::*; pub use macros::*; +pub use type_checker::*; diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs new file mode 100644 index 0000000..4cd2011 --- /dev/null +++ b/src/ast/compiler/type_checker.rs @@ -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, + /// Types of captured variables (passed from outer scope) + upvalue_types: Vec, +} + +impl<'a> TypeContext<'a> { + fn new(slot_count: u32, upvalue_types: Vec, 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>>, +} + +impl TypeChecker { + pub fn new(global_types: Rc>>) -> Self { + Self { global_types } + } + + pub fn check(&self, node: BoundNode) -> Result { + // 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 { + 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, + }) + } +} diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 633c74d..f534cde 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -5,7 +5,7 @@ use crate::ast::types::{Value, StaticType, Object}; use crate::ast::nodes::{Node, UntypedKind, Symbol}; use crate::ast::parser::Parser; 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::compiler::tco::TCO; @@ -13,14 +13,16 @@ use crate::ast::compiler::dumper::Dumper; use crate::ast::compiler::macros::{MacroExpander, MacroRegistry, MacroEvaluator}; pub struct Environment { - pub global_names: Rc>>, + pub global_names: Rc>>, + pub global_types: Rc>>, pub global_values: Rc>>, pub debug_mode: bool, } /// Evaluator used during macro expansion to allow compile-time logic. struct RuntimeMacroEvaluator { - global_names: Rc>>, + global_names: Rc>>, + global_types: Rc>>, global_values: Rc>>, } @@ -34,8 +36,12 @@ impl MacroEvaluator for RuntimeMacroEvaluator { // 2. Full evaluation for complex compile-time expressions 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()); - vm.run(&bound_ast) + vm.run(&typed_ast) } } @@ -49,6 +55,7 @@ impl Environment { pub fn new() -> Self { let env = Self { global_names: Rc::new(RefCell::new(HashMap::new())), + global_types: Rc::new(RefCell::new(HashMap::new())), global_values: Rc::new(RefCell::new(Vec::new())), debug_mode: false, }; @@ -63,6 +70,7 @@ impl Environment { fn get_expander(&self) -> MacroExpander { let evaluator = RuntimeMacroEvaluator { global_names: self.global_names.clone(), + global_types: self.global_types.clone(), global_values: self.global_values.clone(), }; 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 + 'static) { 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 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))); } @@ -150,8 +160,8 @@ impl Environment { Ok(Dumper::dump(&linked)) } - /// Frontend: Parse -> Expand Macros -> Bind & Type Check - pub fn compile(&self, source: &str) -> Result, String> { + /// Frontend: Parse -> Expand Macros -> Bind -> Type Check + pub fn compile(&self, source: &str) -> Result { // 1. Parse let mut parser = Parser::new(source)?; let untyped_ast = parser.parse_expression()?; @@ -164,17 +174,23 @@ impl Environment { // 3. Expand Macros let expanded_ast = self.get_expander().expand(untyped_ast)?; - // 4. Bind & Type Check - Binder::bind_root(self.global_names.clone(), &expanded_ast) + // 4. Bind + 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.) - pub fn link(&self, node: Node) -> Node { + pub fn link(&self, node: TypedNode) -> TypedNode { TCO::optimize(node) } /// Runtime: Execute the linked AST in the VM - pub fn run(&self, node: &Node) -> Result { + pub fn run(&self, node: &TypedNode) -> Result { let mut vm = VM::new(self.global_values.clone()); vm.run(node) } diff --git a/src/ast/vm.rs b/src/ast/vm.rs index b557bf0..8f6f22f 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -3,8 +3,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::any::Any; use crate::ast::compiler::bound_nodes::{Address, BoundKind, TypedNode}; -use crate::ast::nodes::Node; -use crate::ast::types::{Value, Object, StaticType}; +use crate::ast::types::{Value, Object}; #[derive(Debug, Clone)] pub struct Closure { @@ -455,7 +454,8 @@ impl VM { #[cfg(test)] mod tests { 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 { Rc::new(NodeIdentity { diff --git a/src/integration_test.rs b/src/integration_test.rs index 64a7beb..752d8cd 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -80,8 +80,10 @@ mod tests { let mut binder = Binder::new(globals.clone()); let bound_ast = binder.bind(&ast).expect("Failed to bind"); - // BRIDGE: Binder -> VM requires a TypedNode - let typed_ast = crate::ast::environment::dummy_type_check(bound_ast); + // Use the real TypeChecker instead of the dummy placeholder + 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 mut vm = VM::new(vm_globals);