@@ -7,6 +7,7 @@ use std::cell::{Cell, RefCell};
77use std:: collections:: { HashMap , HashSet } ;
88
99use crate :: Backend ;
10+ use crate :: atom:: { Atom , atom} ;
1011use crate :: php_type:: PhpType ;
1112use crate :: types:: * ;
1213
@@ -26,6 +27,9 @@ type BodyReturnInferrerFn = Box<dyn Fn(&str, &MethodInfo) -> Option<PhpType>>;
2627/// down.
2728type AuthUserResolverFn = Box < dyn Fn ( Option < & str > ) -> Option < PhpType > > ;
2829
30+ /// Memoized body-return-inference results, keyed by `(FQN, method)`.
31+ type BodyInferMemo = HashMap < ( Atom , Atom ) , Option < PhpType > > ;
32+
2933thread_local ! {
3034 /// When `Some`, `resolve_instance_method_callable` caches results
3135 /// by `"FQN::method_lower"`. Activated by
@@ -44,14 +48,14 @@ thread_local! {
4448 const { RefCell :: new( None ) } ;
4549
4650 /// Re-entry guard for body return inference. Tracks
47- /// `" FQN:: method" ` keys currently being inferred to prevent
51+ /// `( FQN, method) ` keys currently being inferred to prevent
4852 /// infinite recursion when a method body references another
4953 /// method that also lacks a return type.
50- static BODY_INFER_VISITED : RefCell <HashSet <String >> =
54+ static BODY_INFER_VISITED : RefCell <HashSet <( Atom , Atom ) >> =
5155 RefCell :: new( HashSet :: new( ) ) ;
5256
5357 /// When `Some`, memoizes completed body return inference results by
54- /// `" FQN:: method" `. Activated together with [`BODY_RETURN_INFERRER`]
58+ /// `( FQN, method) `. Activated together with [`BODY_RETURN_INFERRER`]
5559 /// and cleared when the owning guard drops, so the memo lives exactly
5660 /// as long as one request / one file's diagnostic pass.
5761 ///
@@ -60,7 +64,7 @@ thread_local! {
6064 /// files where most methods lack declared return types, the repeated
6165 /// walks compound into a multi-minute blowup (each walk itself
6266 /// triggers inference for the callees it contains).
63- static BODY_INFER_MEMO : RefCell <Option <HashMap < String , Option < PhpType >> >> =
67+ static BODY_INFER_MEMO : RefCell <Option <BodyInferMemo >> =
6468 const { RefCell :: new( None ) } ;
6569
6670 /// Current nesting depth of body return inference. Caps the
@@ -176,8 +180,12 @@ pub(crate) fn with_body_return_inferrer(inferrer: BodyReturnInferrerFn) -> BodyR
176180const MAX_BODY_INFER_DEPTH : u8 = 3 ;
177181
178182pub ( crate ) fn try_infer_body_return_type ( class_fqn : & str , method : & MethodInfo ) -> Option < PhpType > {
179- // Build the memo / re-entry key.
180- let key = format ! ( "{}::{}" , class_fqn, method. name) ;
183+ // Build the memo / re-entry key as an interned `(FQN, method)`
184+ // tuple. Both halves come from bounded symbol-name spaces and are
185+ // already interned, so the key allocates nothing and adds no new
186+ // entries to the global interner (a joined `"FQN::method"` string
187+ // would leak one entry per distinct pair for the process lifetime).
188+ let key = ( atom ( class_fqn) , method. name ) ;
181189
182190 // Serve a memoized result from an earlier completed inference in
183191 // this request. Checked before the depth cap so that deep call
@@ -197,7 +205,7 @@ pub(crate) fn try_infer_body_return_type(class_fqn: &str, method: &MethodInfo) -
197205 // Check + insert into the visited set (re-entry guard).
198206 let already_visiting = BODY_INFER_VISITED . with ( |cell| {
199207 let mut set = cell. borrow_mut ( ) ;
200- !set. insert ( key. clone ( ) )
208+ !set. insert ( key)
201209 } ) ;
202210 if already_visiting {
203211 return None ;
0 commit comments