air_parser/ast/
mod.rs

1mod declarations;
2mod display;
3mod errors;
4mod expression;
5mod module;
6mod statement;
7mod trace;
8mod types;
9pub mod visit;
10
11pub use self::declarations::*;
12pub(crate) use self::display::*;
13pub use self::errors::*;
14pub use self::expression::*;
15pub use self::module::*;
16pub use self::statement::*;
17pub use self::trace::*;
18pub use self::types::*;
19
20use std::{
21    collections::{BTreeMap, HashMap, HashSet, VecDeque},
22    fmt, mem,
23    path::{Path, PathBuf},
24    sync::Arc,
25};
26
27use miden_diagnostics::{
28    CodeMap, DiagnosticsHandler, FileName, Severity, SourceSpan, Span, Spanned,
29};
30
31use crate::{
32    Symbol,
33    parser::ParseError,
34    sema::{self, SemanticAnalysisError},
35};
36
37/// This structure is used to represent parsing arbitrary AirScript files which may
38/// or may not contain a root module.
39///
40/// All of the details described in the documentation for [Program] and [Library]
41/// apply to their respective variants here.
42#[derive(Debug)]
43pub enum Source {
44    /// The source code which was parsed produced a valid [Program],
45    /// i.e. it contained a root module, and optionally, one or more
46    /// library modules.
47    Program(Program),
48    /// The source code which was parsed did not contain a root module,
49    /// and so does not constitute a valid [Program] on its own. However,
50    /// we were still able to produce a library of modules, which can be
51    /// combined with a root module to produce a [Program] later.
52    Library(Library),
53}
54
55/// This represents a fully parsed AirScript program, with all imports resolved/parsed/merged.
56///
57/// It has undergone initial semantic analysis, which guarantees that all names are resolved
58/// to their definitions. Semantic analysis also runs a variety of validation checks while
59/// performing name resolution, including basic type checking, constraint validation, and
60/// more.
61///
62/// Additionally, a [Program] has had most dead code eliminated. Specifically any items which
63/// are not referred to from the root module directly or transitively, are not present in
64/// the [Program] structure. Currently, analysis doesn't check for dead code within functions
65/// or constraint blocks, so that is the only area in which dead code may still exist.
66#[derive(Debug)]
67pub struct Program {
68    /// The name of an AirScript program is the name of its root module.
69    pub name: Identifier,
70    /// The set of used constants referenced in this program.
71    pub constants: BTreeMap<QualifiedIdentifier, Constant>,
72    /// The set of used evaluator functions referenced in this program.
73    pub evaluators: BTreeMap<QualifiedIdentifier, EvaluatorFunction>,
74    /// The set of used pure functions referenced in this program.
75    pub functions: BTreeMap<QualifiedIdentifier, Function>,
76    /// The set of used buses referenced in this program.
77    pub buses: BTreeMap<QualifiedIdentifier, Bus>,
78    /// The set of used periodic columns referenced in this program.
79    pub periodic_columns: BTreeMap<QualifiedIdentifier, PeriodicColumn>,
80    /// The set of public inputs defined in the root module
81    ///
82    /// NOTE: Public inputs are only visible in the root module, so we do
83    /// not use [QualifiedIdentifier] as a key into this collection.
84    pub public_inputs: BTreeMap<Identifier, PublicInput>,
85    /// The set of trace columns of the main trace defined in the root module
86    pub trace_columns: Vec<TraceSegment>,
87    /// The boundary_constraints block defined in the root module
88    ///
89    /// It is guaranteed that this is non-empty
90    pub boundary_constraints: Vec<Statement>,
91    /// The integrity_constraints block in the root module
92    ///
93    /// It is guaranteed that this is non-empty
94    pub integrity_constraints: Vec<Statement>,
95}
96impl Program {
97    /// Creates a new, empty [Program].
98    ///
99    /// # SAFETY
100    ///
101    /// This function technically violates the guarantees described above
102    /// in the module docs, however it is useful for testing purposes to
103    /// allow constructing a valid [Program] piece-by-piece. It is up to
104    /// the caller to ensure that they construct a [Program] that adheres
105    /// to all of the expected guarantees.
106    ///
107    /// NOTE: It isn't strictly unsafe in the Rust sense to fail to uphold
108    /// the guarantees described above; it will simply cause compilation to
109    /// fail unexpectedly with a panic at some point. As a result, this function
110    /// isn't marked `unsafe`, but should be treated like it is anyway.
111    pub fn new(name: Identifier) -> Self {
112        Self {
113            name,
114            constants: Default::default(),
115            evaluators: Default::default(),
116            functions: Default::default(),
117            buses: Default::default(),
118            periodic_columns: Default::default(),
119            public_inputs: Default::default(),
120            trace_columns: vec![],
121            boundary_constraints: vec![],
122            integrity_constraints: vec![],
123        }
124    }
125
126    /// Load a program from a library of modules, of which one should be a root module.
127    ///
128    /// When called, it is expected that the library has had import resolution performed,
129    /// and that the library contains a root module.
130    pub fn load(
131        diagnostics: &DiagnosticsHandler,
132        root: ModuleId,
133        mut library: Library,
134    ) -> Result<Self, SemanticAnalysisError> {
135        use crate::sema::DependencyType;
136        use petgraph::visit::DfsPostOrder;
137
138        let mut program = Program::new(root);
139
140        // Validate that the root module is contained in the library
141        if !library.contains(&root) {
142            return Err(SemanticAnalysisError::MissingRoot);
143        }
144
145        // Add root-only items from root module to program
146        {
147            let root_module = library.get_mut(&root).unwrap();
148            mem::swap(&mut program.public_inputs, &mut root_module.public_inputs);
149            mem::swap(&mut program.trace_columns, &mut root_module.trace_columns);
150        }
151
152        // Build the module graph starting from the root module
153        let mut modgraph = sema::ModuleGraph::new();
154        let mut visited = HashSet::<ModuleId>::default();
155        let mut worklist = VecDeque::new();
156        worklist.push_back(root);
157        while let Some(module_name) = worklist.pop_front() {
158            // If we haven't visited the imported module yet, add it's imports to the graph
159            if visited.insert(module_name) {
160                modgraph.add_node(module_name);
161
162                if let Some(module) = library.get(&module_name) {
163                    for import in module.imports.values() {
164                        let import_module = modgraph.add_node(import.module());
165                        // If an attempt is made to import the root module, raise an error
166                        if import_module == root {
167                            return Err(SemanticAnalysisError::RootImport(import.module().span()));
168                        }
169
170                        assert_eq!(modgraph.add_edge(module_name, import_module, ()), None);
171                        worklist.push_back(import_module);
172                    }
173                } else {
174                    return Err(SemanticAnalysisError::MissingModule(module_name));
175                }
176            }
177        }
178
179        // Construct a dependency graph for the root, by visiting each module in the
180        // module graph in bottom-up order, so we see dependencies before dependents.
181        //
182        // In each dependency module, we resolve all identifiers in that module to
183        // their fully-qualified form, and add edges in the dependency graph which
184        // represent what items are referenced from the functions/constraints in that module.
185        let mut deps = sema::DependencyGraph::new();
186        let mut visitor = DfsPostOrder::new(&modgraph, root);
187        while let Some(module_name) = visitor.next(&modgraph) {
188            // Remove the module from the library temporarily, so that we
189            // can look up other modules in the library while we modify it
190            //
191            // NOTE: This will always succeed, or we would have raised an error
192            // during semantic analysis
193            let mut module = library.modules.remove(&module_name).unwrap();
194
195            // Resolve imports
196            let resolver = sema::ImportResolver::new(diagnostics, &library);
197            let imported = resolver.run(&mut module)?;
198
199            // Perform semantic analysis on the module, updating the
200            // dependency graph with information gathered from this module
201            let analysis =
202                sema::SemanticAnalysis::new(diagnostics, &program, &library, &mut deps, imported);
203            analysis.run(&mut module)?;
204
205            // Put the module back
206            library.modules.insert(module.name, module);
207        }
208
209        // Now that we have a dependency graph for each function/constraint in the root module,
210        // we traverse the graph top-down from the root node, to each of it's dependencies,
211        // adding them to the program struct as we go. The root node represents items referenced
212        // from the boundary_constraints and integrity_constraints sections, or any of the functions
213        // in the root module.
214        let root_node = QualifiedIdentifier::new(
215            program.name,
216            NamespacedIdentifier::Binding(Identifier::new(
217                SourceSpan::UNKNOWN,
218                Symbol::intern("$$root"),
219            )),
220        );
221        let mut root_nodes = VecDeque::from(vec![root_node]);
222        {
223            let root_module = library.get(&root).unwrap();
224            // Make sure we move the boundary_constraints into the program
225            if let Some(bc) = root_module.boundary_constraints.as_ref() {
226                program.boundary_constraints = bc.to_vec();
227            }
228            // Make sure we move the integrity_constraints into the program
229            if let Some(ic) = root_module.integrity_constraints.as_ref() {
230                program.integrity_constraints = ic.to_vec();
231            }
232            // Make sure we move the buses into the program
233            if !root_module.buses.is_empty() {
234                program.buses = BTreeMap::from_iter(root_module.buses.iter().map(|(k, v)| {
235                    (
236                        QualifiedIdentifier::new(root, NamespacedIdentifier::Binding(*k)),
237                        v.clone(),
238                    )
239                }));
240            }
241            for evaluator in root_module.evaluators.values() {
242                root_nodes.push_back(QualifiedIdentifier::new(
243                    root,
244                    NamespacedIdentifier::Function(evaluator.name),
245                ));
246            }
247        }
248
249        let mut visited = HashSet::<QualifiedIdentifier>::default();
250        while let Some(node) = root_nodes.pop_front() {
251            for (_, referenced, dep_type) in
252                deps.edges_directed(node, petgraph::Direction::Outgoing)
253            {
254                // Avoid spinning infinitely in dependency cycles
255                if !visited.insert(referenced) {
256                    continue;
257                }
258
259                // Add dependency to program
260                let referenced_module = library.get(&referenced.module).unwrap();
261                let id = referenced.item.id();
262                match dep_type {
263                    DependencyType::Constant => {
264                        program
265                            .constants
266                            .entry(referenced)
267                            .or_insert_with(|| referenced_module.constants[&id].clone());
268                    }
269                    DependencyType::Evaluator => {
270                        program
271                            .evaluators
272                            .entry(referenced)
273                            .or_insert_with(|| referenced_module.evaluators[&id].clone());
274                    }
275                    DependencyType::Function => {
276                        program
277                            .functions
278                            .entry(referenced)
279                            .or_insert_with(|| referenced_module.functions[&id].clone());
280                    }
281                    DependencyType::PeriodicColumn => {
282                        program
283                            .periodic_columns
284                            .entry(referenced)
285                            .or_insert_with(|| referenced_module.periodic_columns[&id].clone());
286                    }
287                }
288
289                // Make sure we visit all of the dependencies of this dependency
290                root_nodes.push_back(referenced);
291            }
292        }
293
294        Ok(program)
295    }
296}
297impl Eq for Program {}
298impl PartialEq for Program {
299    fn eq(&self, other: &Self) -> bool {
300        self.name == other.name
301            && self.constants == other.constants
302            && self.evaluators == other.evaluators
303            && self.functions == other.functions
304            && self.periodic_columns == other.periodic_columns
305            && self.public_inputs == other.public_inputs
306            && self.trace_columns == other.trace_columns
307            && self.boundary_constraints == other.boundary_constraints
308            && self.integrity_constraints == other.integrity_constraints
309    }
310}
311impl fmt::Display for Program {
312    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
313        writeln!(f, "def {}\n", self.name)?;
314
315        writeln!(f, "trace_columns {{")?;
316        for segment in self.trace_columns.iter() {
317            writeln!(f, "    {segment}")?;
318        }
319        f.write_str("}}")?;
320        f.write_str("\n")?;
321
322        writeln!(f, "public_inputs {{")?;
323        for public_input in self.public_inputs.values() {
324            writeln!(f, "    {}: [{}]", public_input.name(), public_input.size())?;
325        }
326        f.write_str("}}")?;
327        f.write_str("\n")?;
328
329        if !self.periodic_columns.is_empty() {
330            writeln!(f, "periodic_columns {{")?;
331            for (qid, column) in self.periodic_columns.iter() {
332                if qid.module == self.name {
333                    writeln!(
334                        f,
335                        "    {}: {}",
336                        &qid.item,
337                        DisplayList(column.values.as_slice())
338                    )?;
339                } else {
340                    writeln!(f, "    {}: {}", qid, DisplayList(column.values.as_slice()))?;
341                }
342            }
343            f.write_str("}}")?;
344            f.write_str("\n")?;
345        }
346
347        if !self.constants.is_empty() {
348            for (qid, constant) in self.constants.iter() {
349                if qid.module == self.name {
350                    writeln!(f, "const {} = {}", &qid.item, &constant.value)?;
351                } else {
352                    writeln!(f, "const {} = {}", qid, &constant.value)?;
353                }
354            }
355            f.write_str("\n")?;
356        }
357
358        writeln!(f, "boundary_constraints {{")?;
359        for statement in self.boundary_constraints.iter() {
360            writeln!(f, "{}", statement.display(1))?;
361        }
362        f.write_str("}}")?;
363        f.write_str("\n")?;
364
365        writeln!(f, "integrity_constraints {{")?;
366        for statement in self.integrity_constraints.iter() {
367            writeln!(f, "{}", statement.display(1))?;
368        }
369        f.write_str("}}")?;
370        f.write_str("\n")?;
371
372        for (qid, evaluator) in self.evaluators.iter() {
373            f.write_str("ev ")?;
374            if qid.module == self.name {
375                writeln!(
376                    f,
377                    "{}{}",
378                    &qid.item,
379                    DisplayTuple(evaluator.params.as_slice())
380                )?;
381            } else {
382                writeln!(f, "{}{}", qid, DisplayTuple(evaluator.params.as_slice()))?;
383            }
384            f.write_str(" {{")?;
385            for statement in evaluator.body.iter() {
386                writeln!(f, "{}", statement.display(1))?;
387            }
388            f.write_str("}}")?;
389            f.write_str("\n")?;
390        }
391
392        for (qid, function) in self.functions.iter() {
393            f.write_str("fn ")?;
394            if qid.module == self.name {
395                writeln!(
396                    f,
397                    "{}{}",
398                    &qid.item,
399                    DisplayTypedTuple(function.params.as_slice())
400                )?;
401            } else {
402                writeln!(
403                    f,
404                    "{}{}",
405                    qid,
406                    DisplayTypedTuple(function.params.as_slice())
407                )?;
408            }
409
410            for statement in function.body.iter() {
411                writeln!(f, "{}", statement.display(1))?;
412            }
413        }
414
415        Ok(())
416    }
417}
418
419/// This represents a fully parsed AirScript program, with imports resolved/parsed, but not merged.
420///
421/// Libraries are produced when parsing files which do not contain a root module. We defer merging
422/// the modules together until a root module is provided so that we can perform import resolution on
423/// the root module using the contents of the library.
424#[derive(Debug, Default)]
425pub struct Library {
426    pub modules: HashMap<ModuleId, Module>,
427}
428impl Library {
429    pub fn new(
430        diagnostics: &DiagnosticsHandler,
431        codemap: Arc<CodeMap>,
432        mut modules: Vec<Module>,
433    ) -> Result<Self, SemanticAnalysisError> {
434        use std::collections::hash_map::Entry;
435
436        let mut lib = Library::default();
437
438        if modules.is_empty() {
439            return Ok(lib);
440        }
441
442        // Register all parsed modules first
443        let mut found_duplicate = None;
444        for module in modules.drain(..) {
445            match lib.modules.entry(module.name) {
446                Entry::Occupied(entry) => {
447                    let prev_span = entry.key().span();
448                    found_duplicate = Some(prev_span);
449                    diagnostics
450                        .diagnostic(Severity::Error)
451                        .with_message("conflicting module definitions")
452                        .with_primary_label(
453                            module.name.span(),
454                            "this module name is already in use",
455                        )
456                        .with_secondary_label(prev_span, "originally defined here")
457                        .emit();
458                }
459                Entry::Vacant(entry) => {
460                    entry.insert(module);
461                }
462            }
463        }
464
465        if let Some(span) = found_duplicate {
466            return Err(SemanticAnalysisError::NameConflict(span));
467        }
468
469        // Perform import resolution
470        //
471        // First, construct a worklist of modules with imports to be resolved
472        let mut worklist = lib
473            .modules
474            .iter()
475            .filter_map(|(name, module)| {
476                if module.imports.is_empty() {
477                    None
478                } else {
479                    let imports = module
480                        .imports
481                        .values()
482                        .map(|i| i.module())
483                        .collect::<Vec<_>>();
484                    Some((*name, imports))
485                }
486            })
487            .collect::<VecDeque<_>>();
488
489        // Cache the current working directory for use in constructing file paths in case
490        // we need to parse referenced modules from disk, and do not have a file path associated
491        // with the importing module with which to derive the import path.
492        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
493
494        // For each module in the worklist, attempt to resolve all of its imported modules
495        // to modules in the library. If the module is already in the library, we proceed,
496        // if it isn't, then we must parse the desired module from disk, and add it to the
497        // library, visiting any of its imports as well.
498        while let Some((module, mut imports)) = worklist.pop_front() {
499            // We attempt to resolve imports on disk relative to the file path of the
500            // importing module, if it was parsed from disk. If no path is available,
501            // we default to the current working directory.
502            let source_dir = match codemap.name(module.span().source_id()) {
503                // If we have no source span, default to the current working directory
504                Err(_) => cwd.clone(),
505                // If the file is virtual, then we've either already parsed imports for this module,
506                // or we have to fall back to the current working directory, but we have no relative
507                // path from which to base our search.
508                Ok(FileName::Virtual(_)) => cwd.clone(),
509                Ok(FileName::Real(path)) => path
510                    .parent()
511                    .unwrap_or_else(|| Path::new("."))
512                    .to_path_buf(),
513            };
514
515            // For each module imported, try to load the module from the library, if it is unavailable
516            // we must do extra work to load it into the library, as described above.
517            for import in imports.drain(..) {
518                if let Entry::Vacant(entry) = lib.modules.entry(import) {
519                    let filename = source_dir.join(format!("{}.air", import.as_str()));
520                    // Check if the module exists in the codemap first, so that we can add files directly
521                    // to the codemap during testing for convenience
522                    let result = match codemap.get_by_name(&FileName::Real(filename.clone())) {
523                        Some(file) => crate::parse_module(diagnostics, codemap.clone(), file),
524                        None => {
525                            crate::parse_module_from_file(diagnostics, codemap.clone(), &filename)
526                        }
527                    };
528                    match result {
529                        Ok(imported_module) => {
530                            // We must check if the file we parsed actually contains a module with
531                            // the same name as our import, if not, that's an error
532                            if imported_module.name != import {
533                                diagnostics.diagnostic(Severity::Error)
534                                    .with_message("invalid module declaration")
535                                    .with_primary_label(imported_module.name.span(), "module names must be the same as the name of the file they are defined in")
536                                    .emit();
537                                return Err(SemanticAnalysisError::ImportFailed(import.span()));
538                            } else {
539                                // We parsed the module successfully, so add it to the library
540                                if !imported_module.imports.is_empty() {
541                                    let imports = imported_module
542                                        .imports
543                                        .values()
544                                        .map(|i| i.module())
545                                        .collect::<Vec<_>>();
546                                    worklist.push_back((imported_module.name, imports));
547                                }
548                                entry.insert(imported_module);
549                            }
550                        }
551                        Err(ParseError::Failed) => {
552                            // Nothing interesting to emit as a diagnostic here, so just return an error
553                            return Err(SemanticAnalysisError::ImportFailed(import.span()));
554                        }
555                        Err(err) => {
556                            // Emit the error as a diagnostic and return an ImportError instead
557                            diagnostics.emit(err);
558                            return Err(SemanticAnalysisError::ImportFailed(import.span()));
559                        }
560                    }
561                }
562            }
563        }
564
565        // All imports have been resolved, but additional processing is required to merge modules together in a program
566        Ok(lib)
567    }
568
569    #[inline]
570    pub fn is_empty(&self) -> bool {
571        self.modules.is_empty()
572    }
573
574    #[inline]
575    pub fn contains(&self, module: &ModuleId) -> bool {
576        self.modules.contains_key(module)
577    }
578
579    #[inline]
580    pub fn get(&self, module: &ModuleId) -> Option<&Module> {
581        self.modules.get(module)
582    }
583
584    #[inline]
585    pub fn get_mut(&mut self, module: &ModuleId) -> Option<&mut Module> {
586        self.modules.get_mut(module)
587    }
588}