@@ -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. as_ref( ) == 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,38 @@ 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. Bootstrap declarations (Object, Module, Class) are never
366+ /// removable. Singleton classes delegate to `Namespace::is_empty` — they
367+ /// are only removable when they have no definitions AND no members
368+ /// (populated singletons serve as namespace parents for class-level methods).
369+ #[ must_use]
370+ pub fn has_no_backing_definitions ( & self , decl_id : & DeclarationId ) -> bool {
371+ if * decl_id == * super :: graph:: OBJECT_ID
372+ || * decl_id == * super :: graph:: MODULE_ID
373+ || * decl_id == * super :: graph:: CLASS_ID
374+ {
375+ return false ;
376+ }
377+
378+ if let Some ( singleton_class) = self . as_singleton_class ( ) {
379+ return singleton_class. is_empty ( ) ;
380+ }
381+
382+ self . has_no_definitions ( )
383+ }
384+
385+ /// Returns true if this is an empty singleton class eligible for cleanup.
386+ /// Checks structural emptiness via `Namespace::is_empty` (no definitions,
387+ /// no members) plus no constant references (which would cause re-creation
388+ /// cycles if the singleton were removed). The caller must additionally
389+ /// verify there are no live descendants before actually removing.
390+ #[ must_use]
391+ pub fn is_removable_singleton ( & self ) -> bool {
392+ self . as_singleton_class ( )
393+ . is_some_and ( |singleton_class| singleton_class. is_empty ( ) && singleton_class. references ( ) . is_empty ( ) )
394+ }
395+
348396 pub fn add_definition ( & mut self , definition_id : DefinitionId ) {
349397 all_declarations ! ( self , it => {
350398 debug_assert!(
@@ -466,6 +514,12 @@ impl Namespace {
466514 }
467515 }
468516
517+ /// Returns true if this namespace has no definitions and no members.
518+ #[ must_use]
519+ pub fn is_empty ( & self ) -> bool {
520+ all_namespaces ! ( self , it => it. definition_ids. is_empty( ) && it. members. is_empty( ) )
521+ }
522+
469523 #[ must_use]
470524 pub fn references ( & self ) -> & IdentityHashSet < ConstantReferenceId > {
471525 all_namespaces ! ( self , it => & it. references)
@@ -578,6 +632,10 @@ impl Namespace {
578632 all_namespaces ! ( self , it => it. set_singleton_class_id( declaration_id) ) ;
579633 }
580634
635+ pub fn clear_singleton_class_id ( & mut self , expected : & DeclarationId ) {
636+ all_namespaces ! ( self , it => it. clear_singleton_class_id( expected) ) ;
637+ }
638+
581639 #[ must_use]
582640 pub fn owner_id ( & self ) -> & DeclarationId {
583641 all_namespaces ! ( self , it => & it. owner_id)
0 commit comments