mod clause_names;
mod errors;
mod imports;
mod project;
mod symbols;
mod type_refs;
mod unused;
use std::collections::HashSet;
use assura_parser::ast::{BlockKind, ClauseKind, Decl, ServiceItem, SourceFile, SpExpr, TypeBody};
pub use errors::{ResolutionError, ResolvedFile};
pub use imports::{ImportStatus, ModuleMap, ResolvedImport};
pub use project::{
DependencyMap, ProjectResult, discover_and_resolve_project,
discover_and_resolve_project_with_deps, find_project_root, resolve_dependency_map,
resolve_project, resolve_project_with_deps,
};
pub use symbols::{Scope, Symbol, SymbolKind, SymbolTable};
use clause_names::resolve_clause_body_names;
use imports::resolve_imports;
use symbols::try_insert;
use type_refs::resolve_type_refs;
use unused::{check_unused_imports, collect_referenced_names};
const BUILTIN_TYPES: &[&str] = &[
"Int", "Nat", "Float", "Bool", "String", "Bytes", "Unit", "Never", "List", "Map", "Set",
"Option", "Result", "U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64", "Sequence",
];
const BUILTIN_VALUE_NAMES: &[&str] = &[
"result",
"self",
"true",
"false",
"len",
"size",
"abs",
"min",
"max",
"clamp",
"signum",
"gcd",
"lcm",
"divmod",
"pow",
"contains",
"keys",
"values",
"get",
"put",
"set",
"push",
"pop",
"head",
"tail",
"first",
"last",
"map",
"filter",
"fold",
"sum",
"count",
"any",
"all",
"concat",
"split",
"trim",
"substring",
"index_of",
"capacity",
"length",
"is_empty",
"forall",
"exists",
"old",
"ghost",
"pure",
"io",
"mem",
"db",
"net",
"audit",
"crypto",
"read",
"write",
"alloc",
"free",
"log",
"deterministic",
"taint",
"untrusted",
"validated",
"secret",
"incremental",
"monotonic",
];
fn extract_input_param_names(body: &SpExpr) -> Vec<String> {
assura_parser::ast::extract_clause_params(body)
.into_iter()
.map(|p| p.name)
.collect()
}
fn register_clause_params(
clauses: &[assura_parser::ast::Clause],
table: &mut SymbolTable,
errors: &mut Vec<ResolutionError>,
scope_id: usize,
span: &assura_parser::ast::Span,
) {
for clause in clauses {
if clause.kind == ClauseKind::Input || clause.kind == ClauseKind::Output {
for param_name in extract_input_param_names(&clause.body) {
try_insert(
table,
errors,
scope_id,
¶m_name,
SymbolKind::Parameter,
span.clone(),
);
}
}
}
}
pub fn resolve(source: &SourceFile) -> Result<ResolvedFile, Vec<ResolutionError>> {
resolve_with_modules(source, &ModuleMap::new(), &mut HashSet::new())
}
pub fn resolve_with_modules(
source: &SourceFile,
module_map: &ModuleMap,
visited: &mut HashSet<String>,
) -> Result<ResolvedFile, Vec<ResolutionError>> {
let mut table = SymbolTable::new();
let mut errors: Vec<ResolutionError> = Vec::new();
let root = table.push_scope("<root>", None);
for &name in BUILTIN_TYPES {
table
.insert(root, name, SymbolKind::BuiltinType, 0..0)
.expect("built-in types should not collide");
}
for &name in &assura_stdlib::prelude_type_names() {
if table.scopes[root].symbols.contains_key(name) {
continue;
}
table
.insert(root, name, SymbolKind::BuiltinType, 0..0)
.expect("stdlib prelude types should not collide with built-ins");
}
for &name in &assura_stdlib::prelude_contract_names() {
if table.scopes[root].symbols.contains_key(name) {
continue;
}
table
.insert(root, name, SymbolKind::ContractDef, 0..0)
.expect("stdlib prelude contracts should not collide");
}
let module_name = source
.module
.as_ref()
.map(|m| m.path.join("."))
.unwrap_or_else(|| "<anonymous>".to_string());
let module = table.push_scope(&module_name, Some(root));
visited.insert(module_name.clone());
let resolved_imports = resolve_imports(&source.imports, module_map, visited, &mut errors);
for imp in &resolved_imports {
if imp.status == ImportStatus::Circular {
continue;
}
if !imp.items.is_empty() {
for item in &imp.items {
try_insert(
&mut table,
&mut errors,
module,
item,
SymbolKind::BuiltinType,
imp.span.clone(),
);
}
} else if let Some(alias) = &imp.alias {
try_insert(
&mut table,
&mut errors,
module,
alias,
SymbolKind::BuiltinType,
imp.span.clone(),
);
} else if let Some(last) = imp.path.last() {
try_insert(
&mut table,
&mut errors,
module,
last,
SymbolKind::BuiltinType,
imp.span.clone(),
);
}
}
for decl in &source.decls {
match &decl.node {
Decl::Contract(c) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&c.name,
SymbolKind::ContractDef,
decl.span.clone(),
);
if inserted {
let contract_scope = table.push_scope(&c.name, Some(module));
for tp in &c.type_params {
try_insert(
&mut table,
&mut errors,
contract_scope,
tp,
SymbolKind::TypeParam,
decl.span.clone(),
);
}
let mut seen_params: HashSet<String> = HashSet::new();
for clause in &c.clauses {
if clause.kind == ClauseKind::Input || clause.kind == ClauseKind::Output {
for param_name in extract_input_param_names(&clause.body) {
if !seen_params.insert(param_name.clone()) {
try_insert(
&mut table,
&mut errors,
contract_scope,
¶m_name,
SymbolKind::Parameter,
decl.span.clone(),
);
continue;
}
try_insert(
&mut table,
&mut errors,
contract_scope,
¶m_name,
SymbolKind::Parameter,
decl.span.clone(),
);
}
}
}
for p in &c.fn_params {
if !seen_params.insert(p.name.clone()) {
continue;
}
try_insert(
&mut table,
&mut errors,
contract_scope,
&p.name,
SymbolKind::Parameter,
decl.span.clone(),
);
}
}
}
Decl::TypeDef(t) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&t.name,
SymbolKind::TypeDef,
decl.span.clone(),
);
if inserted {
let type_scope = table.push_scope(&t.name, Some(module));
for tp in &t.type_params {
try_insert(
&mut table,
&mut errors,
type_scope,
tp,
SymbolKind::TypeParam,
decl.span.clone(),
);
}
if let TypeBody::Struct(fields) = &t.body {
for f in fields {
try_insert(
&mut table,
&mut errors,
type_scope,
&f.name,
SymbolKind::Field,
decl.span.clone(),
);
}
}
}
}
Decl::EnumDef(e) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&e.name,
SymbolKind::EnumDef,
decl.span.clone(),
);
if inserted {
let enum_scope = table.push_scope(&e.name, Some(module));
for tp in &e.type_params {
try_insert(
&mut table,
&mut errors,
enum_scope,
tp,
SymbolKind::TypeParam,
decl.span.clone(),
);
}
for v in &e.variants {
try_insert(
&mut table,
&mut errors,
enum_scope,
&v.name,
SymbolKind::EnumVariant,
decl.span.clone(),
);
}
}
}
Decl::Extern(ex) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&ex.name,
SymbolKind::ExternFn,
decl.span.clone(),
);
if inserted {
let fn_scope = table.push_scope(&ex.name, Some(module));
for p in &ex.params {
try_insert(
&mut table,
&mut errors,
fn_scope,
&p.name,
SymbolKind::Parameter,
decl.span.clone(),
);
}
}
}
Decl::Bind(b) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&b.name,
SymbolKind::BindFn,
decl.span.clone(),
);
if inserted {
let fn_scope = table.push_scope(&b.name, Some(module));
for p in &b.params {
try_insert(
&mut table,
&mut errors,
fn_scope,
&p.name,
SymbolKind::Parameter,
decl.span.clone(),
);
}
}
}
Decl::FnDef(f) => {
let inserted = try_insert(
&mut table,
&mut errors,
module,
&f.name,
SymbolKind::FnDef,
decl.span.clone(),
);
if inserted {
let fn_scope = table.push_scope(&f.name, Some(module));
for p in &f.params {
try_insert(
&mut table,
&mut errors,
fn_scope,
&p.name,
SymbolKind::Parameter,
decl.span.clone(),
);
}
}
}
Decl::Service(s) => {
let svc_sym_span = decl.span.clone();
let inserted = try_insert(
&mut table,
&mut errors,
module,
&s.name,
SymbolKind::ServiceDef,
svc_sym_span,
);
if inserted {
let svc_scope = table.push_scope(&s.name, Some(module));
for item in &s.items {
match item {
ServiceItem::TypeDef(t) => {
let ins = try_insert(
&mut table,
&mut errors,
svc_scope,
&t.name,
SymbolKind::TypeDef,
decl.span.clone(),
);
if ins {
let td_scope = table.push_scope(&t.name, Some(svc_scope));
for tp in &t.type_params {
try_insert(
&mut table,
&mut errors,
td_scope,
tp,
SymbolKind::TypeParam,
decl.span.clone(),
);
}
if let TypeBody::Struct(fields) = &t.body {
for f in fields {
try_insert(
&mut table,
&mut errors,
td_scope,
&f.name,
SymbolKind::Field,
decl.span.clone(),
);
}
}
}
}
ServiceItem::EnumDef(e) => {
let ins = try_insert(
&mut table,
&mut errors,
svc_scope,
&e.name,
SymbolKind::EnumDef,
decl.span.clone(),
);
if ins {
let ed_scope = table.push_scope(&e.name, Some(svc_scope));
for tp in &e.type_params {
try_insert(
&mut table,
&mut errors,
ed_scope,
tp,
SymbolKind::TypeParam,
decl.span.clone(),
);
}
for v in &e.variants {
try_insert(
&mut table,
&mut errors,
ed_scope,
&v.name,
SymbolKind::EnumVariant,
decl.span.clone(),
);
}
}
}
ServiceItem::Operation { name, clauses, .. } => {
let ins = try_insert(
&mut table,
&mut errors,
svc_scope,
name,
SymbolKind::Operation,
decl.span.clone(),
);
if ins {
let op_scope = table.push_scope(name, Some(svc_scope));
register_clause_params(
clauses,
&mut table,
&mut errors,
op_scope,
&decl.span,
);
}
}
ServiceItem::Query { name, clauses, .. } => {
let ins = try_insert(
&mut table,
&mut errors,
svc_scope,
name,
SymbolKind::Query,
decl.span.clone(),
);
if ins {
let q_scope = table.push_scope(name, Some(svc_scope));
register_clause_params(
clauses,
&mut table,
&mut errors,
q_scope,
&decl.span,
);
}
}
ServiceItem::States(_)
| ServiceItem::Invariant(_)
| ServiceItem::Other { .. } => {}
}
}
}
}
Decl::Prophecy(p) => {
try_insert(
&mut table,
&mut errors,
module,
&p.name,
SymbolKind::Prophecy,
decl.span.clone(),
);
}
Decl::CodecRegistry(cr) => {
try_insert(
&mut table,
&mut errors,
module,
&cr.name,
SymbolKind::CodecRegistry,
decl.span.clone(),
);
}
Decl::Block { kind, name, .. } => {
if *kind == BlockKind::FeatureMax && !name.is_empty() {
try_insert(
&mut table,
&mut errors,
module,
name,
SymbolKind::Field,
decl.span.clone(),
);
} else if !name.is_empty() {
table.push_scope(name, Some(module));
}
}
}
}
resolve_type_refs(source, &table, &resolved_imports, module, &mut errors);
let mut warnings = Vec::new();
if module_map.is_empty() {
for imp in &resolved_imports {
if imp.status == ImportStatus::Unresolved {
let path_str = imp.path.join(".");
warnings.push(ResolutionError {
code: "A02010".into(),
message: format!(
"cannot resolve import `{path_str}`: module not found \
(single-file check has no project module map; use \
`assura check <project-dir>` for multi-file imports)"
),
span: imp.span.clone(),
secondary: None,
suggestion: Some(
"run check on the project directory, or remove the import".into(),
),
});
}
}
}
let referenced_names = collect_referenced_names(source);
check_unused_imports(&resolved_imports, &referenced_names, &mut warnings);
resolve_clause_body_names(source, &table, &resolved_imports, module, &mut warnings);
visited.remove(&module_name);
if errors.is_empty() {
Ok(ResolvedFile {
source: source.clone(),
symbols: table,
imports: resolved_imports,
warnings,
})
} else {
Err(errors)
}
}
#[cfg(test)]
#[path = "resolve_tests.rs"]
mod tests;