rubydex 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +34 -1
  4. data/exe/rdx +131 -55
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/graph.c +14 -3
  8. data/ext/rubydex/query.c +105 -0
  9. data/ext/rubydex/query.h +8 -0
  10. data/ext/rubydex/reference.c +60 -0
  11. data/ext/rubydex/rubydex.c +2 -0
  12. data/ext/rubydex/utils.c +12 -0
  13. data/ext/rubydex/utils.h +5 -0
  14. data/lib/rubydex/version.rb +1 -1
  15. data/rbi/rubydex.rbi +22 -0
  16. data/rust/Cargo.lock +7 -0
  17. data/rust/rubydex/Cargo.toml +1 -0
  18. data/rust/rubydex/benches/graph_memory.rs +22 -4
  19. data/rust/rubydex/src/compile_assertions.rs +15 -0
  20. data/rust/rubydex/src/diagnostic.rs +1 -1
  21. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  22. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  23. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  24. data/rust/rubydex/src/main.rs +2 -124
  25. data/rust/rubydex/src/model/declaration.rs +0 -11
  26. data/rust/rubydex/src/model/definitions.rs +27 -26
  27. data/rust/rubydex/src/model/document.rs +43 -7
  28. data/rust/rubydex/src/model/graph.rs +40 -28
  29. data/rust/rubydex/src/model/id.rs +55 -0
  30. data/rust/rubydex/src/model/ids.rs +21 -9
  31. data/rust/rubydex/src/model/name.rs +35 -7
  32. data/rust/rubydex/src/model/references.rs +16 -13
  33. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  34. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  35. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  36. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  37. data/rust/rubydex/src/query/cypher.rs +57 -0
  38. data/rust/rubydex/src/query.rs +2 -0
  39. data/rust/rubydex/src/resolution.rs +248 -227
  40. data/rust/rubydex/src/resolution_tests.rs +263 -65
  41. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  42. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  43. data/rust/rubydex-sys/src/graph_api.rs +158 -14
  44. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  45. metadata +8 -2
@@ -14,8 +14,9 @@ use crate::model::{
14
14
  definitions::{Definition, Mixin, Receiver},
15
15
  graph::{Graph, Unit},
16
16
  identity_maps::{IdentityHashBuilder, IdentityHashMap, IdentityHashSet},
17
- ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId},
17
+ ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId, UriId},
18
18
  name::{Name, NameRef, ParentScope},
19
+ visibility::Visibility,
19
20
  };
20
21
 
