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<dashmap::DashMap<ustr::Ustr, ustr::Ustr>> =
50            std::sync::OnceLock::new();
51        let cache = CACHE.get_or_init(dashmap::DashMap::default);
52        if let Some(v) = cache.get(&self.0) {
53            return Name(*v);
54        }
55        // `to_ascii_lowercase` allocates but only on first sight of this
56        // name; subsequent calls return from the cache.
57        let lowered = ustr::ustr(&self.as_str().to_ascii_lowercase());
58        // Normally bounded by the workspace's mixed-case identifier
59        // vocabulary, but a long session of renames mints new names forever;
60        // dropping the whole memo is cheap (entries are re-lowered on demand)
61        // and keeps the map from growing without bound.
62        const CACHE_CAP: usize = 1 << 16;
63        if cache.len() >= CACHE_CAP {
64            cache.clear();
65        }
66        cache.insert(self.0, lowered);
67        Name(lowered)
68    }
69}
70
71// ---------------------------------------------------------------------------
72// Conversions
73// ---------------------------------------------------------------------------
74
75impl From<&str> for Name {
76    #[inline]
77    fn from(s: &str) -> Self {
78        Self::new(s)
79    }
80}
81
82impl From<String> for Name {
83    #[inline]
84    fn from(s: String) -> Self {
85        Self::new(&s)
86    }
87}
88
89impl From<Arc<str>> for Name {
90    #[inline]
91    fn from(s: Arc<str>) -> Self {
92        Self::new(&s)
93    }
94}
95
96impl From<Name> for String {
97    #[inline]
98    fn from(s: Name) -> String {
99        s.as_str().to_string()
100    }
101}
102
103impl From<Name> for Arc<str> {
104    #[inline]
105    fn from(s: Name) -> Arc<str> {
106        Arc::from(s.as_str())
107    }
108}
109
110// ---------------------------------------------------------------------------
111// Deref + AsRef
112// ---------------------------------------------------------------------------
113
114impl std::ops::Deref for Name {
115    type Target = str;
116    #[inline]
117    fn deref(&self) -> &str {
118        self.as_str()
119    }
120}
121
122impl AsRef<str> for Name {
123    #[inline]
124    fn as_ref(&self) -> &str {
125        self.as_str()
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Comparisons
131// ---------------------------------------------------------------------------
132
133impl PartialEq<str> for Name {
134    #[inline]
135    fn eq(&self, other: &str) -> bool {
136        self.as_str() == other
137    }
138}
139
140impl PartialEq<Name> for str {
141    #[inline]
142    fn eq(&self, other: &Name) -> bool {
143        self == other.as_str()
144    }
145}
146
147impl PartialEq<String> for Name {
148    #[inline]
149    fn eq(&self, other: &String) -> bool {
150        self.as_str() == other.as_str()
151    }
152}
153
154impl PartialEq<Arc<str>> for Name {
155    #[inline]
156    fn eq(&self, other: &Arc<str>) -> bool {
157        self.as_str() == other.as_ref()
158    }
159}
160
161impl PartialEq<Name> for Arc<str> {
162    #[inline]
163    fn eq(&self, other: &Name) -> bool {
164        self.as_ref() == other.as_str()
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Display / Debug
170// ---------------------------------------------------------------------------
171
172impl fmt::Display for Name {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.write_str(self.as_str())
175    }
176}
177
178impl fmt::Debug for Name {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "Name({:?})", self.as_str())
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Serde
186// ---------------------------------------------------------------------------
187
188impl Serialize for Name {
189    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
190        serializer.serialize_str(self.as_str())
191    }
192}
193
194impl<'de> Deserialize<'de> for Name {
195    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
196        // Use `Cow<str>` instead of `&str` so this round-trips through both
197        // borrowable formats (`serde_json`, `bincode::deserialize(&bytes)`)
198        // *and* streaming formats that cannot borrow (`bincode::deserialize_from(reader)`).
199        // The stub-cache serializer uses the streaming variant, and `<&str>`
200        // would error with `invalid type: string "...", expected a borrowed
201        // string`, silently turning every cache hit into a miss.
202        let s = std::borrow::Cow::<str>::deserialize(deserializer)?;
203        Ok(Self::new(&s))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    /// Overflowing the lowercase memo must clear-and-continue, never return
212    /// a wrong lowering for names cached before the clear.
213    #[test]
214    fn ascii_lowercase_correct_across_cache_overflow() {
215        let early = Name::new("App\\EarlyService");
216        assert_eq!(early.ascii_lowercase().as_str(), "app\\earlyservice");
217        for i in 0..70_000 {
218            let n = Name::new(&format!("App\\Storm{i}"));
219            assert_eq!(
220                n.ascii_lowercase().as_str(),
221                format!("app\\storm{i}").as_str()
222            );
223        }
224        assert_eq!(early.ascii_lowercase().as_str(), "app\\earlyservice");
225    }
226}