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_serde_macros::serde_test(binary_serde(true))
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 => !ident.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
144 }
145 }
146
147 pub fn validate(source: impl AsRef<str>) -> Result<(), IdentError> {
149 let source = source.as_ref();
150 if source.is_empty() {
151 return Err(IdentError::Empty);
152 }
153 if !source
154 .chars()
155 .all(|c| (c.is_ascii_graphic() || c.is_alphanumeric()) && c != '#')
156 {
157 return Err(IdentError::InvalidChars { ident: source.into() });
158 }
159 Ok(())
160 }
161}
162
163impl fmt::Debug for Ident {
164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165 f.debug_tuple("Ident").field(&self.name).finish()
166 }
167}
168
169impl Eq for Ident {}
170
171impl PartialEq for Ident {
172 fn eq(&self, other: &Self) -> bool {
173 self.name == other.name
174 }
175}
176
177impl Ord for Ident {
178 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
179 self.name.cmp(&other.name)
180 }
181}
182
183impl PartialOrd for Ident {
184 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
185 Some(self.cmp(other))
186 }
187}
188
189impl Hash for Ident {
190 fn hash<H: Hasher>(&self, state: &mut H) {
191 self.name.hash(state);
192 }
193}
194
195impl Spanned for Ident {
196 fn span(&self) -> SourceSpan {
197 self.span
198 }
199}
200
201impl core::ops::Deref for Ident {
202 type Target = str;
203
204 fn deref(&self) -> &Self::Target {
205 self.name.as_ref()
206 }
207}
208
209impl AsRef<str> for Ident {
210 #[inline]
211 fn as_ref(&self) -> &str {
212 &self.name
213 }
214}
215
216impl fmt::Display for Ident {
217 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218 if Self::requires_quoting(&self.name) {
219 write!(f, "\"{}\"", self.name.escape_debug())
220 } else {
221 f.write_str(&self.name)
222 }
223 }
224}
225
226impl crate::prettier::PrettyPrint for Ident {
227 fn render(&self) -> crate::prettier::Document {
228 use crate::prettier::*;
229 display(self)
230 }
231}
232
233impl FromStr for Ident {
234 type Err = IdentError;
235
236 fn from_str(s: &str) -> Result<Self, Self::Err> {
237 Self::validate(s)?;
238 let name = Arc::from(s.to_string().into_boxed_str());
239 Ok(Self { span: SourceSpan::default(), name })
240 }
241}
242
243impl From<Ident> for miden_utils_diagnostics::miette::SourceSpan {
244 fn from(value: Ident) -> Self {
245 value.span.into()
246 }
247}
248
249#[cfg(feature = "serde")]
250impl serde::Serialize for Ident {
251 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
252 where
253 S: serde::Serializer,
254 {
255 serializer.serialize_str(self.as_str())
256 }
257}
258
259#[cfg(feature = "serde")]
260impl<'de> serde::Deserialize<'de> for Ident {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: serde::Deserializer<'de>,
264 {
265 let name = <&'de str as serde::Deserialize>::deserialize(deserializer)?;
266 Self::new(name).map_err(serde::de::Error::custom)
267 }
268}
269
270impl Serializable for Ident {
271 fn write_into<W: ByteWriter>(&self, target: &mut W) {
272 target.write_usize(self.len());
273 target.write_bytes(self.as_bytes());
274 }
275}
276
277impl Deserializable for Ident {
278 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
279 use alloc::string::ToString;
280
281 let len = source.read_usize()?;
282 let bytes = source.read_slice(len)?;
283 let id = core::str::from_utf8(bytes)
284 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
285 Self::new(id).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn ident_with_quotes_is_properly_escaped() {
295 let id = Ident::new("a\"b").unwrap();
296 let output = id.to_string();
297 assert_eq!(output, "\"a\\\"b\"")
298 }
299}
300
301#[cfg(feature = "arbitrary")]
302pub mod arbitrary {
303 use alloc::{borrow::Cow, string::String};
304
305 use proptest::{char::CharStrategy, collection::vec, prelude::*};
306
307 use super::*;
308
309 impl Arbitrary for Ident {
310 type Parameters = ();
311
312 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
313 ident_any_random_length().boxed()
314 }
315
316 type Strategy = BoxedStrategy<Self>;
317 }
318
319 const SPECIAL: [char; 32] = const {
322 let mut buf = ['a'; 32];
323 let mut idx = 0;
324 let mut range_idx = 0;
325 while range_idx < SPECIAL_RANGES.len() {
326 let range = &SPECIAL_RANGES[range_idx];
327 range_idx += 1;
328 let mut j = *range.start() as u32;
329 let end = *range.end() as u32;
330 while j <= end {
331 unsafe {
332 buf[idx] = char::from_u32_unchecked(j);
333 }
334 idx += 1;
335 j += 1;
336 }
337 }
338 buf
339 };
340
341 const SPECIAL_RANGES: &[core::ops::RangeInclusive<char>] =
342 &['!'..='/', ':'..='@', '['..='`', '{'..='~'];
343 const PREFERRED_RANGES: &[core::ops::RangeInclusive<char>] = &['a'..='z', 'A'..='Z'];
344 const EXTRA_RANGES: &[core::ops::RangeInclusive<char>] = &['0'..='9', 'à'..='ö', 'ø'..='ÿ'];
345
346 const PREFERRED_CONSTANT_RANGES: &[core::ops::RangeInclusive<char>] =
347 core::slice::from_ref(&('A'..='Z'));
348 const EXTRA_CONSTANT_RANGES: &[core::ops::RangeInclusive<char>] =
349 core::slice::from_ref(&('0'..='9'));
350
351 prop_compose! {
352 fn bare_ident_chars()
355 (c in CharStrategy::new_borrowed(
356 &['_'],
357 PREFERRED_RANGES,
358 core::slice::from_ref(&('0'..='9'))
359 )) -> char {
360 c
361 }
362 }
363
364 prop_compose! {
365 fn ident_chars()
368 (c in CharStrategy::new_borrowed(
369 &SPECIAL,
370 PREFERRED_RANGES,
371 EXTRA_RANGES
372 )) -> char {
373 c
374 }
375 }
376
377 prop_compose! {
378 fn const_ident_chars()
380 (c in CharStrategy::new_borrowed(
381 &['_'],
382 PREFERRED_CONSTANT_RANGES,
383 EXTRA_CONSTANT_RANGES
384 )) -> char {
385 c
386 }
387 }
388
389 prop_compose! {
390 fn ident_raw_any(length: u32)
395 ((leading_char, rest) in (
396 proptest::char::ranges(Cow::Borrowed(&['a'..='z', '_'..='_'])),
397 vec(ident_chars(), 0..=(length as usize))
398 )) -> String {
399 let mut buf = String::with_capacity(length as usize);
400 buf.push(leading_char);
401 for c in rest {
402 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
403 break;
404 }
405 buf.push(c);
406 }
407 buf
408 }
409 }
410
411 prop_compose! {
412 fn bare_ident_raw_any(length: u32)
414 ((leading_char, rest) in (
415 proptest::char::range('a', 'z'),
416 vec(bare_ident_chars(), 0..=(length as usize))
417 )) -> String {
418 let mut buf = String::with_capacity(length as usize);
419 buf.push(leading_char);
420 for c in rest {
421 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
422 break;
423 }
424 buf.push(c);
425 }
426 buf
427 }
428 }
429
430 prop_compose! {
431 fn const_ident_raw_any(length: u32)
433 ((leading_char, rest) in (
434 proptest::char::range('A', 'Z'),
435 vec(const_ident_chars(), 0..=(length as usize))
436 )) -> String {
437 let mut buf = String::with_capacity(length as usize);
438 buf.push(leading_char);
439 for c in rest {
440 if !buf.is_empty() && buf.len() + c.len_utf8() > length as usize {
441 break;
442 }
443 buf.push(c);
444 }
445 buf
446 }
447 }
448
449 prop_compose! {
450 pub fn ident_any(length: u32)
452 (raw in ident_raw_any(length)
453 .prop_filter(
454 "identifiers must be valid",
455 |s| Ident::validate(s).is_ok()
456 )
457 ) -> Ident {
458 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
459 }
460 }
461
462 prop_compose! {
463 pub fn bare_ident_any(length: u32)
466 (raw in bare_ident_raw_any(length)
467 .prop_filter(
468 "identifiers must be valid",
469 |s| Ident::validate(s).is_ok()
470 )
471 ) -> Ident {
472 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
473 }
474 }
475
476 prop_compose! {
477 pub fn const_ident_any(length: u32)
480 (raw in const_ident_raw_any(length)
481 .prop_filter(
482 "identifiers must be valid",
483 |s| Ident::validate(s).is_ok()
484 )
485 ) -> Ident {
486 let id = Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()));
487 assert!(id.is_constant_ident());
488 id
489 }
490 }
491
492 prop_compose! {
493 pub fn builtin_type_any()
495 (name in prop_oneof![
496 Just(crate::ast::types::Type::I1),
497 Just(crate::ast::types::Type::I8),
498 Just(crate::ast::types::Type::U8),
499 Just(crate::ast::types::Type::I16),
500 Just(crate::ast::types::Type::U16),
501 Just(crate::ast::types::Type::I32),
502 Just(crate::ast::types::Type::U32),
503 Just(crate::ast::types::Type::I64),
504 Just(crate::ast::types::Type::U64),
505 Just(crate::ast::types::Type::I128),
506 Just(crate::ast::types::Type::U128),
507 Just(crate::ast::types::Type::Felt),
508 ]) -> Ident {
509 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, name.to_string().into_boxed_str().into()))
510 }
511 }
512
513 prop_compose! {
514 pub fn ident_any_random_length()
516 (length in 1..u8::MAX)
517 (id in ident_any(length as u32)) -> Ident {
518 id
519 }
520 }
521
522 prop_compose! {
523 pub fn bare_ident_any_random_length()
526 (length in 1..u8::MAX)
527 (id in ident_any(length as u32)) -> Ident {
528 id
529 }
530 }
531
532 prop_compose! {
533 pub fn const_ident_any_random_length()
536 (length in 1..u8::MAX)
537 (id in const_ident_any(length as u32)) -> Ident {
538 id
539 }
540 }
541}