Skip to main content

mir_codebase/
storage.rs

1use std::sync::Arc;
2
3use indexmap::IndexMap;
4use mir_types::{Location, Name, Type};
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7
8// ---------------------------------------------------------------------------
9// Interned common types for deduplication
10// ---------------------------------------------------------------------------
11
12/// Interned Type types for common parameter/property types.
13/// Deduplicates allocations when thousands of parameters share types like `string`, `int`, etc.
14mod interned_types {
15    use super::*;
16    use std::sync::OnceLock;
17
18    fn intern_string() -> Arc<Type> {
19        Arc::new(Type::string())
20    }
21
22    fn intern_int() -> Arc<Type> {
23        Arc::new(Type::int())
24    }
25
26    fn intern_float() -> Arc<Type> {
27        Arc::new(Type::float())
28    }
29
30    fn intern_bool() -> Arc<Type> {
31        Arc::new(Type::bool())
32    }
33
34    fn intern_mixed() -> Arc<Type> {
35        Arc::new(Type::mixed())
36    }
37
38    fn intern_null() -> Arc<Type> {
39        Arc::new(Type::null())
40    }
41
42    fn intern_void() -> Arc<Type> {
43        Arc::new(Type::void())
44    }
45
46    static STRING: OnceLock<Arc<Type>> = OnceLock::new();
47    static INT: OnceLock<Arc<Type>> = OnceLock::new();
48    static FLOAT: OnceLock<Arc<Type>> = OnceLock::new();
49    static BOOL: OnceLock<Arc<Type>> = OnceLock::new();
50    static MIXED: OnceLock<Arc<Type>> = OnceLock::new();
51    static NULL: OnceLock<Arc<Type>> = OnceLock::new();
52    static VOID: OnceLock<Arc<Type>> = OnceLock::new();
53
54    pub fn string() -> Arc<Type> {
55        STRING.get_or_init(intern_string).clone()
56    }
57
58    pub fn int() -> Arc<Type> {
59        INT.get_or_init(intern_int).clone()
60    }
61
62    pub fn float() -> Arc<Type> {
63        FLOAT.get_or_init(intern_float).clone()
64    }
65
66    pub fn bool() -> Arc<Type> {
67        BOOL.get_or_init(intern_bool).clone()
68    }
69
70    pub fn mixed() -> Arc<Type> {
71        MIXED.get_or_init(intern_mixed).clone()
72    }
73
74    pub fn null() -> Arc<Type> {
75        NULL.get_or_init(intern_null).clone()
76    }
77
78    pub fn void() -> Arc<Type> {
79        VOID.get_or_init(intern_void).clone()
80    }
81
82    /// Global content-keyed `Arc<Type>` interner. Any structurally-identical
83    /// Type is shared as a single Arc across the session.
84    ///
85    /// Why: PHP codebases re-declare a small set of type shapes thousands of
86    /// times — `string|null` return types, `int` params, `array<string, mixed>`
87    /// property types. Without interning, each declaration allocates its own
88    /// `Arc<Type>` plus the inline `SmallVec<[Atomic; 2]>` and any boxed
89    /// `Atomic` payloads. With interning, only the first occurrence allocates.
90    ///
91    /// Trade-off: every `intern_or_wrap` call hashes + does one DashMap lookup.
92    /// Hashing a `Type` is cheap (SmallVec, small atomics) — measured cost is
93    /// well below the alloc-savings benefit on real workloads.
94    static GLOBAL_UNION_INTERN: std::sync::OnceLock<dashmap::DashMap<Type, Arc<Type>>> =
95        std::sync::OnceLock::new();
96
97    fn global_intern_table() -> &'static dashmap::DashMap<Type, Arc<Type>> {
98        GLOBAL_UNION_INTERN.get_or_init(dashmap::DashMap::default)
99    }
100
101    /// Try to intern a Type if it matches a common type, otherwise wrap in Arc.
102    pub fn intern_or_wrap(union: Type) -> Arc<Type> {
103        // Fast path 1: single-atomic scalar — covered by `OnceLock` constants.
104        // Avoids any DashMap traffic for the most common case.
105        if union.types.len() == 1 && !union.possibly_undefined && !union.from_docblock {
106            match &union.types[0] {
107                mir_types::Atomic::TString => return string(),
108                mir_types::Atomic::TInt => return int(),
109                mir_types::Atomic::TFloat => return float(),
110                mir_types::Atomic::TBool => return bool(),
111                mir_types::Atomic::TMixed => return mixed(),
112                mir_types::Atomic::TNull => return null(),
113                mir_types::Atomic::TVoid => return void(),
114                _ => {}
115            }
116        }
117        // Fast path 2: empty Type — also a common case (e.g. unresolved
118        // return type). Don't pollute the intern table with these.
119        if union.types.is_empty() {
120            return Arc::new(union);
121        }
122        // Global path: dedup against any previously-seen identical Type.
123        let table = global_intern_table();
124        if let Some(existing) = table.get(&union) {
125            return Arc::clone(existing.value());
126        }
127        let arc = Arc::new(union.clone());
128        // `insert` semantics: if a parallel thread beat us, its Arc wins.
129        // The lookup-before-insert race is benign — both Arcs are content-
130        // equal — but we still want to share the canonical one going forward.
131        match table.entry(union) {
132            dashmap::mapref::entry::Entry::Occupied(o) => Arc::clone(o.get()),
133            dashmap::mapref::entry::Entry::Vacant(v) => {
134                v.insert(Arc::clone(&arc));
135                arc
136            }
137        }
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Shared primitives
143// ---------------------------------------------------------------------------
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
146pub enum Visibility {
147    Public,
148    Protected,
149    Private,
150}
151
152impl Visibility {
153    pub fn is_at_least(&self, required: Visibility) -> bool {
154        *self <= required
155    }
156}
157
158impl std::fmt::Display for Visibility {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            Visibility::Public => write!(f, "public"),
162            Visibility::Protected => write!(f, "protected"),
163            Visibility::Private => write!(f, "private"),
164        }
165    }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct TemplateParam {
170    pub name: Name,
171    pub bound: Option<Type>,
172    /// The entity (class or function FQN) that declared this template param.
173    pub defining_entity: Name,
174    pub variance: mir_types::Variance,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct FnParam {
179    pub name: Name,
180    /// Parameter type. Stored as `Option<Arc<Type>>` to enable deduplication of
181    /// common types across parameters. Many parameters share types like `string`,
182    /// `int`, `bool`, etc., so interning via Arc saves allocations.
183    #[serde(
184        deserialize_with = "deserialize_param_type",
185        serialize_with = "serialize_param_type"
186    )]
187    pub ty: Option<Arc<Type>>,
188    /// Whether this parameter has a default value. During analysis, defaults are
189    /// never used for their value — only for marking parameters as optional.
190    pub has_default: bool,
191    pub is_variadic: bool,
192    pub is_byref: bool,
193    pub is_optional: bool,
194}
195
196impl std::hash::Hash for FnParam {
197    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
198        self.name.hash(state);
199        self.has_default.hash(state);
200        self.is_variadic.hash(state);
201        self.is_byref.hash(state);
202        self.is_optional.hash(state);
203        // Hash the type value (not the Arc pointer) so that two FnParams with
204        // equal types (PartialEq) always produce the same hash, even when they
205        // are backed by different Arc allocations.
206        self.ty.as_deref().hash(state);
207    }
208}
209
210// Serde helpers to transparently convert between Option<Type> and Option<Arc<Type>>
211fn deserialize_param_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
212where
213    D: serde::Deserializer<'de>,
214{
215    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
216}
217
218fn serialize_param_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
219where
220    S: serde::Serializer,
221{
222    let opt = value.as_ref().map(|arc| (**arc).clone());
223    opt.serialize(serializer)
224}
225
226fn deserialize_return_type<'de, D>(deserializer: D) -> Result<Option<Arc<Type>>, D::Error>
227where
228    D: serde::Deserializer<'de>,
229{
230    Option::<Type>::deserialize(deserializer).map(|opt| opt.map(interned_types::intern_or_wrap))
231}
232
233fn serialize_return_type<S>(value: &Option<Arc<Type>>, serializer: S) -> Result<S::Ok, S::Error>
234where
235    S: serde::Serializer,
236{
237    let opt = value.as_ref().map(|arc| (**arc).clone());
238    opt.serialize(serializer)
239}
240
241fn deserialize_params<'de, D>(deserializer: D) -> Result<Arc<[FnParam]>, D::Error>
242where
243    D: serde::Deserializer<'de>,
244{
245    Vec::<FnParam>::deserialize(deserializer).map(|v| Arc::from(v.into_boxed_slice()))
246}
247
248fn default_imports() -> Arc<FxHashMap<Name, Name>> {
249    Arc::new(FxHashMap::default())
250}
251
252/// Deserialize imports map. Supports both new (Name-keyed) and legacy
253/// (String-keyed) on-disk formats — older `cache.bin` files have plain
254/// `HashMap<String, String>`. Either way, we intern at load time so the
255/// in-memory representation is always `Arc<FxHashMap<Name, Name>>`.
256fn deserialize_imports<'de, D>(deserializer: D) -> Result<Arc<FxHashMap<Name, Name>>, D::Error>
257where
258    D: serde::Deserializer<'de>,
259{
260    let raw = FxHashMap::<String, String>::deserialize(deserializer)?;
261    let mut out: FxHashMap<Name, Name> =
262        FxHashMap::with_capacity_and_hasher(raw.len(), Default::default());
263    for (k, v) in raw {
264        out.insert(Name::new(&k), Name::new(&v));
265    }
266    Ok(Arc::new(out))
267}
268
269/// Serialize imports as the legacy `HashMap<String, String>` shape so disk
270/// caches written by this version remain compatible with readers that haven't
271/// been recompiled yet (and vice-versa).
272fn serialize_imports<S>(
273    value: &Arc<FxHashMap<Name, Name>>,
274    serializer: S,
275) -> Result<S::Ok, S::Error>
276where
277    S: serde::Serializer,
278{
279    use serde::ser::SerializeMap;
280    let mut map = serializer.serialize_map(Some(value.len()))?;
281    for (k, v) in value.iter() {
282        map.serialize_entry(k.as_str(), v.as_str())?;
283    }
284    map.end()
285}
286
287fn serialize_params<S>(value: &Arc<[FnParam]>, serializer: S) -> Result<S::Ok, S::Error>
288where
289    S: serde::Serializer,
290{
291    value.as_ref().serialize(serializer)
292}
293
294/// Helper to wrap Option<Type> in interned Arc<Type>.
295pub fn wrap_param_type(ty: Option<Type>) -> Option<Arc<Type>> {
296    ty.map(interned_types::intern_or_wrap)
297}
298
299/// Helper to wrap return type Option<Type> in interned Arc<Type>.
300pub fn wrap_return_type(ty: Option<Type>) -> Option<Arc<Type>> {
301    ty.map(interned_types::intern_or_wrap)
302}
303
304// ---------------------------------------------------------------------------
305// Assertion — `@psalm-assert`, `@psalm-assert-if-true`, etc.
306// ---------------------------------------------------------------------------
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub enum AssertionKind {
310    Assert,
311    AssertIfTrue,
312    AssertIfFalse,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub struct Assertion {
317    pub kind: AssertionKind,
318    pub param: Arc<str>,
319    pub ty: Type,
320}
321
322// ---------------------------------------------------------------------------
323// MethodDef
324// ---------------------------------------------------------------------------
325
326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
327pub struct MethodDef {
328    pub name: Arc<str>,
329    pub fqcn: Arc<str>,
330    #[serde(
331        deserialize_with = "deserialize_params",
332        serialize_with = "serialize_params"
333    )]
334    pub params: Arc<[FnParam]>,
335    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
336    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types
337    /// (e.g., `void`, `string`, `mixed`, `bool`) across thousands of methods.
338    #[serde(
339        deserialize_with = "deserialize_return_type",
340        serialize_with = "serialize_return_type"
341    )]
342    pub return_type: Option<Arc<Type>>,
343    /// Type inferred from body analysis (filled in during pass 2).
344    pub inferred_return_type: Option<Type>,
345    pub visibility: Visibility,
346    pub is_static: bool,
347    pub is_abstract: bool,
348    pub is_final: bool,
349    pub is_constructor: bool,
350    pub template_params: Vec<TemplateParam>,
351    pub assertions: Vec<Assertion>,
352    pub throws: Vec<Arc<str>>,
353    pub deprecated: Option<Arc<str>>,
354    pub is_internal: bool,
355    pub is_pure: bool,
356    pub location: Option<Location>,
357    /// Plain-text description from the docblock (text before `@tag` lines).
358    /// Used for hover info.
359    #[serde(default)]
360    pub docstring: Option<Arc<str>>,
361    /// True for methods added via `@method` docblock annotations. Virtual
362    /// methods must not be required as concrete interface implementations.
363    #[serde(default)]
364    pub is_virtual: bool,
365}
366
367impl MethodDef {
368    pub fn effective_return_type(&self) -> Option<&Type> {
369        self.return_type
370            .as_deref()
371            .or(self.inferred_return_type.as_ref())
372    }
373}
374
375// ---------------------------------------------------------------------------
376// PropertyDef
377// ---------------------------------------------------------------------------
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380pub struct PropertyDef {
381    pub name: Arc<str>,
382    pub ty: Option<Type>,
383    pub inferred_ty: Option<Type>,
384    pub visibility: Visibility,
385    pub is_static: bool,
386    pub is_readonly: bool,
387    pub default: Option<Type>,
388    pub location: Option<Location>,
389}
390
391// ---------------------------------------------------------------------------
392// ConstantDef
393// ---------------------------------------------------------------------------
394
395#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
396pub struct ConstantDef {
397    pub name: Arc<str>,
398    pub ty: Type,
399    pub visibility: Option<Visibility>,
400    #[serde(default)]
401    pub is_final: bool,
402    pub location: Option<Location>,
403}
404
405// ---------------------------------------------------------------------------
406// ClassDef
407// ---------------------------------------------------------------------------
408
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410pub struct ClassDef {
411    pub fqcn: Arc<str>,
412    pub short_name: Arc<str>,
413    pub parent: Option<Arc<str>>,
414    pub interfaces: Vec<Arc<str>>,
415    pub traits: Vec<Arc<str>>,
416    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
417    pub own_properties: IndexMap<Arc<str>, PropertyDef>,
418    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
419    #[serde(default)]
420    pub mixins: Vec<Arc<str>>,
421    pub template_params: Vec<TemplateParam>,
422    /// Type arguments from `@extends ParentClass<T1, T2>` — maps parent's template params to concrete types.
423    pub extends_type_args: Vec<Type>,
424    /// Type arguments from `@implements Interface<T1, T2>`.
425    #[serde(default)]
426    pub implements_type_args: Vec<(Arc<str>, Vec<Type>)>,
427    pub is_abstract: bool,
428    pub is_final: bool,
429    pub is_readonly: bool,
430    pub deprecated: Option<Arc<str>>,
431    pub is_internal: bool,
432    pub location: Option<Location>,
433    /// Per-`use` statement locations for each used trait: `(fqcn, location)` in
434    /// declaration order, parallel to `traits`.  Absent from older serialized
435    /// slices; defaults to empty.
436    #[serde(default)]
437    pub trait_use_locations: Vec<(Arc<str>, Location)>,
438    /// Type aliases declared on this class via `@psalm-type` / `@phpstan-type`.
439    #[serde(default)]
440    pub type_aliases: FxHashMap<Arc<str>, Type>,
441    /// Raw import-type declarations (`(local_name, original_name, from_class)`) — resolved during finalization.
442    #[serde(default)]
443    pub pending_import_types: Vec<(Arc<str>, Arc<str>, Arc<str>)>,
444}
445
446impl ClassDef {
447    pub fn get_method(&self, name: &str) -> Option<&MethodDef> {
448        // PHP method names are case-insensitive; caller should pass lowercase name.
449        // Only searches own_methods — inherited method resolution is done by
450        // `db::lookup_method_in_chain`.
451        self.own_methods.get(name).map(Arc::as_ref).or_else(|| {
452            self.own_methods
453                .iter()
454                .find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
455                .map(|(_, v)| v.as_ref())
456        })
457    }
458
459    pub fn get_property(&self, name: &str) -> Option<&PropertyDef> {
460        self.own_properties.get(name)
461    }
462}
463
464// ---------------------------------------------------------------------------
465// InterfaceDef
466// ---------------------------------------------------------------------------
467
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469pub struct InterfaceDef {
470    pub fqcn: Arc<str>,
471    pub short_name: Arc<str>,
472    pub extends: Vec<Arc<str>>,
473    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
474    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
475    pub template_params: Vec<TemplateParam>,
476    pub location: Option<Location>,
477}
478
479// ---------------------------------------------------------------------------
480// TraitDef
481// ---------------------------------------------------------------------------
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484pub struct TraitDef {
485    pub fqcn: Arc<str>,
486    pub short_name: Arc<str>,
487    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
488    pub own_properties: IndexMap<Arc<str>, PropertyDef>,
489    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
490    pub template_params: Vec<TemplateParam>,
491    /// Traits used by this trait (`use OtherTrait;` inside a trait body).
492    pub traits: Vec<Arc<str>>,
493    pub location: Option<Location>,
494    /// `@psalm-require-extends` / `@phpstan-require-extends` — FQCNs that using classes must extend.
495    #[serde(default)]
496    pub require_extends: Vec<Arc<str>>,
497    /// `@psalm-require-implements` / `@phpstan-require-implements` — FQCNs that using classes must implement.
498    #[serde(default)]
499    pub require_implements: Vec<Arc<str>>,
500}
501
502// ---------------------------------------------------------------------------
503// EnumDef
504// ---------------------------------------------------------------------------
505
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507pub struct EnumCaseDef {
508    pub name: Arc<str>,
509    pub value: Option<Type>,
510    pub location: Option<Location>,
511}
512
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
514pub struct EnumDef {
515    pub fqcn: Arc<str>,
516    pub short_name: Arc<str>,
517    pub scalar_type: Option<Type>,
518    pub interfaces: Vec<Arc<str>>,
519    pub cases: IndexMap<Arc<str>, EnumCaseDef>,
520    pub own_methods: IndexMap<Arc<str>, Arc<MethodDef>>,
521    pub own_constants: IndexMap<Arc<str>, ConstantDef>,
522    pub location: Option<Location>,
523}
524
525// ---------------------------------------------------------------------------
526// FunctionDef
527// ---------------------------------------------------------------------------
528
529#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
530pub struct FunctionDef {
531    pub fqn: Arc<str>,
532    pub short_name: Arc<str>,
533    #[serde(
534        deserialize_with = "deserialize_params",
535        serialize_with = "serialize_params"
536    )]
537    pub params: Arc<[FnParam]>,
538    /// Type from annotation (`@return` / native type hint). `None` means unannotated.
539    /// Stored as `Option<Arc<Type>>` to enable deduplication of common return types.
540    #[serde(
541        deserialize_with = "deserialize_return_type",
542        serialize_with = "serialize_return_type"
543    )]
544    pub return_type: Option<Arc<Type>>,
545    pub inferred_return_type: Option<Type>,
546    pub template_params: Vec<TemplateParam>,
547    pub assertions: Vec<Assertion>,
548    pub throws: Vec<Arc<str>>,
549    pub deprecated: Option<Arc<str>>,
550    pub is_pure: bool,
551    pub location: Option<Location>,
552    /// Plain-text description from the docblock (text before `@tag` lines).
553    /// Used for hover info.
554    #[serde(default)]
555    pub docstring: Option<Arc<str>>,
556}
557
558impl FunctionDef {
559    pub fn effective_return_type(&self) -> Option<&Type> {
560        self.return_type
561            .as_deref()
562            .or(self.inferred_return_type.as_ref())
563    }
564}
565
566// ---------------------------------------------------------------------------
567// StubSlice — serializable bundle of definitions from one extension's stubs
568// ---------------------------------------------------------------------------
569
570/// A snapshot of all PHP definitions contributed by a single stub file set.
571///
572/// Produced by `mir-stubs-gen` at code-generation time and deserialized at
573/// runtime to ingest definitions into the salsa db via
574/// `MirDatabase::ingest_stub_slice`.
575#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
576pub struct StubSlice {
577    pub classes: Vec<Arc<ClassDef>>,
578    pub interfaces: Vec<Arc<InterfaceDef>>,
579    pub traits: Vec<Arc<TraitDef>>,
580    pub enums: Vec<Arc<EnumDef>>,
581    pub functions: Vec<Arc<FunctionDef>>,
582    #[serde(default)]
583    pub constants: Vec<(Arc<str>, Type)>,
584    /// Source file this slice was collected from. `None` for bundled stub slices
585    /// that were pre-computed and are not tied to a specific on-disk file.
586    #[serde(default)]
587    pub file: Option<Arc<str>>,
588    /// Types of `@var`-annotated global variables collected from this file.
589    /// Populated by `DefinitionCollector`; ingested into the salsa db's
590    /// `global_vars` table by `ingest_stub_slice` when `file` is `Some`.
591    #[serde(default)]
592    pub global_vars: Vec<(Arc<str>, Type)>,
593    /// The first namespace declared in this file (e.g. `"App\\Service"`).
594    /// Populated by `DefinitionCollector`; ingested into the salsa db's
595    /// `file_namespaces` table by `ingest_stub_slice` when `file` is `Some`.
596    #[serde(default)]
597    pub namespace: Option<Arc<str>>,
598    /// `use` alias map for this file: alias → FQCN.
599    ///
600    /// Stored as `Arc<FxHashMap<Name, Name>>` so that `file_imports()`
601    /// returns a cheap Arc clone instead of deep-cloning the map on every
602    /// `resolve_name` call (which fires once per symbol reference in
603    /// Pass 2). `Name` keys/values shrink each entry from ~108 bytes
604    /// (two `String` headers + two heap allocs averaging ~30 chars) to
605    /// 16 bytes (two `Ustr` u64 handles); the global ustr interner holds
606    /// one copy of each unique alias / FQCN string for the whole session.
607    #[serde(
608        deserialize_with = "deserialize_imports",
609        serialize_with = "serialize_imports"
610    )]
611    #[serde(default = "default_imports")]
612    pub imports: Arc<FxHashMap<Name, Name>>,
613    /// Set to `true` after `deduplicate_params_in_slice` has run on this slice.
614    /// `ingest_stub_slice` skips the clone+re-dedup when this flag is set.
615    #[serde(skip)]
616    pub is_deduped: bool,
617}
618
619// ---------------------------------------------------------------------------
620// Param list deduplication
621// ---------------------------------------------------------------------------
622
623use std::sync::Mutex;
624
625type ParamCache = Mutex<FxHashMap<Vec<FnParam>, Arc<[FnParam]>>>;
626
627/// Global cache of canonical Arc<[FnParam]> instances for deduplication.
628/// Shared across all StubSlices to deduplicate vendor code with millions of
629/// methods that often have identical parameter lists.
630static PARAM_DEDUP_CACHE: std::sync::OnceLock<ParamCache> = std::sync::OnceLock::new();
631
632/// Deduplicate parameter lists across all methods and functions in a StubSlice.
633/// Many PHP framework methods share identical parameter lists (e.g., thousands
634/// of `(string $arg, array $opts)` signatures). This function groups identical
635/// param lists globally (across all slices processed so far) and replaces them
636/// with Arc<[FnParam]> pointers to shared allocations.
637///
638/// Expected memory savings: 100–150 MiB on cold start (vendor collection).
639pub fn deduplicate_params_in_slice(slice: &mut StubSlice) {
640    let cache: &ParamCache = PARAM_DEDUP_CACHE.get_or_init(|| Mutex::new(FxHashMap::default()));
641    let mut canonical_params = cache.lock().unwrap();
642
643    let mut deduplicate = |params: &mut Arc<[FnParam]>| {
644        if let Some(existing) = canonical_params.get(params.as_ref()) {
645            *params = existing.clone();
646        } else {
647            canonical_params.insert(params.as_ref().to_vec(), params.clone());
648        }
649    };
650
651    // Deduplicate method params in all classes
652    for cls in &mut slice.classes {
653        for method in Arc::make_mut(cls).own_methods.values_mut() {
654            deduplicate(&mut Arc::make_mut(method).params);
655        }
656    }
657
658    // Deduplicate method params in all interfaces
659    for iface in &mut slice.interfaces {
660        for method in Arc::make_mut(iface).own_methods.values_mut() {
661            deduplicate(&mut Arc::make_mut(method).params);
662        }
663    }
664
665    // Deduplicate method params in all traits
666    for tr in &mut slice.traits {
667        for method in Arc::make_mut(tr).own_methods.values_mut() {
668            deduplicate(&mut Arc::make_mut(method).params);
669        }
670    }
671
672    // Deduplicate method params in all enums
673    for en in &mut slice.enums {
674        for method in Arc::make_mut(en).own_methods.values_mut() {
675            deduplicate(&mut Arc::make_mut(method).params);
676        }
677    }
678
679    // Deduplicate function params
680    for func in &mut slice.functions {
681        deduplicate(&mut Arc::make_mut(func).params);
682    }
683    slice.is_deduped = true;
684}