mod callgraph;
mod debug;
mod errors;
mod library;
mod module;
mod namespaces;
mod resolver;
mod rewrites;
mod symbols;
use alloc::{boxed::Box, collections::BTreeMap, string::ToString, sync::Arc, vec::Vec};
use core::{
cell::RefCell,
ops::{ControlFlow, Index},
};
use miden_assembly_syntax::{
Report,
ast::{
self, AttributeSet, GlobalItemIndex, InvocationTarget, ItemIndex, Module, ModuleIndex,
Path, SymbolResolution, Visibility, types,
},
debuginfo::{SourceManager, SourceSpan, Span, Spanned},
module::{ItemInfo, ModuleInfo},
};
use miden_core::{Word, advice::AdviceMap, mast::MastNodeId, program::Kernel};
use miden_mast_package::Package as MastPackage;
use smallvec::{SmallVec, smallvec};
pub use self::{
callgraph::{CallGraph, CycleError},
errors::LinkerError,
library::{LinkLibrary, Linkage},
resolver::{ResolverCache, SymbolResolutionContext, SymbolResolver},
symbols::{Import, Symbol, SymbolItem},
};
use self::{
module::{LinkModule, ModuleSource},
namespaces::{NamespaceGraph, ResolvedImports},
resolver::*,
};
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum LinkStatus {
#[default]
Unlinked,
PartiallyLinked,
Linked,
}
#[derive(Clone)]
pub struct Linker {
libraries: BTreeMap<Word, LinkLibrary>,
modules: Vec<LinkModule>,
callgraph: CallGraph,
procedures_by_mast_root: BTreeMap<Word, SmallVec<[GlobalItemIndex; 1]>>,
kernel_index: Option<ModuleIndex>,
kernel: Kernel,
kernel_package: Option<Arc<MastPackage>>,
source_manager: Arc<dyn SourceManager>,
}
impl Linker {
pub fn new(source_manager: Arc<dyn SourceManager>) -> Self {
Self {
libraries: Default::default(),
modules: Default::default(),
callgraph: Default::default(),
procedures_by_mast_root: Default::default(),
kernel_index: None,
kernel: Default::default(),
kernel_package: None,
source_manager,
}
}
pub fn link_library(&mut self, library: LinkLibrary) -> Result<(), LinkerError> {
use alloc::collections::btree_map::Entry;
let module_infos =
library.module_infos().map_err(|err| LinkerError::InvalidPackageModuleSurface {
package: library.package.name.to_string(),
reason: err.to_string(),
})?;
match self.libraries.entry(library.mast().commitment()) {
Entry::Vacant(entry) => {
entry.insert(library.clone());
self.link_assembled_modules(module_infos)
},
Entry::Occupied(mut entry) => {
let prev = entry.get_mut();
if matches!(prev.linkage, Linkage::Dynamic) {
prev.linkage = library.linkage;
}
Ok(())
},
}
}
pub fn link_assembled_modules(
&mut self,
modules: impl IntoIterator<Item = ModuleInfo>,
) -> Result<(), LinkerError> {
for module in modules {
self.link_assembled_module(module)?;
}
Ok(())
}
pub fn link_assembled_module(
&mut self,
module: ModuleInfo,
) -> Result<ModuleIndex, LinkerError> {
log::debug!(target: "linker", "adding pre-assembled module {} to module graph", module.path());
let module_path = module.path();
let is_duplicate = self.find_module_index(module_path).is_some();
if is_duplicate {
return Err(LinkerError::DuplicateModule {
path: module_path.to_path_buf().into_boxed_path().into(),
});
}
let module_index = self.next_module_id();
let submodules = module.submodules().to_vec();
let items = module.items();
let mut symbols = Vec::with_capacity(items.len());
for (idx, item) in items {
let gid = module_index + idx;
self.callgraph.get_or_insert_node(gid);
match &item {
ItemInfo::Procedure(item) => {
self.register_procedure_root(gid, item.digest);
},
ItemInfo::Constant(_) | ItemInfo::Type(_) => (),
}
symbols.push(Symbol::new(
item.name().clone(),
Visibility::Public,
LinkStatus::Linked,
SymbolItem::Compiled(item.clone()),
));
}
let link_module = LinkModule::new(
module_index,
ast::ModuleKind::Library,
LinkStatus::Linked,
ModuleSource::Mast,
module_path.into(),
)
.with_submodules(submodules)
.with_symbols(symbols);
self.modules.push(link_module);
Ok(module_index)
}
pub fn link_modules(
&mut self,
modules: impl IntoIterator<Item = Box<Module>>,
) -> Result<Vec<ModuleIndex>, LinkerError> {
modules.into_iter().map(|mut m| self.link_module(&mut m)).collect()
}
pub fn link_module(&mut self, module: &mut Module) -> Result<ModuleIndex, LinkerError> {
log::debug!(target: "linker", "adding unprocessed module {}", module.path());
let is_duplicate = self.find_module_index(module.path()).is_some();
if is_duplicate {
return Err(LinkerError::DuplicateModule { path: module.path().into() });
}
let module_index = self.next_module_id();
let submodules = module.submodules().to_vec();
let mut symbols = Vec::new();
let imports = module.take_imports().into_iter().map(Import::new).collect::<Vec<_>>();
for item in module.take_items() {
match item {
ast::Item::Type(item) => {
let gid = module_index + ItemIndex::new(symbols.len());
self.callgraph.get_or_insert_node(gid);
symbols.push(Symbol::new(
item.name().clone(),
item.visibility(),
LinkStatus::Unlinked,
SymbolItem::Type(item),
));
},
ast::Item::Constant(item) => {
let gid = module_index + ItemIndex::new(symbols.len());
self.callgraph.get_or_insert_node(gid);
symbols.push(Symbol::new(
item.name().clone(),
item.visibility,
LinkStatus::Unlinked,
SymbolItem::Constant(item),
));
},
ast::Item::Procedure(item) => {
let gid = module_index + ItemIndex::new(symbols.len());
self.callgraph.get_or_insert_node(gid);
symbols.push(Symbol::new(
item.name().clone().into(),
item.visibility(),
LinkStatus::Unlinked,
SymbolItem::Procedure(RefCell::new(Box::new(item))),
));
},
}
}
let link_module = LinkModule::new(
module_index,
module.kind(),
LinkStatus::Unlinked,
ModuleSource::Ast,
module.path().into(),
)
.with_advice_map(module.advice_map().clone())
.with_submodules(submodules)
.with_imports(imports)
.with_symbols(symbols);
self.modules.push(link_module);
Ok(module_index)
}
#[inline]
fn next_module_id(&self) -> ModuleIndex {
ModuleIndex::new(self.modules.len())
}
}
impl Linker {
pub(super) fn with_kernel(
source_manager: Arc<dyn SourceManager>,
kernel_package: Arc<MastPackage>,
) -> Result<Self, Report> {
log::debug!(target: "linker", "instantiating linker with kernel package {}@{}", kernel_package.name, kernel_package.version);
let mut linker = Self::new(source_manager);
linker.link_with_kernel(kernel_package)?;
Ok(linker)
}
pub(super) fn link_with_kernel(
&mut self,
kernel_package: Arc<MastPackage>,
) -> Result<(), Report> {
if !kernel_package.is_kernel() {
return Err(Report::msg("invalid kernel package: not a kernel"));
}
let kernel = kernel_package.to_kernel()?;
if kernel.is_empty() {
return Err(Report::msg("invalid kernel package: kernel cannot be empty"));
}
assert!(self.kernel.is_empty());
assert!(self.kernel_package.is_none());
log::debug!(target: "linker", "modifying linker with kernel package {}@{}", kernel_package.name, kernel_package.version);
let mut kernel_index = None;
let module_infos = kernel_package.try_module_infos().map_err(|err| {
LinkerError::InvalidPackageModuleSurface {
package: kernel_package.name.to_string(),
reason: err.to_string(),
}
})?;
for module_info in module_infos {
let is_kernel_module = module_info.path().is_kernel_path();
let module_index = self.link_assembled_module(module_info)?;
if is_kernel_module {
kernel_index = Some(module_index);
}
}
assert!(kernel_index.is_some());
self.kernel_index = kernel_index;
self.kernel = kernel;
self.kernel_package = Some(kernel_package);
Ok(())
}
pub fn kernel(&self) -> &Kernel {
&self.kernel
}
pub fn kernel_package(&self) -> Option<Arc<MastPackage>> {
self.kernel_package.clone()
}
pub fn has_nonempty_kernel(&self) -> bool {
self.kernel_index.is_some() || !self.kernel.is_empty()
}
}
impl Linker {
fn cycle_error(&self, cycle: CycleError) -> LinkerError {
let iter = cycle.into_node_ids();
let mut nodes = Vec::with_capacity(iter.len());
for node in iter {
let module = self[node.module].path();
let item = self[node].name();
nodes.push(module.join(item).to_string());
}
LinkerError::Cycle { nodes: nodes.into() }
}
pub fn link(
&mut self,
roots: impl IntoIterator<Item = Box<Module>>,
support: impl IntoIterator<Item = Box<Module>>,
) -> Result<Vec<ModuleIndex>, LinkerError> {
use alloc::collections::BTreeSet;
let root_indices = self.link_modules(roots)?;
let _support_indices = self.link_modules(support)?;
let namespaces = NamespaceGraph::build(self)?;
let imports = namespaces.resolve_imports(self)?;
self.link_and_rewrite(&namespaces, &imports)?;
let mut reachable = BTreeSet::new();
for root in root_indices {
reachable.extend(namespaces.reachable_from_root(root));
}
Ok(reachable.into_iter().collect())
}
pub fn link_kernel(
&mut self,
mut kernel: Box<Module>,
support: impl IntoIterator<Item = Box<Module>>,
) -> Result<Vec<ModuleIndex>, LinkerError> {
self.link_modules(support)?;
let original_module_len = self.modules.len();
let original_callgraph = self.callgraph.clone();
let module_index = self.link_module(&mut kernel)?;
let original_kernel_index = self.kernel_index;
let original_module_kinds = self
.modules
.iter()
.enumerate()
.take(module_index.as_usize())
.filter(|(_, module)| matches!(module.source(), ModuleSource::Ast))
.map(|(module_index, module)| (module_index, module.kind()))
.collect::<Vec<_>>();
for module in self.modules.iter_mut().take(module_index.as_usize()) {
if matches!(module.source(), ModuleSource::Ast) {
module.set_kind(ast::ModuleKind::Kernel);
}
}
self.kernel_index = Some(module_index);
let result = (|| {
let namespaces = NamespaceGraph::build(self)?;
let imports = namespaces.resolve_imports(self)?;
self.link_and_rewrite(&namespaces, &imports)?;
Ok(namespaces.reachable_from_root(module_index))
})();
match result {
ok @ Ok(_) => ok,
err => {
self.kernel_index = original_kernel_index;
self.callgraph = original_callgraph;
self.modules.truncate(original_module_len);
for (module_index, module_kind) in original_module_kinds {
self.modules[module_index].set_kind(module_kind);
}
err
},
}
}
fn link_and_rewrite(
&mut self,
namespaces: &NamespaceGraph,
imports: &ResolvedImports,
) -> Result<(), LinkerError> {
log::debug!(
target: "linker",
"processing {} unlinked/partially-linked modules, and recomputing module graph",
self.modules.iter().filter(|m| !m.is_linked()).count()
);
if self.modules.is_empty() {
return Err(LinkerError::Empty);
}
if self.modules.iter().all(LinkModule::is_linked) {
return Ok(());
}
let pending_modules = self
.modules
.iter()
.enumerate()
.filter(|(_, module)| module.is_unlinked())
.map(|(module_index, module)| (module_index, module.clone()))
.collect::<Vec<_>>();
let original_callgraph = self.callgraph.clone();
let result = {
let resolver = SymbolResolver::with_namespaces(self, namespaces, imports);
let mut edges = Vec::new();
let mut cache = ResolverCache::default();
let mut linked_modules = Vec::new();
for (module_index, module) in self.modules.iter().enumerate() {
if !module.is_unlinked() {
continue;
}
let module_index = ModuleIndex::new(module_index);
for import in module.imports() {
if let Some(namespaces::ResolvedUse::Item(gid)) =
imports.get(module_index, import.local_name().as_str())
{
import.set_resolved(gid);
}
}
for (symbol_idx, symbol) in module.symbols().enumerate() {
let gid = module_index + ItemIndex::new(symbol_idx);
rewrites::rewrite_symbol(gid, symbol, &resolver, &mut cache)?;
match symbol.item() {
SymbolItem::Compiled(_) | SymbolItem::Type(_) | SymbolItem::Constant(_) => {
},
SymbolItem::Procedure(proc) => {
let proc = proc.borrow();
for invoke in proc.invoked() {
log::debug!(target: "linker", " | recording {} dependency on {}", invoke.kind, invoke.target);
let context = SymbolResolutionContext {
span: invoke.span(),
module: module_index,
kind: Some(invoke.kind),
};
if let Some(callee) = resolver
.resolve_invoke_target(&context, &invoke.target)?
.into_global_id()
{
log::debug!(
target: "linker",
" | resolved dependency to gid {}:{}",
callee.module.as_usize(),
callee.index.as_usize()
);
edges.push((gid, callee));
}
}
},
}
}
linked_modules.push(module_index);
}
let mut callgraph = self.callgraph.clone();
for (caller, callee) in edges {
callgraph.add_edge(caller, callee).map_err(|cycle| self.cycle_error(cycle))?;
}
callgraph.toposort().map_err(|cycle| self.cycle_error(cycle))?;
Ok::<_, LinkerError>((linked_modules, callgraph))
};
match result {
Ok((linked_modules, callgraph)) => {
self.callgraph = callgraph;
for module_index in linked_modules {
self.modules[module_index.as_usize()].set_status(LinkStatus::Linked);
}
},
Err(err) => {
self.callgraph = original_callgraph;
for (module_index, module) in pending_modules {
self.modules[module_index] = module;
}
return Err(err);
},
}
Ok(())
}
}
impl Linker {
pub fn libraries(&self) -> impl Iterator<Item = &LinkLibrary> {
self.libraries.values()
}
pub fn topological_sort_from_root(
&self,
caller: GlobalItemIndex,
) -> Result<Vec<GlobalItemIndex>, CycleError> {
self.callgraph.toposort_caller(caller)
}
pub fn get_procedure_index_by_digest(
&self,
procedure_digest: &Word,
) -> Option<GlobalItemIndex> {
self.procedures_by_mast_root.get(procedure_digest).map(|indices| indices[0])
}
pub fn conflicting_dynamic_procedure_export_root(
&self,
source_library_commitment: Word,
mast_root: Word,
selected_root_id: MastNodeId,
) -> Option<MastNodeId> {
let library = self.libraries.get(&source_library_commitment)?;
if !matches!(library.linkage, Linkage::Dynamic) {
return None;
}
library
.module_infos()
.ok()?
.into_iter()
.flat_map(|module| {
module
.procedures()
.filter_map(|(_, proc)| {
(proc.digest == mast_root).then(|| proc.source_root_id()).flatten()
})
.collect::<Vec<_>>()
})
.find(|&root_id| root_id != selected_root_id)
}
pub fn resolve_invoke_target(
&self,
caller: &SymbolResolutionContext,
target: &InvocationTarget,
) -> Result<SymbolResolution, LinkerError> {
let namespaces = NamespaceGraph::build(self)?;
let imports = namespaces.resolve_imports(self)?;
let resolver = SymbolResolver::with_namespaces(self, &namespaces, &imports);
resolver.resolve_invoke_target(caller, target)
}
pub fn resolve_path(
&self,
caller: &SymbolResolutionContext,
path: &Path,
) -> Result<SymbolResolution, LinkerError> {
let namespaces = NamespaceGraph::build(self)?;
let imports = namespaces.resolve_imports(self)?;
let resolver = SymbolResolver::with_namespaces(self, &namespaces, &imports);
resolver.resolve_path(caller, Span::new(caller.span, path))
}
pub(super) fn resolve_signature(
&self,
gid: GlobalItemIndex,
) -> Result<Option<Arc<types::FunctionType>>, LinkerError> {
match self[gid].item() {
SymbolItem::Compiled(ItemInfo::Procedure(proc)) => Ok(proc.signature.clone()),
SymbolItem::Procedure(proc) => {
let proc = proc.borrow();
match proc.signature() {
Some(ty) => self.translate_function_type(gid.module, ty).map(Some),
None => Ok(None),
}
},
SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
panic!("procedure index unexpectedly refers to non-procedure item")
},
}
}
fn translate_function_type(
&self,
module_index: ModuleIndex,
ty: &ast::FunctionType,
) -> Result<Arc<types::FunctionType>, LinkerError> {
use miden_assembly_syntax::ast::TypeResolver;
let cc = ty.cc;
let mut args = Vec::with_capacity(ty.args.len());
let symbol_resolver = SymbolResolver::new(self);
let mut cache = ResolverCache::default();
let mut resolver = Resolver {
resolver: &symbol_resolver,
cache: &mut cache,
current_module: module_index,
};
for arg in ty.args.iter() {
if let Some(arg) = resolver.resolve(arg)? {
args.push(arg);
} else {
let span = arg.span();
return Err(LinkerError::UndefinedType {
span,
source_file: self.source_manager.get(span.source_id()).ok(),
});
}
}
let mut results = Vec::with_capacity(ty.results.len());
for result in ty.results.iter() {
if let Some(result) = resolver.resolve(result)? {
results.push(result);
} else {
let span = result.span();
return Err(LinkerError::UndefinedType {
span,
source_file: self.source_manager.get(span.source_id()).ok(),
});
}
}
Ok(Arc::new(types::FunctionType::new(cc, args, results)))
}
pub(super) fn resolve_attributes(&self, gid: GlobalItemIndex) -> AttributeSet {
match self[gid].item() {
SymbolItem::Compiled(ItemInfo::Procedure(proc)) => proc.attributes.clone(),
SymbolItem::Procedure(proc) => {
let proc = proc.borrow();
proc.attributes().clone()
},
SymbolItem::Compiled(_) | SymbolItem::Constant(_) | SymbolItem::Type(_) => {
panic!("procedure index unexpectedly refers to non-procedure item")
},
}
}
pub(super) fn resolve_type(
&self,
span: SourceSpan,
gid: GlobalItemIndex,
) -> Result<types::Type, LinkerError> {
use miden_assembly_syntax::ast::TypeResolver;
let symbol_resolver = SymbolResolver::new(self);
let mut cache = ResolverCache::default();
let mut resolver = Resolver {
cache: &mut cache,
resolver: &symbol_resolver,
current_module: gid.module,
};
resolver.get_type(span, gid)
}
pub(crate) fn register_procedure_root(
&mut self,
id: GlobalItemIndex,
procedure_mast_root: Word,
) {
use alloc::collections::btree_map::Entry;
match self.procedures_by_mast_root.entry(procedure_mast_root) {
Entry::Occupied(ref mut entry) => {
let prev_id = entry.get()[0];
if prev_id != id {
entry.get_mut().push(id);
}
},
Entry::Vacant(entry) => {
entry.insert(smallvec![id]);
},
}
}
pub fn find_module_index(&self, path: &Path) -> Option<ModuleIndex> {
self.modules.iter().position(|m| path == m.path()).map(ModuleIndex::new)
}
pub fn find_module(&self, path: &Path) -> Option<&LinkModule> {
self.modules.iter().find(|m| path == m.path())
}
}
impl Linker {
pub(super) fn const_eval(
&self,
gid: GlobalItemIndex,
expr: &ast::ConstantExpr,
cache: &mut ResolverCache,
) -> Result<ast::ConstantValue, LinkerError> {
let symbol_resolver = SymbolResolver::new(self);
let mut resolver = Resolver {
resolver: &symbol_resolver,
cache,
current_module: gid.module,
};
ast::constants::eval::expr(expr, &mut resolver).map(|expr| expr.expect_value())
}
}
impl Index<ModuleIndex> for Linker {
type Output = LinkModule;
fn index(&self, index: ModuleIndex) -> &Self::Output {
&self.modules[index.as_usize()]
}
}
impl Index<GlobalItemIndex> for Linker {
type Output = Symbol;
fn index(&self, index: GlobalItemIndex) -> &Self::Output {
&self.modules[index.module.as_usize()][index.index]
}
}
#[cfg(test)]
mod tests {
use std::{
panic::{AssertUnwindSafe, catch_unwind},
string::String,
sync::Arc,
};
use miden_assembly_syntax::{
ast::{
Ident, InvocationTarget, InvokeKind, ItemIndex, Path, SymbolResolutionError,
Visibility, types,
},
debuginfo::{SourceSpan, Span},
module::{ItemInfo, TypeInfo},
};
use super::*;
use crate::testing::{TestContext, source_file};
#[test]
fn failed_kernel_link_restores_kernel_state() {
let context = TestContext::default();
let source_manager = context.source_manager();
let kernel_source = r#"
pub proc a
call.b
end
proc b
call.a
end
"#;
let userspace = context
.parse_module(source_file!(
&context,
r#"
namespace userspace
pub proc helper
push.1
end
"#
))
.expect("userspace module parsing must succeed");
let mut linker = Linker::new(source_manager);
let userspace_index = linker
.link([userspace], None)
.expect("userspace module must link successfully")
.into_iter()
.next()
.expect("linked module index must be returned");
let first_err = linker
.link_kernel(
context
.parse_kernel(source_file!(&context, kernel_source))
.expect("kernel parsing must succeed"),
None,
)
.expect_err("expected cyclic kernel to be rejected");
assert!(first_err.to_string().contains("found a cycle in the call graph"));
assert!(!linker.has_nonempty_kernel(), "failed kernel link must not leave a kernel set");
assert_eq!(linker[userspace_index].kind(), ast::ModuleKind::Library);
let second_err = linker
.link_kernel(
context
.parse_kernel(source_file!(&context, kernel_source))
.expect("kernel parsing must succeed"),
None,
)
.expect_err("expected cyclic kernel retry to be rejected");
assert!(second_err.to_string().contains("found a cycle in the call graph"));
assert!(!second_err.to_string().contains("duplicate module"));
let syscall_context = SymbolResolutionContext {
span: SourceSpan::UNKNOWN,
module: userspace_index,
kind: Some(InvokeKind::SysCall),
};
let err = linker
.resolve_invoke_target(
&syscall_context,
&InvocationTarget::Symbol(Ident::new("a").expect("valid identifier")),
)
.expect_err("expected syscall without a linked kernel to be rejected");
assert!(matches!(err, LinkerError::InvalidSysCallTarget { .. }));
}
#[test]
fn oversized_link_module_resolution_returns_structured_error() {
let context = TestContext::default();
let mut linker = Linker::new(context.source_manager());
let module_id = ModuleIndex::new(0);
let path = Arc::<Path>::from(Path::new("::m::huge"));
let mut symbols = Vec::with_capacity(ItemIndex::MAX_ITEMS + 1);
for i in 0..=ItemIndex::MAX_ITEMS {
let name = Ident::new(format!("a{i}")).expect("valid identifier");
symbols.push(Symbol::new(
name.clone(),
Visibility::Private,
LinkStatus::Unlinked,
SymbolItem::Compiled(ItemInfo::Type(TypeInfo { name, ty: types::Type::Felt })),
));
}
linker.modules.push(
LinkModule::new(
module_id,
ast::ModuleKind::Library,
LinkStatus::Unlinked,
ModuleSource::Mast,
path,
)
.with_symbols(symbols),
);
let result = catch_unwind(AssertUnwindSafe(|| {
linker[module_id].resolve(Span::unknown("a0"), &SymbolResolver::new(&linker))
}));
let result = match result {
Ok(result) => result,
Err(panic) => {
let message = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.expect("panic payload should be a string");
panic!("expected graceful error, got panic: {message}");
},
};
assert!(matches!(
result,
Err(err) if matches!(*err, SymbolResolutionError::TooManyItemsInModule { .. })
));
}
}