rubydex 0.2.7 → 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
@@ -845,19 +853,20 @@ impl<'a> Resolver<'a> {
845
853
  /// If the declaration is a `Constant` with all-promotable definitions, it is automatically promoted to a `Class`
846
854
  /// namespace before creating the singleton. Returns `None` if the declaration is not a namespace and cannot be
847
855
  /// promoted (e.g., `FOO = 42`).
848
- /// When `eager_ancestors` is `true`, ancestor chains are linearized inline (used after the convergence loop when all
849
- /// 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`].
850
859
  fn get_or_create_singleton_class(
851
860
  &mut self,
852
861
  attached_id: DeclarationId,
853
- eager_ancestors: bool,
862
+ mode: SingletonAncestors,
854
863
  ) -> Option<DeclarationId> {
855
864
  let attached_decl = self.graph.declarations().get(&attached_id).unwrap();
856
865
 
857
866
  // If the attached object is a constant alias, follow the alias chain to find the actual namespace
858
867
  if matches!(attached_decl, Declaration::ConstantAlias(_)) {
859
868
  return match self.resolve_to_namespace(attached_id) {
860
- Some(id) => self.get_or_create_singleton_class(id, eager_ancestors),
869
+ Some(id) => self.get_or_create_singleton_class(id, mode),
861
870
  None => None,
862
871
  };
863
872
  }
@@ -868,11 +877,7 @@ impl<'a> Resolver<'a> {
868
877
  Declaration::Namespace(Namespace::Module(Box::new(ModuleDeclaration::new(name, owner_id))))
869
878
  });
870
879
 
871
- if eager_ancestors {
872
- let _ = self.ancestors_of(attached_id);
873
- } else {
874
- self.unit_queue.push_back(Unit::Ancestors(attached_id));
875
- }
880
+ self.schedule_singleton_ancestors(attached_id, mode);
876
881
  } else {
877
882
  return None;
878
883
  }
@@ -900,15 +905,26 @@ impl<'a> Resolver<'a> {
900
905
  )))),
901
906
  );
902
907
 
903
- if eager_ancestors {
904
- let _ = self.ancestors_of(decl_id);
905
- } else {
906
- self.unit_queue.push_back(Unit::Ancestors(decl_id));
907
- }
908
+ self.schedule_singleton_ancestors(decl_id, mode);
908
909
 
909
910
  Some(decl_id)
910
911
  }
911
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
+
912
928
  /// Linearizes the ancestors of a declaration, returning the list of ancestor declaration IDs
913
929
  ///
914
930
  /// # Panics
@@ -1011,7 +1027,7 @@ impl<'a> Resolver<'a> {
1011
1027
 
1012
1028
  // Ensure that we create the singleton and enqueue it for linearization if we see an extend
1013
1029
  if has_extends && !is_singleton_class {
1014
- self.get_or_create_singleton_class(declaration_id, false);
1030
+ self.get_or_create_singleton_class(declaration_id, SingletonAncestors::Enqueue);
1015
1031
  }
1016
1032
 
1017
1033
  let (linearized_prepends, linearized_includes) =
@@ -1251,15 +1267,17 @@ impl<'a> Resolver<'a> {
1251
1267
  // `set_singleton_class_id`, not `add_member`, so a TODO receiver would never gain a
1252
1268
  // member. Emit Retry so the unit is preserved for a later resolve where the receiver
1253
1269
  // may exist.
1254
- Outcome::Unresolved(None) if singleton => Outcome::Retry(None),
1255
- 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)),
1256
1274
  other => other,
1257
1275
  };
1258
1276
 
1259
1277
  // The name of the declaration is determined by the name of its owner, which may or may not require resolution
1260
1278
  // depending on whether the name has a parent scope
