diff --git a/rust/src/env/method_registry.rs b/rust/src/env/method_registry.rs index 12b6440..5b1cad6 100644 --- a/rust/src/env/method_registry.rs +++ b/rust/src/env/method_registry.rs @@ -2,8 +2,12 @@ use crate::graph::VertexId; use crate::types::Type; +use smallvec::SmallVec; use std::collections::HashMap; +const OBJECT_CLASS: &str = "Object"; +const KERNEL_MODULE: &str = "Kernel"; + /// Method information #[derive(Debug, Clone)] pub struct MethodInfo { @@ -70,26 +74,40 @@ impl MethodRegistry { ); } - /// Resolve a method for a receiver type + /// Build the method resolution order (MRO) fallback chain for a receiver type. /// - /// For generic types like `Array[Integer]`, first tries exact match, - /// then falls back to base class match (`Array`). - pub fn resolve(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> { - // First, try exact match - if let Some(info) = self - .methods - .get(&(recv_ty.clone(), method_name.to_string())) - { - return Some(info); - } + /// Returns a list of types to search in order: + /// 1. Exact receiver type + /// 2. Generic → base class (e.g., Array[Integer] → Array) + /// 3. Object (for Instance/Generic types only) + /// 4. Kernel (for Instance/Generic types only) + fn fallback_chain(recv_ty: &Type) -> SmallVec<[Type; 4]> { + let mut chain = SmallVec::new(); + chain.push(recv_ty.clone()); - // For generic types, fall back to base class if let Type::Generic { name, .. } = recv_ty { - let base_type = Type::Instance { name: name.clone() }; - return self.methods.get(&(base_type, method_name.to_string())); + chain.push(Type::Instance { name: name.clone() }); } - None + // NOTE: Kernel is a module, not a class. Represented as Type::Instance + // due to lack of Type::Module variant. + if matches!(recv_ty, Type::Instance { .. } | Type::Generic { .. }) { + chain.push(Type::instance(OBJECT_CLASS)); + chain.push(Type::instance(KERNEL_MODULE)); + } + + chain + } + + /// Resolve a method for a receiver type + /// + /// Searches the MRO fallback chain: exact type → base class (for generics) → Object → Kernel. + /// For non-instance types (Singleton, Nil, Union, Bot), only exact match is attempted. + pub fn resolve(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> { + let method_key = method_name.to_string(); + Self::fallback_chain(recv_ty) + .into_iter() + .find_map(|ty| self.methods.get(&(ty, method_key.clone()))) } } @@ -142,4 +160,103 @@ mod tests { assert_eq!(pvs[0], VertexId(20)); assert_eq!(pvs[1], VertexId(21)); } + + // --- Object/Kernel fallback --- + + #[test] + fn test_resolve_falls_back_to_object() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Object"), "nil?", Type::instance("TrueClass")); + let info = registry.resolve(&Type::instance("CustomClass"), "nil?").unwrap(); + assert_eq!(info.return_type.base_class_name(), Some("TrueClass")); + } + + #[test] + fn test_resolve_falls_back_to_kernel() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + let info = registry.resolve(&Type::instance("MyApp"), "puts").unwrap(); + assert_eq!(info.return_type, Type::Nil); + } + + #[test] + fn test_resolve_object_before_kernel() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Object"), "to_s", Type::string()); + registry.register(Type::instance("Kernel"), "to_s", Type::integer()); + let info = registry.resolve(&Type::instance("Anything"), "to_s").unwrap(); + assert_eq!(info.return_type.base_class_name(), Some("String")); + } + + #[test] + fn test_resolve_exact_match_over_fallback() { + let mut registry = MethodRegistry::new(); + registry.register(Type::string(), "length", Type::integer()); + registry.register(Type::instance("Object"), "length", Type::string()); + let info = registry.resolve(&Type::string(), "length").unwrap(); + assert_eq!(info.return_type.base_class_name(), Some("Integer")); + } + + // --- Types that skip fallback --- + + #[test] + fn test_singleton_type_skips_fallback() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + assert!(registry.resolve(&Type::singleton("User"), "puts").is_none()); + } + + #[test] + fn test_nil_type_skips_fallback() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + assert!(registry.resolve(&Type::Nil, "puts").is_none()); + } + + #[test] + fn test_union_type_skips_fallback() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + let union_ty = Type::Union(vec![Type::string(), Type::integer()]); + assert!(registry.resolve(&union_ty, "puts").is_none()); + } + + #[test] + fn test_bot_type_skips_fallback() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + assert!(registry.resolve(&Type::Bot, "puts").is_none()); + } + + // --- Generic type fallback chain --- + + #[test] + fn test_resolve_generic_falls_back_to_kernel() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "puts", Type::Nil); + let generic_type = Type::array_of(Type::integer()); + let info = registry.resolve(&generic_type, "puts").unwrap(); + assert_eq!(info.return_type, Type::Nil); + } + + #[test] + fn test_resolve_generic_full_chain() { + // Verify the 4-step fallback: Generic[T] → Base → Object → Kernel + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Kernel"), "object_id", Type::integer()); + let generic_type = Type::array_of(Type::string()); + // Array[String] → Array (none) → Object (none) → Kernel (exists) + let info = registry.resolve(&generic_type, "object_id").unwrap(); + assert_eq!(info.return_type.base_class_name(), Some("Integer")); + } + + // --- Namespaced class fallback --- + + #[test] + fn test_resolve_namespaced_class_falls_back_to_object() { + let mut registry = MethodRegistry::new(); + registry.register(Type::instance("Object"), "class", Type::string()); + let info = registry.resolve(&Type::instance("Api::V1::User"), "class").unwrap(); + assert_eq!(info.return_type.base_class_name(), Some("String")); + } } diff --git a/test/kernel_test.rb b/test/kernel_test.rb index d57f221..0e4e9c5 100644 --- a/test/kernel_test.rb +++ b/test/kernel_test.rb @@ -104,6 +104,59 @@ def process assert_no_check_errors(source) end + # ============================================ + # Fallback (user-defined class → Kernel) + # ============================================ + + def test_puts_on_user_defined_class + source = <<~RUBY + class MyApp + def run + puts "hello" + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_raise_on_user_defined_class + source = <<~RUBY + class Validator + def validate! + raise "invalid" + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_block_given_on_user_defined_class + source = <<~RUBY + class Runner + def execute + block_given? + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_kernel_method_on_user_defined_class_explicit_receiver + source = <<~RUBY + class Wrapper + def check(other) + other = Wrapper.new + other.frozen? + end + end + RUBY + + assert_no_check_errors(source) + end + # ============================================ # Override # ============================================ diff --git a/test/object_test.rb b/test/object_test.rb index a93c9f0..05e5db5 100644 --- a/test/object_test.rb +++ b/test/object_test.rb @@ -167,6 +167,49 @@ def process assert_no_check_errors(source) end + # ============================================ + # Fallback (user-defined class → Object) + # ============================================ + + def test_nil_check_on_user_defined_class + source = <<~RUBY + class Account + def valid? + other = Account.new + other.nil? + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_class_on_user_defined_class + source = <<~RUBY + class Entity + def type_name + other = Entity.new + other.class + end + end + RUBY + + assert_no_check_errors(source) + end + + def test_to_s_on_user_defined_class + source = <<~RUBY + class Label + def display + other = Label.new + other.to_s + end + end + RUBY + + assert_no_check_errors(source) + end + # ============================================ # Override # ============================================