From d7f93bfe59e412c49048472a89928c2afc64326a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 15 Jul 2026 19:06:41 +0100 Subject: [PATCH 1/4] Model BasicObject as a parentless class Assisted-By: devx/7a36e49d-f7b5-4912-8df7-4798f1f95608 --- docs/ruby-behaviors.md | 19 ++++++++ rust/rubydex/src/resolution.rs | 66 ++++++++++++++++------------ rust/rubydex/src/resolution_tests.rs | 51 +++++++++++++++++++++ 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/docs/ruby-behaviors.md b/docs/ruby-behaviors.md index ea55ccdd5..cc2451e0d 100644 --- a/docs/ruby-behaviors.md +++ b/docs/ruby-behaviors.md @@ -1267,6 +1267,25 @@ Ruby builds ancestors through the following steps recursively: And then it continues up the inheritance chain. +A class without an explicit superclass inherits from `Object`. Exceptions are: + +- `Object` itself inherits from `BasicObject` +- `BasicObject` does NOT have any superclass. + +```ruby +class Foo; end + +Foo.superclass # => Object +Object.superclass # => BasicObject +BasicObject.superclass # => nil + +Foo.ancestors # => [Foo, Object, Kernel, BasicObject] +Object.ancestors # => [Object, Kernel, BasicObject] +BasicObject.ancestors # => [BasicObject] +``` + +Singleton classes use a separate root rule: `BasicObject.singleton_class.superclass` is `Class`. + ```rb module PrependedInPrepended end diff --git a/rust/rubydex/src/resolution.rs b/rust/rubydex/src/resolution.rs index a414340f1..6c7b16e73 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -1067,27 +1067,25 @@ impl<'a> Resolver<'a> { declaration_id: DeclarationId, context: &mut LinearizationContext, ) -> Option> { - if declaration_id == *BASIC_OBJECT_ID { - return None; - } - let declaration = self.graph.declarations().get(&declaration_id).unwrap(); match declaration { Declaration::Namespace(Namespace::Class(_)) => { let definition_ids = declaration.definitions().to_vec(); - Some(match self.linearize_parent_class(&definition_ids, context) { - Ancestors::Complete(ids) => ids, - Ancestors::Cyclic(ids) => { - context.cyclic = true; - ids - } - Ancestors::Partial(ids) => { - context.partial = true; - ids - } - }) + Some( + match self.linearize_parent_class(declaration_id, &definition_ids, context)? { + Ancestors::Complete(ids) => ids, + Ancestors::Cyclic(ids) => { + context.cyclic = true; + ids + } + Ancestors::Partial(ids) => { + context.partial = true; + ids + } + }, + ) } Declaration::Namespace(Namespace::SingletonClass(_)) => { let owner_id = *declaration.owner_id(); @@ -2020,11 +2018,6 @@ impl<'a> Resolver<'a> { /// - Class: parent is the singleton class of the original parent class /// - Singleton class: recurse as many times as necessary to wrap the original attached object's parent class fn singleton_parent_id(&mut self, attached_id: DeclarationId) -> (DeclarationId, bool) { - // Base case: if we reached `BasicObject`, then the parent is `Class` - if attached_id == *BASIC_OBJECT_ID { - return (*CLASS_ID, false); - } - let decl = self.graph.declarations().get(&attached_id).unwrap(); match decl { @@ -2045,7 +2038,11 @@ impl<'a> Resolver<'a> { // For classes (the regular case), we need to return the singleton class of its parent let definition_ids = decl.definitions().to_vec(); - let (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids); + let Some((picked_parent, unresolved_parent)) = self.get_parent_class(attached_id, &definition_ids) + else { + // BasicObject has no ordinary parent, but its singleton class inherits from Class + return (*CLASS_ID, false); + }; ( self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred) .expect("parent class should always be a namespace"), @@ -2060,7 +2057,17 @@ impl<'a> Resolver<'a> { } } - fn get_parent_class(&self, definition_ids: &[DefinitionId]) -> (DeclarationId, Option) { + /// Returns the selected parent class and any unresolved explicit parent. The top-level `BasicObject` declaration is + /// Ruby's root class and is the only class without a parent. + fn get_parent_class( + &self, + declaration_id: DeclarationId, + definition_ids: &[DefinitionId], + ) -> Option<(DeclarationId, Option)> { + if declaration_id == *BASIC_OBJECT_ID { + return None; + } + let mut explicit_parents = Vec::new(); let mut unresolved_parent = None; @@ -2088,18 +2095,19 @@ impl<'a> Resolver<'a> { // If there's more than one parent class that isn't `Object` and they are different, then there's a superclass // mismatch error. TODO: We should add a diagnostic here - ( + Some(( explicit_parents.first().copied().unwrap_or(*OBJECT_ID), unresolved_parent, - ) + )) } fn linearize_parent_class( &mut self, + declaration_id: DeclarationId, definition_ids: &[DefinitionId], context: &mut LinearizationContext, - ) -> Ancestors { - let (picked_parent, unresolved_parent) = self.get_parent_class(definition_ids); + ) -> Option { + let (picked_parent, unresolved_parent) = self.get_parent_class(declaration_id, definition_ids)?; let mut result = self.linearize_ancestors(picked_parent, context); if let Some(name_id) = unresolved_parent { @@ -2112,10 +2120,10 @@ impl<'a> Resolver<'a> { }; ancestors.insert(0, Ancestor::Partial(name_id)); - result.to_partial() - } else { - result + result = result.to_partial(); } + + Some(result) } fn mixins_of(&self, definition_id: DefinitionId) -> Option> { diff --git a/rust/rubydex/src/resolution_tests.rs b/rust/rubydex/src/resolution_tests.rs index 3eb618172..8f503486a 100644 --- a/rust/rubydex/src/resolution_tests.rs +++ b/rust/rubydex/src/resolution_tests.rs @@ -1944,6 +1944,32 @@ mod mixin_dedup_tests { mod object_ancestors_tests { use super::*; + #[test] + fn core_class_parent_hierarchy() { + let mut context = graph_test(); + context.index_uri( + "file:///foo.rb", + " + class Foo; end + + module M + class BasicObject; end + end + ", + ); + context.resolve(); + + assert_no_diagnostics!(&context); + assert_ancestors_eq!(context, "Foo", ["Foo", "Object", "Kernel", "BasicObject"]); + assert_ancestors_eq!(context, "Object", ["Object", "Kernel", "BasicObject"]); + assert_ancestors_eq!(context, "BasicObject", ["BasicObject"]); + assert_ancestors_eq!( + context, + "M::BasicObject", + ["M::BasicObject", "Object", "Kernel", "BasicObject"] + ); + } + #[test] fn ancestors_with_missing_core() { let mut context = graph_test(); @@ -2164,6 +2190,31 @@ mod singleton_ancestors_tests { assert_no_diagnostics!(&context); + assert_ancestors_eq!( + context, + "Object::", + [ + "Object::", + "BasicObject::", + "Class", + "Module", + "Object", + "Kernel", + "BasicObject" + ] + ); + assert_ancestors_eq!( + context, + "BasicObject::", + [ + "BasicObject::", + "Class", + "Module", + "Object", + "Kernel", + "BasicObject" + ] + ); assert_ancestors_eq!( context, "Baz::", From a9c3b4ef34ca435645099426801f08e04ec832a2 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 15 Jul 2026 19:49:56 +0100 Subject: [PATCH 2/4] Make superclass lookup declaration-centric Assisted-By: devx/7a36e49d-f7b5-4912-8df7-4798f1f95608 --- rust/rubydex/src/resolution.rs | 81 +++++++++++++++------------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/rust/rubydex/src/resolution.rs b/rust/rubydex/src/resolution.rs index 6c7b16e73..caabc6f80 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -1071,21 +1071,17 @@ impl<'a> Resolver<'a> { match declaration { Declaration::Namespace(Namespace::Class(_)) => { - let definition_ids = declaration.definitions().to_vec(); - - Some( - match self.linearize_parent_class(declaration_id, &definition_ids, context)? { - Ancestors::Complete(ids) => ids, - Ancestors::Cyclic(ids) => { - context.cyclic = true; - ids - } - Ancestors::Partial(ids) => { - context.partial = true; - ids - } - }, - ) + Some(match self.linearize_superclass(declaration_id, context)? { + Ancestors::Complete(ids) => ids, + Ancestors::Cyclic(ids) => { + context.cyclic = true; + ids + } + Ancestors::Partial(ids) => { + context.partial = true; + ids + } + }) } Declaration::Namespace(Namespace::SingletonClass(_)) => { let owner_id = *declaration.owner_id(); @@ -2035,18 +2031,15 @@ impl<'a> Resolver<'a> { ) } Declaration::Namespace(Namespace::Class(_)) => { - // For classes (the regular case), we need to return the singleton class of its parent - let definition_ids = decl.definitions().to_vec(); - - let Some((picked_parent, unresolved_parent)) = self.get_parent_class(attached_id, &definition_ids) - else { - // BasicObject has no ordinary parent, but its singleton class inherits from Class + // For classes (the regular case), we need to return the singleton class of its superclass + let Some((superclass_id, unresolved_superclass)) = self.get_superclass(attached_id) else { + // BasicObject has no superclass, but its singleton class inherits from Class return (*CLASS_ID, false); }; ( - self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred) - .expect("parent class should always be a namespace"), - unresolved_parent.is_some(), + self.get_or_create_singleton_class(superclass_id, SingletonAncestors::Deferred) + .expect("superclass should always be a namespace"), + unresolved_superclass.is_some(), ) } _ => { @@ -2057,21 +2050,18 @@ impl<'a> Resolver<'a> { } } - /// Returns the selected parent class and any unresolved explicit parent. The top-level `BasicObject` declaration is - /// Ruby's root class and is the only class without a parent. - fn get_parent_class( - &self, - declaration_id: DeclarationId, - definition_ids: &[DefinitionId], - ) -> Option<(DeclarationId, Option)> { + /// Returns the selected superclass and any unresolved explicit superclass. The top-level `BasicObject` declaration + /// is Ruby's root class and is the only class without a superclass. + fn get_superclass(&self, declaration_id: DeclarationId) -> Option<(DeclarationId, Option)> { if declaration_id == *BASIC_OBJECT_ID { return None; } - let mut explicit_parents = Vec::new(); - let mut unresolved_parent = None; + let declaration = self.graph.declarations().get(&declaration_id).unwrap(); + let mut explicit_superclasses = Vec::new(); + let mut unresolved_superclass = None; - for definition_id in definition_ids { + for definition_id in declaration.definitions() { let definition = self.graph.definitions().get(definition_id).unwrap(); if let Definition::Class(class) = definition @@ -2082,38 +2072,37 @@ impl<'a> Resolver<'a> { match name { NameRef::Resolved(resolved) => { - if let Some(parent_id) = self.resolve_to_namespace(*resolved.declaration_id()) { - explicit_parents.push(parent_id); + if let Some(superclass_id) = self.resolve_to_namespace(*resolved.declaration_id()) { + explicit_superclasses.push(superclass_id); } } NameRef::Unresolved(_) => { - unresolved_parent = Some(*constant_reference.name_id()); + unresolved_superclass = Some(*constant_reference.name_id()); } } } } - // If there's more than one parent class that isn't `Object` and they are different, then there's a superclass + // If there's more than one superclass that isn't `Object` and they are different, then there's a superclass // mismatch error. TODO: We should add a diagnostic here Some(( - explicit_parents.first().copied().unwrap_or(*OBJECT_ID), - unresolved_parent, + explicit_superclasses.first().copied().unwrap_or(*OBJECT_ID), + unresolved_superclass, )) } - fn linearize_parent_class( + fn linearize_superclass( &mut self, declaration_id: DeclarationId, - definition_ids: &[DefinitionId], context: &mut LinearizationContext, ) -> Option { - let (picked_parent, unresolved_parent) = self.get_parent_class(declaration_id, definition_ids)?; - let mut result = self.linearize_ancestors(picked_parent, context); + let (superclass_id, unresolved_superclass) = self.get_superclass(declaration_id)?; + let mut result = self.linearize_ancestors(superclass_id, context); - if let Some(name_id) = unresolved_parent { + if let Some(name_id) = unresolved_superclass { context.partial = true; - // Insert the unresolved parent as a Partial ancestor at the front of the chain, so it + // Insert the unresolved superclass as a Partial ancestor at the front of the chain, so it // appears before the default Object ancestors let ancestors = match &mut result { Ancestors::Complete(ids) | Ancestors::Cyclic(ids) | Ancestors::Partial(ids) => ids, From 96166a4079e99b90df73f9c21faf51d9b1931e35 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 15 Jul 2026 20:26:59 +0100 Subject: [PATCH 3/4] Rename superclass hierarchy test Assisted-By: devx/7a36e49d-f7b5-4912-8df7-4798f1f95608 --- rust/rubydex/src/resolution_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/rubydex/src/resolution_tests.rs b/rust/rubydex/src/resolution_tests.rs index 8f503486a..d01f52287 100644 --- a/rust/rubydex/src/resolution_tests.rs +++ b/rust/rubydex/src/resolution_tests.rs @@ -1945,7 +1945,7 @@ mod object_ancestors_tests { use super::*; #[test] - fn core_class_parent_hierarchy() { + fn core_superclass_hierarchy() { let mut context = graph_test(); context.index_uri( "file:///foo.rb", From 233225e3828e59cde8a925f9aa04beb62c4428ff Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 16 Jul 2026 18:04:19 +0100 Subject: [PATCH 4/4] Remove duplicate superclass tests --- rust/rubydex/src/resolution_tests.rs | 51 ---------------------------- 1 file changed, 51 deletions(-) diff --git a/rust/rubydex/src/resolution_tests.rs b/rust/rubydex/src/resolution_tests.rs index d01f52287..3eb618172 100644 --- a/rust/rubydex/src/resolution_tests.rs +++ b/rust/rubydex/src/resolution_tests.rs @@ -1944,32 +1944,6 @@ mod mixin_dedup_tests { mod object_ancestors_tests { use super::*; - #[test] - fn core_superclass_hierarchy() { - let mut context = graph_test(); - context.index_uri( - "file:///foo.rb", - " - class Foo; end - - module M - class BasicObject; end - end - ", - ); - context.resolve(); - - assert_no_diagnostics!(&context); - assert_ancestors_eq!(context, "Foo", ["Foo", "Object", "Kernel", "BasicObject"]); - assert_ancestors_eq!(context, "Object", ["Object", "Kernel", "BasicObject"]); - assert_ancestors_eq!(context, "BasicObject", ["BasicObject"]); - assert_ancestors_eq!( - context, - "M::BasicObject", - ["M::BasicObject", "Object", "Kernel", "BasicObject"] - ); - } - #[test] fn ancestors_with_missing_core() { let mut context = graph_test(); @@ -2190,31 +2164,6 @@ mod singleton_ancestors_tests { assert_no_diagnostics!(&context); - assert_ancestors_eq!( - context, - "Object::", - [ - "Object::", - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject" - ] - ); - assert_ancestors_eq!( - context, - "BasicObject::", - [ - "BasicObject::", - "Class", - "Module", - "Object", - "Kernel", - "BasicObject" - ] - ); assert_ancestors_eq!( context, "Baz::",