rubydex 0.2.6 → 0.2.8

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.
@@ -19,26 +19,37 @@ use crate::model::{
19
19
  };
20
20
 
21
21
  enum Outcome {
22
- /// The constant was successfully resolved to the given declaration ID. The second optional tuple element is a
23
- /// declaration that still needs to have its ancestors linearized
24
- Resolved(DeclarationId, Option<DeclarationId>),
25
- /// We had everything we needed to resolved this constant, but we couldn't find it. This means it's not defined (or
26
- /// defined in a way that static analysis won't discover it). Failing to resolve a constant may also uncovered
27
- /// ancestors that require linearization, which is the second element
28
- Unresolved(Option<DeclarationId>),
29
- /// We couldn't resolve this constant right now because certain dependencies were missing. For example, a constant
30
- /// reference involved in computing ancestors (like an include) was found, but wasn't resolved yet. We need to place
31
- /// this back in the queue to retry once we have progressed further. The optional declaration ID is an ancestor that
32
- /// needs to be linearized before we can retry.
33
- Retry(Option<DeclarationId>),
22
+ /// The constant was successfully resolved to the given declaration ID.
23
+ Resolved(DeclarationId),
24
+ /// We had everything we needed to resolve this constant (all relevant ancestor chains are complete), but the member
25
+ /// isn't there. This is definitive: the unit can be dropped and a diagnostic emitted.
26
+ Unresolved,
27
+ /// We couldn't resolve this constant right now because a dependency was missing, so it may succeed later. The unit
28
+ /// must be placed back in the queue and retried once we have progressed further. `partial` is `true` when the
29
+ /// blocker was a still-partial ancestor chain (the member may appear once the chain completes); `false` when it was
30
+ /// a plain missing dependency (an unresolved parent scope, alias, or lexical nesting).
31
+ Retry { partial_ancestors: bool },
34
32
  }
35
33
 
36
34
  impl Outcome {
37
35
  fn is_resolved_or_retry(&self) -> bool {
38
- matches!(self, Outcome::Resolved(_, _) | Outcome::Retry(_))
36
+ matches!(self, Outcome::Resolved(_) | Outcome::Retry { .. })
39
37
  }
40
38
  }
41
39
 
