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::serde::{
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)]
57#[cfg_attr(
58 all(feature = "arbitrary", test),
59 miden_test_serialization_macros::serialization_test
60)]
61pub struct Ident {
62 span: SourceSpan,
71 name: Arc<str>,
73}
74
75impl Ident {
76 pub const MAIN: &'static str = "$main";
78
79 pub fn new(source: impl AsRef<str>) -> Result<Self, IdentError> {
87 source.as_ref().parse()
88 }
89
90 pub fn new_with_span(span: SourceSpan, source: impl AsRef<str>) -> Result<Self, IdentError> {
98 source.as_ref().parse::<Self>().map(|id| id.with_span(span))
99 }
100
101 pub fn with_span(mut self, span: SourceSpan) -> Self {
103 self.span = span;
104 self
105 }
106
107 pub fn from_raw_parts(name: Span<Arc<str>>) -> Self {
116 let (span, name) = name.into_parts();
117 Self { span, name }
118 }
119
120 pub fn into_inner(self) -> Arc<str> {
122 self.name
123 }
124
125 pub fn as_str(&self) -> &str {
127 self.name.as_ref()
128 }
129
130 pub fn is_constant_ident(&self) -> bool {
132 self.name
133 .chars()
134 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
135 }
136
137 pub fn requires_quoting(ident: impl AsRef<str>) -> bool {
139 match ident.as_ref() {
140 crate::Path::KERNEL_PATH
141 | crate::Path::EXEC_PATH
142 | crate::ast::ProcedureName::MAIN_PROC_NAME => false,
143 ident => {
144 ident.starts_with(|c: char| c.is_ascii_digit())
145 || !ident.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
146 },
147 }
148 }
149
150 pub fn validate(source: impl AsRef<str>) -> Result<(), IdentError> {
152 let source = source.as_ref();
153 if source.is_empty() {
154 return Err(IdentError::Empty);
155 }
156 if !source
157 .chars()
158 .all(|c| (c.is_ascii_graphic() || c.is_alphanumeric()) && c != '#')
159 {
160 return Err(IdentError::InvalidChars { ident: source.into() });
161 }
162 Ok(())
163 }
164}
165
166impl fmt::Debug for Ident {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 f.debug_tuple("Ident").field(&self.name).finish()
169 }
170}
171
172impl Eq for Ident {}
173
174impl PartialEq for Ident {
175 fn eq(&self, other: &Self) -> bool {
176 self.name == other.name
177 }
178}
179
180impl Ord for Ident {
181 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
182 self.name.cmp(&other.name)
183 }
184}
185
186impl PartialOrd for Ident {
187 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
188 Some(self.cmp(other))
189 }
190}
191
192impl Hash for Ident {
193 fn hash<H: Hasher>(&self, state: &mut H) {
194 self.name.hash(state);
195 }
196}
197
198impl Spanned for Ident {
199 fn span(&self) -> SourceSpan {
200 self.span
201 }
202}
203
204impl core::ops::Deref for Ident {
205 type Target = str;
206
207 fn deref(&self) -> &Self::Target {
208 self.name.as_ref()
209 }
210}
211
212impl AsRef<str> for Ident {
213 #[inline]
214 fn as_ref(&self) -> &str {
215 &self.name
216 }
217}
218
219impl fmt::Display for Ident {
220 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221 if Self::requires_quoting(&self.name) {
222 write!(f, "\"{}\"", self.name.escape_debug())
223 } else {
224 f.write_str(&self.name)
225 }
226 }
227}
228
229impl crate::prettier::PrettyPrint for Ident {
230 fn render(&self) -> crate::prettier::Document {
231 use crate::prettier::*;
232 display(self)
233 }
234}
235
236impl FromStr for Ident {
237 type Err = IdentError;
238
239 fn from_str(s: &str) -> Result<Self, Self::Err> {
240 Self::validate(s)?;
241 let name = Arc::from(s.to_string().into_boxed_str());
242 Ok(Self { span: SourceSpan::default(), name })
243 }
244}
245
246impl From<Ident> for miden_utils_diagnostics::miette::SourceSpan {
247 fn from(value: Ident) -> Self {
248 value.span.into()
249 }
250}
251
252impl Serializable for Ident {
253 fn write_into<W: ByteWriter>(&self, target: &mut W) {
254 target.write_usize(self.len());
255 target.write_bytes(self.as_bytes());
256 }
257}
258
259impl Deserializable for Ident {
260 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
261 use alloc::string::ToString;
262
263 let len = source.read_usize()?;
264 let bytes = source.read_slice(len)?;
265 let id = core::str::from_utf8(bytes)
266 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
267 Self::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn ident_with_quotes_is_properly_escaped() {
277 let id = Ident::new("a\"b").unwrap();
278 let output = id.to_string();
279 assert_eq!(output, "\"a\\\"b\"")
280 }
281}
282
283#[cfg(feature = "arbitrary")]
284pub mod arbitrary {
285 use alloc::{borrow::Cow, string::String};
286
287 use proptest::{char::CharStrategy, collection::vec, prelude::*};
288
289 use super::*;
290
291 impl Arbitrary for Ident {
292 type Parameters = ();
293
294 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
295 ident_any_random_length().boxed()
296 }
297
298 type Strategy = BoxedStrategy<Self>;
299 }
300
301 const SPECIAL: [char; 32] = const {
304 let mut buf = ['a'; 32];
305 let mut idx = 0;
306 let mut range_idx = 0;
307 while range_idx < SPECIAL_RANGES.len() {
308 let range = &SPECIAL_RANGES[range_idx];
309 range_idx += 1;
310 let mut j = *range.start() as u32;
311 let end = *range.end() as u32;
312 while j <= end {
313 unsafe {
314 buf[idx] = char::from_u32_unchecked(j);
315 }
316 idx += 1;
317 j += 1;
318 }
319 }
320 buf
321 };
322
323 const SPECIAL_RANGES: &[core::ops::RangeInclusive<char>] =
324 &['!'..='/', ':'..='@', '['..='`', '{'..='~'];
325 const PREFERRED_RANGES: &[core::ops::RangeInclusive<char>] = &['a'..='z', 'A'..='Z'];
326 const EXTRA_RANGES: &[core::ops::RangeInclusive<char>] = &['0'..='9', 'à'..='ö', 'ø'..='ÿ'];
327
328 const PREFERRED_CONSTANT_RANGES: &[core::ops::RangeInclusive<char>] =
329 core::slice::from_ref(&('A'..='Z'));
330 const EXTRA_CONSTANT_RANGES: &[core::ops::RangeInclusive<char>] =
331 core::slice::from_ref(&('0'..='9'));
332
333 prop_compose! {
334 #[allow(clippy::single_range_in_vec_init)]
337 fn bare_ident_chars()
338 (c in CharStrategy::new_borrowed(
339 &['_'],
340 PREFERRED_RANGES,
341 core::slice::from_ref(&('0'..='9'))
342 )) -> char {
343 c
344 }
345 }
346
347 prop_compose! {
348 fn ident_chars()
351 (c in CharStrategy::new_borrowed(
352 &SPECIAL,
353 PREFERRED_RANGES,
354 EXTRA_RANGES
355 )) -> char {
356 c
357 }
358 }
359
360 prop_compose! {
361 fn const_ident_chars()
363 (c in CharStrategy::new_borrowed(
364 &['_'],
365 PREFERRED_CONSTANT_RANGES,
366 EXTRA_CONSTANT_RANGES
367 )) -> char {
368 c
369 }
370 }
371
372 prop_compose! {
373 fn ident_raw_any(length: u32)
378 ((leading_char, rest) in (
379 proptest::char::ranges(Cow::Borrowed(&['a'..='z', '_'..='_'])),
380 vec(ident_chars(), 0..=(length as usize))
381 )) -> String {
382 let mut buf = String::with_capacity(length as usize);
383 buf.push(leading_char);
384 for c in rest {
385 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
386 break;
387 }
388 buf.push(c);
389 }
390 buf
391 }
392 }
393
394 prop_compose! {
395 fn bare_ident_raw_any(length: u32)
397 ((leading_char, rest) in (
398 proptest::char::range('a', 'z'),
399 vec(bare_ident_chars(), 0..=(length as usize))
400 )) -> String {
401 let mut buf = String::with_capacity(length as usize);
402 buf.push(leading_char);
403 for c in rest {
404 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
405 break;
406 }
407 buf.push(c);
408 }
409 buf
410 }
411 }
412
413 prop_compose! {
414 fn const_ident_raw_any(length: u32)
416 ((leading_char, rest) in (
417 proptest::char::range('A', 'Z'),
418 vec(const_ident_chars(), 0..=(length as usize))
419 )) -> String {
420 let mut buf = String::with_capacity(length as usize);
421 buf.push(leading_char);
422 for c in rest {
423 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
424 break;
425 }
426 buf.push(c);
427 }
428 buf
429 }
430 }
431
432 prop_compose! {
433 pub fn ident_any(length: u32)
435 (raw in ident_raw_any(length)
436 .prop_filter(
437 "identifiers must be valid",
438 |s| Ident::validate(s).is_ok()
439 )
440 ) -> Ident {
441 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
442 }
443 }
444
445 prop_compose! {
446 pub fn bare_ident_any(length: u32)
449 (raw in bare_ident_raw_any(length)
450 .prop_filter(
451 "identifiers must be valid",
452 |s| Ident::validate(s).is_ok()
453 )
454 ) -> Ident {
455 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
456 }
457 }
458
459 prop_compose! {
460 pub fn const_ident_any(length: u32)
463 (raw in const_ident_raw_any(length)
464 .prop_filter(
465 "identifiers must be valid",
466 |s| Ident::validate(s).is_ok()
467 )
468 ) -> Ident {
469 let id = Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()));
470 assert!(id.is_constant_ident());
471 id
472 }
473 }
474
475 prop_compose! {
476 pub fn builtin_type_any()
478 (name in prop_oneof![
479 Just(crate::ast::types::Type::I1),
480 Just(crate::ast::types::Type::I8),
481 Just(crate::ast::types::Type::U8),
482 Just(crate::ast::types::Type::I16),
483 Just(crate::ast::types::Type::U16),
484 Just(crate::ast::types::Type::I32),
485 Just(crate::ast::types::Type::U32),
486 Just(crate::ast::types::Type::I64),
487 Just(crate::ast::types::Type::U64),
488 Just(crate::ast::types::Type::I128),
489 Just(crate::ast::types::Type::U128),
490 Just(crate::ast::types::Type::Felt),
491 ]) -> Ident {
492 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, name.to_string().into_boxed_str().into()))
493 }
494 }
495
496 prop_compose! {
497 pub fn ident_any_random_length()
499 (length in 1..u8::MAX)
500 (id in ident_any(length as u32)) -> Ident {
501 id
502 }
503 }
504
505 prop_compose! {
506 pub fn bare_ident_any_random_length()
509 (length in 1..u8::MAX)
510 (id in ident_any(length as u32)) -> Ident {
511 id
512 }
513 }
514
515 prop_compose! {
516 pub fn const_ident_any_random_length()
519 (length in 1..u8::MAX)
520 (id in const_ident_any(length as u32)) -> Ident {
521 id
522 }
523 }
524}