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
+15 -13
View File
@@ -1,15 +1,21 @@
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use crate::ast::types::Value;
use crate::ast::types::{Value, StaticType};
use crate::ast::parser::Parser;
use crate::ast::compiler::binder::Binder;
use crate::ast::vm::VM;
pub struct Environment {
pub global_names: Arc<Mutex<HashMap<String, u32>>>,
pub global_names: Arc<Mutex<HashMap<String, (u32, StaticType)>>>,
pub global_values: Arc<Mutex<Vec<Value>>>,
}
impl Default for Environment {
fn default() -> Self {
Self::new()
}
}
impl Environment {
pub fn new() -> Self {
let env = Self {
@@ -20,18 +26,18 @@ impl Environment {
env
}
pub fn register_native(&self, name: &str, func: fn(Vec<Value>) -> Value) {
pub fn register_native(&self, name: &str, ty: StaticType, func: fn(Vec<Value>) -> Value) {
let mut names = self.global_names.lock().unwrap();
let mut values = self.global_values.lock().unwrap();
let idx = values.len() as u32;
names.insert(name.to_string(), idx);
values.push(Value::Function(Arc::new(move |args| func(args))));
names.insert(name.to_string(), (idx, ty));
values.push(Value::Function(Arc::new(func)));
}
fn register_stdlib(&self) {
// Simple math for testing
self.register_native("+", |args| {
self.register_native("+", StaticType::Any, |args| {
let mut acc = 0.0;
for arg in args {
if let Value::Int(i) = arg { acc += i as f64; }
@@ -40,7 +46,7 @@ impl Environment {
if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) }
});
self.register_native(">", |args| {
self.register_native(">", StaticType::Any, |args| {
if args.len() != 2 { return Value::Void; }
match (&args[0], &args[1]) {
(Value::Int(a), Value::Int(b)) => Value::Bool(a > b),
@@ -52,17 +58,13 @@ impl Environment {
pub fn run_script(&self, source: &str) -> Result<Value, String> {
// 1. Parse
let mut parser = Parser::new(source)?;
let untyped_ast = parser.parse_expression()?; // Or loop for script
let untyped_ast = parser.parse_expression()?;
// 2. Bind
// 2. Bind & Type Check
let mut binder = Binder::new(self.global_names.clone());
let bound_ast = binder.bind(&untyped_ast)?;
// 3. Execute
// Note: VM needs to borrow globals, not own them exclusively?
// My VM impl takes Vec<Value>, but here we have Arc<Mutex<Vec>>.
// I need to update VM to accept shared globals.
let mut vm = VM::new(self.global_values.clone());
vm.run(&bound_ast)
}