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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ pub enum BoundKind<T = ()> {
|
||||
},
|
||||
|
||||
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.
|
||||
@@ -95,6 +95,9 @@ pub enum BoundKind<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.
|
||||
pub type BoundNode = Node<BoundKind<()>, ()>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user