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 cache.insert(self.0, lowered);
59 Name(lowered)
60 }
61}
62
63// ---------------------------------------------------------------------------
64// Conversions
65// ---------------------------------------------------------------------------
66
67impl From<&str> for Name {
68 #[inline]
69 fn from(s: &str) -> Self {
70 Self::new(s)
71 }
72}
73
74impl From<String> for Name {
75 #[inline]
76 fn from(s: String) -> Self {
77 Self::new(&s)
78 }
79}
80
81impl From<Arc<str>> for Name {
82 #[inline]
83 fn from(s: Arc<str>) -> Self {
84 Self::new(&s)
85 }
86}
87
88impl From<Name> for String {
89 #[inline]
90 fn from(s: Name) -> String {
91 s.as_str().to_string()
92 }
93}
94
95impl From<Name> for Arc<str> {
96 #[inline]
97 fn from(s: Name) -> Arc<str> {
98 Arc::from(s.as_str())
99 }
100}
101
102// ---------------------------------------------------------------------------
103// Deref + AsRef
104// ---------------------------------------------------------------------------
105
106impl std::ops::Deref for Name {
107 type Target = str;
108 #[inline]
109 fn deref(&self) -> &str {
110 self.as_str()
111 }
112}
113
114impl AsRef<str> for Name {
115 #[inline]
116 fn as_ref(&self) -> &str {
117 self.as_str()
118 }
119}
120
121// ---------------------------------------------------------------------------
122// Comparisons
123// ---------------------------------------------------------------------------
124
125impl PartialEq<str> for Name {
126 #[inline]
127 fn eq(&self, other: &str) -> bool {
128 self.as_str() == other
129 }
130}
131
132impl PartialEq<Name> for str {
133 #[inline]
134 fn eq(&self, other: &Name) -> bool {
135 self == other.as_str()
136 }
137}
138
139impl PartialEq<String> for Name {
140 #[inline]
141 fn eq(&self, other: &String) -> bool {
142 self.as_str() == other.as_str()
143 }
144}
145
146impl PartialEq<Arc<str>> for Name {
147 #[inline]
148 fn eq(&self, other: &Arc<str>) -> bool {
149 self.as_str() == other.as_ref()
150 }
151}
152
153impl PartialEq<Name> for Arc<str> {
154 #[inline]
155 fn eq(&self, other: &Name) -> bool {
156 self.as_ref() == other.as_str()
157 }
158}
159
160// ---------------------------------------------------------------------------
161// Display / Debug
162// ---------------------------------------------------------------------------
163
164impl fmt::Display for Name {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(self.as_str())
167 }
168}
169
170impl fmt::Debug for Name {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "Name({:?})", self.as_str())
173 }
174}
175
176// ---------------------------------------------------------------------------
177// Serde
178// ---------------------------------------------------------------------------
179
180impl Serialize for Name {
181 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
182 serializer.serialize_str(self.as_str())
183 }
184}
185
186impl<'de> Deserialize<'de> for Name {
187 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
188 // Use `Cow<str>` instead of `&str` so this round-trips through both
189 // borrowable formats (`serde_json`, `bincode::deserialize(&bytes)`)
190 // *and* streaming formats that cannot borrow (`bincode::deserialize_from(reader)`).
191 // The stub-cache serializer uses the streaming variant, and `<&str>`
192 // would error with `invalid type: string "...", expected a borrowed
193 // string`, silently turning every cache hit into a miss.
194 let s = std::borrow::Cow::<str>::deserialize(deserializer)?;
195 Ok(Self::new(&s))
196 }
197}