miden_assembly/ast/
ident.rs1use alloc::{string::ToString, sync::Arc};
2use core::{
3 fmt,
4 hash::{Hash, Hasher},
5 str::FromStr,
6};
7
8use crate::{SourceSpan, Span, Spanned};
9
10#[derive(Debug, thiserror::Error)]
12pub enum IdentError {
13 #[error("invalid identifier: cannot be empty")]
14 Empty,
15 #[error(
16 "invalid identifier '{ident}': must contain only unicode alphanumeric or ascii graphic characters"
17 )]
18 InvalidChars { ident: Arc<str> },
19 #[error("invalid identifier: length exceeds the maximum of {max} bytes")]
20 InvalidLength { max: usize },
21 #[error("invalid identifier: {0}")]
22 Casing(CaseKindError),
23}
24
25#[derive(Debug, thiserror::Error)]
28pub enum CaseKindError {
29 #[error(
30 "only uppercase characters or underscores are allowed, and must start with an alphabetic character"
31 )]
32 Screaming,
33 #[error(
34 "only lowercase characters or underscores are allowed, and must start with an alphabetic character"
35 )]
36 Snake,
37 #[error(
38 "only alphanumeric characters are allowed, and must start with a lowercase alphabetic character"
39 )]
40 Camel,
41}
42
43#[derive(Clone)]
54pub struct Ident {
55 span: SourceSpan,
64 name: Arc<str>,
66}
67
68impl Ident {
69 pub fn new(source: impl AsRef<str>) -> Result<Self, IdentError> {
77 source.as_ref().parse()
78 }
79
80 pub fn new_with_span(span: SourceSpan, source: impl AsRef<str>) -> Result<Self, IdentError> {
88 source.as_ref().parse::<Self>().map(|id| id.with_span(span))
89 }
90
91 pub fn with_span(mut self, span: SourceSpan) -> Self {
93 self.span = span;
94 self
95 }
96
97 pub fn from_raw_parts(name: Span<Arc<str>>) -> Self {
106 let (span, name) = name.into_parts();
107 Self { span, name }
108 }
109
110 pub fn into_inner(self) -> Arc<str> {
112 self.name
113 }
114
115 pub fn as_str(&self) -> &str {
117 self.name.as_ref()
118 }
119
120 pub fn validate(source: impl AsRef<str>) -> Result<(), IdentError> {
122 let source = source.as_ref();
123 if source.is_empty() {
124 return Err(IdentError::Empty);
125 }
126 if !source.chars().all(|c| c.is_ascii_graphic() || c.is_alphanumeric()) {
127 return Err(IdentError::InvalidChars { ident: source.into() });
128 }
129 Ok(())
130 }
131}
132
133impl fmt::Debug for Ident {
134 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
135 f.debug_tuple("Ident").field(&self.name).finish()
136 }
137}
138
139impl Eq for Ident {}
140
141impl PartialEq for Ident {
142 fn eq(&self, other: &Self) -> bool {
143 self.name == other.name
144 }
145}
146
147impl Ord for Ident {
148 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
149 self.name.cmp(&other.name)
150 }
151}
152
153impl PartialOrd for Ident {
154 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
155 Some(self.cmp(other))
156 }
157}
158
159impl Hash for Ident {
160 fn hash<H: Hasher>(&self, state: &mut H) {
161 self.name.hash(state);
162 }
163}
164
165impl Spanned for Ident {
166 fn span(&self) -> SourceSpan {
167 self.span
168 }
169}
170
171impl core::ops::Deref for Ident {
172 type Target = str;
173
174 fn deref(&self) -> &Self::Target {
175 self.name.as_ref()
176 }
177}
178
179impl AsRef<str> for Ident {
180 #[inline]
181 fn as_ref(&self) -> &str {
182 &self.name
183 }
184}
185
186impl fmt::Display for Ident {
187 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
188 fmt::Display::fmt(&self.name, f)
189 }
190}
191
192impl FromStr for Ident {
193 type Err = IdentError;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 Self::validate(s)?;
197 let name = Arc::from(s.to_string().into_boxed_str());
198 Ok(Self { span: SourceSpan::default(), name })
199 }
200}
201
202#[cfg(feature = "testing")]
203pub(crate) mod testing {
204 use alloc::string::String;
205
206 use proptest::{char::CharStrategy, collection::vec, prelude::*};
207
208 use super::*;
209
210 impl Arbitrary for Ident {
211 type Parameters = ();
212
213 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
214 ident_any_random_length().boxed()
215 }
216
217 type Strategy = BoxedStrategy<Self>;
218 }
219
220 const SPECIAL: [char; 32] = const {
223 let mut buf = ['a'; 32];
224 let mut idx = 0;
225 let mut range_idx = 0;
226 while range_idx < SPECIAL_RANGES.len() {
227 let range = &SPECIAL_RANGES[range_idx];
228 range_idx += 1;
229 let mut j = *range.start() as u32;
230 let end = *range.end() as u32;
231 while j <= end {
232 unsafe {
233 buf[idx] = char::from_u32_unchecked(j);
234 }
235 idx += 1;
236 j += 1;
237 }
238 }
239 buf
240 };
241
242 const SPECIAL_RANGES: &[core::ops::RangeInclusive<char>] =
243 &['!'..='/', ':'..='@', '['..='`', '{'..='~'];
244 const PREFERRED_RANGES: &[core::ops::RangeInclusive<char>] = &['a'..='z', 'A'..='Z'];
245 const EXTRA_RANGES: &[core::ops::RangeInclusive<char>] = &['0'..='9', 'à'..='ö', 'ø'..='ÿ'];
246
247 prop_compose! {
248 fn ident_chars()
251 (c in CharStrategy::new_borrowed(
252 &SPECIAL,
253 PREFERRED_RANGES,
254 EXTRA_RANGES
255 )) -> char {
256 c
257 }
258 }
259
260 prop_compose! {
261 fn ident_raw_any(length: u32)
266 (chars in vec(ident_chars(), 1..=(length as usize))) -> String {
267 String::from_iter(chars)
268 }
269 }
270
271 prop_compose! {
272 pub fn ident_any(length: u32)
274 (raw in ident_raw_any(length)
275 .prop_filter(
276 "identifiers must be valid",
277 |s| Ident::validate(s).is_ok()
278 )
279 ) -> Ident {
280 Ident::from_raw_parts(Span::new(SourceSpan::UNKNOWN, raw.into_boxed_str().into()))
281 }
282 }
283
284 prop_compose! {
285 pub fn ident_any_random_length()
287 (length in 1..u8::MAX)
288 (id in ident_any(length as u32)) -> Ident {
289 id
290 }
291 }
292}