Skip to main content

mir_types/
symbol.rs

1use std::fmt;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6/// Interned string identity for PHP class FQCNs, method names, and other
7/// identifiers that appear repeatedly across the type system.
8///
9/// Backed by the process-global [`ustr`] interner: equal string values share a
10/// single heap allocation.  Equality is pointer-based (O(1)) rather than
11/// content-based (O(n)).  `Name` is `Copy` — cloning is a pointer copy, not a
12/// refcount increment.
13///
14/// ## Serde
15/// Serialised as a plain string; deserialised by interning the string value.
16/// Round-trips transparently through `bincode` / `serde_json`.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct Name(ustr::Ustr);
19
20impl Name {
21    #[inline]
22    pub fn new(s: &str) -> Self {
23        Self(ustr::ustr(s))
24    }
25
26    #[inline]
27    pub fn as_str(&self) -> &str {
28        self.0.as_str()
29    }
30
31    /// ASCII-lowercased twin of this name, memoized.
32    ///
33    /// PHP class and function names are case-insensitive for resolution, so
34    /// every workspace symbol-index lookup needs the lowercase form. The
35    /// naive `name.to_ascii_lowercase()` allocates a fresh `String` per call
36    /// — measured at ~9% of total CLI CPU on Laravel-scale fixtures.
37    ///
38    /// This caches `self → lowercase(self)` in a process-global DashMap so
39    /// every unique identifier is lowercased at most once. The result is
40    /// itself a `Name`, so downstream HashMap lookups become `u64`-keyed
41    /// (`ustr::Ustr` equality is pointer-eq, not content-eq).
42    ///
43    /// Fast path: if `self` is already all-lowercase, returns `self`
44    /// directly without touching the cache.
45    pub fn ascii_lowercase(self) -> Self {
46        if self.as_str().bytes().all(|b| !b.is_ascii_uppercase()) {
47            return self;
48        }
49        static CACHE: std::sync::OnceLock<
50            dashmap::DashMap<ustr::Ustr, ustr::Ustr, rustc_hash::FxBuildHasher>,
51        > = std::sync::OnceLock::new();
52        let cache = CACHE.get_or_init(|| dashmap::DashMap::with_hasher(Default::default()));
53        if let Some(v) = cache.get(&self.0) {
54            return Name(*v);
55        }
56        // `to_ascii_lowercase` allocates but only on first sight of this
57        // name; subsequent calls return from the cache.
58        let lowered = ustr::ustr(&self.as_str().to_ascii_lowercase());
59        // Normally bounded by the workspace's mixed-case identifier
60        // vocabulary, but a long session of renames mints new names forever;
61        // dropping the whole memo is cheap (entries are re-lowered on demand)
62        // and keeps the map from growing without bound.
63        const CACHE_CAP: usize = 1 << 16;
64        if cache.len() >= CACHE_CAP {
65            cache.clear();
66        }
67        cache.insert(self.0, lowered);
68        Name(lowered)
69    }
70}
71
72// ---------------------------------------------------------------------------
73// Conversions
74// ---------------------------------------------------------------------------
75
76impl From<&str> for Name {
77    #[inline]
78    fn from(s: &str) -> Self {
79        Self::new(s)
80    }
81}
82
83impl From<String> for Name {
84    #[inline]
85    fn from(s: String) -> Self {
86        Self::new(&s)
87    }
88}
89
90impl From<Arc<str>> for Name {
91    #[inline]
92    fn from(s: Arc<str>) -> Self {
93        Self::new(&s)
94    }
95}
96
97impl From<Name> for String {
98    #[inline]
99    fn from(s: Name) -> String {
100        s.as_str().to_string()
101    }
102}
103
104impl From<Name> for Arc<str> {
105    #[inline]
106    fn from(s: Name) -> Arc<str> {
107        Arc::from(s.as_str())
108    }
109}
110
111// ---------------------------------------------------------------------------
112// Deref + AsRef
113// ---------------------------------------------------------------------------
114
115impl std::ops::Deref for Name {
116    type Target = str;
117    #[inline]
118    fn deref(&self) -> &str {
119        self.as_str()
120    }
121}
122
123impl AsRef<str> for Name {
124    #[inline]
125    fn as_ref(&self) -> &str {
126        self.as_str()
127    }
128}
129
130// ---------------------------------------------------------------------------
131// Comparisons
132// ---------------------------------------------------------------------------
133
134impl PartialEq<str> for Name {
135    #[inline]
136    fn eq(&self, other: &str) -> bool {
137        self.as_str() == other
138    }
139}
140
141impl PartialEq<Name> for str {
142    #[inline]
143    fn eq(&self, other: &Name) -> bool {
144        self == other.as_str()
145    }
146}
147
148impl PartialEq<String> for Name {
149    #[inline]
150    fn eq(&self, other: &String) -> bool {
151        self.as_str() == other.as_str()
152    }
153}
154
155impl PartialEq<Arc<str>> for Name {
156    #[inline]
157    fn eq(&self, other: &Arc<str>) -> bool {
158        self.as_str() == other.as_ref()
159    }
160}
161
162impl PartialEq<Name> for Arc<str> {
163    #[inline]
164    fn eq(&self, other: &Name) -> bool {
165        self.as_ref() == other.as_str()
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Display / Debug
171// ---------------------------------------------------------------------------
172
173impl fmt::Display for Name {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        f.write_str(self.as_str())
176    }
177}
178
179impl fmt::Debug for Name {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "Name({:?})", self.as_str())
182    }
183}
184
185// ---------------------------------------------------------------------------
186// Serde
187// ---------------------------------------------------------------------------
188
189impl Serialize for Name {
190    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
191        serializer.serialize_str(self.as_str())
192    }
193}
194
195impl<'de> Deserialize<'de> for Name {
196    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
197        // Use `Cow<str>` instead of `&str` so this round-trips through both
198        // borrowable formats (`serde_json`, `bincode::deserialize(&bytes)`)
199        // *and* streaming formats that cannot borrow (`bincode::deserialize_from(reader)`).
200        // The stub-cache serializer uses the streaming variant, and `<&str>`
201        // would error with `invalid type: string "...", expected a borrowed
202        // string`, silently turning every cache hit into a miss.
203        let s = std::borrow::Cow::<str>::deserialize(deserializer)?;
204        Ok(Self::new(&s))
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    /// Overflowing the lowercase memo must clear-and-continue, never return
213    /// a wrong lowering for names cached before the clear.
214    #[test]
215    fn ascii_lowercase_correct_across_cache_overflow() {
216        let early = Name::new("App\\EarlyService");
217        assert_eq!(early.ascii_lowercase().as_str(), "app\\earlyservice");
218        for i in 0..70_000 {
219            let n = Name::new(&format!("App\\Storm{i}"));
220            assert_eq!(
221                n.ascii_lowercase().as_str(),
222                format!("app\\storm{i}").as_str()
223            );
224        }
225        assert_eq!(early.ascii_lowercase().as_str(), "app\\earlyservice");
226    }
227}