21
22
  enum Outcome {
@@ -635,7 +636,7 @@ impl<'a> Resolver<'a> {
635
636
  }
636
637
 
637
638
  /// Resolves retroactive method visibility changes (`private :foo`, `protected :foo`, `public :foo`,
638
- /// `private_class_method :foo`, `public_class_method :foo`).
639
+ /// `private_class_method :foo`, `public_class_method :foo`, `module_function :foo`).
639
640
  ///
640
641
  /// Runs as a second pass after all methods/attrs are declared, so `private :bar` works
641
642
  /// regardless of whether `def bar` appeared before or after it in source.
@@ -652,12 +653,14 @@ impl<'a> Resolver<'a> {
652
653
  let offset = method_visibility.offset().clone();
653
654
  let lexical_nesting_id = *method_visibility.lexical_nesting_id();
654
655
  let is_singleton = method_visibility.flags().is_singleton_method_visibility();
656
+ let is_module_function = *method_visibility.visibility() == Visibility::ModuleFunction;
657
+ let is_module_function_singleton = is_singleton && is_module_function;
655
658
 
656
659
  let Some(lexical_owner_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else {
657
660
  continue;
658
661
  };
659
662
 
660
- let owner_id = if is_singleton {
663
+ let namespace_id = if is_singleton && !is_module_function {
661
664
  let Some(singleton_id) =
662
665
  self.get_or_create_singleton_class(lexical_owner_id, SingletonAncestors::Eager)
663
666
  else {
@@ -668,12 +671,12 @@ impl<'a> Resolver<'a> {
668
671
  lexical_owner_id
669
672
  };
670
673
 
671
- let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&owner_id) else {
674
+ let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&namespace_id) else {
672
675
  continue;
673
676
  };
674
677
 
675
- let mut visibility_applied = false;
676
- let mut has_partial = false;
678
+ let mut visibility_definition_attached = false;
679
+ let mut ancestor_chain_is_incomplete = false;
677
680
 
678
681
  for ancestor in namespace.ancestors() {
679
682
  match ancestor {
@@ -687,40 +690,51 @@ impl<'a> Resolver<'a> {
687
690
  .is_some();
688
691
 
689
692
  if has_member {
690
- // Direct member: `create_declaration`'s fully qualified name dedup attaches
691
- // this visibility definition to the existing method declaration.
692
- // Inherited: a new child-owned declaration is created.
693
- self.create_declaration(str_id, id, owner_id, |name| {
694
- Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
695
- });
696
- visibility_applied = true;
693
+ if is_module_function_singleton {
694
+ let Some(singleton_id) =
695
+ self.get_or_create_singleton_class(lexical_owner_id, SingletonAncestors::Eager)
696
+ else {
697
+ break;
698
+ };
699
+
700
+ self.create_declaration(str_id, id, singleton_id, |name| {
701
+ Declaration::Method(Box::new(MethodDeclaration::new(name, singleton_id)))
702
+ });
703
+ } else {
704
+ self.create_declaration(str_id, id, namespace_id, |name| {
705
+ Declaration::Method(Box::new(MethodDeclaration::new(name, namespace_id)))
706
+ });
707
+ }
708
+ visibility_definition_attached = true;
697
709
  break;
698
710
  }
699
711
  }
700
- Ancestor::Partial(_) => has_partial = true,
712
+ Ancestor::Partial(_) => ancestor_chain_is_incomplete = true,
701
713
  }
702
714
  }
703
715
 
704
- if visibility_applied {
716
+ if visibility_definition_attached {
705
717
  continue;
706
718
  }
707
719
 
708
- if has_partial {
709
- // Method might exist on an unresolved ancestor — requeue for retry.
720
+ if ancestor_chain_is_incomplete {
710
721
  pending_work.push(Unit::Definition(id));
711
- } else {
712
- // Ancestors are fully resolved — method definitively doesn't exist.
713
- let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string();
714
- let owner_name = self.graph.declarations().get(&owner_id).unwrap().name().to_string();
715
- let diagnostic = Diagnostic::new(
716
- Rule::UndefinedMethodVisibilityTarget,
717
- uri_id,
718
- offset,
719
- format!("undefined method `{owner_name}#{method_name}` for visibility change"),
720
- );
721
-
722
- self.graph.add_document_diagnostic(uri_id, diagnostic);
722
+ continue;
723
+ }
724
+
725
+ if is_module_function_singleton {
726
+ continue;
723
727
  }
728
+
729
+ let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string();
730
+ let owner_name = self.graph.declarations().get(&namespace_id).unwrap().name().to_string();
731
+ let diagnostic = Diagnostic::new(
732
+ Rule::UndefinedMethodVisibilityTarget,
733
+ uri_id,
734
+ offset,
735
+ format!("undefined method `{owner_name}#{method_name}` for visibility change"),
736
+ );
737
+ self.graph.add_document_diagnostic(uri_id, diagnostic);
724
738
  }
725
739
 
726
740
  // Must extend work here so incremental resolution can resolve previously unresolved visibility operations
@@ -850,7 +864,7 @@ impl<'a> Resolver<'a> {
850
864
  /// Gets or creates a singleton class declaration for a given class/module declaration. For class `Foo`, this
851
865
  /// returns the declaration for `Foo::<Foo>`.
852
866
  ///
853
- /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to a `Class`
867
+ /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to an unknown
854
868
  /// namespace before creating the singleton. Returns `None` if the declaration is not a namespace and cannot be
855
869
  /// promoted (e.g., `FOO = 42`).
856
870
  /// `ancestors` controls how the singleton's ancestor chain is scheduled for linearization —
@@ -863,6 +877,10 @@ impl<'a> Resolver<'a> {
863
877
  ) -> Option<DeclarationId> {
864
878
  let attached_decl = self.graph.declarations().get(&attached_id).unwrap();
865
879
 
880
+ if let Some(singleton_id) = attached_decl.as_namespace().and_then(|d| d.singleton_class()) {
881
+ return Some(*singleton_id);
882
+ }
883
+
866
884
  // If the attached object is a constant alias, follow the alias chain to find the actual namespace
867
885
  if matches!(attached_decl, Declaration::ConstantAlias(_)) {
868
886
  return match self.resolve_to_namespace(attached_id) {
@@ -874,7 +892,7 @@ impl<'a> Resolver<'a> {
874
892
  if matches!(attached_decl, Declaration::Constant(_)) {
875
893
  if self.graph.all_definitions_promotable(attached_decl) {
876
894
  self.graph.promote_constant_to_namespace(attached_id, |name, owner_id| {
877
- Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
895
+ Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(name, owner_id))))
878
896
  });
879
897
 
880
898
  self.schedule_singleton_ancestors(attached_id, mode);
@@ -890,10 +908,6 @@ impl<'a> Resolver<'a> {
890
908
  .as_namespace_mut()
891
909
  .expect("constants are handled above; all other callers pass namespace declarations");
892
910
 
893
- if let Some(singleton_id) = namespace_decl.singleton_class() {
894
- return Some(*singleton_id);
895
- }
896
-
897
911
  let decl_id = DeclarationId::from(&fully_qualified_name);
898
912
  namespace_decl.set_singleton_class_id(decl_id);
899
913
 
@@ -1067,17 +1081,11 @@ impl<'a> Resolver<'a> {
1067
1081
  declaration_id: DeclarationId,
1068
1082
  context: &mut LinearizationContext,
1069
1083
  ) -> Option<Vec<Ancestor>> {
1070
- if declaration_id == *BASIC_OBJECT_ID {
1071
- return None;
1072
- }
1073
-
1074
1084
  let declaration = self.graph.declarations().get(&declaration_id).unwrap();
1075
1085
 
1076
1086
  match declaration {
1077
1087
  Declaration::Namespace(Namespace::Class(_)) => {
1078
- let definition_ids = declaration.definitions().to_vec();
1079
-
1080
- Some(match self.linearize_parent_class(&definition_ids, context) {
1088
+ Some(match self.linearize_superclass(declaration_id, context)? {
1081
1089
  Ancestors::Complete(ids) => ids,
1082
1090
  Ancestors::Cyclic(ids) => {
1083
1091
  context.cyclic = true;
@@ -1229,14 +1237,16 @@ impl<'a> Resolver<'a> {
1229
1237
  if !descendants.is_empty() {
1230
1238
  for ancestor in cached {
1231
1239
  if let Ancestor::Complete(ancestor_id) = ancestor {
1240
+ let namespace = self
1241
+ .graph
1242
+ .declarations_mut()
1243
+ .get_mut(ancestor_id)
1244
+ .unwrap()
1245
+ .as_namespace_mut()
1246
+ .unwrap();
1247
+
1232
1248
  for descendant in descendants.iter() {
1233
- self.graph
1234
- .declarations_mut()
1235
- .get_mut(ancestor_id)
1236
- .unwrap()
1237
- .as_namespace_mut()
1238
- .unwrap()
1239
- .add_descendant(*descendant);
1249
+ namespace.add_descendant(*descendant);
1240
1250
  }
1241
1251
  }
1242
1252
  }
@@ -1280,8 +1290,8 @@ impl<'a> Resolver<'a> {
1280
1290
  Outcome::Resolved(owner_id) => {
1281
1291
  let mut fully_qualified_name = self.graph.strings().get(&str_id).unwrap().to_string();
1282
1292
 
1283
- // If the owner is a promotable constant and something is being defined inside it, promote it to a
1284
- // module
1293
+ // If the owner is a promotable constant and something is being defined inside it, promote it to an
1294
+ // unknown namespace. A later explicit class or module definition selects the concrete kind.
1285
1295
  {
1286
1296
  let owner = self.graph.declarations().get(&owner_id).unwrap();
1287
1297
  let is_promotable_constant =
@@ -1289,7 +1299,7 @@ impl<'a> Resolver<'a> {
1289
1299
 
1290
1300
  if is_promotable_constant {
1291
1301
  self.graph.promote_constant_to_namespace(owner_id, |name, owner_id| {
1292
- Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
1302
+ Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(name, owner_id))))
1293
1303
  });
1294
1304
  self.unit_queue.push_back(Unit::Ancestors(owner_id));
1295
1305
  }
@@ -1468,139 +1478,139 @@ impl<'a> Resolver<'a> {
1468
1478
  /// resolved
1469
1479
  #[allow(clippy::too_many_lines)]
1470
1480
  fn resolve_constant_internal(&mut self, name_id: NameId) -> Outcome {
1471
- let name_ref = self.graph.names().get(&name_id).unwrap().clone();
1481
+ let name = match self.graph.names().get(&name_id).unwrap() {
1482
+ NameRef::Resolved(resolved) => return Outcome::Resolved(*resolved.declaration_id()),
1483
+ NameRef::Unresolved(name) => name.as_ref().clone(),
1484
+ };
1472
1485
 
1473
- match name_ref {
1474
- NameRef::Unresolved(name) => {
1475
- match name.parent_scope() {
1476
- ParentScope::TopLevel => {
1477
- let result = self.search_ancestors(*OBJECT_ID, *name.str());
1486
+ match name.parent_scope() {
1487
+ ParentScope::TopLevel => {
1488
+ let result = self.search_ancestors(*OBJECT_ID, *name.str());
1478
1489
 
1479
- if let Outcome::Resolved(declaration_id) = result {
1480
- self.graph.record_resolved_name(name_id, declaration_id);
1481
- }
1490
+ if let Outcome::Resolved(declaration_id) = result {
1491
+ self.graph.record_resolved_name(name_id, declaration_id);
1492
+ }
1482
1493
 
1483
- result
1484
- }
1485
- ParentScope::Attached(parent_scope_id) => {
1486
- let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1487
- return Outcome::Retry {
1488
- partial_ancestors: false,
1489
- };
1494
+ result
1495
+ }
1496
+ ParentScope::Attached(parent_scope_id) => {
1497
+ let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1498
+ return Outcome::Retry {
1499
+ partial_ancestors: false,
1500
+ };
1501
+ };
1502
+
1503
+ let mut target_decl_id = *parent_scope.declaration_id();
1504
+ let target_decl = self.graph.declarations().get(&target_decl_id).unwrap();
1505
+
1506
+ // If the attached object is a constant alias, resolve it to the target namespace
1507
+ // (e.g., ALIAS.bar where ALIAS = Foo should create the singleton class on Foo, not ALIAS)
1508
+ if matches!(target_decl, Declaration::ConstantAlias(_)) {
1509
+ let resolved_ids = self.resolve_alias_chains(target_decl_id);
1510
+
1511
+ if resolved_ids
1512
+ .iter()
1513
+ .any(|id| matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_))))
1514
+ {
1515
+ return Outcome::Retry {
1516
+ partial_ancestors: false,
1490
1517
  };
1518
+ }
1491
1519
 
1492
- let mut target_decl_id = *parent_scope.declaration_id();
1493
- let target_decl = self.graph.declarations().get(&target_decl_id).unwrap();
1520
+ let Some(&namespace_id) = resolved_ids
1521
+ .iter()
1522
+ .find(|id| matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_))))
1523
+ else {
1524
+ return Outcome::Unresolved;
1525
+ };
1494
1526
 
1495
- // If the attached object is a constant alias, resolve it to the target namespace
1496
- // (e.g., ALIAS.bar where ALIAS = Foo should create the singleton class on Foo, not ALIAS)
1497
- if matches!(target_decl, Declaration::ConstantAlias(_)) {
1498
- let resolved_ids = self.resolve_alias_chains(target_decl_id);
1527
+ target_decl_id = namespace_id;
1528
+ }
1499
1529
 
1500
- if resolved_ids.iter().any(|id| {
1501
- matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_)))
1502
- }) {
1503
- return Outcome::Retry {
1504
- partial_ancestors: false,
1505
- };
1506
- }
1530
+ // If we found a singleton reference with a resolved attached object parent scope, we
1531
+ // automatically create the singleton class
1532
+ let Some(singleton_id) =
1533
+ self.get_or_create_singleton_class(target_decl_id, SingletonAncestors::Enqueue)
1534
+ else {
1535
+ return Outcome::Unresolved;
1536
+ };
1537
+ self.graph.record_resolved_name(name_id, singleton_id);
1538
+ // `get_or_create_singleton_class` already enqueued the singleton's ancestors on
1539
+ // creation, so there is nothing to hand back for re-enqueueing.
1540
+ Outcome::Resolved(singleton_id)
1541
+ }
1542
+ ParentScope::None => {
1543
+ // Otherwise, it's a simple constant read and we can resolve it directly
1544
+ let result = self.run_resolution(&name);
1507
1545
 
1508
- let Some(&namespace_id) = resolved_ids.iter().find(|id| {
1509
- matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_)))
1510
- }) else {
1511
- return Outcome::Unresolved;
1512
- };
1546
+ if let Outcome::Resolved(declaration_id) = result {
1547
+ self.graph.record_resolved_name(name_id, declaration_id);
1548
+ }
1513
1549
 
1514
- target_decl_id = namespace_id;
1515
- }
1550
+ result
1551
+ }
1552
+ ParentScope::Some(parent_scope_id) => {
1553
+ let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1554
+ return Outcome::Retry {
1555
+ partial_ancestors: false,
1556
+ };
1557
+ };
1516
1558
 
1517
- // If we found a singleton reference with a resolved attached object parent scope, we
1518
- // automatically create the singleton class
1519
- let Some(singleton_id) =
1520
- self.get_or_create_singleton_class(target_decl_id, SingletonAncestors::Enqueue)
1521
- else {
1522
- return Outcome::Unresolved;
1523
- };
1524
- self.graph.record_resolved_name(name_id, singleton_id);
1525
- // `get_or_create_singleton_class` already enqueued the singleton's ancestors on
1526
- // creation, so there is nothing to hand back for re-enqueueing.
1527
- Outcome::Resolved(singleton_id)
1528
- }
1529
- ParentScope::None => {
1530
- // Otherwise, it's a simple constant read and we can resolve it directly
1531
- let result = self.run_resolution(&name);
1559
+ // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
1560
+ // An alias can have multiple targets, so we try all of them in order.
1561
+ let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1532
1562
 
1533
- if let Outcome::Resolved(declaration_id) = result {
1534
- self.graph.record_resolved_name(name_id, declaration_id);
1535
- }
1563
+ // Search each resolved target for the constant. Return early if found.
1564
+ let mut missing_partial = false;
1565
+ let mut found_namespace = false;
1536
1566
 
1537
- result
1538
- }
1539
- ParentScope::Some(parent_scope_id) => {
1540
- let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1567
+ for &id in &resolved_ids {
1568
+ match self.graph.declarations().get(&id) {
1569
+ Some(Declaration::ConstantAlias(_)) => {
1570
+ // Alias not fully resolved yet
1541
1571
  return Outcome::Retry {
1542
1572
  partial_ancestors: false,
1543
1573
  };
1544
- };
1574
+ }
1575
+ Some(Declaration::Namespace(_)) => {
1576
+ found_namespace = true;
1545
1577
 
1546
- // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
1547
- // An alias can have multiple targets, so we try all of them in order.
1548
- let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1549
-
1550
- // Search each resolved target for the constant. Return early if found.
1551
- let mut missing_partial = false;
1552
- let mut found_namespace = false;
1553
-
1554
- for &id in &resolved_ids {
1555
- match self.graph.declarations().get(&id) {
1556
- Some(Declaration::ConstantAlias(_)) => {
1557
- // Alias not fully resolved yet
1558
- return Outcome::Retry {
1559
- partial_ancestors: false,
1560
- };
1578
+ match self.search_ancestors(id, *name.str()) {
1579
+ Outcome::Resolved(declaration_id) => {
1580
+ self.graph.record_resolved_name(name_id, declaration_id);
1581
+ return Outcome::Resolved(declaration_id);
1561
1582
  }
1562
- Some(Declaration::Namespace(_)) => {
1563
- found_namespace = true;
1564
-
1565
- match self.search_ancestors(id, *name.str()) {
1566
- Outcome::Resolved(declaration_id) => {
1567
- self.graph.record_resolved_name(name_id, declaration_id);
1568
- return Outcome::Resolved(declaration_id);
1569
- }
1570
- Outcome::Retry {
1571
- partial_ancestors: true,
1572
- } => {
1573
- missing_partial = true;
1574
- }
1575
- Outcome::Unresolved => {}
1576
- Outcome::Retry {
1577
- partial_ancestors: false,
1578
- } => {
1579
- unreachable!("search_ancestors never returns a non-partial Retry")
1580
- }
1581
- }
1583
+ Outcome::Retry {
1584
+ partial_ancestors: true,
1585
+ } => {
1586
+ missing_partial = true;
1582
1587
  }
1583
- _ => {
1584
- // Not a namespace (e.g., a constant) - skip
1588
+ Outcome::Unresolved => {}
1589
+ Outcome::Retry {
1590
+ partial_ancestors: false,
1591
+ } => {
1592
+ unreachable!("search_ancestors never returns a non-partial Retry")
1585
1593
  }
1586
1594
  }
1587
1595
  }
1588
-
1589
- // If no namespaces were found, this constant path can never resolve.
1590
- if !found_namespace {
1591
- return Outcome::Unresolved;
1592
- }
1593
-
1594
- // Member not found in any namespace yet - retry in case it's added later. `partial` records
1595
- // whether the miss was due to a still-partial ancestor chain, so the unit is re-checked once
1596
- // that chain completes.
1597
- Outcome::Retry {
1598
- partial_ancestors: missing_partial,
1596
+ _ => {
1597
+ // Not a namespace (e.g., a constant) - skip
1599
1598
  }
1600
1599
  }
1601
1600
  }
1601
+
1602
+ // If no namespaces were found, this constant path can never resolve.
1603
+ if !found_namespace {
1604
+ return Outcome::Unresolved;
1605
+ }
1606
+
1607
+ // Member not found in any namespace yet - retry in case it's added later. `partial` records
1608
+ // whether the miss was due to a still-partial ancestor chain, so the unit is re-checked once
1609
+ // that chain completes.
1610
+ Outcome::Retry {
1611
+ partial_ancestors: missing_partial,
1612
+ }
1602
1613
  }
1603
- NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id()),
1604
1614
  }
1605
1615
  }
1606
1616
 
@@ -1884,19 +1894,6 @@ impl<'a> Resolver<'a> {
1884
1894
  depth
1885
1895
  }
1886
1896
 
1887
- /// Pre-compute name depths for all names into a `NameId → depth` map. Each name's depth is
1888
- /// computed once via memoized recursion, then used as an O(1) lookup key during sorting in
1889
- /// `prepare_units`.
1890
- pub(crate) fn compute_name_depths(names: &IdentityHashMap<NameId, NameRef>) -> IdentityHashMap<NameId, u32> {
1891
- let mut cache = IdentityHashMap::with_capacity_and_hasher(names.len(), IdentityHashBuilder);
1892
-
1893
- for &name_id in names.keys() {
1894
- Self::name_depth(name_id, names, &mut cache);
1895
- }
1896
-
1897
- cache
1898
- }
1899
-
1900
1897
  /// Drains `pending_work` and classifies items into the resolution queue.
1901
1898
  /// Namespace definitions and constant references are sorted by name depth for deterministic
1902
1899
  /// resolution order. Non-namespace definitions (methods, attrs, variables) are returned
@@ -1910,7 +1907,28 @@ impl<'a> Resolver<'a> {
1910
1907
  let mut const_refs = Vec::new();
1911
1908
  let mut ancestors = vec![*BASIC_OBJECT_ID, *KERNEL_ID, *OBJECT_ID, *MODULE_ID, *CLASS_ID];
1912
1909
  let names = self.graph.names();
1913
- let depths = Self::compute_name_depths(names);
1910
+ // Memoize name depths on demand for only the names referenced by the pending work. During incremental
1911
+ // resolution this avoids walking every name in the graph just to sort the small subset of units below.
1912
+ // A pending unit references at most one name, so `estimated` (bounded by the total number of names) is a
1913
+ // reasonable capacity that avoids rehashing as depths accumulate.
1914
+ let mut depths: IdentityHashMap<NameId, u32> =
1915
+ IdentityHashMap::with_capacity_and_hasher(estimated.min(names.len()), IdentityHashBuilder);
1916
+
1917
+ // Precompute the lexicographic rank of every document URI. Definitions and references are sorted by
1918
+ // (name depth, URI, offset) below; storing precomputed integer sort keys instead of looking up name
1919
+ // depths and comparing URI strings on every comparison makes sorting substantially cheaper on large graphs.
1920
+ let mut uris: Vec<(&str, UriId)> = self
1921
+ .graph
1922
+ .documents()
1923
+ .iter()
1924
+ .map(|(uri_id, document)| (document.uri(), *uri_id))
1925
+ .collect();
1926
+ uris.sort_unstable();
1927
+ let mut uri_ranks: IdentityHashMap<UriId, u32> =
1928
+ IdentityHashMap::with_capacity_and_hasher(uris.len(), IdentityHashBuilder);
1929
+ for (rank, (_, uri_id)) in uris.into_iter().enumerate() {
1930
+ uri_ranks.insert(uri_id, u32::try_from(rank).expect("more documents than u32::MAX"));
1931
+ }
1914
1932
 
1915
1933
  // Dedup: when multiple files are indexed before resolution runs, pending_work accumulates
1916
1934
  // and the same definition/reference ID can be enqueued more than once.
@@ -1928,23 +1946,28 @@ impl<'a> Resolver<'a> {
1928
1946
  let Some(definition) = self.graph.definitions().get(&id) else {
1929
1947
  continue;
1930
1948
  };
1931
- let uri = self.graph.documents().get(definition.uri_id()).unwrap().uri();
1949
+ let uri_rank = *uri_ranks.get(definition.uri_id()).unwrap();
1932
1950
 
1933
1951
  match definition {
1934
1952
  Definition::Class(def) => {
1935
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1953
+ let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1954
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1936
1955
  }
1937
1956
  Definition::Module(def) => {
1938
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1957
+ let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1958
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1939
1959
  }
1940
1960
  Definition::Constant(def) => {
1941
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1961
+ let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1962
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1942
1963
  }
1943
1964
  Definition::ConstantAlias(def) => {
1944
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1965
+ let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1966
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1945
1967
  }
1946
1968
  Definition::SingletonClass(def) => {
1947
- definitions.push((Unit::Definition(id), (*def.name_id(), uri, definition.offset())));
1969
+ let depth = Self::name_depth(*def.name_id(), names, &mut depths);
1970
+ definitions.push((Unit::Definition(id), (depth, uri_rank, definition.offset())));
1948
1971
  }
1949
1972
  // SelfReceiver methods create singleton classes, which need
1950
1973
  // ancestor linearization. Process them in the convergence loop
@@ -1965,11 +1988,9 @@ impl<'a> Resolver<'a> {
1965
1988
  let Some(constant_ref) = self.graph.constant_references().get(&id) else {
1966
1989
  continue;
1967
1990
  };
1968
- let uri = self.graph.documents().get(&constant_ref.uri_id()).unwrap().uri();
1969
- const_refs.push((
1970
- Unit::ConstantRef(id),
1971
- (*constant_ref.name_id(), uri, constant_ref.offset()),
1972
- ));
1991
+ let uri_rank = *uri_ranks.get(&constant_ref.uri_id()).unwrap();
1992
+ let depth = Self::name_depth(*constant_ref.name_id(), names, &mut depths);
1993
+ const_refs.push((Unit::ConstantRef(id), (depth, uri_rank, constant_ref.offset())));
1973
1994
  }
1974
1995
  Unit::Ancestors(id) => {
1975
1996
  if !seen_ancestors.insert(id) {
@@ -1984,14 +2005,10 @@ impl<'a> Resolver<'a> {
1984
2005
  }
1985
2006
 
1986
2007
  // Sort namespaces based on their name complexity so that simpler names are always first
1987
- // When the depth is the same, sort by URI and offset to maintain determinism
1988
- definitions.sort_unstable_by(|(_, (name_a, uri_a, offset_a)), (_, (name_b, uri_b, offset_b))| {
1989
- (depths.get(name_a).unwrap(), uri_a, offset_a).cmp(&(depths.get(name_b).unwrap(), uri_b, offset_b))
1990
- });
2008
+ // When the depth is the same, sort by URI rank and offset to maintain determinism
2009
+ definitions.sort_unstable_by_key(|(_, key)| *key);
1991
2010
 
1992
- const_refs.sort_unstable_by(|(_, (name_a, uri_a, offset_a)), (_, (name_b, uri_b, offset_b))| {
1993
- (depths.get(name_a).unwrap(), uri_a, offset_a).cmp(&(depths.get(name_b).unwrap(), uri_b, offset_b))
1994
- });
2011
+ const_refs.sort_unstable_by_key(|(_, key)| *key);
1995
2012
 
1996
2013
  others.sort_unstable_by_key(|(_, key)| *key);
1997
2014
 
@@ -2011,15 +2028,11 @@ impl<'a> Resolver<'a> {
2011
2028
  /// - Class: parent is the singleton class of the original parent class
2012
2029
  /// - Singleton class: recurse as many times as necessary to wrap the original attached object's parent class
2013
2030
  fn singleton_parent_id(&mut self, attached_id: DeclarationId) -> (DeclarationId, bool) {
2014
- // Base case: if we reached `BasicObject`, then the parent is `Class`
2015
- if attached_id == *BASIC_OBJECT_ID {
2016
- return (*CLASS_ID, false);
2017
- }
2018
-
2019
2031
  let decl = self.graph.declarations().get(&attached_id).unwrap();
2020
2032
 
2021
2033
  match decl {
2022
- Declaration::Namespace(Namespace::Module(_)) => (*MODULE_ID, false),
2034
+ // Todo may later become a class or module. Until then, use Module as the singleton-parent fallback.
2035
+ Declaration::Namespace(Namespace::Module(_) | Namespace::Todo(_)) => (*MODULE_ID, false),
2023
2036
  Declaration::Namespace(Namespace::SingletonClass(_)) => {
2024
2037
  // For singleton classes, we keep recursively wrapping parents until we can reach the original attached
2025
2038
  // object
@@ -2033,14 +2046,15 @@ impl<'a> Resolver<'a> {
2033
2046
  )
2034
2047
  }
2035
2048
  Declaration::Namespace(Namespace::Class(_)) => {
2036
- // For classes (the regular case), we need to return the singleton class of its parent
2037
- let definition_ids = decl.definitions().to_vec();
2038
-
2039
- let (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids);
2049
+ // For classes (the regular case), we need to return the singleton class of its superclass
2050
+ let Some((superclass_id, unresolved_superclass)) = self.get_superclass(attached_id) else {
2051
+ // BasicObject has no superclass, but its singleton class inherits from Class
2052
+ return (*CLASS_ID, false);
2053
+ };
2040
2054
  (
2041
- self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred)
2042
- .expect("parent class should always be a namespace"),
2043
- unresolved_parent.is_some(),
2055
+ self.get_or_create_singleton_class(superclass_id, SingletonAncestors::Deferred)
2056
+ .expect("superclass should always be a namespace"),
2057
+ unresolved_superclass.is_some(),
2044
2058
  )
2045
2059
  }
2046
2060
  _ => {
@@ -2051,11 +2065,18 @@ impl<'a> Resolver<'a> {
2051
2065
  }
2052
2066
  }
2053
2067
 
2054
- fn get_parent_class(&self, definition_ids: &[DefinitionId]) -> (DeclarationId, Option<NameId>) {
2055
- let mut explicit_parents = Vec::new();
2056
- let mut unresolved_parent = None;
2068
+ /// Returns the selected superclass and any unresolved explicit superclass. The top-level `BasicObject` declaration
2069
+ /// is Ruby's root class and is the only class without a superclass.
2070
+ fn get_superclass(&self, declaration_id: DeclarationId) -> Option<(DeclarationId, Option<NameId>)> {
2071
+ if declaration_id == *BASIC_OBJECT_ID {
2072
+ return None;
2073
+ }
2057
2074
 
2058
- for definition_id in definition_ids {
2075
+ let declaration = self.graph.declarations().get(&declaration_id).unwrap();
2076
+ let mut explicit_superclasses = Vec::new();
2077
+ let mut unresolved_superclass = None;
2078
+
2079
+ for definition_id in declaration.definitions() {
2059
2080
  let definition = self.graph.definitions().get(definition_id).unwrap();
2060
2081
 
2061
2082
  if let Definition::Class(class) = definition
@@ -2066,47 +2087,47 @@ impl<'a> Resolver<'a> {
2066
2087
 
2067
2088
  match name {
2068
2089
  NameRef::Resolved(resolved) => {
2069
- if let Some(parent_id) = self.resolve_to_namespace(*resolved.declaration_id()) {
2070
- explicit_parents.push(parent_id);
2090
+ if let Some(superclass_id) = self.resolve_to_namespace(*resolved.declaration_id()) {
2091
+ explicit_superclasses.push(superclass_id);
2071
2092
  }
2072
2093
  }
2073
2094
  NameRef::Unresolved(_) => {
2074
- unresolved_parent = Some(*constant_reference.name_id());
2095
+ unresolved_superclass = Some(*constant_reference.name_id());
2075
2096
  }
2076
2097
  }
2077
2098
  }
2078
2099
  }
2079
2100
 
2080
- // If there's more than one parent class that isn't `Object` and they are different, then there's a superclass
2101
+ // If there's more than one superclass that isn't `Object` and they are different, then there's a superclass
2081
2102
  // mismatch error. TODO: We should add a diagnostic here
2082
- (
2083
- explicit_parents.first().copied().unwrap_or(*OBJECT_ID),
2084
- unresolved_parent,
2085
- )
2103
+ Some((
2104
+ explicit_superclasses.first().copied().unwrap_or(*OBJECT_ID),
2105
+ unresolved_superclass,
2106
+ ))
2086
2107
  }
2087
2108
 
2088
- fn linearize_parent_class(
2109
+ fn linearize_superclass(
2089
2110
  &mut self,
2090
- definition_ids: &[DefinitionId],
2111
+ declaration_id: DeclarationId,
2091
2112
  context: &mut LinearizationContext,
2092
- ) -> Ancestors {
2093
- let (picked_parent, unresolved_parent) = self.get_parent_class(definition_ids);
2094
- let mut result = self.linearize_ancestors(picked_parent, context);
2113
+ ) -> Option<Ancestors> {
2114
+ let (superclass_id, unresolved_superclass) = self.get_superclass(declaration_id)?;
2115
+ let mut result = self.linearize_ancestors(superclass_id, context);
2095
2116
 
2096
- if let Some(name_id) = unresolved_parent {
2117
+ if let Some(name_id) = unresolved_superclass {
2097
2118
  context.partial = true;
2098
2119
 
2099
- // Insert the unresolved parent as a Partial ancestor at the front of the chain, so it
2120
+ // Insert the unresolved superclass as a Partial ancestor at the front of the chain, so it
2100
2121
  // appears before the default Object ancestors
2101
2122
  let ancestors = match &mut result {
2102
2123
  Ancestors::Complete(ids) | Ancestors::Cyclic(ids) | Ancestors::Partial(ids) => ids,
2103
2124
  };
2104
2125
  ancestors.insert(0, Ancestor::Partial(name_id));
2105
2126
 
2106
- result.to_partial()
2107
- } else {
2108
- result
2127
+ result = result.to_partial();
2109
2128
  }
2129
+
2130
+ Some(result)
2110
2131
  }
2111
2132
 
2112
2133
  fn mixins_of(&self, definition_id: DefinitionId) -> Option<Vec<Mixin>> {