@@ -4,54 +4,63 @@ use crate::{InferFailReason, LuaTypeDeclId};
44
55pub type InferGuardRef = Rc < InferGuard > ;
66
7- /// Guard to prevent infinite recursion
8- /// Some type may reference itself, so we need to check if we have already inferred this type
7+ /// Guard to prevent infinite recursion with optimized lazy allocation
98///
10- /// This guard supports inheritance through Rc parent chain, allowing child guards to see
11- /// parent's visited types while maintaining their own independent tracking for branch protection.
9+ /// This guard uses a lazy allocation strategy:
10+ /// - Fork is zero-cost (no HashSet allocation)
11+ /// - `current` HashSet is only created when needed (write-on-create)
12+ /// - Most child guards never allocate memory if they only read from parents
13+ ///
14+ /// # Memory Layout
15+ /// ```text
16+ /// Root: current=[A, B] parent=None
17+ /// |
18+ /// +-- Child1: current=None parent=Root (no allocation!)
19+ /// | |
20+ /// | +-- GrandChild: current=[C] parent=Child1 (allocated on first write)
21+ /// |
22+ /// +-- Child2: current=None parent=Root (no allocation!)
23+ /// ```
1224#[ derive( Debug , Clone ) ]
1325pub struct InferGuard {
14- /// Current level's visited types
15- current : RefCell < HashSet < LuaTypeDeclId > > ,
26+ /// Current level's visited types (lazily allocated)
27+ /// Only created when we need to add a new type not in parent chain
28+ current : RefCell < Option < HashSet < LuaTypeDeclId > > > ,
1629 /// Parent guard (shared reference)
1730 parent : Option < Rc < InferGuard > > ,
1831}
1932
2033impl InferGuard {
2134 pub fn new ( ) -> Rc < Self > {
2235 Rc :: new ( Self {
23- current : RefCell :: new ( HashSet :: default ( ) ) ,
36+ current : RefCell :: new ( None ) ,
2437 parent : None ,
2538 } )
2639 }
2740
2841 /// Create a child guard that inherits from parent
29- /// This allows branching while preventing infinite recursion across the entire call stack
42+ ///
43+ /// Zero-cost operation: no HashSet allocation until first write
3044 pub fn fork ( self : & Rc < Self > ) -> Rc < Self > {
3145 Rc :: new ( Self {
32- current : RefCell :: new ( HashSet :: default ( ) ) ,
46+ current : RefCell :: new ( None ) , // Lazy allocation
3347 parent : Some ( Rc :: clone ( self ) ) ,
3448 } )
3549 }
3650
37- /// Create a child guard from a non-Rc guard
38- /// This is a convenience method for when you have a stack-allocated guard
39- pub fn fork_owned ( & self ) -> Self {
40- Self {
41- current : RefCell :: new ( HashSet :: default ( ) ) ,
42- parent : self . parent . clone ( ) ,
43- }
44- }
45-
4651 /// Check if a type has been visited in current branch or any parent
4752 pub fn check ( & self , type_id : & LuaTypeDeclId ) -> Result < ( ) , InferFailReason > {
4853 // Check in all parent levels first
4954 if self . contains_in_parents ( type_id) {
5055 return Err ( InferFailReason :: RecursiveInfer ) ;
5156 }
5257
53- // Check in current level
54- let mut current = self . current . borrow_mut ( ) ;
58+ // Check in current level (if exists)
59+ let mut current_opt = self . current . borrow_mut ( ) ;
60+
61+ // Lazy allocation: create HashSet only when needed
62+ let current = current_opt. get_or_insert_with ( HashSet :: default) ;
63+
5564 if current. contains ( type_id) {
5665 return Err ( InferFailReason :: RecursiveInfer ) ;
5766 }
@@ -65,8 +74,10 @@ impl InferGuard {
6574 fn contains_in_parents ( & self , type_id : & LuaTypeDeclId ) -> bool {
6675 let mut current_parent = self . parent . as_ref ( ) ;
6776 while let Some ( parent) = current_parent {
68- if parent. current . borrow ( ) . contains ( type_id) {
69- return true ;
77+ if let Some ( ref set) = * parent. current . borrow ( ) {
78+ if set. contains ( type_id) {
79+ return true ;
80+ }
7081 }
7182 current_parent = parent. parent . as_ref ( ) ;
7283 }
@@ -75,20 +86,27 @@ impl InferGuard {
7586
7687 /// Check if a type has been visited (without modifying the guard)
7788 pub fn contains ( & self , type_id : & LuaTypeDeclId ) -> bool {
78- self . current . borrow ( ) . contains ( type_id) || self . contains_in_parents ( type_id)
89+ // Check current level
90+ if let Some ( ref set) = * self . current . borrow ( ) {
91+ if set. contains ( type_id) {
92+ return true ;
93+ }
94+ }
95+ // Check parents
96+ self . contains_in_parents ( type_id)
7997 }
8098
8199 /// Get the depth of current level
82100 pub fn current_depth ( & self ) -> usize {
83- self . current . borrow ( ) . len ( )
101+ self . current . borrow ( ) . as_ref ( ) . map_or ( 0 , |set| set . len ( ) )
84102 }
85103
86104 /// Get the total depth of the entire guard chain
87105 pub fn total_depth ( & self ) -> usize {
88- let mut depth = self . current . borrow ( ) . len ( ) ;
106+ let mut depth = self . current_depth ( ) ;
89107 let mut current_parent = self . parent . as_ref ( ) ;
90108 while let Some ( parent) = current_parent {
91- depth += parent. current . borrow ( ) . len ( ) ;
109+ depth += parent. current_depth ( ) ;
92110 current_parent = parent. parent . as_ref ( ) ;
93111 }
94112 depth
@@ -104,4 +122,121 @@ impl InferGuard {
104122 }
105123 level
106124 }
125+
126+ /// Check if current level has allocated memory
127+ /// Useful for debugging and performance analysis
128+ #[ cfg( test) ]
129+ pub fn has_allocated ( & self ) -> bool {
130+ self . current . borrow ( ) . is_some ( )
131+ }
132+ }
133+
134+ #[ cfg( test) ]
135+ mod tests {
136+ use super :: * ;
137+
138+ #[ test]
139+ fn test_lazy_allocation ( ) {
140+ let root = InferGuard :: new ( ) ;
141+ assert ! ( !root. has_allocated( ) , "New guard should not allocate" ) ;
142+
143+ // Fork should NOT allocate
144+ let child = root. fork ( ) ;
145+ assert ! ( !child. has_allocated( ) , "Fork should not allocate memory" ) ;
146+
147+ // Check on child should allocate
148+ let type_b = LuaTypeDeclId :: new ( "TestTypeB" ) ;
149+ child. check ( & type_b) . unwrap ( ) ;
150+ assert ! (
151+ child. has_allocated( ) ,
152+ "Check should trigger lazy allocation"
153+ ) ;
154+ assert ! ( !root. has_allocated( ) , "Root should not be affected" ) ;
155+ }
156+
157+ #[ test]
158+ fn test_fork_without_write ( ) {
159+ let root = InferGuard :: new ( ) ;
160+ let type_a = LuaTypeDeclId :: new ( "TestTypeA" ) ;
161+ root. check ( & type_a) . unwrap ( ) ;
162+
163+ // Create multiple forks
164+ let child1 = root. fork ( ) ;
165+ let child2 = root. fork ( ) ;
166+ let grandchild = child1. fork ( ) ;
167+
168+ // None of them should allocate if they don't write
169+ assert ! ( !child1. has_allocated( ) ) ;
170+ assert ! ( !child2. has_allocated( ) ) ;
171+ assert ! ( !grandchild. has_allocated( ) ) ;
172+
173+ // They should still see parent's types
174+ assert ! ( child1. contains( & type_a) ) ;
175+ assert ! ( child2. contains( & type_a) ) ;
176+ assert ! ( grandchild. contains( & type_a) ) ;
177+ }
178+
179+ #[ test]
180+ fn test_recursive_detection ( ) {
181+ let root = InferGuard :: new ( ) ;
182+ let type_a = LuaTypeDeclId :: new ( "TestTypeA" ) ;
183+
184+ // First check should succeed
185+ assert ! ( root. check( & type_a) . is_ok( ) ) ;
186+
187+ // Second check should fail (recursive)
188+ assert ! ( root. check( & type_a) . is_err( ) ) ;
189+ }
190+
191+ #[ test]
192+ fn test_parent_chain_detection ( ) {
193+ let root = InferGuard :: new ( ) ;
194+ let type_a = LuaTypeDeclId :: new ( "TestTypeA" ) ;
195+ let type_b = LuaTypeDeclId :: new ( "TestTypeB" ) ;
196+
197+ root. check ( & type_a) . unwrap ( ) ;
198+
199+ let child = root. fork ( ) ;
200+
201+ // Child should detect type_a from parent
202+ assert ! ( child. check( & type_a) . is_err( ) ) ;
203+
204+ // But can add type_b
205+ assert ! ( child. check( & type_b) . is_ok( ) ) ;
206+
207+ let grandchild = child. fork ( ) ;
208+
209+ // Grandchild should detect both
210+ assert ! ( grandchild. check( & type_a) . is_err( ) ) ;
211+ assert ! ( grandchild. check( & type_b) . is_err( ) ) ;
212+ }
213+
214+ #[ test]
215+ fn test_memory_efficiency ( ) {
216+ let root = InferGuard :: new ( ) ;
217+ let type_a = LuaTypeDeclId :: new ( "TestTypeA" ) ;
218+ root. check ( & type_a) . unwrap ( ) ;
219+
220+ // Create a deep fork chain
221+ let mut guards = vec ! [ root] ;
222+ for _ in 0 ..10 {
223+ let child = guards. last ( ) . unwrap ( ) . fork ( ) ;
224+ guards. push ( child) ;
225+ }
226+
227+ // Only root and last guard (if it wrote) should have allocation
228+ // All intermediate forks should have NO allocation
229+ for ( i, guard) in guards. iter ( ) . enumerate ( ) {
230+ if i == 0 {
231+ assert ! ( guard. has_allocated( ) , "Root should be allocated" ) ;
232+ } else if i < guards. len ( ) - 1 {
233+ // Intermediate nodes that didn't write
234+ assert ! (
235+ !guard. has_allocated( ) ,
236+ "Intermediate fork {} should not allocate" ,
237+ i
238+ ) ;
239+ }
240+ }
241+ }
107242}
0 commit comments