feat: Add StaticType and type checking

This commit introduces `StaticType` and enhances the `Binder` to perform
basic type checking during the binding phase.

Key changes include:

- **`StaticType` enum:** Represents static types such as `Any`, `Void`,
  `Bool`, `Int`, `Float`, `Text`, `List`, `Record`, and `Function`.
- **`Node<K, T>`:** The generic `Node` now includes a `ty` field to
  store its inferred static type.
- **Binder enhancements:**
    - `CompilerScope` now stores `LocalInfo` containing the variable's
      slot and `StaticType`.
    - `FunctionCompiler` stores upvalues with their associated
      `StaticType`.
    - `Binder::bind` now returns `Node<BoundKind, StaticType>`,
      propagating type information.
    - Type checking is added for `If` conditions and `Assign`
      operations.
    - `Def` nodes now infer and store the type of the defined variable.
- **`Value::static_type()`:** A new method to determine the `StaticType`
  of a `Value`.
- **Environment modifications:** `global_names` now stores `(u32,
  StaticType)` to associate global variables with their types.
- **Parser modifications:** The `ty` field of nodes is initialized to
  `()` by default.
- **VM modifications:** `VM::run` and `VM::eval` now operate on
  `Node<BoundKind, StaticType>`.
- **Tests:** Added basic evaluation and type error tests.
  feat: Add StaticType and type checking