1261
1279
  match outcome {
1262
- Outcome::Resolved(owner_id, id_needing_linearization) => {
1280
+ Outcome::Resolved(owner_id) => {
1263
1281
  let mut fully_qualified_name = self.graph.strings().get(&str_id).unwrap().to_string();
1264
1282
 
1265
1283
  // If the owner is a promotable constant and something is being defined inside it, promote it to a
@@ -1284,7 +1302,7 @@ impl<'a> Resolver<'a> {
1284
1302
  // Foo = 1
1285
1303
  // class << Foo; end
1286
1304
  if singleton && !owner_is_namespace {
1287
- return Outcome::Unresolved(None);
1305
+ return Outcome::Unresolved;
1288
1306
  }
1289
1307
 
1290
1308
  // We don't prefix declarations with `Object::`
@@ -1314,7 +1332,7 @@ impl<'a> Resolver<'a> {
1314
1332
  }
1315
1333
 
1316
1334
  self.graph.record_resolved_name(name_id, declaration_id);
1317
- Outcome::Resolved(declaration_id, id_needing_linearization)
1335
+ Outcome::Resolved(declaration_id)
1318
1336
  }
1319
1337
  other => other,
1320
1338
  }
@@ -1334,12 +1352,14 @@ impl<'a> Resolver<'a> {
1334
1352
  // If we have `A::B`, the owner of `B` is whatever `A` resolves to.
1335
1353
  // If `A` is an alias, resolve through to get the actual namespace.
1336
1354
  match self.resolve_constant_internal(parent_scope) {
1337
- Outcome::Resolved(id, linearization) => self.resolve_to_primary_namespace(id, linearization),
1355
+ Outcome::Resolved(id) => self.resolve_to_primary_namespace(id),
1338
1356
  // The parent scope is genuinely unknown — not a circular alias or pending
1339
1357
  // linearization, but a name that doesn't exist anywhere in the graph.
1340
- Outcome::Unresolved(None) => Outcome::Unresolved(None),
1341
- Outcome::Retry(None) if !preserve_retry => Outcome::Unresolved(None),
1342
- 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,
1343
1363
  }
1344
1364
  } else if let Some(nesting_id) = name_ref.nesting()
1345
1365
  && !name_ref.parent_scope().is_top_level()
@@ -1350,16 +1370,18 @@ impl<'a> Resolver<'a> {
1350
1370
  // end
1351
1371
  // If `ALIAS` points to `Outer`, `CONST` should be owned by `Outer::Target`, not `ALIAS::Target`.
1352
1372
  match self.graph.names().get(nesting_id).unwrap() {
1353
- NameRef::Resolved(resolved) => self.resolve_to_primary_namespace(*resolved.declaration_id(), None),
1373
+ NameRef::Resolved(resolved) => self.resolve_to_primary_namespace(*resolved.declaration_id()),
1354
1374
  NameRef::Unresolved(_) => {
1355
1375
  // The only case where we wouldn't have the nesting resolved at this point is if it's available through
1356
1376
  // inheritance or if it doesn't exist, so we need to retry later
1357
- Outcome::Retry(None)
1377
+ Outcome::Retry {
1378
+ partial_ancestors: false,
1379
+ }
1358
1380
  }
1359
1381
  }
1360
1382
  } else {
1361
1383
  // Any constants at the top level are owned by Object
1362
- Outcome::Resolved(*OBJECT_ID, None)
1384
+ Outcome::Resolved(*OBJECT_ID)
1363
1385
  }
1364
1386
  }
1365
1387
 
@@ -1380,7 +1402,7 @@ impl<'a> Resolver<'a> {
1380
1402
  // promotes it correctly. Using nesting would incorrectly create `SomeModule::A`.
1381
1403
  let parent_owner_id = if parent_has_parent_scope {
1382
1404
  match self.name_owner_id(parent_scope, false) {
1383
- Outcome::Resolved(id, _) => id,
1405
+ Outcome::Resolved(id) => id,
1384
1406
  _ => self.create_todo_for_parent(parent_scope),
1385
1407
  }
1386
1408
  } else {
@@ -1388,8 +1410,8 @@ impl<'a> Resolver<'a> {
1388
1410
  };
1389
1411
 
1390
1412
  // Ensure we follow constant aliases if that's the parent
1391
- let parent_owner_id = match self.resolve_to_primary_namespace(parent_owner_id, None) {
1392
- Outcome::Resolved(id, _) => id,
1413
+ let parent_owner_id = match self.resolve_to_primary_namespace(parent_owner_id) {
1414
+ Outcome::Resolved(id) => id,
1393
1415
  _ => *OBJECT_ID,
1394
1416
  };
1395
1417
 
@@ -1418,16 +1440,14 @@ impl<'a> Resolver<'a> {
1418
1440
 
1419
1441
  /// Resolves a declaration ID through any alias chain to get the primary (first) namespace.
1420
1442
  /// Returns `Retry` if the primary alias target hasn't been resolved yet.
1421
- fn resolve_to_primary_namespace(
1422
- &self,
1423
- declaration_id: DeclarationId,
1424
- linearization: Option<DeclarationId>,
1425
- ) -> Outcome {
1443
+ fn resolve_to_primary_namespace(&self, declaration_id: DeclarationId) -> Outcome {
1426
1444
  let resolved_ids = self.resolve_alias_chains(declaration_id);
1427
1445
 
1428
1446
  // Get the primary (first) resolved target
1429
1447
  let Some(&primary_id) = resolved_ids.first() else {
1430
- return Outcome::Retry(None);
1448
+ return Outcome::Retry {
1449
+ partial_ancestors: false,
1450
+ };
1431
1451
  };
1432
1452
 
1433
1453
  // Check if the primary result is still an unresolved alias
@@ -1435,15 +1455,18 @@ impl<'a> Resolver<'a> {
1435
1455
  self.graph.declarations().get(&primary_id),
1436
1456
  Some(Declaration::ConstantAlias(_))
1437
1457
  ) {
1438
- return Outcome::Retry(None);
1458
+ return Outcome::Retry {
1459
+ partial_ancestors: false,
1460
+ };
1439
1461
  }
1440
1462
 
1441
- Outcome::Resolved(primary_id, linearization)
1463
+ Outcome::Resolved(primary_id)
1442
1464
  }
1443
1465
 
1444
1466
  /// Attempts to resolve a constant reference against the graph. Returns the fully qualified declaration ID that the
1445
1467
  /// reference is related to or `None`. This method mutates the graph to remember which constants have already been
1446
1468
  /// resolved
1469
+ #[allow(clippy::too_many_lines)]
1447
1470
  fn resolve_constant_internal(&mut self, name_id: NameId) -> Outcome {
1448
1471
  let name_ref = self.graph.names().get(&name_id).unwrap().clone();
1449
1472
 
@@ -1453,7 +1476,7 @@ impl<'a> Resolver<'a> {
1453
1476
  ParentScope::TopLevel => {
1454
1477
  let result = self.search_ancestors(*OBJECT_ID, *name.str());
1455
1478
 
1456
- if let Outcome::Resolved(declaration_id, _) = result {
1479
+ if let Outcome::Resolved(declaration_id) = result {
1457
1480
  self.graph.record_resolved_name(name_id, declaration_id);
1458
1481
  }
1459
1482
 
@@ -1461,7 +1484,9 @@ impl<'a> Resolver<'a> {
1461
1484
  }
1462
1485
  ParentScope::Attached(parent_scope_id) => {
1463
1486
  let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1464
- return Outcome::Retry(None);
1487
+ return Outcome::Retry {
1488
+ partial_ancestors: false,
1489
+ };
1465
1490
  };
1466
1491
 
1467
1492
  let mut target_decl_id = *parent_scope.declaration_id();
@@ -1475,13 +1500,15 @@ impl<'a> Resolver<'a> {
1475
1500
  if resolved_ids.iter().any(|id| {
1476
1501
  matches!(self.graph.declarations().get(id), Some(Declaration::ConstantAlias(_)))
1477
1502
  }) {
1478
- return Outcome::Retry(None);
1503
+ return Outcome::Retry {
1504
+ partial_ancestors: false,
1505
+ };
1479
1506
  }
1480
1507
 
1481
1508
  let Some(&namespace_id) = resolved_ids.iter().find(|id| {
1482
1509
  matches!(self.graph.declarations().get(id), Some(Declaration::Namespace(_)))
1483
1510
  }) else {
1484
- return Outcome::Unresolved(None);
1511
+ return Outcome::Unresolved;
1485
1512
  };
1486
1513
 
1487
1514
  target_decl_id = namespace_id;
@@ -1489,17 +1516,21 @@ impl<'a> Resolver<'a> {
1489
1516
 
1490
1517
  // If we found a singleton reference with a resolved attached object parent scope, we
1491
1518
  // automatically create the singleton class
1492
- let Some(singleton_id) = self.get_or_create_singleton_class(target_decl_id, false) else {
1493
- 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;
1494
1523
  };
1495
1524
  self.graph.record_resolved_name(name_id, singleton_id);
1496
- 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)
1497
1528
  }
1498
1529
  ParentScope::None => {
1499
1530
  // Otherwise, it's a simple constant read and we can resolve it directly
1500
1531
  let result = self.run_resolution(&name);
1501
1532
 
1502
- if let Outcome::Resolved(declaration_id, _) = result {
1533
+ if let Outcome::Resolved(declaration_id) = result {
1503
1534
  self.graph.record_resolved_name(name_id, declaration_id);
1504
1535
  }
1505
1536
 
@@ -1507,7 +1538,9 @@ impl<'a> Resolver<'a> {
1507
1538
  }
1508
1539
  ParentScope::Some(parent_scope_id) => {
1509
1540
  let NameRef::Resolved(parent_scope) = self.graph.names().get(parent_scope_id).unwrap() else {
1510
- return Outcome::Retry(None);
1541
+ return Outcome::Retry {
1542
+ partial_ancestors: false,
1543
+ };
1511
1544
  };
1512
1545
 
1513
1546
  // Resolve the namespace in case it's an alias (e.g., ALIAS::CONST where ALIAS = Foo)
@@ -1515,29 +1548,36 @@ impl<'a> Resolver<'a> {
1515
1548
  let resolved_ids = self.resolve_alias_chains(*parent_scope.declaration_id());
1516
1549
 
1517
1550
  // Search each resolved target for the constant. Return early if found.
1518
- let mut missing_linearization_id = None;
1551
+ let mut missing_partial = false;
1519
1552
  let mut found_namespace = false;
1520
1553
 
1521
1554
  for &id in &resolved_ids {
1522
1555
  match self.graph.declarations().get(&id) {
1523
1556
  Some(Declaration::ConstantAlias(_)) => {
1524
1557
  // Alias not fully resolved yet
1525
- return Outcome::Retry(None);
1558
+ return Outcome::Retry {
1559
+ partial_ancestors: false,
1560
+ };
1526
1561
  }
1527
1562
  Some(Declaration::Namespace(_)) => {
1528
1563
  found_namespace = true;
1529
1564
 
1530
1565
  match self.search_ancestors(id, *name.str()) {
1531
- Outcome::Resolved(declaration_id, missing_linearization_id) => {
1566
+ Outcome::Resolved(declaration_id) => {
1532
1567
  self.graph.record_resolved_name(name_id, declaration_id);
1533
- 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;
1534
1574
  }
1535
- Outcome::Retry(Some(needs_linearization_id))
1536
- | Outcome::Unresolved(Some(needs_linearization_id)) => {
1537
- 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")
1538
1580
  }
1539
- Outcome::Unresolved(None) => {}
1540
- Outcome::Retry(_) => unreachable!("search_ancestors never returns Retry"),
1541
1581
  }
1542
1582
  }
1543
1583
  _ => {
@@ -1548,15 +1588,19 @@ impl<'a> Resolver<'a> {
1548
1588
 
1549
1589
  // If no namespaces were found, this constant path can never resolve.
1550
1590
  if !found_namespace {
1551
- return Outcome::Unresolved(None);
1591
+ return Outcome::Unresolved;
1552
1592
  }
1553
1593
 
1554
- // Member not found in any namespace yet - retry in case it's added later
1555
- 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
+ }
1556
1600
  }
1557
1601
  }
1558
1602
  }
1559
- NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id(), None),
1603
+ NameRef::Resolved(resolved) => Outcome::Resolved(*resolved.declaration_id()),
1560
1604
  }
1561
1605
  }
1562
1606
 
@@ -1629,13 +1673,15 @@ impl<'a> Resolver<'a> {
1629
1673
  let (ancestor_outcome, nesting_decl_id) = match self.graph.names().get(nesting).unwrap() {
1630
1674
  NameRef::Resolved(nesting_name_ref) => {
1631
1675
  let resolved_ids = self.resolve_alias_chains(*nesting_name_ref.declaration_id());
1632
- let mut result = Outcome::Unresolved(None);
1676
+ let mut result = Outcome::Unresolved;
1633
1677
  let mut decl_id = None;
1634
1678
 
1635
1679
  for &id in &resolved_ids {
1636
1680
  match self.graph.declarations().get(&id) {
1637
1681
  Some(Declaration::ConstantAlias(_)) => {
1638
- result = Outcome::Retry(None);
1682
+ result = Outcome::Retry {
1683
+ partial_ancestors: false,
1684
+ };
1639
1685
  break;
1640
1686
  }
1641
1687
  Some(Declaration::Namespace(_)) => {
@@ -1649,7 +1695,12 @@ impl<'a> Resolver<'a> {
1649
1695
 
1650
1696
  (result, decl_id)
1651
1697
  }
1652
- NameRef::Unresolved(_) => (Outcome::Retry(None), None),
1698
+ NameRef::Unresolved(_) => (
1699
+ Outcome::Retry {
1700
+ partial_ancestors: false,
1701
+ },
1702
+ None,
1703
+ ),
1653
1704
  };
1654
1705
 
1655
1706
  if matches!(ancestor_outcome, Outcome::Resolved(..)) {
@@ -1664,18 +1715,18 @@ impl<'a> Resolver<'a> {
1664
1715
  Some(Declaration::Namespace(Namespace::Module(_) | Namespace::Todo(_)))
1665
1716
  )
1666
1717
  });
1667
- 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
+ );
1668
1724
 
1669
1725
  if is_module || chain_incomplete {
1670
1726
  let object_outcome = self.search_ancestors(*OBJECT_ID, str_id);
1671
1727
 
1672
- if let Outcome::Resolved(decl_id, _) = object_outcome {
1673
- // Preserve the linearization ID so the chain gets re-checked once complete
1674
- let linearization_id = match ancestor_outcome {
1675
- Outcome::Retry(id) | Outcome::Unresolved(id) => id,
1676
- Outcome::Resolved(..) => unreachable!("guarded by early return above"),
1677
- };
1678
- return Outcome::Resolved(decl_id, linearization_id);
1728
+ if let Outcome::Resolved(decl_id) = object_outcome {
1729
+ return Outcome::Resolved(decl_id);
1679
1730
  }
1680
1731
  }
1681
1732
 
@@ -1702,12 +1753,12 @@ impl<'a> Resolver<'a> {
1702
1753
  .as_namespace()
1703
1754
  .unwrap()
1704
1755
  .member(&str_id)
1705
- .map(|id| Outcome::Resolved(*id, None))
1756
+ .map(|id| Outcome::Resolved(*id))
1706
1757
  } else {
1707
1758
  None
1708
1759
  }
1709
1760
  })
1710
- .unwrap_or(Outcome::Unresolved(None)),
1761
+ .unwrap_or(Outcome::Unresolved),
1711
1762
  Ancestors::Partial(ids) => {
1712
1763
  for ancestor_id in ids {
1713
1764
  match ancestor_id {
@@ -1715,7 +1766,9 @@ impl<'a> Resolver<'a> {
1715
1766
  // Stop at unresolved ancestors to avoid resolving to a later one.
1716
1767
  // Skip if the name matches what we're searching for.
1717
1768
  if *self.graph.names().get(&name_id).unwrap().str() != str_id {
1718
- return Outcome::Retry(Some(declaration_id));
1769
+ return Outcome::Retry {
1770
+ partial_ancestors: true,
1771
+ };
1719
1772
  }
1720
1773
  }
1721
1774
  Ancestor::Complete(ancestor_id) => {
@@ -1728,12 +1781,14 @@ impl<'a> Resolver<'a> {
1728
1781
  .unwrap()
1729
1782
  .member(&str_id)
1730
1783
  {
1731
- return Outcome::Resolved(*id, Some(declaration_id));
1784
+ return Outcome::Resolved(*id);
1732
1785
  }
1733
1786
  }
1734
1787
  }
1735
1788
  }
1736
- Outcome::Unresolved(Some(declaration_id))
1789
+ Outcome::Retry {
1790
+ partial_ancestors: true,
1791
+ }
1737
1792
  }
1738
1793
  }
1739
1794
  }
@@ -1750,16 +1805,18 @@ impl<'a> Resolver<'a> {
1750
1805
  && let Some(namespace) = self.graph.declarations().get(&namespace_id).unwrap().as_namespace()
1751
1806
  && let Some(member) = namespace.member(&str_id)
1752
1807
  {
1753
- return Outcome::Resolved(*member, None);
1808
+ return Outcome::Resolved(*member);
1754
1809
  }
1755
1810
 
1756
1811
  current_name = nesting_name_ref.name();
1757
1812
  } else {
1758
- return Outcome::Retry(None);
1813
+ return Outcome::Retry {
1814
+ partial_ancestors: false,
1815
+ };
1759
1816
  }
1760
1817
  }
1761
1818
 
1762
- Outcome::Unresolved(None)
1819
+ Outcome::Unresolved
1763
1820
  }
1764
1821
 
1765
1822
  /// Returns a complexity score for a given name, which is used to sort names for resolution. The complexity is based
@@ -1970,7 +2027,7 @@ impl<'a> Resolver<'a> {
1970
2027
 
1971
2028
  let (inner_parent, partial) = self.singleton_parent_id(owner_id);
1972
2029
  (
1973
- self.get_or_create_singleton_class(inner_parent, false)
2030
+ self.get_or_create_singleton_class(inner_parent, SingletonAncestors::Deferred)
1974
2031
  .expect("singleton parent should always be a namespace"),
1975
2032
  partial,
1976
2033
  )
@@ -1981,7 +2038,7 @@ impl<'a> Resolver<'a> {
1981
2038
 
1982
2039
  let (picked_parent, unresolved_parent) = self.get_parent_class(&definition_ids);
1983
2040
  (
1984
- self.get_or_create_singleton_class(picked_parent, false)
2041
+ self.get_or_create_singleton_class(picked_parent, SingletonAncestors::Deferred)
1985
2042
  .expect("parent class should always be a namespace"),
1986
2043
  unresolved_parent.is_some(),
1987
2044
  )