40
+ /// Controls how `get_or_create_singleton_class` schedules linearization of the singleton it touches.
41
+ #[derive(Clone, Copy)]
42
+ enum SingletonAncestors {
43
+ /// Linearize the ancestors inline, immediately. Should only be used after the convergence loop, when every
44
+ /// namespace is already resolved.
45
+ Eager,
46
+ /// Enqueue a `Unit::Ancestors` so the convergence loop linearizes it on a later pass.
47
+ Enqueue,
48
+ /// Do nothing: the caller is already mid-linearization and will linearize this singleton inline
49
+ /// within the current `LinearizationContext`, so a separate unit would be pure duplicate work.
50
+ Deferred,
51
+ }
52
+
42
53
  struct LinearizationContext {
43
54
  descendants: IdentityHashSet<DeclarationId>,
44
55
  seen_ids: IdentityHashSet<DeclarationId>,
@@ -140,8 +151,8 @@ impl<'a> Resolver<'a> {
140
151
  /// the Ruby API
141
152
  pub fn resolve_constant(&mut self, name_id: NameId) -> Option<DeclarationId> {
142
153
  match self.resolve_constant_internal(name_id) {
143
- Outcome::Resolved(id, _) => Some(id),
144
- Outcome::Unresolved(_) | Outcome::Retry(_) => None,
154
+ Outcome::Resolved(id) => Some(id),
155
+ Outcome::Unresolved | Outcome::Retry { .. } => None,
145
156
  }
146
157
  }
147
158
 
@@ -186,45 +197,40 @@ impl<'a> Resolver<'a> {
186
197
  };
187
198
  let str_id = *method.str_id();
188
199
  match self.graph.definition_id_to_declaration_id(*def_id) {
189
- Some(&owner_decl_id) => match self.get_or_create_singleton_class(owner_decl_id, false) {
190
- Some(singleton_id) => {
191
- self.create_declaration(str_id, id, singleton_id, |name| {
192
- Declaration::Method(Box::new(MethodDeclaration::new(name, singleton_id)))
193
- });
194
- Outcome::Resolved(singleton_id, None)
200
+ Some(&owner_decl_id) => {
201
+ match self.get_or_create_singleton_class(owner_decl_id, SingletonAncestors::Enqueue) {
202
+ Some(singleton_id) => {
203
+ self.create_declaration(str_id, id, singleton_id, |name| {
204
+ Declaration::Method(Box::new(MethodDeclaration::new(name, singleton_id)))
205
+ });
206
+ Outcome::Resolved(singleton_id)
207
+ }
208
+ // Owner is a non-promotable constant — method is orphaned
209
+ None => Outcome::Unresolved,
195
210
  }
196
- // Owner is a non-promotable constant — method is orphaned
197
- None => Outcome::Unresolved(None),
198
- },
211
+ }
199
212
  // Owning class not resolved yet — retry next pass
200
- None => Outcome::Retry(None),
213
+ None => Outcome::Retry {
214
+ partial_ancestors: false,
215
+ },
201
216
  }
202
217
  }
203
218
  _ => panic!("Expected constant or singleton method definitions"),
204
219
  };
205
220
 
206
221
  match outcome {
207
- Outcome::Retry(None) => {
208
- // There might be dependencies we haven't figured out yet, so we need to retry
222
+ Outcome::Retry { .. } => {
209
223
  self.unit_queue.push_back(unit_id);
210
224
  }
211
- Outcome::Unresolved(None) => {
225
+ Outcome::Unresolved => {
212
226
  // We couldn't resolve this name. Emit a diagnostic
213
227
  }
214
- Outcome::Retry(Some(id_needing_linearization)) | Outcome::Unresolved(Some(id_needing_linearization)) => {
215
- self.unit_queue.push_back(unit_id);
216
- self.unit_queue.push_back(Unit::Ancestors(id_needing_linearization));
217
- }
218
- Outcome::Resolved(id, None) => {
228
+ Outcome::Resolved(id) => {
219
229
  if needs_linearization {
220
230
  self.unit_queue.push_back(Unit::Ancestors(id));
221
231
  }
222
232
  self.made_progress = true;
223
233
  }
224
- Outcome::Resolved(_, Some(id_needing_linearization)) => {
225
- self.unit_queue.push_back(Unit::Ancestors(id_needing_linearization));
226
- self.made_progress = true;
227
- }
228
234
  }
229
235
  }
230
236
 
@@ -233,24 +239,17 @@ impl<'a> Resolver<'a> {
233
239
  let constant_ref = self.graph.constant_references().get(&id).unwrap();
234
240
 
235
241
  match self.resolve_constant_internal(*constant_ref.name_id()) {
236
- Outcome::Retry(None) | Outcome::Unresolved(None) => {
237
- // Retry: dependencies not resolved yet, or name genuinely unknown
238
- // (which can be temporary during incremental invalidation when the
239
- // parent namespace was deleted but will be re-added).
240
- self.unit_queue.push_back(unit_id);
241
- }
242
- Outcome::Retry(Some(id_needing_linearization)) | Outcome::Unresolved(Some(id_needing_linearization)) => {
243
- self.unit_queue.push_back(unit_id);
244
- self.unit_queue.push_back(Unit::Ancestors(id_needing_linearization));
245
- }
246
- Outcome::Resolved(declaration_id, None) => {
242
+ Outcome::Resolved(declaration_id) => {
247
243
  self.graph.record_resolved_reference(id, declaration_id);
248
244
  self.made_progress = true;
249
245
  }
250
- Outcome::Resolved(resolved_id, Some(id_needing_linearization)) => {
251
- self.graph.record_resolved_reference(id, resolved_id);
252
- self.made_progress = true;
253
- self.unit_queue.push_back(Unit::Ancestors(id_needing_linearization));
246
+ Outcome::Retry { .. } => {
247
+ self.unit_queue.push_back(unit_id);
248
+ }
249
+ Outcome::Unresolved => {
250
+ // If we had everything we needed to resolve this constant and still failed, then remember it as work
251
+ // for the next incremental resolution pass, but avoid trying again in this cycle
252
+ self.graph.push_work(unit_id);
254
253
  }
255
254
  }
256
255
  }
@@ -291,7 +290,9 @@ impl<'a> Resolver<'a> {
291
290
  continue;
292
291
  };
293
292
 
294
- let Some(singleton_id) = self.get_or_create_singleton_class(receiver_decl_id, true) else {
293
+ let Some(singleton_id) =
294
+ self.get_or_create_singleton_class(receiver_decl_id, SingletonAncestors::Eager)
295
+ else {
295
296
  continue;
296
297
  };
297
298
 
@@ -395,7 +396,9 @@ impl<'a> Resolver<'a> {
395
396
  };
396
397
 
397
398
  // Instance variable in singleton method - owned by the receiver's singleton class
398
- let Some(owner_id) = self.get_or_create_singleton_class(receiver_decl_id, true) else {
399
+ let Some(owner_id) =
400
+ self.get_or_create_singleton_class(receiver_decl_id, SingletonAncestors::Eager)
401
+ else {
399
402
  continue;
400
403
  };
401
404
  {
@@ -453,7 +456,9 @@ impl<'a> Resolver<'a> {
453
456
  .copied()
454
457
  .unwrap_or(*OBJECT_ID);
455
458
 
456
- let Some(owner_id) = self.get_or_create_singleton_class(nesting_decl_id, true) else {
459
+ let Some(owner_id) =
460
+ self.get_or_create_singleton_class(nesting_decl_id, SingletonAncestors::Eager)
461
+ else {
457
462
  continue;
458
463
  };
459
464
  {
@@ -483,7 +488,7 @@ impl<'a> Resolver<'a> {
483
488
  continue;
484
489
  };
485
490
  let owner_id = self
486
- .get_or_create_singleton_class(singleton_class_decl_id, true)
491
+ .get_or_create_singleton_class(singleton_class_decl_id, SingletonAncestors::Eager)
487
492
  .expect("singleton class nesting should always be a namespace");
488
493
  {
489
494
  debug_assert!(
@@ -524,7 +529,8 @@ impl<'a> Resolver<'a> {
524
529
  continue;
525
530
  };
526
531
 
527
- let Some(owner_id) = self.get_or_create_singleton_class(decl_id, true) else {
532
+ let Some(owner_id) = self.get_or_create_singleton_class(decl_id, SingletonAncestors::Eager)
533
+ else {
528
534
  continue;
529
535
  };
530
536
 
@@ -652,7 +658,9 @@ impl<'a> Resolver<'a> {
652
658
  };
653
659
 
654
660
  let owner_id = if is_singleton {
655
- let Some(singleton_id) = self.get_or_create_singleton_class(lexical_owner_id, true) else {
661
+ let Some(singleton_id) =
662
+ self.get_or_create_singleton_class(lexical_owner_id, SingletonAncestors::Eager)
663
+ else {
656
664
  continue;
657
665
  };
658
666
  singleton_id
@@ -710,18 +718,8 @@ impl<'a> Resolver<'a> {
710
718
  offset,
711
719
  format!("undefined method `{owner_name}#{method_name}` for visibility change"),
712
720
  );
713
- if is_singleton {
714
- // Document-scoped: the singleton class may be synthetic (created by this
715
- // visibility resolution) and won't be cleaned up on file delete, so attaching
716
- // the diagnostic to the declaration would leave it orphaned.
717
- self.graph.add_document_diagnostic(uri_id, diagnostic);
718
- } else {
719
- self.graph
720
- .declarations_mut()
721
- .get_mut(&owner_id)
722
- .unwrap()
723
- .add_diagnostic(diagnostic);
724
- }
721
+
722
+ self.graph.add_document_diagnostic(uri_id, diagnostic);
725
723
  }
726
724
  }
727
725
 
@@ -855,19 +853,20 @@ impl<'a> Resolver<'a> {
855
853
  /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to a `Class`
856
854
  /// namespace before creating the singleton. Returns `None` if the declaration is not a namespace and cannot be
857
855
  /// promoted (e.g., `FOO = 42`).
858
- /// When `eager_ancestors` is `true`, ancestor chains are linearized inline (used after the convergence loop when all
859
- /// namespaces are resolved). When `false`, a `Unit::Ancestors` item is enqueued for the convergence loop to process.
856
+ /// `ancestors` controls how the singleton's ancestor chain is scheduled for linearization
857
+ /// inline now (`Eager`), enqueued for the convergence loop (`Enqueue`), or left to the caller
858
+ /// that is already linearizing it inline (`Deferred`). See [`SingletonAncestors`].
860
859
  fn get_or_create_singleton_class(
861
860
  &mut self,
862
861
  attached_id: DeclarationId,
863
- eager_ancestors: bool,
862
+ mode: SingletonAncestors,
864
863
  ) -> Option<DeclarationId> {
865
864
  let attached_decl = self.graph.declarations().get(&attached_id).unwrap();
866
865
 
867
866
  // If the attached object is a constant alias, follow the alias chain to find the actual namespace
868
867
  if matches!(attached_decl, Declaration::ConstantAlias(_)) {
869
868
  return match self.resolve_to_namespace(attached_id) {
870
- Some(id) => self.get_or_create_singleton_class(id, eager_ancestors),
869
+ Some(id) => self.get_or_create_singleton_class(id, mode),
871
870
  None => None,
872
871
  };
873
872
  }
@@ -878,11 +877,7 @@ impl<'a> Resolver<'a> {
878
877
  Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
879
878
  });
880
879
 
881
- if eager_ancestors {
882
- let _ = self.ancestors_of(attached_id);
883
- } else {
884
- self.unit_queue.push_back(Unit::Ancestors(attached_id));
885
- }
880
+ self.schedule_singleton_ancestors(attached_id, mode);
886
881
  } else {
887
882
  return None;
888
883
  }
@@ -910,15 +905,26 @@ impl<'a> Resolver<'a> {
910
905
  )))),
911
906
  );
912
907
 
913
- if eager_ancestors {
914
- let _ = self.ancestors_of(decl_id);
915
- } else {
916
- self.unit_queue.push_back(Unit::Ancestors(decl_id));
917
- }
908
+ self.schedule_singleton_ancestors(decl_id, mode);
918
909
 
919
910
  Some(decl_id)
920
911
  }
921
912
 
913
+ /// Schedules linearization of a singleton's ancestor chain according to the requested policy.
914
+ fn schedule_singleton_ancestors(&mut self, id: DeclarationId, mode: SingletonAncestors) {
915
+ match mode {
916
+ SingletonAncestors::Eager => {
917
+ let _ = self.ancestors_of(id);
918
+ }
919
+ SingletonAncestors::Enqueue => {
920
+ self.unit_queue.push_back(Unit::Ancestors(id));
921
+ }
922
+ // The caller is already linearizing this singleton inline within the current
923
+ // `LinearizationContext`, so a separate unit would be redundant work.
924
+ SingletonAncestors::Deferred => {}
925
+ }
926
+ }
927
+
922
928
  /// Linearizes the ancestors of a declaration, returning the list of ancestor declaration IDs
923
929
  ///
924
930
  /// # Panics
@@ -1021,7 +1027,7 @@ impl<'a> Resolver<'a> {
1021
1027
 
1022
1028
  // Ensure that we create the singleton and enqueue it for linearization if we see an extend
1023
1029
  if has_extends && !is_singleton_class {
1024
- self.get_or_create_singleton_class(declaration_id, false);
1030
+ self.get_or_create_singleton_class(declaration_id, SingletonAncestors::Enqueue);
1025
1031
  }
1026
1032
 
1027
1033
  let (linearized_prepends, linearized_includes) =
@@ -1261,15 +1267,17 @@ impl<'a> Resolver<'a> {
1261
1267
  // `set_singleton_class_id`, not `add_member`, so a TODO receiver would never gain a
1262
1268
  // member. Emit Retry so the unit is preserved for a later resolve where the receiver
1263
1269
  // may exist.
1264
- Outcome::Unresolved(None) if singleton => Outcome::Retry(None),
1265
- Outcome::Unresolved(None) => Outcome::Resolved(self.create_todo_for_parent(name_id), None),
1270
+ Outcome::Unresolved if singleton => Outcome::Retry {
1271
+ partial_ancestors: false,
1272
+ },
1273
+ Outcome::Unresolved => Outcome::Resolved(self.create_todo_for_parent(name_id)),
1266
1274
  other => other,
1267
1275
  };
1268
1276
 
1269
1277
  // The name of the declaration is determined by the name of its owner, which may or may not require resolution
1270
1278
  // depending on whether the name has a parent scope
1271
1279
  match outcome {
1272
- Outcome::Resolved(owner_id, id_needing_linearization) => {
1280
+ Outcome::Resolved(owner_id) => {
1273
1281
  let mut fully_qualified_name = self.graph.strings().get(&str_id).unwrap().to_string();
1274
1282
 
1275
1283
  // If the owner is a promotable constant and something is being defined inside it, promote it to a
@@ -1294,7 +1302,7 @@ impl<'a> Resolver<'a> {
1294
1302
  // Foo = 1
1295
1303
  // class << Foo; end
1296
1304
  if singleton && !owner_is_namespace {
1297
- return Outcome::Unresolved(None);
1305
+ return Outcome::Unresolved;
1298
1306
  }
1299
1307
 
1300
1308
  // We don't prefix declarations with `Object::`
@@ -1324,7 +1332,7 @@ impl<'a> Resolver<'a> {
1324
1332
  }
1325
1333
 
1326
1334
  self.graph.record_resolved_name(name_id, declaration_id);
1327
- Outcome::Resolved(declaration_id, id_needing_linearization)
1335
+ Outcome::Resolved(declaration_id)
1328
1336
  }
1329
1337
  other => other,
1330
1338
  }
@@ -1344,12 +1352,14 @@ impl<'a> Resolver<'a> {
1344
1352
  // If we have `A::B`, the owner of `B` is whatever `A` resolves to.
1345
1353
  // If `A` is an alias, resolve through to get the actual namespace.
1346
1354
  match self.resolve_constant_internal(parent_scope) {
1347
- Outcome::Resolved(id, linearization) => self.resolve_to_primary_namespace(id, linearization),
1355
+ Outcome::Resolved(id) => self.resolve_to_primary_namespace(id),
1348
1356
  // The parent scope is genuinely unknown — not a circular alias or pending
1349
1357
  // linearization, but a name that doesn't exist anywhere in the graph.
1350
- Outcome::Unresolved(None) => Outcome::Unresolved(None),
1351
- Outcome::Retry(None) if !preserve_retry => Outcome::Unresolved(None),
1352
- other => other,
1358
+ Outcome::Unresolved => Outcome::Unresolved,
1359
+ Outcome::Retry {
1360
+ partial_ancestors: false,
1361
+ } if !preserve_retry => Outcome::Unresolved,
1362
+ retry @ Outcome::Retry { .. } => retry,
1353
1363
  }
1354
1364
  } else if let Some(nesting_id) = name_ref.nesting()
1355
1365
  && !name_ref.parent_scope().is_top_level()
@@ -1360,16 +1370,18 @@ impl<'a> Resolver<'a> {
1360
1370
  // end
1361
1371
  // If `ALIAS` points to `Outer`, `CONST` should be owned by `Outer::Target`, not `ALIAS::Target`.
1362
1372
  match self.graph.names().get(nesting_id).unwrap() {
1363
- NameRef::Resolved(resolved) => self.resolve_to_primary_namespace(*resolved.declaration_id(), None),
1373
+ NameRef::Resolved(resolved) => self.resolve_to_primary_namespace(*resolved.declaration_id()),
1364
1374
  NameRef::Unresolved(_) => {
1365
1375
  // The only case where we wouldn't have the nesting resolved at this point is if it's available through
1366
1376
  // inheritance or if it doesn't exist, so we need to retry later
1367
- Outcome::Retry(None)
1377
+ Outcome::Retry {
1378
+ partial_ancestors: false,
1379
+ }
1368
1380
  }
1369
1381
  }
1370
1382
  } else {
1371
1383
  // Any constants at the top level are owned by Object
1372
- Outcome::Resolved(*OBJECT_ID, None)
1384
+ Outcome::Resolved(*OBJECT_ID)
1373
1385
  }
1374
1386
  }
1375
1387
 
@@ -1390,7 +1402,7 @@ impl<'a> Resolver<'a> {
1390
1402
  // promotes it correctly. Using nesting would incorrectly create `SomeModule::A`.
1391
1403
  let parent_owner_id = if parent_has_parent_scope {
1392
1404
  match self.name_owner_id(parent_scope, false) {
1393
- Outcome::Resolved(id, _) => id,
1405
+ Outcome::Resolved(id) => id,
1394
1406
  _ => self.create_todo_for_parent(parent_scope),
1395
1407
  }
1396
1408
  } else {
@@ -1398,8 +1410,8 @@ impl<'a> Resolver<'a> {
1398
1410
  };
1399
1411
 
1400
1412
  // Ensure we follow constant aliases if that's the parent
1401
- let parent_owner_id = match self.resolve_to_primary_namespace(parent_owner_id, None) {
1402
- Outcome::Resolved(id, _) => id,
1413
+ let parent_owner_id = match self.resolve_to_primary_namespace(parent_owner_id) {
1414
+ Outcome::Resolved(id) => id,
1403
1415
  _ => *OBJECT_ID,
1404
1416
  };
1405
1417
 
@@ -1428,16 +1440,14 @@ impl<'a> Resolver<'a> {
1428
1440
 
1429
1441
  /// Resolves a declaration ID through any alias chain to get the primary (first) namespace.
1430
1442
  /// Returns `Retry` if the primary alias target hasn't been resolved yet.
1431
- fn resolve_to_primary_namespace(
1432
- &self,
1433
- declaration_id: DeclarationId,
1434
- linearization: Option<DeclarationId>,
1435
- ) -> Outcome {
1443
+ fn resolve_to_primary_namespace(&self, declaration_id: DeclarationId) -> Outcome {
1436
1444
  let resolved_ids = self.resolve_alias_chains(declaration_id);
1437
1445
 
1438
1446
  // Get the primary (first) resolved target
1439
1447
  let Some(&primary_id) = resolved_ids.first() else {
1440
- return Outcome::Retry(None);
1448
+ return Outcome::Retry {
1449
+ partial_ancestors: false,
1450
+ };
1441
1451
  };
1442
1452
 
1443
1453
  // Check if the primary result is still an unresolved alias
@@ -1445,15 +1455,18 @@ impl<'a> Resolver<'a> {
1445
1455
  self.graph.declarations().get(&primary_id),
1446
1456
  Some(Declaration::ConstantAlias(_))
1447
1457
  ) {
1448
- return Outcome::Retry(None);
1458
+ return Outcome::Retry {
1459
+ partial_ancestors: false,
1460
+ };
1449
1461
  }
1450
1462
 
1451
- Outcome::Resolved(primary_id, linearization)
1463
+ Outcome::Resolved(primary_id)
1452
1464
  }
1453
1465
 
1454
1466
  /// Attempts to resolve a constant reference against the graph. Returns the fully qualified declaration ID that the
1455
1467
  /// reference is related to or `None`. This method mutates the graph to remember which constants have already been
1456
1468
  /// resolved
1469
+ #[allow(clippy::too_many_lines)]
1457
1470
  fn resolve_constant_internal(&mut self, name_id: NameId) -> Outcome {
1458
1471
  let name_ref = self.graph.names().get(&name_id).unwrap().clone();
1459
1472
 
@@ -1463,7 +1476,7 @@ impl<'a> Resolver<'a> {
1463
1476
  ParentScope::TopLevel => {
1464
1477
  let result = self.search_ancestors(*OBJECT_ID, *name.str());
1465
1478
 
1466
- if let Outcome::Resolved(declaration_id, _) = result {
1479
+ if let Outcome::Resolved(declaration_id) = result {
1467
1480
  self.graph.record_resolved_name(name_id, declaration_id);
1468
1481
  }
1469
1482
 
@@ -1471,7 +1484,9 @@ impl<'a> Resolver<'a> {
1471
1484
  }
1472
1485
  ParentScope::Attached(parent_scope_id) => {
1473
1486
  let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1474
- return Outcome::Retry(None);
1487
+ return Outcome::Retry {
1488
+ partial_ancestors: false,
1489
+ };
1475
1490
  };
1476
1491
 
1477
1492
  let mut target_decl_id = *parent_scope.declaration_id();
@@ -1485,13 +1500,15 @@ impl<'a> Resolver<'a> {
1485
1500
  if resolved_ids.iter().any(|id| {
1486
1501
  matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_)))
1487
1502
  }) {
1488
- return Outcome::Retry(None);
1503
+ return Outcome::Retry {
1504
+ partial_ancestors: false,
1505
+ };
1489
1506
  }
1490
1507
 
1491
1508
  let Some(&namespace_id) = resolved_ids.iter().find(|id| {
1492
1509
  matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_)))
1493
1510
  }) else {
1494
- return Outcome::Unresolved(None);
1511
+ return Outcome::Unresolved;
1495
1512
  };
1496
1513
 
1497
1514
  target_decl_id = namespace_id;
@@ -1499,17 +1516,21 @@ impl<'a> Resolver<'a> {
1499
1516
 
1500
1517
  // If we found a singleton reference with a resolved attached object parent scope, we
1501
1518
  // automatically create the singleton class
1502
- let Some(singleton_id) = self.get_or_create_singleton_class(target_decl_id, false) else {
1503
- return Outcome::Unresolved(None);
1519
+ let Some(singleton_id) =
1520
+ self.get_or_create_singleton_class(target_decl_id, SingletonAncestors::Enqueue)
1521
+ else {
1522
+ return Outcome::Unresolved;
1504
1523
  };
1505
1524
  self.graph.record_resolved_name(name_id, singleton_id);
1506
- Outcome::Resolved(singleton_id, Some(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)
1507
1528
  }
1508
1529
  ParentScope::None => {
1509
1530
  // Otherwise, it's a simple constant read and we can resolve it directly
1510
1531
  let result = self.run_resolution(&name);
1511
1532
 
1512
- if let Outcome::Resolved(declaration_id, _) = result {
1533
+ if let Outcome::Resolved(declaration_id) = result {
1513
1534
  self.graph.record_resolved_name(name_id, declaration_id);
1514
1535
  }
1515
1536
 
@@ -1517,7 +1538,9 @@ impl<'a> Resolver<'a> {
1517
1538
  }
1518
1539
  ParentScope::Some(parent_scope_id) => {
1519
1540
  let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1520
- return Outcome::Retry(None);
1541
+ return Outcome::Retry {
1542
+ partial_ancestors: false,
1543
+ };
1521
1544
  };
1522
1545
 
1523
1546
  // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
@@ -1525,29 +1548,36 @@ impl<'a> Resolver<'a> {
1525
1548
  let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1526
1549
 
1527
1550
  // Search each resolved target for the constant. Return early if found.
1528
- let mut missing_linearization_id = None;
1551
+ let mut missing_partial = false;
1529
1552
  let mut found_namespace = false;
1530
1553
 
1531
1554
  for &id in &resolved_ids {
1532
1555
  match self.graph.declarations().get(&id) {
1533
1556
  Some(Declaration::ConstantAlias(_)) => {
1534
1557
  // Alias not fully resolved yet
1535
- return Outcome::Retry(None);
1558
+ return Outcome::Retry {
1559
+ partial_ancestors: false,
1560
+ };
1536
1561
  }
1537
1562
  Some(Declaration::Namespace(_)) => {
1538
1563
  found_namespace = true;
1539
1564
 
1540
1565
  match self.search_ancestors(id, *name.str()) {
1541
- Outcome::Resolved(declaration_id, missing_linearization_id) => {
1566
+ Outcome::Resolved(declaration_id) => {
1542
1567
  self.graph.record_resolved_name(name_id, declaration_id);
1543
- return Outcome::Resolved(declaration_id, missing_linearization_id);
1568
+ return Outcome::Resolved(declaration_id);
1569
+ }
1570
+ Outcome::Retry {
1571
+ partial_ancestors: true,
1572
+ } => {
1573
+ missing_partial = true;
1544
1574
  }
1545
- Outcome::Retry(Some(needs_linearization_id))
1546
- | Outcome::Unresolved(Some(needs_linearization_id)) => {
1547
- missing_linearization_id.get_or_insert(needs_linearization_id);
1575
+ Outcome::Unresolved => {}
1576
+ Outcome::Retry {
1577
+ partial_ancestors: false,
1578
+ } => {
1579
+ unreachable!("search_ancestors never returns a non-partial Retry")
1548
1580
  }
1549
- Outcome::Unresolved(None) => {}
1550
- Outcome::Retry(_) => unreachable!("search_ancestors never returns Retry"),
1551
1581
  }
1552
1582
  }
1553
1583
  _ => {
@@ -1558,15 +1588,19 @@ impl<'a> Resolver<'a> {
1558
1588
 
1559
1589
  // If no namespaces were found, this constant path can never resolve.
1560
1590
  if !found_namespace {
1561
- return Outcome::Unresolved(None);
1591
+ return Outcome::Unresolved;
1562
1592
  }
1563
1593
 
1564
- // Member not found in any namespace yet - retry in case it's added later
1565
- missing_linearization_id.map_or(Outcome::Retry(None), |id| Outcome::Unresolved(Some(id)))
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,
1599
+ }
1566
1600
  }
1567
1601
  }
1568
1602
  }
1569
- NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id(), None),
1603
+ NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id()),
1570
1604
  }
1571
1605
  }
1572
1606
 
@@ -1639,13 +1673,15 @@ impl<'a> Resolver<'a> {
1639
1673
  let (ancestor_outcome, nesting_decl_id) = match self.graph.names().get(nesting).unwrap() {
1640
1674
  NameRef::Resolved(nesting_name_ref) => {
1641
1675
  let resolved_ids = self.resolve_alias_chains(*nesting_name_ref.declaration_id());
1642
- let mut result = Outcome::Unresolved(None);
1676
+ let mut result = Outcome::Unresolved;
1643
1677
  let mut decl_id = None;
1644
1678
 
1645
1679
  for &id in &resolved_ids {
1646
1680
  match self.graph.declarations().get(&id) {
1647
1681
  Some(Declaration::ConstantAlias(_)) => {
1648
- result = Outcome::Retry(None);
1682
+ result = Outcome::Retry {
1683
+ partial_ancestors: false,
1684
+ };
1649
1685
  break;
1650
1686
  }
1651
1687
  Some(Declaration::Namespace(_)) => {
@@ -1659,7 +1695,12 @@ impl<'a> Resolver<'a> {
1659
1695
 
1660
1696
  (result, decl_id)
1661
1697
  }
1662
- NameRef::Unresolved(_) => (Outcome::Retry(None), None),
1698
+ NameRef::Unresolved(_) => (
1699
+ Outcome::Retry {
1700
+ partial_ancestors: false,
1701
+ },
1702
+ None,
1703
+ ),
1663
1704
  };
1664
1705
 
1665
1706
  if matches!(ancestor_outcome, Outcome::Resolved(..)) {
@@ -1674,18 +1715,18 @@ impl<'a> Resolver<'a> {
1674
1715
  Some(Declaration::Namespace(Namespace::Module(_) | Namespace::Todo(_)))
1675
1716
  )
1676
1717
  });
1677
- let chain_incomplete = matches!(ancestor_outcome, Outcome::Retry(Some(_)) | Outcome::Unresolved(Some(_)));
1718
+ let chain_incomplete = matches!(
1719
+ ancestor_outcome,
1720
+ Outcome::Retry {
1721
+ partial_ancestors: true
1722
+ }
1723
+ );
1678
1724
 
1679
1725
  if is_module || chain_incomplete {
1680
1726
  let object_outcome = self.search_ancestors(*OBJECT_ID, str_id);
1681
1727
 
1682
- if let Outcome::Resolved(decl_id, _) = object_outcome {
1683
- // Preserve the linearization ID so the chain gets re-checked once complete
1684
- let linearization_id = match ancestor_outcome {
1685
- Outcome::Retry(id) | Outcome::Unresolved(id) => id,
1686
- Outcome::Resolved(..) => unreachable!("guarded by early return above"),
1687
- };
1688
- return Outcome::Resolved(decl_id, linearization_id);
1728
+ if let Outcome::Resolved(decl_id) = object_outcome {
1729
+ return Outcome::Resolved(decl_id);
1689
1730
  }
1690
1731
  }
1691
1732
 
@@ -1712,12 +1753,12 @@ impl<'a> Resolver<'a> {
1712
1753
  .as_namespace()
1713
1754
  .unwrap()
1714
1755
  .member(&str_id)
1715
- .map(|id| Outcome::Resolved(*id, None))
1756
+ .map(|id| Outcome::Resolved(*id))
1716
1757
  } else {
1717
1758
  None
1718
1759
  }
1719
1760
  })
1720
- .unwrap_or(Outcome::Unresolved(None)),
1761
+ .unwrap_or(Outcome::Unresolved),
1721
1762
  Ancestors::Partial(ids) => {
1722
1763
  for ancestor_id in ids {
1723
1764
  match ancestor_id {
@@ -1725,7 +1766,9 @@ impl<'a> Resolver<'a> {
1725
1766
  // Stop at unresolved ancestors to avoid resolving to a later one.
1726
1767
  // Skip if the name matches what we're searching for.
1727
1768
  if *self.graph.names().get(&name_id).unwrap().str() != str_id {
1728
- return Outcome::Retry(Some(declaration_id));
1769
+ return Outcome::Retry {
1770
+ partial_ancestors: true,
1771
+ };
1729
1772
  }
1730
1773
  }
1731
1774
  Ancestor::Complete(ancestor_id) => {
@@ -1738,12 +1781,14 @@ impl<'a> Resolver<'a> {
1738
1781
  .unwrap()
1739
1782
  .member(&str_id)
1740
1783
  {
1741
- return Outcome::Resolved(*id, Some(declaration_id));
1784
+ return Outcome::Resolved(*id);
1742
1785
  }
1743
1786
  }
1744
1787
  }
1745
1788
  }
1746
- Outcome::Unresolved(Some(declaration_id))
1789
+ Outcome::Retry {
1790
+ partial_ancestors: true,
1791
+ }
1747
1792
  }
1748
1793
  }
1749
1794
  }
@@ -1760,16 +1805,18 @@ impl<'a> Resolver<'a> {
1760
1805
  && let Some(namespace) = self.graph.declarations().get(&namespace_id).unwrap().as_namespace()
1761
1806
  && let Some(member) = namespace.member(&str_id)
1762
1807
  {
1763
- return Outcome::Resolved(*member, None);
1808
+ return Outcome::Resolved(*member);
1764
1809
  }
1765
1810
 
1766
1811
  current_name = nesting_name_ref.name();
1767
1812
  } else {
1768
- return Outcome::Retry(None);
1813
+ return Outcome::Retry {
1814
+ partial_ancestors: false,
1815
+ };
1769
1816
  }
1770
1817
  }
1771
1818
 
1772
- Outcome::Unresolved(None)
1819
+ Outcome::Unresolved
1773
1820
  }
1774
1821
 
1775
1822
  /// Returns a complexity score for a given name, which is used to sort names for resolution. The complexity is based
@@ -1980,7 +2027,7 @@ impl<'a> Resolver<'a> {
1980
2027
 
1981
2028
  let (inner_parent, partial) = self.singleton_parent_id(owner_id);
1982
2029
  (
1983
- self.get_or_create_singleton_class(inner_parent, false)
2030
+ self.get_or_create_singleton_class(inner_parent, SingletonAncestors::Deferred)
1984
2031
  .expect("singleton parent should always be a namespace"),
1985
2032
  partial,
1986
2033
  )
@@ -1991,7 +2038,7 @@ impl<'a> Resolver<'a> {
1991
2038
 
1992
2039
  let (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids);
1993
2040
  (
1994
- self.get_or_create_singleton_class(picked_parent, false)
2041
+ self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred)
1995
2042
  .expect("parent class should always be a namespace"),
1996
2043
  unresolved_parent.is_some(),
1997
2044
  )