Skip to content

Commit 79ccbcf

Browse files
committed
Defer cleanup of emptied and synthetic declarations during invalidation
After incremental invalidation, declarations can lose all their definitions. Previously these were removed immediately, destroying accumulated state (singleton links, member lists) the resolver needs. This defers TODO cleanup to after resolution via `cleanup_stale_todos()`. TODOs created speculatively by `create_todo_for_parent` are marked via `defer_todo_cleanup` and evaluated post-resolution — promoted TODOs and those with members are kept, empty stubs are removed. Singleton classes use references and members as "still in use" signals in `has_no_backing_definitions`, preventing premature removal when callers (Foo.new) or class-level state (@x = 1) still exist. Other empty declarations (from file deletion or member removal) cascade immediately via the invalidation worklist.
1 parent ff3eaf4 commit 79ccbcf

3 files changed

Lines changed: 595 additions & 23 deletions

File tree

rust/rubydex/src/model/declaration.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ macro_rules! namespace_declaration {
162162
self.singleton_class_id = Some(declaration_id);
163163
}
164164

165+
/// Clears `singleton_class_id` only if it matches `expected`.
166+
/// Prevents accidentally clearing a pointer to a re-created singleton.
167+
pub fn clear_singleton_class_id(&mut self, expected: DeclarationId) {
168+
if self.singleton_class_id == Some(expected) {
169+
self.singleton_class_id = None;
170+
}
171+
}
172+
165173
pub fn singleton_class_id(&self) -> Option<&DeclarationId> {
166174
self.singleton_class_id.as_ref()
167175
}
@@ -327,6 +335,14 @@ impl Declaration {
327335
}
328336
}
329337

338+
#[must_use]
339+
pub fn as_singleton_class(&self) -> Option<&Namespace> {
340+
match self {
341+
Declaration::Namespace(ns @ Namespace::SingletonClass(_)) => Some(ns),
342+
_ => None,
343+
}
344+
}
345+
330346
#[must_use]
331347
pub fn as_namespace_mut(&mut self) -> Option<&mut Namespace> {
332348
match self {
@@ -345,6 +361,19 @@ impl Declaration {
345361
all_declarations!(self, it => it.definition_ids.is_empty())
346362
}
347363

364+
/// Returns true if this declaration has no backing definitions and is eligible
365+
/// for removal. Singleton classes are only removable when they have no
366+
/// definitions, no members, and no constant references — any of these
367+
/// signals means the singleton is still in use.
368+
#[must_use]
369+
pub fn has_no_backing_definitions(&self) -> bool {
370+
if let Some(singleton_class) = self.as_singleton_class() {
371+
return singleton_class.is_empty() && singleton_class.references().is_empty();
372+
}
373+
374+
self.has_no_definitions()
375+
}
376+
348377
pub fn add_definition(&mut self, definition_id: DefinitionId) {
349378
all_declarations!(self, it => {
350379
debug_assert!(
@@ -466,6 +495,12 @@ impl Namespace {
466495
}
467496
}
468497

498+
/// Returns true if this namespace has no definitions and no members.
499+
#[must_use]
500+
pub fn is_empty(&self) -> bool {
501+
all_namespaces!(self, it => it.definition_ids.is_empty() && it.members.is_empty())
502+
}
503+
469504
#[must_use]
470505
pub fn references(&self) -> &IdentityHashSet<ConstantReferenceId> {
471506
all_namespaces!(self, it => &it.references)
@@ -578,6 +613,10 @@ impl Namespace {
578613
all_namespaces!(self, it => it.set_singleton_class_id(declaration_id));
579614
}
580615

616+
pub fn clear_singleton_class_id(&mut self, expected: DeclarationId) {
617+
all_namespaces!(self, it => it.clear_singleton_class_id(expected));
618+
}
619+
581620
#[must_use]
582621
pub fn owner_id(&self) -> &DeclarationId {
583622
all_namespaces!(self, it => &it.owner_id)

0 commit comments

Comments
 (0)