mod context;
mod errors;
mod passes;
#[cfg(test)]
mod tests;
use alloc::{
boxed::Box,
collections::{BTreeSet, VecDeque},
sync::Arc,
vec::Vec,
};
use miden_core::{Word, crypto::hash::Poseidon2};
use miden_debug_types::{SourceFile, SourceManager, Span, Spanned};
use smallvec::SmallVec;
pub use self::{
context::AnalysisContext,
errors::{SemanticAnalysisError, SyntaxError},
passes::{ConstEvalVisitor, VerifyInvokeTargets, VerifyRepeatCounts},
};
use crate::{ast::*, parser::WordValue};
pub fn analyze(
source: Arc<SourceFile>,
kind: ModuleKind,
path: &Path,
forms: Vec<Form>,
warnings_as_errors: bool,
source_manager: Arc<dyn SourceManager>,
) -> Result<Box<Module>, SyntaxError> {
log::debug!(target: "sema", "starting semantic analysis for '{path}' (kind = {kind})");
let mut analyzer = AnalysisContext::new(source.clone(), source_manager);
analyzer.set_warnings_as_errors(warnings_as_errors);
let mut module = Box::new(Module::new(kind, path).with_span(source.source_span()));
let mut forms = VecDeque::from(forms);
let mut enums = SmallVec::<[EnumType; 1]>::new_const();
let mut docs = None;
while let Some(form) = forms.pop_front() {
match form {
Form::ModuleDoc(docstring) => {
assert!(docs.is_none());
module.set_docs(Some(docstring));
},
Form::Doc(docstring) => {
if let Some(unused) = docs.replace(docstring) {
analyzer.error(SemanticAnalysisError::UnusedDocstring { span: unused.span() });
}
},
Form::Type(ty) => {
if let Err(err) = module.define_type(ty.with_docs(docs.take())) {
analyzer.error(err);
}
},
Form::Enum(ty) => {
for variant in ty.variants() {
let Variant { span, name, discriminant, .. } = variant;
analyzer.register_constant(Constant {
span: *span,
docs: None,
visibility: ty.visibility(),
name: name.clone(),
value: discriminant.clone(),
});
}
enums.push(ty.with_docs(docs.take()));
},
Form::Constant(constant) => {
analyzer.define_constant(&mut module, constant.with_docs(docs.take()));
},
Form::Alias(item) if item.visibility().is_public() => match kind {
ModuleKind::Kernel if module.is_kernel() => {
docs.take();
analyzer.error(SemanticAnalysisError::ReexportFromKernel { span: item.span() });
},
ModuleKind::Executable => {
docs.take();
analyzer.error(SemanticAnalysisError::UnexpectedExport { span: item.span() });
},
_ => {
define_alias(item.with_docs(docs.take()), &mut module, &mut analyzer)?;
},
},
Form::Alias(item) => {
define_alias(item.with_docs(docs.take()), &mut module, &mut analyzer)?
},
Form::Procedure(export) => match kind {
ModuleKind::Executable
if export.visibility().is_public() && !export.is_entrypoint() =>
{
docs.take();
analyzer.error(SemanticAnalysisError::UnexpectedExport { span: export.span() });
},
_ => {
define_procedure(export.with_docs(docs.take()), &mut module, &mut analyzer)?;
},
},
Form::Begin(body) if matches!(kind, ModuleKind::Executable) => {
let docs = docs.take();
let procedure =
Procedure::new(body.span(), Visibility::Public, ProcedureName::main(), 0, body)
.with_docs(docs);
define_procedure(procedure, &mut module, &mut analyzer)?;
},
Form::Begin(body) => {
docs.take();
analyzer.error(SemanticAnalysisError::UnexpectedEntrypoint { span: body.span() });
},
Form::AdviceMapEntry(entry) => {
add_advice_map_entry(&mut module, entry.with_docs(docs.take()), &mut analyzer)?;
},
}
}
if let Some(unused) = docs.take() {
analyzer.error(SemanticAnalysisError::UnusedDocstring { span: unused.span() });
}
analyzer.simplify_constants();
for item in module.items_mut().iter_mut() {
let Export::Constant(constant) = item else {
continue;
};
constant.value = analyzer
.get_constant(&constant.name)
.expect("semantic analysis tracks all module constants")
.clone();
}
for mut ty in enums {
for variant in ty.variants_mut() {
variant.discriminant = analyzer.get_constant(&variant.name).unwrap().clone();
}
if let Err(err) = module.define_enum(ty) {
analyzer.error(err);
}
}
if matches!(kind, ModuleKind::Executable) && !module.has_entrypoint() {
analyzer.error(SemanticAnalysisError::MissingEntrypoint);
}
analyzer.has_failed()?;
visit_items(&mut module, &mut analyzer)?;
for import in module.aliases() {
if !import.is_used() {
analyzer.error(SemanticAnalysisError::UnusedImport { span: import.span() });
}
}
analyzer.into_result().map(move |_| module)
}
fn visit_items(module: &mut Module, analyzer: &mut AnalysisContext) -> Result<(), SyntaxError> {
let is_kernel = module.is_kernel();
let locals = BTreeSet::from_iter(module.items().iter().map(|p| p.name().clone()));
let mut items = VecDeque::from(core::mem::take(&mut module.items));
while let Some(item) = items.pop_front() {
match item {
Export::Procedure(mut procedure) => {
if is_kernel && procedure.visibility().is_public() {
procedure.set_syscall(true);
}
log::debug!(target: "const-eval", "visiting procedure {}", procedure.name());
{
let mut visitor = ConstEvalVisitor::new(analyzer);
let _ = visitor.visit_mut_procedure(&mut procedure);
if let Err(errs) = visitor.into_result() {
for err in errs {
log::error!(target: "const-eval", "error found in procedure {}: {err}", procedure.name());
analyzer.error(err);
}
}
}
log::debug!(target: "verify-repeat", "visiting procedure {}", procedure.name());
{
let mut visitor = VerifyRepeatCounts::new(analyzer);
let _ = visitor.visit_procedure(&procedure);
}
log::debug!(target: "verify-invoke", "visiting procedure {}", procedure.name());
{
let mut visitor = VerifyInvokeTargets::new(
analyzer,
module,
&locals,
Some(procedure.name().clone()),
);
let _ = visitor.visit_mut_procedure(&mut procedure);
}
module.items.push(Export::Procedure(procedure));
},
Export::Alias(mut alias) => {
log::debug!(target: "verify-invoke", "visiting alias {}", alias.target());
{
let mut visitor = VerifyInvokeTargets::new(analyzer, module, &locals, None);
let _ = visitor.visit_mut_alias(&mut alias);
}
module.items.push(Export::Alias(alias));
},
Export::Constant(mut constant) => {
log::debug!(target: "verify-invoke", "visiting constant {}", constant.name());
{
let mut visitor = VerifyInvokeTargets::new(analyzer, module, &locals, None);
let _ = visitor.visit_mut_constant(&mut constant);
}
module.items.push(Export::Constant(constant));
},
Export::Type(mut ty) => {
log::debug!(target: "verify-invoke", "visiting type {}", ty.name());
{
let mut visitor = VerifyInvokeTargets::new(analyzer, module, &locals, None);
let _ = visitor.visit_mut_type_decl(&mut ty);
}
module.items.push(Export::Type(ty));
},
}
}
Ok(())
}
fn define_alias(
item: Alias,
module: &mut Module,
context: &mut AnalysisContext,
) -> Result<(), SyntaxError> {
let name = item.name().clone();
if let Err(err) = module.define_alias(item, context.source_manager()) {
match err {
SemanticAnalysisError::SymbolConflict { .. } => {
context.error(err);
},
err => {
context.error(err);
context.has_failed()?;
},
}
}
context.register_imported_name(name);
Ok(())
}
fn define_procedure(
procedure: Procedure,
module: &mut Module,
context: &mut AnalysisContext,
) -> Result<(), SyntaxError> {
let name = procedure.name().clone();
if let Err(err) = module.define_procedure(procedure, context.source_manager()) {
match err {
SemanticAnalysisError::SymbolConflict { .. } => {
context.error(err);
},
err => {
context.error(err);
context.has_failed()?;
},
}
}
context.register_procedure_name(name);
Ok(())
}
fn add_advice_map_entry(
module: &mut Module,
entry: AdviceMapEntry,
context: &mut AnalysisContext,
) -> Result<(), SyntaxError> {
let key = match entry.key {
Some(key) => Word::from(key.inner().0),
None => Poseidon2::hash_elements(&entry.value),
};
let cst = Constant::new(
entry.span,
Visibility::Private,
entry.name.clone(),
ConstantExpr::Word(Span::new(entry.span, WordValue(*key))),
);
context.define_constant(module, cst);
match module.advice_map.get(&key) {
Some(_) => {
context.error(SemanticAnalysisError::AdvMapKeyAlreadyDefined { span: entry.span });
},
None => {
module.advice_map.insert(key, entry.value);
},
}
Ok(())
}