@@ -20,7 +20,7 @@ use crate::{
2020 AsObject , Py , PyObject , PyObjectRef , PyPayload , PyRef , PyResult ,
2121 builtins:: {
2222 self , PyBaseExceptionRef , PyDict , PyDictRef , PyInt , PyList , PyModule , PyStr , PyStrInterned ,
23- PyStrRef , PyTypeRef ,
23+ PyStrRef , PyTypeRef , PyWeak ,
2424 code:: PyCode ,
2525 dict:: { PyDictItems , PyDictKeys , PyDictValues } ,
2626 pystr:: AsPyStr ,
@@ -628,18 +628,25 @@ impl VirtualMachine {
628628 self . finalize_modules_delete_special ( ) ;
629629
630630 // Phase 2: Remove all modules from sys.modules (set values to None),
631- // and collect references to module dicts preserving import order.
632- // NOTE: CPython uses weakrefs here and relies on GC to collect cyclic garbage.
633- // Since RustPython's GC is a stub, we store strong dict refs to ensure
634- // phase 4 can clear them (breaking __globals__ cycles and triggering __del__).
635- let module_dicts = self . finalize_remove_modules ( ) ;
631+ // and collect weakrefs to modules preserving import order.
632+ // Also keeps strong refs (module_refs) to prevent premature deallocation.
633+ // CPython uses _PyGC_CollectNoFail() here to collect __globals__ cycles;
634+ // since RustPython has no working GC, we keep modules alive through
635+ // Phase 4 so their dicts can be explicitly cleared.
636+ let ( module_weakrefs, module_refs) = self . finalize_remove_modules ( ) ;
636637
637638 // Phase 3: Clear sys.modules dict
638639 self . finalize_clear_modules_dict ( ) ;
639640
640641 // Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
641- // This drops references to objects in module namespaces, triggering __del__.
642- self . finalize_clear_module_dicts ( & module_dicts) ;
642+ // All modules are still alive (held by module_refs), so all weakrefs are valid.
643+ // This breaks __globals__ cycles: dict entries set to None → functions freed →
644+ // __globals__ refs dropped → dict refcount decreases.
645+ self . finalize_clear_module_dicts ( & module_weakrefs) ;
646+
647+ // Drop strong refs → modules freed with already-cleared dicts.
648+ // No __globals__ cycles remain (broken by Phase 4).
649+ drop ( module_refs) ;
643650
644651 // Phase 5: Clear sys and builtins dicts last
645652 self . finalize_clear_sys_builtins_dict ( ) ;
@@ -685,16 +692,17 @@ impl VirtualMachine {
685692 let _ = self . builtins . dict ( ) . set_item ( "_" , none, self ) ;
686693 }
687694
688- /// Phase 2: Set all sys.modules values to None and collect module dicts.
689- /// Returns a list of (name, dict) preserving import order.
690- fn finalize_remove_modules ( & self ) -> Vec < ( String , PyDictRef ) > {
691- let mut module_dicts = Vec :: new ( ) ;
695+ /// Phase 2: Set all sys.modules values to None and collect weakrefs to modules.
696+ /// Returns (weakrefs for Phase 4, strong refs to keep modules alive).
697+ fn finalize_remove_modules ( & self ) -> ( Vec < ( String , PyRef < PyWeak > ) > , Vec < PyObjectRef > ) {
698+ let mut module_weakrefs = Vec :: new ( ) ;
699+ let mut module_refs = Vec :: new ( ) ;
692700
693701 let Ok ( modules) = self . sys_module . get_attr ( identifier ! ( self , modules) , self ) else {
694- return module_dicts ;
702+ return ( module_weakrefs , module_refs ) ;
695703 } ;
696704 let Some ( modules_dict) = modules. downcast_ref :: < PyDict > ( ) else {
697- return module_dicts ;
705+ return ( module_weakrefs , module_refs ) ;
698706 } ;
699707
700708 let none = self . ctx . none ( ) ;
@@ -706,16 +714,19 @@ impl VirtualMachine {
706714 . map ( |s| s. as_str ( ) . to_owned ( ) )
707715 . unwrap_or_default ( ) ;
708716
709- // Save reference to module dict for later clearing
710- if let Some ( module) = value. downcast_ref :: < PyModule > ( ) {
711- module_dicts. push ( ( name, module. dict ( ) ) ) ;
717+ // Save weakref and strong ref to module for later clearing
718+ if value. downcast_ref :: < PyModule > ( ) . is_some ( ) {
719+ if let Ok ( weak) = value. downgrade ( None , self ) {
720+ module_weakrefs. push ( ( name, weak) ) ;
721+ }
722+ module_refs. push ( value. clone ( ) ) ;
712723 }
713724
714725 // Set the value to None in sys.modules
715726 let _ = modules_dict. set_item ( & * key, none. clone ( ) , self ) ;
716727 }
717728
718- module_dicts
729+ ( module_weakrefs , module_refs )
719730 }
720731
721732 /// Phase 3: Clear sys.modules dict.
@@ -728,27 +739,37 @@ impl VirtualMachine {
728739 }
729740
730741 /// Phase 4: Clear module dicts in reverse import order.
742+ /// All modules are alive (held by module_refs from Phase 2).
731743 /// Skips builtins and sys (they are cleared last in phase 5).
732- fn finalize_clear_module_dicts ( & self , module_dicts : & [ ( String , PyDictRef ) ] ) {
744+ fn finalize_clear_module_dicts ( & self , module_weakrefs : & [ ( String , PyRef < PyWeak > ) ] ) {
733745 let sys_dict = self . sys_module . dict ( ) ;
734746 let builtins_dict = self . builtins . dict ( ) ;
735747
736748 // Iterate in reverse (last imported → first cleared)
737- for ( _name, dict) in module_dicts. iter ( ) . rev ( ) {
749+ for ( _name, weakref) in module_weakrefs. iter ( ) . rev ( ) {
750+ // Try to upgrade weakref - skip if module was already freed
751+ let Some ( module_obj) = weakref. upgrade ( ) else {
752+ continue ;
753+ } ;
754+ let Some ( module) = module_obj. downcast_ref :: < PyModule > ( ) else {
755+ continue ;
756+ } ;
757+ let module_dict = module. dict ( ) ;
758+
738759 // Skip builtins and sys dicts (cleared last in phase 5)
739- if dict . is ( & sys_dict) || dict . is ( & builtins_dict) {
760+ if module_dict . is ( & sys_dict) || module_dict . is ( & builtins_dict) {
740761 continue ;
741762 }
742763
743764 // 2-pass clearing
744- Self :: module_clear_dict ( dict , self ) ;
765+ Self :: module_clear_dict ( & module_dict , self ) ;
745766 }
746767 }
747768
748769 /// 2-pass module dict clearing (_PyModule_ClearDict algorithm).
749770 /// Pass 1: Set names starting with '_' (except __builtins__) to None.
750771 /// Pass 2: Set all remaining names (except __builtins__) to None.
751- fn module_clear_dict ( dict : & Py < PyDict > , vm : & VirtualMachine ) {
772+ pub ( crate ) fn module_clear_dict ( dict : & Py < PyDict > , vm : & VirtualMachine ) {
752773 let none = vm. ctx . none ( ) ;
753774
754775 // Pass 1: names starting with '_' (except __builtins__)
0 commit comments