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}