From 49db73800a0e7ae8f0a506ce44fdd65faaed462a Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Thu, 19 Feb 2026 13:10:28 +0100 Subject: [PATCH] Refactor type checking for functions and operators This commit refactors the type checking logic for functions and operators to improve type safety and expressiveness. Key changes include: - Introduced `StaticType::FunctionOverloads` to represent functions with multiple possible signatures, enabling better handling of operator overloading. - Updated the `TypeChecker` to correctly infer function types and resolve calls with overloaded functions. - Modified the `Environment` to register standard library functions with specific `FunctionOverloads` signatures, replacing the previous `StaticType::Any` approach. - Enhanced the `StaticType::resolve_call` method to intelligently match argument types against expected parameters, including handling of overloaded functions. - Added new tests to verify the correctness of type inference for lambda returns and operator overloading. --- gemini.md | 9 ++- src/ast/compiler/type_checker.rs | 40 ++++++----- src/ast/environment.rs | 114 ++++++++++++++++++++++++------- src/ast/types.rs | 61 ++++++++++++++--- 4 files changed, 166 insertions(+), 58 deletions(-) diff --git a/gemini.md b/gemini.md index 81094f4..c6e0f9b 100644 --- a/gemini.md +++ b/gemini.md @@ -4,9 +4,7 @@ * Das Ziel ist, die alte Delphi-Codebase nach Rust zu portieren. * Die alte Delphi-Codebase ist nicht zu verändern. -Wichtig: zentraler Einstiegspunkt ist -@Delphi/Myc.Ast.Environment.pas -und die dort referenzierten Units. +Wichtig: zentraler Einstiegspunkt ist Delphi/Myc.Ast.Environment.pasund die dort referenzierten Units. ### Design @@ -38,3 +36,8 @@ und die dort referenzierten Units. * Ich kann Rust noch nicht so gut. Erkläre mir, was du machst und welche Konzepte du nutzt. Ich will was dazulernen. * Wenn clippy etwas vorschlägt, von dem du glaubst, dass es nicht stabil ist, dann überprüfe die aktuelle Version. Wahrscheinlich ist das Feature mittlerweile im trunk. Wir gehen immer davon aus, dass clippy recht hat. + +## Testing + +* Das Projekt enthält eine Testsuite für Skripte. Logik in src/utils/tester.rs. Diese Testes werden automatisch in "cargo test" eingebunden. +* Nach Fertigstellung einer Aufgabe: Warnungen sind zu eliminieren. Tests müssen durchlaufen. Clippy auch. diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 9d1d8ef..f865685 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -155,10 +155,10 @@ impl TypeChecker { let ret_ty = body_typed.ty.clone(); // 4. Construct function type: fn(any ... any) -> Ret - let fn_ty = StaticType::Function { + let fn_ty = StaticType::Function(Box::new(crate::ast::types::Signature { params: vec![StaticType::Any; param_count as usize], - ret: Box::new(ret_ty), - }; + ret: ret_ty, + })); (BoundKind::Lambda { param_count, @@ -174,10 +174,8 @@ impl TypeChecker { typed_args.push(self.check_node(arg, ctx)?); } - let ret_ty = match &callee_typed.ty { - StaticType::Function { ret, .. } => *ret.clone(), - _ => StaticType::Any, - }; + let arg_types: Vec<_> = typed_args.iter().map(|a| a.ty.clone()).collect(); + let ret_ty = callee_typed.ty.resolve_call(&arg_types).unwrap_or(StaticType::Any); (BoundKind::Call { callee: Box::new(callee_typed), @@ -229,19 +227,11 @@ impl TypeChecker { #[cfg(test)] mod tests { use super::*; - use crate::ast::parser::Parser; - use crate::ast::compiler::binder::Binder; use crate::ast::types::StaticType; - use std::cell::RefCell; fn check_source(source: &str) -> TypedNode { - let mut parser = Parser::new(source).unwrap(); - let untyped = parser.parse_expression().unwrap(); - let globals = Rc::new(RefCell::new(HashMap::new())); - let bound = Binder::bind_root(globals, &untyped).unwrap(); - let global_types = Rc::new(RefCell::new(HashMap::new())); - let checker = TypeChecker::new(global_types); - checker.check(bound).unwrap() + let env = crate::ast::environment::Environment::new(); + env.compile(source).unwrap() } #[test] @@ -273,14 +263,14 @@ mod tests { fn test_inference_lambda_return() { // (fn [a] 10) -> fn(any) -> Int let typed = check_source("(fn [a] 10)"); - if let StaticType::Function { ret, .. } = typed.ty { - assert_eq!(*ret, StaticType::Int); + if let StaticType::Function(sig) = typed.ty { + assert_eq!(sig.ret, StaticType::Int); } else { panic!("Expected function type, got {:?}", typed.ty); } // Nested: (fn [] (do 1 2.5)) -> fn() -> Float let typed_nested = check_source("(fn [] (do 1 2.5))"); - if let StaticType::Function { ret, .. } = typed_nested.ty { - assert_eq!(*ret, StaticType::Float); + if let StaticType::Function(sig) = typed_nested.ty { + assert_eq!(sig.ret, StaticType::Float); } else { panic!("Expected function type"); } } @@ -293,4 +283,12 @@ mod tests { assert_eq!(last_expr.ty, StaticType::Float, "Variable 'x' should be specialized to Float after assignment"); } else { panic!("Expected block"); } } + + #[test] + fn test_operator_overloading_inference() { + assert_eq!(check_source("(+ 1 2)").ty, StaticType::Int); + assert_eq!(check_source("(+ 1.0 2.0)").ty, StaticType::Float); + assert_eq!(check_source("(+ \"a\" \"b\")").ty, StaticType::Text); + assert_eq!(check_source("(/ 1 2)").ty, StaticType::Float); + } } diff --git a/src/ast/environment.rs b/src/ast/environment.rs index f534cde..5f5ba68 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -88,50 +88,112 @@ impl Environment { } fn register_stdlib(&self) { - 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; } - else if let Value::Float(f) = arg { acc += f; } + use crate::ast::types::Signature; + + let plus_ty = StaticType::FunctionOverloads(vec![ + Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Int }, + Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, + Signature { params: vec![StaticType::Text, StaticType::Text], ret: StaticType::Text }, + ]); + self.register_native("+", plus_ty, |args| { + if args.len() == 2 { + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Value::Int(a + b), + (Value::Float(a), Value::Float(b)) => Value::Float(a + b), + (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 + b), + (Value::Float(a), Value::Int(b)) => Value::Float(a + *b as f64), + (Value::Text(a), Value::Text(b)) => { + let mut res = a.to_string(); + res.push_str(b); + Value::Text(Rc::from(res)) + } + _ => Value::Void, + } + } else { + let mut acc = 0.0; + for arg in args { + if let Value::Int(i) = arg { acc += i as f64; } + else if let Value::Float(f) = arg { acc += f; } + } + if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } } - if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } }); - self.register_native("-", StaticType::Any, |args| { + let minus_ty = StaticType::FunctionOverloads(vec![ + Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Int }, + Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, + Signature { params: vec![StaticType::Int], ret: StaticType::Int }, + Signature { params: vec![StaticType::Float], ret: StaticType::Float }, + ]); + self.register_native("-", minus_ty, |args| { if args.is_empty() { return Value::Void; } + if args.len() == 1 { + return match args[0] { + Value::Int(i) => Value::Int(-i), + Value::Float(f) => Value::Float(-f), + _ => Value::Void, + }; + } + if args.len() == 2 { + return match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Value::Int(a - b), + (Value::Float(a), Value::Float(b)) => Value::Float(a - b), + (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 - b), + (Value::Float(a), Value::Int(b)) => Value::Float(a - *b as f64), + _ => Value::Void, + }; + } let mut acc = match args[0] { Value::Int(i) => i as f64, Value::Float(f) => f, _ => return Value::Void, }; - if args.len() == 1 { - acc = -acc; - } else { - for arg in &args[1..] { - if let Value::Int(i) = arg { acc -= *i as f64; } - else if let Value::Float(f) = arg { acc -= f; } + for arg in &args[1..] { + if let Value::Int(i) = arg { acc -= *i as f64; } + else if let Value::Float(f) = arg { acc -= f; } + } + if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } + }); + + let mult_ty = StaticType::FunctionOverloads(vec![ + Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Int }, + Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, + ]); + self.register_native("*", mult_ty, |args| { + if args.len() == 2 { + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Value::Int(a * b), + (Value::Float(a), Value::Float(b)) => Value::Float(a * b), + (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 * b), + (Value::Float(a), Value::Int(b)) => Value::Float(a * *b as f64), + _ => Value::Void, } + } else { + let mut acc = 1.0; + for arg in args { + if let Value::Int(i) = arg { acc *= i as f64; } + else if let Value::Float(f) = arg { acc *= f; } + } + if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } } - if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } }); - self.register_native("*", StaticType::Any, |args| { - let mut acc = 1.0; - for arg in args { - if let Value::Int(i) = arg { acc *= i as f64; } - else if let Value::Float(f) = arg { acc *= f; } - } - if acc.fract() == 0.0 { Value::Int(acc as i64) } else { Value::Float(acc) } - }); - - self.register_native("/", StaticType::Any, |args| { + let div_ty = StaticType::FunctionOverloads(vec![ + Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Float }, + Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, + ]); + self.register_native("/", div_ty, |args| { if args.len() != 2 { return Value::Void; } let a = match args[0] { Value::Int(i) => i as f64, Value::Float(f) => f, _ => return Value::Void }; let b = match args[1] { Value::Int(i) => i as f64, Value::Float(f) => f, _ => return Value::Void }; Value::Float(a / b) }); - self.register_native(">", StaticType::Any, |args| { + let cmp_ty = StaticType::FunctionOverloads(vec![ + Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Bool }, + Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Bool }, + ]); + self.register_native(">", cmp_ty.clone(), |args| { if args.len() != 2 { return Value::Void; } match (&args[0], &args[1]) { (Value::Int(a), Value::Int(b)) => Value::Bool(a > b), @@ -142,7 +204,7 @@ impl Environment { } }); - self.register_native("<", StaticType::Any, |args| { + self.register_native("<", cmp_ty, |args| { if args.len() != 2 { return Value::Void; } match (&args[0], &args[1]) { (Value::Int(a), Value::Int(b)) => Value::Bool(a < b), diff --git a/src/ast/types.rs b/src/ast/types.rs index ae74f07..823760f 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -71,6 +71,12 @@ pub enum Value { TailCallRequest(Box<(Rc, Vec)>), // Internal: For TCO (Boxed to keep Value small) } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Signature { + pub params: Vec, + pub ret: StaticType, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum StaticType { Any, @@ -82,10 +88,8 @@ pub enum StaticType { Keyword, List(Box), Record(Rc>), - Function { - params: Vec, - ret: Box, - }, + Function(Box), + FunctionOverloads(Vec), Object(&'static str), } @@ -108,19 +112,60 @@ impl fmt::Display for StaticType { } write!(f, "}}") }, - StaticType::Function { params, ret } => { + StaticType::Function(sig) => { write!(f, "fn(")?; - for (i, p) in params.iter().enumerate() { + for (i, p) in sig.params.iter().enumerate() { if i > 0 { write!(f, " ")?; } write!(f, "{}", p)?; } - write!(f, ") -> {}", ret) + write!(f, ") -> {}", sig.ret) }, + StaticType::FunctionOverloads(sigs) => { + write!(f, "overloads({} variants)", sigs.len()) + } StaticType::Object(name) => write!(f, "{}", name), } } } +impl StaticType { + /// Returns true if `other` can be assigned to a location of type `self`. + pub fn is_assignable_from(&self, other: &StaticType) -> bool { + if self == other || matches!(self, StaticType::Any) || matches!(other, StaticType::Any) { + return true; + } + // Future: Numeric promotion (Int -> Float) + false + } + + /// Tries to resolve a call with the given argument types and returns the return type. + pub fn resolve_call(&self, arg_types: &[StaticType]) -> Option { + match self { + StaticType::Any => Some(StaticType::Any), + StaticType::Function(sig) => { + if self.match_params(&sig.params, arg_types) { + Some(sig.ret.clone()) + } else { + None + } + } + StaticType::FunctionOverloads(sigs) => { + sigs.iter() + .find(|sig| self.match_params(&sig.params, arg_types)) + .map(|sig| sig.ret.clone()) + } + _ => None, + } + } + + fn match_params(&self, expected: &[StaticType], actual: &[StaticType]) -> bool { + if expected.len() != actual.len() { + return false; + } + expected.iter().zip(actual).all(|(e, a)| e.is_assignable_from(a)) + } +} + impl Value { pub fn is_truthy(&self) -> bool { match self { @@ -141,7 +186,7 @@ impl Value { Value::Keyword(_) => StaticType::Keyword, Value::List(_) => StaticType::List(Box::new(StaticType::Any)), Value::Record(_) => StaticType::Record(Rc::new(BTreeMap::new())), // Empty for now - Value::Function { .. } => StaticType::Any, // Dynamic function + Value::Function(_) => StaticType::Any, // Dynamic function Value::Object(o) => StaticType::Object(o.type_name()), Value::Cell(c) => c.borrow().static_type(), Value::TailCallRequest(_) => StaticType::Any, // Internal state, but typable as Any