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..caabc6f80 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -1067,17 +1067,11 @@ 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) { + Some(match self.linearize_superclass(declaration_id, context)? { Ancestors::Complete(ids) => ids, Ancestors::Cyclic(ids) => { context.cyclic = true; @@ -2020,11 +2014,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 { @@ -2042,14 +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 (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids); + // 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(), ) } _ => { @@ -2060,11 +2050,18 @@ impl<'a> Resolver<'a> { } } - fn get_parent_class(&self, definition_ids: &[DefinitionId]) -> (DeclarationId, Option) { - let mut explicit_parents = Vec::new(); - let mut unresolved_parent = None; + /// 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 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 @@ -2075,47 +2072,47 @@ 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 - ( - explicit_parents.first().copied().unwrap_or(*OBJECT_ID), - unresolved_parent, - ) + Some(( + explicit_superclasses.first().copied().unwrap_or(*OBJECT_ID), + unresolved_superclass, + )) } - fn linearize_parent_class( + fn linearize_superclass( &mut self, - definition_ids: &[DefinitionId], + declaration_id: DeclarationId, context: &mut LinearizationContext, - ) -> Ancestors { - let (picked_parent, unresolved_parent) = self.get_parent_class(definition_ids); - let mut result = self.linearize_ancestors(picked_parent, context); + ) -> Option { + 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, }; 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> {