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}