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}