Introduces `StaticType` enum and integrates it into the AST. The binder
now performs type checking during compilation, ensuring that expressions
conform to expected types, especially for control flow structures like
`if`. This lays the groundwork for static type analysis and error
reporting.
This commit is contained in:
Michael Schimmel
2026-02-17 11:54:52 +01:00
parent 2c612adc49
commit 49a879045e
9 changed files with 243 additions and 106 deletions
+119 -65
View File
@@ -2,11 +2,17 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::compiler::bound_nodes::{BoundKind, Address};
use crate::ast::types::Identity;
use crate::ast::types::{Identity, StaticType};
#[derive(Debug, Clone)]
struct LocalInfo {
slot: u32,
ty: StaticType,
}
#[derive(Debug, Clone)]
struct CompilerScope {
locals: HashMap<String, u32>,
locals: HashMap<String, LocalInfo>,
slot_count: u32,
}
@@ -18,23 +24,21 @@ impl CompilerScope {
}
}
fn define(&mut self, name: &str) -> u32 {
fn define(&mut self, name: &str, ty: StaticType) -> u32 {
let slot = self.slot_count;
self.locals.insert(name.to_string(), slot);
self.locals.insert(name.to_string(), LocalInfo { slot, ty });
self.slot_count += 1;
slot
}
fn resolve(&self, name: &str) -> Option<u32> {
self.locals.get(name).copied()
fn resolve(&self, name: &str) -> Option<LocalInfo> {
self.locals.get(name).cloned()
}
}
struct FunctionCompiler {
scope: CompilerScope,
// Upvalues capture variables from the *immediate* enclosing function.
// The address stored here refers to a Local/Upvalue in the Parent Scope.
upvalues: Vec<Address>,
upvalues: Vec<(Address, StaticType)>,
}
impl FunctionCompiler {
@@ -45,23 +49,23 @@ impl FunctionCompiler {
}
}
fn add_upvalue(&mut self, addr: Address) -> u32 {
if let Some(idx) = self.upvalues.iter().position(|&a| a == addr) {
fn add_upvalue(&mut self, addr: Address, ty: StaticType) -> u32 {
if let Some(idx) = self.upvalues.iter().position(|(a, _)| *a == addr) {
return idx as u32;
}
let idx = self.upvalues.len() as u32;
self.upvalues.push(addr);
self.upvalues.push((addr, ty));
idx
}
}
pub struct Binder {
functions: Vec<FunctionCompiler>,
globals: Arc<Mutex<HashMap<String, u32>>>,
globals: Arc<Mutex<HashMap<String, (u32, StaticType)>>>,
}
impl Binder {
pub fn new(globals: Arc<Mutex<HashMap<String, u32>>>) -> Self {
pub fn new(globals: Arc<Mutex<HashMap<String, (u32, StaticType)>>>) -> Self {
let mut binder = Self {
functions: Vec::new(),
globals,
@@ -70,53 +74,73 @@ impl Binder {
binder
}
pub fn bind(&mut self, node: &Node<UntypedKind>) -> Result<Node<BoundKind>, String> {
pub fn bind(&mut self, node: &Node<UntypedKind>) -> Result<Node<BoundKind, StaticType>, String> {
match &node.kind {
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop)),
UntypedKind::Constant(v) => Ok(self.make_node(node.identity.clone(), BoundKind::Constant(v.clone()))),
UntypedKind::Nop => Ok(self.make_node(node.identity.clone(), BoundKind::Nop, StaticType::Void)),
UntypedKind::Constant(v) => {
let ty = v.static_type();
Ok(self.make_node(node.identity.clone(), BoundKind::Constant(v.clone()), ty))
},
UntypedKind::Identifier(name) => {
let addr = self.resolve_variable(name)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Get(addr)))
let (addr, ty) = self.resolve_variable(name)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Get(addr), ty))
},
UntypedKind::If { cond, then_br, else_br } => {
let cond = Box::new(self.bind(cond)?);
let then_br = Box::new(self.bind(then_br)?);
let else_br = match else_br {
Some(e) => Some(Box::new(self.bind(e)?)),
None => None,
let cond = self.bind(cond)?;
// Type check condition: Must be Bool or Any
if cond.ty != StaticType::Bool && cond.ty != StaticType::Any {
return Err(format!("Condition must be boolean, found {:?}", cond.ty));
}
let then_br = self.bind(then_br)?;
let mut else_br_bound = None;
let final_ty = if let Some(e) = else_br {
let eb = self.bind(e)?;
let ty = if then_br.ty == eb.ty {
then_br.ty.clone()
} else {
StaticType::Any // Common supertype logic could go here
};
else_br_bound = Some(Box::new(eb));
ty
} else {
StaticType::Void // If without else returns Void if false
};
Ok(self.make_node(node.identity.clone(), BoundKind::If { cond, then_br, else_br }))
Ok(self.make_node(node.identity.clone(), BoundKind::If {
cond: Box::new(cond),
then_br: Box::new(then_br),
else_br: else_br_bound
}, final_ty))
},
UntypedKind::Def { name, value } => {
// Determine scope: Global (root) or Local (inside fn)?
let val_node = self.bind(value)?;
let ty = val_node.ty.clone();
if self.functions.len() == 1 {
// Global Definition
let val_node = self.bind(value)?;
let mut globals = self.globals.lock().unwrap();
let idx = if let Some(&idx) = globals.get(name.as_ref()) {
idx
let idx = if let Some((idx, _)) = globals.get(name.as_ref()) {
*idx
} else {
let idx = globals.len() as u32;
globals.insert(name.to_string(), idx);
globals.insert(name.to_string(), (idx, ty.clone()));
idx
};
Ok(self.make_node(node.identity.clone(), BoundKind::DefGlobal { global_index: idx, value: Box::new(val_node) }))
Ok(self.make_node(node.identity.clone(), BoundKind::DefGlobal {
global_index: idx,
value: Box::new(val_node)
}, ty))
} else {
// Local Variable Definition
// We bind the value *before* defining the name to avoid self-reference in init?
// Usually yes: (def x 10) -> bind(10), define(x).
let val_node = self.bind(value)?;
let current_fn = self.functions.last_mut().unwrap();
let slot = current_fn.scope.define(name);
let slot = current_fn.scope.define(name, ty.clone());
// We treat local 'def' as a Set to the new slot
Ok(self.make_node(node.identity.clone(), BoundKind::Set {
addr: Address::Local(slot),
value: Box::new(val_node)
}))
}, ty))
}
},
@@ -124,8 +148,17 @@ impl Binder {
let val_node = self.bind(value)?;
if let UntypedKind::Identifier(name) = &target.kind {
let addr = self.resolve_variable(name)?;
Ok(self.make_node(node.identity.clone(), BoundKind::Set { addr, value: Box::new(val_node) }))
let (addr, target_ty) = self.resolve_variable(name)?;
// Type check: value must be compatible with target
if target_ty != StaticType::Any && val_node.ty != StaticType::Any && target_ty != val_node.ty {
return Err(format!("Cannot assign {:?} to variable of type {:?}", val_node.ty, target_ty));
}
Ok(self.make_node(node.identity.clone(), BoundKind::Set {
addr,
value: Box::new(val_node)
}, target_ty))
} else {
Err("Assignment target must be an identifier".to_string())
}
@@ -134,40 +167,65 @@ impl Binder {
UntypedKind::Lambda { params, body } => {
self.functions.push(FunctionCompiler::new());
let mut param_types = Vec::new();
{
let current_fn = self.functions.last_mut().unwrap();
for param in params {
current_fn.scope.define(param);
// For now, lambda params are Any unless we have a way to specify them
current_fn.scope.define(param, StaticType::Any);
param_types.push(StaticType::Any);
}
}
let body_bound = self.bind(body)?;
let ret_ty = body_bound.ty.clone();
let compiled_fn = self.functions.pop().unwrap();
let upvalues = compiled_fn.upvalues;
let upvalues: Vec<Address> = compiled_fn.upvalues.into_iter().map(|(a, _)| a).collect();
let fn_ty = StaticType::Function {
params: param_types,
ret: Box::new(ret_ty),
};
Ok(self.make_node(node.identity.clone(), BoundKind::Lambda {
param_count: params.len() as u32,
upvalues,
body: Arc::new(body_bound),
}))
}, fn_ty))
},
UntypedKind::Call { callee, args } => {
let callee = Box::new(self.bind(callee)?);
let callee = self.bind(callee)?;
let mut bound_args = Vec::new();
let mut arg_types = Vec::new();
for arg in args {
bound_args.push(self.bind(arg)?);
let b = self.bind(arg)?;
arg_types.push(b.ty.clone());
bound_args.push(b);
}
Ok(self.make_node(node.identity.clone(), BoundKind::Call { callee, args: bound_args }))
let ret_ty = match &callee.ty {
StaticType::Function { ret, .. } => *ret.clone(),
StaticType::Any => StaticType::Any,
_ => return Err(format!("Attempt to call non-function of type {:?}", callee.ty)),
};
Ok(self.make_node(node.identity.clone(), BoundKind::Call {
callee: Box::new(callee),
args: bound_args
}, ret_ty))
},
UntypedKind::Block { exprs } => {
let mut bound_exprs = Vec::new();
let mut last_ty = StaticType::Void;
for expr in exprs {
bound_exprs.push(self.bind(expr)?);
let b = self.bind(expr)?;
last_ty = b.ty.clone();
bound_exprs.push(b);
}
Ok(self.make_node(node.identity.clone(), BoundKind::Block { exprs: bound_exprs }))
Ok(self.make_node(node.identity.clone(), BoundKind::Block { exprs: bound_exprs }, last_ty))
},
UntypedKind::Extension(_) => {
@@ -176,41 +234,37 @@ impl Binder {
}
}
fn resolve_variable(&mut self, name: &str) -> Result<Address, String> {
fn resolve_variable(&mut self, name: &str) -> Result<(Address, StaticType), String> {
let current_fn_idx = self.functions.len() - 1;
// 1. Try local in current function
if let Some(slot) = self.functions[current_fn_idx].scope.resolve(name) {
return Ok(Address::Local(slot));
if let Some(info) = self.functions[current_fn_idx].scope.resolve(name) {
return Ok((Address::Local(info.slot), info.ty));
}
// 2. Try enclosing scopes (capture chain)
// Walk backwards from parent of current function up to root (0)
// We look for where the variable is DEFINED.
for i in (0..current_fn_idx).rev() {
let func = &self.functions[i];
if let Some(slot) = func.scope.resolve(name) {
// Found definition! It's a Local variable in function 'i'.
let mut addr = Address::Local(slot);
if let Some(info) = self.functions[i].scope.resolve(name) {
let mut addr = Address::Local(info.slot);
let ty = info.ty.clone();
// Now we must propagate this capture down through all functions from i+1 to current.
for k in (i + 1)..=current_fn_idx {
addr = Address::Upvalue(self.functions[k].add_upvalue(addr));
addr = Address::Upvalue(self.functions[k].add_upvalue(addr, ty.clone()));
}
return Ok(addr);
return Ok((addr, ty));
}
}
// 3. Try Global
let globals = self.globals.lock().unwrap();
if let Some(&idx) = globals.get(name) {
return Ok(Address::Global(idx));
if let Some((idx, ty)) = globals.get(name) {
return Ok((Address::Global(*idx), ty.clone()));
}
Err(format!("Undefined variable '{}'", name))
}
fn make_node(&self, identity: Identity, kind: BoundKind) -> Node<BoundKind> {
Node { identity, kind }
fn make_node(&self, identity: Identity, kind: BoundKind, ty: StaticType) -> Node<BoundKind, StaticType> {
Node { identity, kind, ty }
}
}
+10 -10
View File
@@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::ast::types::Value;
use crate::ast::types::{Value, StaticType};
use crate::ast::nodes::Node;
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -20,35 +20,35 @@ pub enum BoundKind {
// Variable Update (Resolved)
Set {
addr: Address,
value: Box<Node<BoundKind>>,
value: Box<Node<BoundKind, StaticType>>,
},
If {
cond: Box<Node<BoundKind>>,
then_br: Box<Node<BoundKind>>,
else_br: Option<Box<Node<BoundKind>>>,
cond: Box<Node<BoundKind, StaticType>>,
then_br: Box<Node<BoundKind, StaticType>>,
else_br: Option<Box<Node<BoundKind, StaticType>>>,
},
// Global Definition (Locals are implicit by stack position)
DefGlobal {
global_index: u32,
value: Box<Node<BoundKind>>,
value: Box<Node<BoundKind, StaticType>>,
},
Lambda {
param_count: u32,
// The list of variables captured from enclosing scopes
upvalues: Vec<Address>,
body: Arc<Node<BoundKind>>,
body: Arc<Node<BoundKind, StaticType>>,
},
Call {
callee: Box<Node<BoundKind>>,
args: Vec<Node<BoundKind>>,
callee: Box<Node<BoundKind, StaticType>>,
args: Vec<Node<BoundKind, StaticType>>,
},
Block {
exprs: Vec<Node<BoundKind>>,
exprs: Vec<Node<BoundKind, StaticType>>,
},
// Extension points (need to be adapted for bound nodes if they use variables)