miden_assembly_syntax/ast/
ident.rs1use alloc::{string::ToString, sync::Arc};
2use core::{
3 fmt,
4 hash::{Hash, Hasher},
5 str::FromStr,
6};
7
8use miden_core::utils::{
9 ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
10};
11use miden_debug_types::{SourceSpan, Span, Spanned};
12
13#[derive(Debug, thiserror::Error)]
15pub enum IdentError {
16 #[error("invalid identifier: cannot be empty")]
17 Empty,
18 #[error(
19 "invalid identifier '{ident}': must contain only unicode alphanumeric or ascii graphic characters"
20 )]
21 InvalidChars { ident: Arc<str> },
22 #[error("invalid identifier: length exceeds the maximum of {max} bytes")]
23 InvalidLength { max: usize },
24 #[error("invalid identifier: {0}")]
25 Casing(CaseKindError),
26}
27
28#[derive(Debug, thiserror::Error)]
31pub enum CaseKindError {
32 #[error(
33 "only uppercase characters or underscores are allowed, and must start with an alphabetic character"
34 )]
35 Screaming,
36 #[error(
37 "only lowercase characters or underscores are allowed, and must start with an alphabetic character"
38 )]
39 Snake,
40 #[error(
41 "only alphanumeric characters are allowed, and must start with a lowercase alphabetic character"
42 )]
43 Camel,
44}
45
46#[derive(Clone)]
57pub struct Ident {
58 span: SourceSpan,
67 name: Arc<str>,
69}
70
71impl Ident {
72 pub fn new(source: impl AsRef<str>) -> Result<Self, IdentError> {
80 source.as_ref().parse()
81 }
82
83 pub fn new_with_span(span: SourceSpan, source: impl AsRef<str>) -> Result<Self, IdentError> {
91 source.as_ref().parse::<Self>().map(|id| id.with_span(span))
92 }
93
94 pub fn with_span(mut self, span: SourceSpan) -> Self {
96 self.span = span;
97 self
98 }
99
100 pub fn from_raw_parts(name: Span<Arc<str>>) -> Self {
109 let (span, name) = name.into_parts();
110 Self { span, name }
111 }
112
113 pub fn into_inner(self) -> Arc<str> {
115 self.name
116 }
117
118 pub fn as_str(&self) -> &str {
120 self.name.as_ref()
121 }
122
123 pub fn validate(source: impl AsRef<str>) -> Result<(), IdentError> {
125 let source = source.as_ref();
126 if source.is_empty() {
127 return Err(IdentError::Empty);
128 }
129 if !source.chars().all(|c| c.is_ascii_graphic() || c.is_alphanumeric()) {
130 return Err(IdentError::InvalidChars { ident: source.into() });
131 }
132 Ok(())
133 }
134}
135
136impl fmt::Debug for Ident {
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 f.debug_tuple("Ident").field(&self.name).finish()
139 }
140}
141
142impl Eq for Ident {}
143
144impl PartialEq for Ident {
145 fn eq(&self, other: &Self) -> bool {
146 self.name == other.name
147 }
148}
149
150impl Ord for Ident {
151 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
152 self.name.cmp(&other.name)
153 }
154}
155
156impl PartialOrd for Ident {
157 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
158 Some(self.cmp(other))
159 }
160}
161
162impl Hash for Ident {
163 fn hash<H: Hasher>(&self, state: &mut H) {
164 self.name.hash(state);
165 }
166}
167
168impl Spanned for Ident {
169 fn span(&self) -> SourceSpan {
170 self.span
171 }
172}
173
174impl core::ops::Deref for Ident {
175 type Target = str;
176
177 fn deref(&self) -> &Self::Target {
178 self.name.as_ref()
179 }
180}
181
182impl AsRef<str> for Ident {
183 #[inline]
184 fn as_ref(&self) -> &str {
185 &self.name
186 }
187}
188
189impl fmt::Display for Ident {
190 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
191 fmt::Display::fmt(&self.name, f)
192 }
193}
194
195impl FromStr for Ident {
196 type Err = IdentError;
197
198 fn from_str(s: &str) -> Result<Self, Self::Err> {
199 Self::validate(s)?;
200 let name = Arc::from(s.to_string().into_boxed_str());
201 Ok(Self { span: SourceSpan::default(), name })
202 }
203}
204
205#[cfg(feature = "serde")]
206impl serde::Serialize for Ident {
207 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208 where
209 S: serde::Serializer,
210 {
211 serializer.serialize_str(self.as_str())
212 }
213}
214
215#[cfg(feature = "serde")]
216impl<'de> serde::Deserialize<'de> for Ident {
217 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
218 where
219 D: serde::Deserializer<'de>,
220 {
221 let name = <&'de str as serde::Deserialize>::deserialize(deserializer)?;
222 Self::new(name).map_err(serde::de::Error::custom)
223 }
224}
225
226impl Serializable for Ident {
227 fn write_into<W: ByteWriter>(&self, target: &mut W) {
228 target.write_usize(self.len());
229 target.write_bytes(self.as_bytes());
230 }
231}
232
233impl Deserializable for Ident {
234 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
235 use alloc::string::ToString;
236
237 let len = source.read_usize()?;
238 let bytes = source.read_slice(len)?;
239 let id = core::str::from_utf8(bytes)
240 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
241 Self::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
242 }
243}
244
245#[cfg(feature = "arbitrary")]
246pub(crate) mod testing {
247 use alloc::string::String;
248
249 use proptest::{char::CharStrategy, collection::vec, prelude::*};
250
251 use super::*;
252
253 impl Arbitrary for Ident {
254 type Parameters = ();
255
256 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
257 ident_any_random_length().boxed()
258 }
259
260 type Strategy = BoxedStrategy<Self>;
261 }
262
263 const SPECIAL: [char; 32] = const {
266 let mut buf = ['a'; 32];
267 let mut idx = 0;
268 let mut range_idx = 0;
269 while range_idx < SPECIAL_RANGES.len() {
270 let range = &SPECIAL_RANGES[range_idx];
271 range_idx += 1;
272 let mut j = *range.start() as u32;
273 let end = *range.end() as u32;
274 while j <= end {
275 unsafe {
276 buf[idx] = char::from_u32_unchecked(j);
277 }
278 idx += 1;
279 j += 1;
280 }
281 }
282 buf
283 };
284
285 const SPECIAL_RANGES: &[core::ops::RangeInclusive<char>] =
286 &['!'..='/', ':'..='@', '['..='`', '{'..='~'];
287 const PREFERRED_RANGES: &[core::ops::RangeInclusive<char>] = &['a'..='z', 'A'..='Z'];
288 const EXTRA_RANGES: &[core::ops::RangeInclusive<char>] = &['0'..='9', 'à'..='ö', 'ø'..='ÿ'];
289
290 prop_compose! {
291 fn ident_chars()
294 (c in CharStrategy::new_borrowed(
295 &SPECIAL,
296 PREFERRED_RANGES,
297 EXTRA_RANGES
298 )) -> char {
299 c
300 }
301 }
302
303 prop_compose! {
304 fn ident_raw_any(length: u32)
309 (chars in vec(ident_chars(), 1..=(length as usize))) -> String {
310 String::from_iter(chars)
311 }
312 }
313
314 prop_compose! {
315 pub fn ident_any(length: u32)
317 (raw in ident_raw_any(length)
318 .prop_filter(
319 "identifiers must be valid",
320 |s| Ident::validate(s).is_ok()
321 )
322 ) -> Ident {
323 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
324 }
325 }
326
327 prop_compose! {
328 pub fn ident_any_random_length()
330 (length in 1..u8::MAX)
331 (id in ident_any(length as u32)) -> Ident {
332 id
333 }
334 }
335}