Skip to main content

miden_assembly_syntax/parser/
mod.rs

1mod cst;
2mod error;
3#[cfg(test)]
4mod tests;
5mod value;
6
7use alloc::{boxed::Box, collections::BTreeSet, string::ToString, sync::Arc, vec::Vec};
8
9use miden_debug_types::{SourceFile, SourceLanguage, SourceManager, Uri};
10use miden_utils_diagnostics::{IntoDiagnostic, Report};
11
12pub use self::{
13    cst::parse_inline_masm,
14    error::{BinErrorKind, HexErrorKind, LiteralErrorKind, ParsingError},
15    value::{IntValue, PushValue, WordValue},
16};
17use crate::{Path, ast, sema};
18
19/// Maximum allowed nesting depth of parenthesized constant expressions.
20pub(crate) const MAX_CONSTANT_EXPR_NESTING: usize = 256;
21
22// MODULE PARSER
23// ================================================================================================
24
25/// This is a wrapper around the lower-level parser infrastructure which handles orchestrating all
26/// of the pieces needed to parse a [ast::Module] from source, and run semantic analysis on it.
27#[derive(Default)]
28pub struct ModuleParser {
29    /// The kind of module we're parsing, if known in advance.
30    ///
31    /// This is used when performing semantic analysis to detect when various invalid constructions
32    /// are encountered, such as use of the `syscall` instruction in a kernel module.
33    kind: Option<ast::ModuleKind>,
34    /// A set of interned strings allocated during parsing/semantic analysis.
35    ///
36    /// This is a very primitive and imprecise way of interning strings, but was the least invasive
37    /// at the time the new parser was implemented. In essence, we avoid duplicating allocations
38    /// for frequently occurring strings, by tracking which strings we've seen before, and
39    /// sharing a reference counted pointer instead.
40    ///
41    /// We may want to replace this eventually with a proper interner, so that we can also gain the
42    /// benefits commonly provided by interned string handles (e.g. cheap equality comparisons, no
43    /// ref- counting overhead, copyable and of smaller size).
44    ///
45    /// Note that [Ident], [ProcedureName], [LibraryPath] and others are all implemented in terms
46    /// of either the actual reference-counted string, e.g. `Arc<str>`, or in terms of [Ident],
47    /// which is essentially the former wrapped in a [SourceSpan]. If we ever replace this with
48    /// a better interner, we will also want to update those types to be in terms of whatever
49    /// the handle type of the interner is.
50    interned: BTreeSet<Arc<str>>,
51    /// When true, all warning diagnostics are promoted to error severity
52    warnings_as_errors: bool,
53}
54
55impl ModuleParser {
56    /// Construct a new parser for the given `kind` of [ast::Module].
57    pub fn new(kind: Option<ast::ModuleKind>) -> Self {
58        Self {
59            kind,
60            interned: Default::default(),
61            warnings_as_errors: false,
62        }
63    }
64
65    /// Configure this parser so that any warning diagnostics are promoted to errors.
66    pub fn set_warnings_as_errors(&mut self, yes: bool) {
67        self.warnings_as_errors = yes;
68    }
69
70    /// Parse a [ast::Module] from `source`, and give it the provided `path`.
71    ///
72    /// If `path` is unset, then it must be derivable in one of two ways:
73    ///
74    /// 1. From a `namespace` declaration in the module source
75    /// 2. Inferred as `$exec` from the presence of a `begin .. end` block in the module source
76    ///
77    /// If neither is present, then an error will be raised. It can be fixed by simply providing
78    /// `path` explicitly.
79    pub fn parse(
80        &mut self,
81        path: Option<&Path>,
82        source: Arc<SourceFile>,
83        source_manager: Arc<dyn SourceManager>,
84    ) -> Result<Box<ast::Module>, Report> {
85        use alloc::borrow::Cow;
86
87        let path = match path {
88            Some(path) => Some(Arc::<Path>::from(
89                path.canonicalize()
90                    .and_then(|p| p.to_absolute().map(Cow::into_owned))
91                    .into_diagnostic()?,
92            )),
93            None => None,
94        };
95        let forms = parse_forms_internal(source.clone(), &mut self.interned)?;
96        sema::analyze(
97            source,
98            self.kind,
99            path.as_deref(),
100            forms,
101            self.warnings_as_errors,
102            source_manager,
103        )
104        .map_err(Report::new)
105    }
106
107    /// Parse a [ast::Module], `name`, from `path`.
108    #[cfg(feature = "std")]
109    pub fn parse_file<P>(
110        &mut self,
111        path: Option<&Path>,
112        file_path: P,
113        source_manager: Arc<dyn SourceManager>,
114    ) -> Result<Box<ast::Module>, Report>
115    where
116        P: AsRef<std::path::Path>,
117    {
118        use miden_debug_types::SourceManagerExt;
119        use miden_utils_diagnostics::{IntoDiagnostic, WrapErr};
120
121        let file_path = file_path.as_ref();
122        let source_file =
123            source_manager.load_file(file_path).into_diagnostic().wrap_err_with(|| {
124                format!("failed to load source file from '{}'", file_path.display())
125            })?;
126        self.parse(path, source_file, source_manager)
127    }
128
129    /// Parse a [ast::Module], `name`, from `source`.
130    pub fn parse_str(
131        &mut self,
132        path: Option<&Path>,
133        source: impl ToString,
134        source_manager: Arc<dyn SourceManager>,
135    ) -> Result<Box<ast::Module>, Report> {
136        use miden_debug_types::SourceContent;
137
138        let source = source.to_string();
139        let source_file = match path {
140            Some(path) => {
141                let uri = Uri::from(path.as_str().to_string().into_boxed_str());
142                let content =
143                    SourceContent::new(SourceLanguage::Masm, uri.clone(), source.into_boxed_str());
144                source_manager.load_from_raw_parts(uri, content)
145            },
146            None => source_manager.load_anonymous(SourceLanguage::Masm, source),
147        };
148        self.parse(path, source_file, source_manager)
149    }
150}
151
152/// This is used in tests to parse `source` as a set of raw [ast::Form]s rather than as a
153/// [ast::Module].
154///
155/// NOTE: This does _not_ run semantic analysis.
156#[cfg(any(test, feature = "testing"))]
157pub fn parse_forms(source: Arc<SourceFile>) -> Result<Vec<ast::Form>, Report> {
158    let mut interned = BTreeSet::default();
159    parse_forms_internal(source, &mut interned)
160}
161
162/// Parse `source` as a set of [ast::Form]s
163///
164/// Aside from catching syntax errors, this does little validation of the resulting forms, that is
165/// handled by semantic analysis, which the caller is expected to perform next.
166fn parse_forms_internal(
167    source: Arc<SourceFile>,
168    interned: &mut BTreeSet<Arc<str>>,
169) -> Result<Vec<ast::Form>, Report> {
170    cst::parse_forms(source, interned)
171}
172
173// DIRECTORY PARSER
174// ================================================================================================
175
176/// Read the contents (modules) of this library from `dir`, returning any errors that occur
177/// while traversing the file system.
178///
179/// Errors may also be returned if traversal discovers issues with the modules, such as
180/// invalid names, etc.
181///
182/// Returns an iterator over all parsed modules.
183#[cfg(feature = "std")]
184pub fn read_modules_from_root(
185    root: impl AsRef<std::path::Path>,
186    namespace: Option<Arc<Path>>,
187    kind: Option<ast::ModuleKind>,
188    source_manager: Arc<dyn SourceManager>,
189    warnings_as_errors: bool,
190) -> Result<(Box<ast::Module>, Vec<Box<ast::Module>>), Report> {
191    use miden_utils_diagnostics::report;
192
193    let root = root.as_ref();
194    let root = Arc::<std::path::Path>::from(
195        root.canonicalize()
196            .map_err(|err| {
197                Report::msg(format!("invalid root module path '{}': {err}", root.display()))
198            })?
199            .into_boxed_path(),
200    );
201
202    // Make sure the path has the right file extension
203    if root
204        .extension()
205        .is_none_or(|ext| !ext.eq_ignore_ascii_case(ast::Module::FILE_EXTENSION))
206    {
207        return Err(Report::msg(format!(
208            "invalid root module path '{}': expected a .masm file",
209            root.display()
210        )));
211    }
212
213    // Make sure it is a file
214    if !root.is_file() {
215        return Err(Report::msg(format!(
216            "invalid root module path '{}': not a file",
217            root.display()
218        )));
219    }
220
221    // Capture the parent directory for resolving submodules
222    let root_dir = root
223        .parent()
224        .ok_or_else(|| {
225            Report::msg(format!(
226                "invalid root module path '{}': expected path to have a parent directory",
227                root.display()
228            ))
229        })?
230        .to_path_buf();
231
232    let mut seen = BTreeSet::<Arc<Path>>::new();
233    let mut modules = Vec::new();
234
235    let mut parser = ModuleParser::new(kind);
236    parser.set_warnings_as_errors(warnings_as_errors);
237    let root_ast = parser.parse_file(namespace.as_deref(), &root, source_manager.clone())?;
238
239    let namespace = Arc::<Path>::from(root_ast.path().to_path_buf().into_boxed_path());
240    let submodules = root_ast.submodules().to_vec();
241    seen.insert(namespace.clone());
242    walk_module_tree(
243        namespace,
244        root,
245        root_dir,
246        submodules,
247        source_manager,
248        warnings_as_errors,
249        |module| {
250            if !seen.insert(module.path().into()) {
251                Err(report!("duplicate module '{0}'", module.path()))
252            } else {
253                modules.push(module);
254                Ok(())
255            }
256        },
257    )?;
258
259    Ok((root_ast, modules))
260}
261
262#[cfg(feature = "std")]
263pub fn walk_module_tree<F>(
264    namespace: Arc<Path>,
265    root: Arc<std::path::Path>,
266    current_dir: std::path::PathBuf,
267    submodules: Vec<ast::SubmoduleDecl>,
268    source_manager: Arc<dyn SourceManager>,
269    warnings_as_errors: bool,
270    mut callback: F,
271) -> Result<(), Report>
272where
273    F: FnMut(Box<ast::Module>) -> Result<(), Report>,
274{
275    use miden_debug_types::{Spanned, Uri};
276
277    struct ModuleEntry {
278        pub name: ast::Ident,
279        pub namespace: Arc<Path>,
280        pub directory: Arc<std::path::Path>,
281        pub parent: Arc<std::path::Path>,
282    }
283
284    let current_dir = Arc::<std::path::Path>::from(current_dir.into_boxed_path());
285    let mut visited = BTreeSet::<Arc<std::path::Path>>::from_iter([root.clone()]);
286    let mut worklist = submodules
287        .iter()
288        .map(|sm| ModuleEntry {
289            name: sm.name.clone(),
290            namespace: namespace.clone(),
291            directory: current_dir.clone(),
292            parent: root.clone(),
293        })
294        .collect::<Vec<_>>();
295
296    while let Some(entry) = worklist.pop() {
297        let basename = entry.name.replace('-', "_");
298        let mod_dir = entry.directory.join(&basename);
299        let mod_file = mod_dir.with_extension("masm");
300        let mod_dir_mod_masm = mod_dir.join("mod.masm");
301
302        // If the parent module is at `mod_file`, then the parent module and submodule have the
303        // same name. We explicitly do not allow this, because what we should do is unclear. We
304        // could attempt to add an extra level of nesting, e.g.
305        // `<mod_dir>/<basename>/<basename>.masm` or `<mod_dir>/<basename>/<basename>/mod.masm`,
306        // but that may not be intended.
307        if mod_file.as_path() == &*entry.parent {
308            let span = entry.name.span();
309            let source_file = source_manager.get(span.source_id()).ok();
310            return Err(ParsingError::SelfReferentialSubmodule {
311                name: entry.name.clone(),
312                parent_module_uri: Uri::from(entry.parent),
313                span,
314                source_file,
315            }
316            .into());
317        }
318
319        let actual_path = if mod_file.is_file() {
320            if mod_dir_mod_masm.is_file() {
321                let span = entry.name.span();
322                let source_file = source_manager.get(span.source_id()).ok();
323                return Err(ParsingError::AmbiguousSubmoduleLocation {
324                    name: entry.name,
325                    first: Uri::from(mod_file),
326                    second: Uri::from(mod_dir_mod_masm),
327                    span,
328                    source_file,
329                }
330                .into());
331            }
332            mod_file
333        } else if mod_dir_mod_masm.is_file() {
334            mod_dir_mod_masm
335        } else {
336            let span = entry.name.span();
337            let source_file = source_manager.get(span.source_id()).ok();
338            return Err(ParsingError::UndefinedSubmodule {
339                name: entry.name,
340                basename: basename.into_boxed_str(),
341                directory: Uri::from(mod_dir),
342                span,
343                source_file,
344            }
345            .into());
346        };
347
348        let actual_path = Arc::<std::path::Path>::from(actual_path);
349        if !visited.insert(actual_path.clone()) {
350            let span = entry.name.span();
351            let source_file = source_manager.get(span.source_id()).ok();
352            return Err(ParsingError::DuplicateSubmoduleSource {
353                name: entry.name,
354                module_uri: Uri::from(actual_path.as_ref()),
355                span,
356                source_file,
357            }
358            .into());
359        }
360
361        let mut parser = ModuleParser::new(Some(ast::ModuleKind::Library));
362        parser.set_warnings_as_errors(warnings_as_errors);
363        let module_path = Arc::<Path>::from(entry.namespace.join(&entry.name).into_boxed_path());
364        let ast = parser.parse_file(Some(&module_path), &actual_path, source_manager.clone())?;
365
366        let directory = Arc::<std::path::Path>::from(mod_dir);
367        worklist.extend(ast.submodules().iter().map(|sm| ModuleEntry {
368            name: sm.name.clone(),
369            namespace: module_path.clone(),
370            directory: directory.clone(),
371            parent: actual_path.clone(),
372        }));
373
374        callback(ast)?;
375    }
376
377    Ok(())
378}