miden_assembly_syntax/ast/procedure/
name.rs

1use alloc::{
2    string::{String, ToString},
3    sync::Arc,
4};
5use core::{
6    fmt,
7    hash::{Hash, Hasher},
8    str::FromStr,
9};
10
11use miden_core::utils::{
12    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
13};
14use miden_debug_types::{SourceSpan, Span, Spanned};
15use miden_utils_diagnostics::{IntoDiagnostic, Report, miette};
16
17use crate::{
18    LibraryNamespace, LibraryPath,
19    ast::{CaseKindError, Ident, IdentError},
20};
21
22// QUALIFIED PROCEDURE NAME
23// ================================================================================================
24
25/// Represents a qualified procedure name, e.g. `std::math::u64::add`, parsed into it's
26/// constituent [LibraryPath] and [ProcedureName] components.
27///
28/// A qualified procedure name can be context-sensitive, i.e. the module path might refer
29/// to an imported
30#[derive(Clone)]
31#[cfg_attr(feature = "arbitrary", derive(proptest_derive::Arbitrary))]
32pub struct QualifiedProcedureName {
33    /// The source span associated with this identifier.
34    #[cfg_attr(feature = "arbitrary", proptest(value = "SourceSpan::default()"))]
35    pub span: SourceSpan,
36    /// The module path for this procedure.
37    pub module: LibraryPath,
38    /// The name of the procedure.
39    pub name: ProcedureName,
40}
41
42impl QualifiedProcedureName {
43    /// Create a new [QualifiedProcedureName] with the given fully-qualified module path
44    /// and procedure name.
45    pub fn new(module: LibraryPath, name: ProcedureName) -> Self {
46        Self {
47            span: SourceSpan::default(),
48            module,
49            name,
50        }
51    }
52
53    /// Returns the namespace of this fully-qualified procedure name.
54    pub fn namespace(&self) -> &LibraryNamespace {
55        self.module.namespace()
56    }
57}
58
59impl FromStr for QualifiedProcedureName {
60    type Err = Report;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        match s.rsplit_once("::") {
64            None => Err(Report::msg("invalid fully-qualified procedure name, expected namespace")),
65            Some((path, name)) => {
66                let name = name.parse::<ProcedureName>().into_diagnostic()?;
67                let path = path.parse::<LibraryPath>().into_diagnostic()?;
68                Ok(Self::new(path, name))
69            },
70        }
71    }
72}
73
74impl TryFrom<&str> for QualifiedProcedureName {
75    type Error = Report;
76
77    fn try_from(name: &str) -> Result<Self, Self::Error> {
78        Self::from_str(name)
79    }
80}
81
82impl TryFrom<String> for QualifiedProcedureName {
83    type Error = Report;
84
85    fn try_from(name: String) -> Result<Self, Self::Error> {
86        Self::from_str(&name)
87    }
88}
89
90impl Eq for QualifiedProcedureName {}
91
92impl PartialEq for QualifiedProcedureName {
93    fn eq(&self, other: &Self) -> bool {
94        self.name == other.name && self.module == other.module
95    }
96}
97
98impl Ord for QualifiedProcedureName {
99    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
100        self.module.cmp(&other.module).then_with(|| self.name.cmp(&other.name))
101    }
102}
103
104impl PartialOrd for QualifiedProcedureName {
105    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
106        Some(self.cmp(other))
107    }
108}
109
110impl From<QualifiedProcedureName> for miette::SourceSpan {
111    fn from(fqn: QualifiedProcedureName) -> Self {
112        fqn.span.into()
113    }
114}
115
116impl Spanned for QualifiedProcedureName {
117    fn span(&self) -> SourceSpan {
118        self.span
119    }
120}
121
122impl fmt::Debug for QualifiedProcedureName {
123    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
124        f.debug_struct("FullyQualifiedProcedureName")
125            .field("module", &self.module)
126            .field("name", &self.name)
127            .finish()
128    }
129}
130
131impl crate::prettier::PrettyPrint for QualifiedProcedureName {
132    fn render(&self) -> miden_core::prettier::Document {
133        use crate::prettier::*;
134
135        display(self)
136    }
137}
138
139impl fmt::Display for QualifiedProcedureName {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        write!(f, "{}::{}", &self.module, &self.name)
142    }
143}
144
145impl Serializable for QualifiedProcedureName {
146    fn write_into<W: ByteWriter>(&self, target: &mut W) {
147        self.module.write_into(target);
148        self.name.write_into(target);
149    }
150}
151
152impl Deserializable for QualifiedProcedureName {
153    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
154        let module = LibraryPath::read_from(source)?;
155        let name = ProcedureName::read_from(source)?;
156        Ok(Self::new(module, name))
157    }
158}
159
160// PROCEDURE NAME
161// ================================================================================================
162
163/// Procedure name.
164///
165/// The symbol represented by this type must comply with the following rules:
166///
167/// - It must consist only of alphanumeric characters, or ASCII graphic characters.
168/// - If it starts with a non-alphabetic character, it must contain at least one alphanumeric
169///   character, e.g. `_`, `$_` are not valid procedure symbols, but `_a` or `$_a` are.
170///
171/// NOTE: In Miden Assembly source files, a procedure name must be quoted in double-quotes if it
172/// contains any characters other than ASCII alphanumerics, or `_`. See examples below.
173///
174/// ## Examples
175///
176/// ```masm,ignore
177/// # All ASCII alphanumeric, bare identifier
178/// proc.foo
179///   ...
180/// end
181///
182/// # All ASCII alphanumeric, leading underscore
183/// proc._foo
184///   ...
185/// end
186///
187/// # A symbol which contains `::`, which would be treated as a namespace operator, so requires
188/// # quoting
189/// proc."std::foo"
190///   ...
191/// end
192///
193/// # A complex procedure name representing a monomorphized Rust function, requires quoting
194/// proc."alloc::alloc::box_free::<dyn alloc::boxed::FnBox<(), Output = ()>>"
195///   ...
196/// end
197/// ```
198#[derive(Debug, Clone)]
199pub struct ProcedureName(Ident);
200
201impl ProcedureName {
202    /// Reserved name for a main procedure.
203    pub const MAIN_PROC_NAME: &'static str = "$main";
204
205    /// Creates a [ProcedureName] from `name`.
206    pub fn new(name: impl AsRef<str>) -> Result<Self, IdentError> {
207        name.as_ref().parse()
208    }
209
210    /// Creates a [ProcedureName] from `name`
211    pub fn new_with_span(span: SourceSpan, name: impl AsRef<str>) -> Result<Self, IdentError> {
212        name.as_ref().parse::<Self>().map(|name| name.with_span(span))
213    }
214
215    /// Sets the span for this [ProcedureName].
216    pub fn with_span(self, span: SourceSpan) -> Self {
217        Self(self.0.with_span(span))
218    }
219
220    /// Creates a [ProcedureName] from its raw components.
221    ///
222    /// It is expected that the caller has already validated that the name meets all validity
223    /// criteria for procedure names, for example, the parser only lexes/parses valid identifiers,
224    /// so by construction all such identifiers are valid.
225    ///
226    /// NOTE: This function is perma-unstable, it may be removed or modified at any time.
227    pub fn from_raw_parts(name: Ident) -> Self {
228        Self(name)
229    }
230
231    /// Obtains a procedure name representing the reserved name for the executable entrypoint
232    /// (i.e., `main`).
233    pub fn main() -> Self {
234        let name = Arc::from(Self::MAIN_PROC_NAME.to_string().into_boxed_str());
235        Self(Ident::from_raw_parts(Span::unknown(name)))
236    }
237
238    /// Is this the reserved name for the executable entrypoint (i.e. `main`)?
239    pub fn is_main(&self) -> bool {
240        self.0.as_str() == Self::MAIN_PROC_NAME
241    }
242
243    /// Returns a string reference for this procedure name.
244    pub fn as_str(&self) -> &str {
245        self.as_ref()
246    }
247}
248
249impl Eq for ProcedureName {}
250
251impl PartialEq for ProcedureName {
252    fn eq(&self, other: &Self) -> bool {
253        self.0 == other.0
254    }
255}
256
257impl Ord for ProcedureName {
258    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
259        self.0.cmp(&other.0)
260    }
261}
262
263impl PartialOrd for ProcedureName {
264    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
265        Some(self.cmp(other))
266    }
267}
268
269impl Hash for ProcedureName {
270    fn hash<H: Hasher>(&self, state: &mut H) {
271        self.0.hash(state);
272    }
273}
274
275impl Spanned for ProcedureName {
276    fn span(&self) -> SourceSpan {
277        self.0.span()
278    }
279}
280
281impl From<ProcedureName> for miette::SourceSpan {
282    fn from(name: ProcedureName) -> Self {
283        name.span().into()
284    }
285}
286
287impl core::ops::Deref for ProcedureName {
288    type Target = str;
289
290    #[inline(always)]
291    fn deref(&self) -> &Self::Target {
292        self.0.as_str()
293    }
294}
295
296impl AsRef<Ident> for ProcedureName {
297    #[inline(always)]
298    fn as_ref(&self) -> &Ident {
299        &self.0
300    }
301}
302
303impl AsRef<str> for ProcedureName {
304    #[inline(always)]
305    fn as_ref(&self) -> &str {
306        self.0.as_str()
307    }
308}
309
310impl PartialEq<str> for ProcedureName {
311    fn eq(&self, other: &str) -> bool {
312        self.0.as_ref() == other
313    }
314}
315
316impl PartialEq<Ident> for ProcedureName {
317    fn eq(&self, other: &Ident) -> bool {
318        &self.0 == other
319    }
320}
321
322impl fmt::Display for ProcedureName {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        f.write_str(&self.0)
325    }
326}
327
328/// Parsing
329impl FromStr for ProcedureName {
330    type Err = IdentError;
331
332    fn from_str(s: &str) -> Result<Self, Self::Err> {
333        let mut chars = s.char_indices().peekable();
334
335        // peek the first char
336        match chars.peek() {
337            None => return Err(IdentError::Empty),
338            Some((_, '"')) => chars.next(),
339            Some((_, c)) if is_valid_unquoted_identifier_char(*c) => {
340                // All character for unqouted should be valid
341                let all_chars_valid =
342                    chars.all(|(_, char)| is_valid_unquoted_identifier_char(char));
343
344                if all_chars_valid {
345                    return Ok(Self(Ident::from_raw_parts(Span::unknown(s.into()))));
346                } else {
347                    return Err(IdentError::InvalidChars { ident: s.into() });
348                }
349            },
350            Some((_, c)) if c.is_ascii_uppercase() => {
351                return Err(IdentError::Casing(CaseKindError::Snake));
352            },
353            Some(_) => return Err(IdentError::InvalidChars { ident: s.into() }),
354        };
355
356        // parsing the qouted identifier
357        while let Some((pos, char)) = chars.next() {
358            match char {
359                '"' => {
360                    if chars.next().is_some() {
361                        return Err(IdentError::InvalidChars { ident: s.into() });
362                    }
363                    let token = &s[0..pos];
364                    return Ok(Self(Ident::from_raw_parts(Span::unknown(token.into()))));
365                },
366                c => {
367                    // if char is not alphanumeric or asciigraphic then return err
368                    if !(c.is_alphanumeric() || c.is_ascii_graphic()) {
369                        return Err(IdentError::InvalidChars { ident: s.into() });
370                    }
371                },
372            }
373        }
374
375        // if while loop has not returned then the qoute was not closed
376        Err(IdentError::InvalidChars { ident: s.into() })
377    }
378}
379
380// FROM STR HELPER
381fn is_valid_unquoted_identifier_char(c: char) -> bool {
382    c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '$' | '.')
383}
384
385impl Serializable for ProcedureName {
386    fn write_into<W: ByteWriter>(&self, target: &mut W) {
387        self.as_str().write_into(target)
388    }
389}
390
391impl Deserializable for ProcedureName {
392    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
393        let str: String = source.read()?;
394        let proc_name = ProcedureName::new(str)
395            .map_err(|e| DeserializationError::InvalidValue(e.to_string()))?;
396        Ok(proc_name)
397    }
398}
399
400// ARBITRARY IMPLEMENTATION
401// ================================================================================================
402
403#[cfg(any(test, feature = "arbitrary"))]
404impl proptest::prelude::Arbitrary for ProcedureName {
405    type Parameters = ();
406
407    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
408        use proptest::prelude::*;
409        // see https://doc.rust-lang.org/rustc/symbol-mangling/v0.html#symbol-grammar-summary
410        let all_possible_chars_in_mangled_name =
411            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.$";
412        let mangled_rustc_name = ProcedureName::new(all_possible_chars_in_mangled_name).unwrap();
413        let plain = ProcedureName::new("user_func").unwrap();
414        let wasm_cm_style = ProcedureName::new("kebab-case-func").unwrap();
415        prop_oneof![Just(mangled_rustc_name), Just(plain), Just(wasm_cm_style)].boxed()
416    }
417
418    type Strategy = proptest::prelude::BoxedStrategy<Self>;
419}
420
421// TESTS
422// ================================================================================================
423
424/// Tests
425#[cfg(test)]
426mod tests {
427    use miden_core::utils::{Deserializable, Serializable};
428    use proptest::prelude::*;
429
430    use super::ProcedureName;
431
432    proptest! {
433        #[test]
434        fn procedure_name_serialization_roundtrip(path in any::<ProcedureName>()) {
435            let bytes = path.to_bytes();
436            let deserialized = ProcedureName::read_from_bytes(&bytes).unwrap();
437            assert_eq!(path, deserialized);
438        }
439    }
440}