Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 132 additions & 15 deletions rust/src/env/method_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())))
}
}

Expand Down Expand Up @@ -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"));
}
}
53 changes: 53 additions & 0 deletions test/kernel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ============================================
Expand Down
43 changes: 43 additions & 0 deletions test/object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ============================================
Expand Down