From 98668cc683bde0c0cf17258c01145fa5581e5bad Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Thu, 19 Feb 2026 16:06:13 +0100 Subject: [PATCH] Refactor testing guidelines and add specializer module Update the testing section to clarify when warnings should be eliminated and tests run. Add the new `specializer` module to the compiler's public API. Add the `type_registry` module to the `rtl` module's public API. --- gemini.md | 3 +- src/ast/compiler/mod.rs | 2 + src/ast/compiler/specializer.rs | 352 ++++++++++++++++++++++++++++++++ src/ast/rtl/mod.rs | 1 + src/ast/rtl/type_registry.rs | 239 ++++++++++++++++++++++ 5 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 src/ast/compiler/specializer.rs create mode 100644 src/ast/rtl/type_registry.rs diff --git a/gemini.md b/gemini.md index 1ace2d7..b023d13 100644 --- a/gemini.md +++ b/gemini.md @@ -41,5 +41,4 @@ Wichtig: zentraler Einstiegspunkt ist Delphi/Myc.Ast.Environment.pasund die dort ## 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. -* Wenn alles funktioniert, muss auch clippy ohne Beanstandungen durchlaufen. +* Nach Fertigstellung einer Aufgabe, oder wenn ich zum Testen auffordere: Warnungen sind zu eliminieren. Tests müssen durchlaufen. Wenn alles funktioniert, muss auch clippy ohne Beanstandungen durchlaufen. diff --git a/src/ast/compiler/mod.rs b/src/ast/compiler/mod.rs index b02d2fb..b362d28 100644 --- a/src/ast/compiler/mod.rs +++ b/src/ast/compiler/mod.rs @@ -5,6 +5,7 @@ pub mod upvalues; pub mod dumper; pub mod macros; pub mod type_checker; +pub mod specializer; pub use binder::*; pub use bound_nodes::*; @@ -13,3 +14,4 @@ pub use upvalues::*; pub use dumper::*; pub use macros::*; pub use type_checker::*; +pub use specializer::*; diff --git a/src/ast/compiler/specializer.rs b/src/ast/compiler/specializer.rs new file mode 100644 index 0000000..c1e06da --- /dev/null +++ b/src/ast/compiler/specializer.rs @@ -0,0 +1,352 @@ + +use std::collections::HashMap; +use std::rc::Rc; +use std::cell::RefCell; +use crate::ast::types::{StaticType, Value, Signature}; +use crate::ast::compiler::bound_nodes::{BoundKind, Address, TypedNode, BoundNode}; +use crate::ast::nodes::Node; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MonoCacheKey { + pub address: Address, + pub arg_types: Vec, +} + +pub type CompileFunc = Rc, &[StaticType]) -> Result<(Value, StaticType), String>>; +pub type RtlLookupFunc = Rc Option<(Value, StaticType)>>; + +pub trait FunctionRegistry { + fn resolve(&self, addr: Address) -> Option>; +} + +pub struct Specializer { + cache: RefCell>, + registry: Option>, + compiler: Option, + _rtl_lookup: Option, +} + +impl Specializer { + pub fn new( + registry: Option>, + compiler: Option, + rtl_lookup: Option, + ) -> Self { + Self { + cache: RefCell::new(HashMap::new()), + registry, + compiler, + _rtl_lookup: rtl_lookup, + } + } + + pub fn specialize(&self, node: TypedNode) -> TypedNode { + self.visit_node(node) + } + + fn visit_node(&self, node: TypedNode) -> TypedNode { + let (new_kind, new_ty) = match node.kind { + BoundKind::Call { callee, args } => self.visit_call(*callee, args, node.ty.clone()), + + // Recursive traversal for other nodes + BoundKind::If { cond, then_br, else_br } => { + let cond = Box::new(self.visit_node(*cond)); + let then_br = Box::new(self.visit_node(*then_br)); + let else_br = else_br.map(|e| Box::new(self.visit_node(*e))); + (BoundKind::If { cond, then_br, else_br }, node.ty) + }, + BoundKind::Block { exprs } => { + let exprs = exprs.into_iter().map(|e| self.visit_node(e)).collect(); + (BoundKind::Block { exprs }, node.ty) + }, + BoundKind::Lambda { param_count, upvalues, body } => { + // We do NOT specialize inside lambdas automatically unless called? + // Actually, we should specialize the body as generic code. + // But without known types for parameters, we can't do much deep specialization. + // Delphi code: "Cannot specialize closures safely without more complex analysis". + // We'll just visit the body essentially. + // Wait, if we visit body, we might specialize calls inside it that don't depend on params. + let body = Rc::new(self.visit_node((*body).clone())); + (BoundKind::Lambda { param_count, upvalues, body }, node.ty) + }, + BoundKind::DefLocal { slot, value, captured_by } => { + let value = Box::new(self.visit_node(*value)); + (BoundKind::DefLocal { slot, value, captured_by }, node.ty) + }, + BoundKind::DefGlobal { global_index, value } => { + let value = Box::new(self.visit_node(*value)); + (BoundKind::DefGlobal { global_index, value }, node.ty) + }, + BoundKind::Set { addr, value } => { + let value = Box::new(self.visit_node(*value)); + (BoundKind::Set { addr, value }, node.ty) + }, + BoundKind::Tuple { elements } => { + let elements = elements.into_iter().map(|e| self.visit_node(e)).collect(); + (BoundKind::Tuple { elements }, node.ty) + }, + BoundKind::Map { entries } => { + let entries = entries.into_iter().map(|(k, v)| (self.visit_node(k), self.visit_node(v))).collect(); + (BoundKind::Map { entries }, node.ty) + }, + BoundKind::Expansion { original_call, bound_expanded } => { + let bound_expanded = Box::new(self.visit_node(*bound_expanded)); + (BoundKind::Expansion { original_call, bound_expanded }, node.ty) + }, + + // Leaf nodes or uninteresting nodes + k => (k, node.ty), + }; + + Node { + identity: node.identity, + kind: new_kind, + ty: new_ty, + } + } + + fn visit_call(&self, callee: TypedNode, args: Vec, original_ty: StaticType) -> (BoundKind, StaticType) { + // 1. Specialize children first + let new_callee = self.visit_node(callee); + let new_args: Vec = args.into_iter().map(|a| self.visit_node(a)).collect(); + + // 2. Check if this call is a candidate (Callee is Get(Address)) + let address = if let BoundKind::Get(addr) = &new_callee.kind { + *addr + } else { + // Not a direct call to a named function/variable + return (BoundKind::Call { callee: Box::new(new_callee), args: new_args }, original_ty); + }; + + // 3. Check if all argument types are statically known + let arg_types: Vec = new_args.iter().map(|a| a.ty.clone()).collect(); + if arg_types.iter().any(|t| matches!(t, StaticType::Any)) { + // Cannot specialize with unknown types + return (BoundKind::Call { callee: Box::new(new_callee), args: new_args }, original_ty); + } + + // --- Optimization Candidate --- + let key = MonoCacheKey { address, arg_types: arg_types.clone() }; + + // 4. Check Cache + if let Some((val, ret_ty)) = self.cache.borrow().get(&key) { + // Cache Hit! Replace Callee with Constant(Function) + let specialized_callee = Node { + identity: new_callee.identity.clone(), + kind: BoundKind::Constant(val.clone()), + ty: StaticType::Function(Box::new(Signature { + params: arg_types, + ret: ret_ty.clone(), + })), + }; + return (BoundKind::Call { callee: Box::new(specialized_callee), args: new_args }, ret_ty.clone()); + } + + // 5. Check RTL (Host Functions) - TODO: Need Name lookup from Address? + // Wait, Address::Global(idx) -> Name? We don't have the Name here easily unless we look up in global map. + // But RTL functions are usually bound to Globals. + // IF the Registry can give us the Name, we can look up RTL. + // For now, let's assume we can resolve the function definition. + + // 6. Resolve Function Definition + if let Some(func_node) = self.registry.as_ref().and_then(|r| r.resolve(address)) { + // Check constraints (no closures with state) + if let BoundKind::Lambda { upvalues, .. } = &func_node.kind { + if !upvalues.is_empty() { + // Cannot specialize stateful closures trivially + return (BoundKind::Call { callee: Box::new(new_callee), args: new_args }, original_ty); + } + } else { + // Not a lambda? + return (BoundKind::Call { callee: Box::new(new_callee), args: new_args }, original_ty); + } + + // 7. Compile Specialization (User Code) + if let Some(compiler) = &self.compiler { + match compiler(func_node, &arg_types) { + Ok((compiled_val, ret_ty)) => { + // Store in cache + self.cache.borrow_mut().insert(key, (compiled_val.clone(), ret_ty.clone())); + + let specialized_callee = Node { + identity: new_callee.identity.clone(), + kind: BoundKind::Constant(compiled_val), + ty: StaticType::Function(Box::new(Signature { + params: arg_types, + ret: ret_ty.clone(), + })), + }; + return (BoundKind::Call { callee: Box::new(specialized_callee), args: new_args }, ret_ty); + }, + Err(_) => { + // Fallback on error + } + } + } + } + + // Fallback: Dynamic Call + (BoundKind::Call { callee: Box::new(new_callee), args: new_args }, original_ty) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::types::{Identity, NodeIdentity, SourceLocation, StaticType, Value, Signature}; + use crate::ast::compiler::bound_nodes::{BoundKind, Address, BoundNode, TypedNode}; + use std::rc::Rc; + + fn make_identity() -> Identity { + Rc::new(NodeIdentity { location: SourceLocation { line: 0, col: 0 } }) + } + + fn make_typed_node(kind: BoundKind, ty: StaticType) -> TypedNode { + crate::ast::nodes::Node { + identity: make_identity(), + kind, + ty, + } + } + + // Mock Registry + struct MockRegistry { + functions: HashMap>, + } + + impl MockRegistry { + fn new() -> Self { + Self { functions: HashMap::new() } + } + + fn register(&mut self, addr: Address, node: BoundNode) { + self.functions.insert(addr, Rc::new(node)); + } + } + + impl FunctionRegistry for MockRegistry { + fn resolve(&self, addr: Address) -> Option> { + self.functions.get(&addr).cloned() + } + } + + #[test] + fn test_specialize_compiles_user_function() { + // Setup Registry with a function definition + let mut registry = MockRegistry::new(); + let addr = Address::Local(0); + + // Def: (fn [x] x) -- generic identity + let func_node = BoundNode { + identity: make_identity(), + kind: BoundKind::Lambda { + param_count: 1, + upvalues: vec![], + body: Rc::new(BoundNode { identity: make_identity(), kind: BoundKind::Nop, ty: () }) + }, + ty: () + }; + registry.register(addr, func_node); + + // Setup Compiler Mock + let compiler: CompileFunc = Rc::new(|_node: Rc, _args: &[StaticType]| -> Result<(Value, StaticType), String> { + // Return a specialized "compiled" value + Ok((Value::Int(12345), StaticType::Int)) + }); + + let spec = Specializer::new(Some(Rc::new(registry)), Some(compiler), None); + + // Call(Get(Local(0)), [Arg(Int)]) + let callee = make_typed_node(BoundKind::Get(addr), StaticType::Any); + let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int); + + let call_node = make_typed_node( + BoundKind::Call { callee: Box::new(callee), args: vec![arg] }, + StaticType::Any + ); + + let result = spec.specialize(call_node); + + // Should be Call(Constant(12345), ...) + if let BoundKind::Call { callee, .. } = result.kind { + if let BoundKind::Constant(val) = callee.kind { + match val { + Value::Int(12345) => (), + _ => panic!("Expected compiled value 12345"), + } + } else { + panic!("Expected Constant callee"); + } + } else { + panic!("Expected Call node"); + } + } + + #[test] + fn test_specialize_skips_unknown_types() { + let spec = Specializer::new(None, None, None); + + // Call(Get(Local(0)), [Get(Local(1))]) where arg is Any + let callee = make_typed_node(BoundKind::Get(Address::Local(0)), StaticType::Function(Box::new(Signature { params: vec![StaticType::Any], ret: StaticType::Void }))); + let arg = make_typed_node(BoundKind::Get(Address::Local(1)), StaticType::Any); + + let call_node = make_typed_node( + BoundKind::Call { callee: Box::new(callee), args: vec![arg] }, + StaticType::Void + ); + + let result = spec.specialize(call_node); + + // Should remain a generic Call because arg type is Any + if let BoundKind::Call { callee, .. } = result.kind { + if let BoundKind::Get(_) = callee.kind { + // Correct: Still a Get, not a Constant(Function) + } else { + panic!("Expected generic Call to Get, got {:?}", callee.kind); + } + } else { + panic!("Expected Call node"); + } + } + + #[test] + fn test_specialize_uses_cache() { + // Setup cache with a pre-specialized function for (Int) -> Int + let spec = Specializer::new(None, None, None); + + let addr = Address::Local(0); + let arg_types = vec![StaticType::Int]; + let key = MonoCacheKey { address: addr, arg_types: arg_types.clone() }; + + // Mock a specialized function pointer + let specialized_val = Value::Int(999); // Dummy value representing function + let ret_ty = StaticType::Int; + + spec.cache.borrow_mut().insert(key, (specialized_val.clone(), ret_ty.clone())); + + // Create the call node: Call(Get(0), [Arg(Int)]) + let callee = make_typed_node(BoundKind::Get(addr), StaticType::Any); + let arg = make_typed_node(BoundKind::Constant(Value::Int(1)), StaticType::Int); + + let call_node = make_typed_node( + BoundKind::Call { callee: Box::new(callee), args: vec![arg] }, + StaticType::Any + ); + + let result = spec.specialize(call_node); + + // Should now be Call(Constant(999), ...) + if let BoundKind::Call { callee, .. } = result.kind { + if let BoundKind::Constant(val) = callee.kind { + match val { + Value::Int(999) => (), // Success + _ => panic!("Expected specialized value 999"), + } + } else { + panic!("Expected Constant callee, got {:?}", callee.kind); + } + } else { + panic!("Expected Call node"); + } + } +} diff --git a/src/ast/rtl/mod.rs b/src/ast/rtl/mod.rs index 118d240..3617682 100644 --- a/src/ast/rtl/mod.rs +++ b/src/ast/rtl/mod.rs @@ -1,5 +1,6 @@ pub mod core; pub mod datetime; +pub mod type_registry; use crate::ast::environment::Environment; diff --git a/src/ast/rtl/type_registry.rs b/src/ast/rtl/type_registry.rs new file mode 100644 index 0000000..41210b0 --- /dev/null +++ b/src/ast/rtl/type_registry.rs @@ -0,0 +1,239 @@ +use std::collections::{HashMap, BTreeMap}; +use std::rc::Rc; +use std::any::{TypeId}; +use crate::ast::types::{Value, StaticType, Keyword, Signature}; + +/// Represents a Rust type that can be exposed to the script environment. +pub trait Scriptable: 'static + Sized { + /// Returns the name of the type for debugging/AST. + fn type_name() -> &'static str; + + /// Returns the static type definition (methods, properties) for the AST. + fn static_type() -> StaticType; + + /// Wraps the instance into a Value (usually a Record of closures). + fn wrap(self) -> Value; +} + +/// A registry for tracking registered types and their static definitions. +pub struct TypeRegistry { + known_types: HashMap, +} + +impl Default for TypeRegistry { + fn default() -> Self { + Self::new() + } +} + +impl TypeRegistry { + pub fn new() -> Self { + Self { + known_types: HashMap::new(), + } + } + + /// Registers a type T. Equivalent to `RegisterType` in Delphi. + pub fn register(&mut self) { + let id = TypeId::of::(); + self.known_types.entry(id).or_insert_with(T::static_type); + } + + /// Resolves the static type for a Rust type. + pub fn resolve_type(&self) -> StaticType { + self.known_types.get(&TypeId::of::()).cloned().unwrap_or(StaticType::Any) + } + + /// Creates a factory function value that can be bound in the environment. + /// + /// # Arguments + /// * `factory_func` - A Rust closure that takes script arguments and returns a Result. + pub fn create_factory( + factory_func: F + ) -> Value + where + T: Scriptable, + F: Fn(Vec) -> Result + 'static + { + // The factory is a script function that calls the Rust factory, gets T, then wraps it. + let closure = move |args: Vec| -> Value { + match factory_func(args) { + Ok(instance) => instance.wrap(), + Err(msg) => { + // In a real system, we'd propagate this error. For now, panic or return Void. + // Delphi returns Void if nil, but raises exception on error. + // Since Value doesn't have Error, we panic to stop execution. + panic!("Runtime Error in Factory for {}: {}", T::type_name(), msg); + } + } + }; + Value::Function(Rc::new(closure)) + } +} + +/// Helper to build the shadow record for an instance. +/// This is used inside `Scriptable::wrap`. +pub struct RecordBuilder { + fields: HashMap, +} + +impl Default for RecordBuilder { + fn default() -> Self { + Self::new() + } +} + +impl RecordBuilder { + pub fn new() -> Self { + Self { + fields: HashMap::new(), + } + } + + pub fn method(mut self, name: &str, func: F) -> Self + where F: Fn(Vec) -> Value + 'static + { + let key = Keyword::intern(name); + self.fields.insert(key, Value::Function(Rc::new(func))); + self + } + + // Helper for methods that return Result (propagating panics for now) + pub fn method_checked(self, name: &str, func: F) -> Self + where F: Fn(Vec) -> Result + 'static + { + let closure = move |args: Vec| { + match func(args) { + Ok(v) => v, + Err(e) => panic!("Method call error: {}", e), + } + }; + self.method(name, closure) + } + + pub fn build(self) -> Value { + Value::Record(Rc::new(self.fields)) + } +} + +/// Helper to build the StaticType definition. +pub struct TypeBuilder { + fields: BTreeMap, +} + +impl Default for TypeBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TypeBuilder { + pub fn new() -> Self { + Self { + fields: BTreeMap::new(), + } + } + + pub fn method(mut self, name: &str, params: Vec, ret: StaticType) -> Self { + let key = Keyword::intern(name); + let sig = StaticType::Function(Box::new(Signature { params, ret })); + self.fields.insert(key, sig); + self + } + + pub fn build(self) -> StaticType { + StaticType::Record(Rc::new(self.fields)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::types::{Value, StaticType}; + + #[derive(Debug, Clone)] + struct Person { + name: String, + age: u32, + } + + impl Scriptable for Person { + fn type_name() -> &'static str { "Person" } + + fn static_type() -> StaticType { + TypeBuilder::new() + .method("greet", vec![], StaticType::Text) + .method("older", vec![], StaticType::Int) + .build() + } + + fn wrap(self) -> Value { + let name = self.name.clone(); + let age = self.age; + + RecordBuilder::new() + .method("greet", move |_| Value::Text(format!("Hello {}", name).into())) + .method("older", move |_| Value::Int((age + 1) as i64)) + .build() + } + } + + #[test] + fn test_register_and_wrap() { + let mut registry = TypeRegistry::new(); + registry.register::(); + + let p = Person { name: "Alice".to_string(), age: 30 }; + let wrapped = p.wrap(); + + // Check static type + let st = TypeRegistry::resolve_type::(®istry); + if let StaticType::Record(fields) = st { + assert!(fields.contains_key(&Keyword::intern("greet"))); + } else { + panic!("Expected Record type"); + } + + // Check runtime behavior + if let Value::Record(fields) = wrapped { + let greet_fn = fields.get(&Keyword::intern("greet")).unwrap(); + if let Value::Function(f) = greet_fn { + let res = f(vec![]); + if let Value::Text(s) = res { + assert_eq!(&*s, "Hello Alice"); + } else { + panic!("Expected Text result"); + } + } else { + panic!("Expected Function value for method"); + } + } else { + panic!("Expected Record value"); + } + } + + #[test] + fn test_factory() { + let factory_val = TypeRegistry::create_factory(|args: Vec| { + if args.len() != 2 { + return Err("Expected 2 args".to_string()); + } + let name = match &args[0] { Value::Text(t) => t.to_string(), _ => return Err("Name must be text".to_string()) }; + let age = match &args[1] { Value::Int(i) => *i as u32, _ => return Err("Age must be int".to_string()) }; + Ok(Person { name, age }) + }); + + if let Value::Function(f) = factory_val { + let instance = f(vec![Value::Text("Bob".into()), Value::Int(40)]); + if let Value::Record(fields) = instance { + let greet_fn = fields.get(&Keyword::intern("greet")).unwrap(); + if let Value::Function(gf) = greet_fn { + let res = gf(vec![]); + if let Value::Text(s) = res { + assert_eq!(&*s, "Hello Bob"); + } else { panic!("Wrong return type"); } + } + } else { panic!("Factory should return Record"); } + } else { panic!("Factory is not a function"); } + } +}