use alloc::{
collections::{BTreeMap, BTreeSet},
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use miden_assembly_syntax::{
Path, PathBuf,
ast::{GlobalItemIndex, ImportKind, ItemIndex, ModuleIndex, SymbolResolutionError, Visibility},
debuginfo::{SourceManager, SourceSpan, Span, Spanned},
diagnostics::RelatedLabel,
};
use super::{Linker, LinkerError, ModuleSource};
#[derive(Debug, Clone)]
pub struct NamespaceGraph {
modules: Vec<ModuleNode>,
modules_by_path: BTreeMap<Arc<Path>, ModuleIndex>,
}
#[derive(Debug, Clone)]
pub struct ModuleNode {
id: ModuleIndex,
path: Arc<Path>,
source: ModuleSource,
parent: Option<ModuleIndex>,
items: BTreeMap<String, ItemDef>,
submodules: BTreeMap<String, ModuleEdge>,
imports: BTreeMap<String, UseDecl>,
}
#[derive(Debug, Clone)]
pub struct ModuleEdge {
name: String,
child: ModuleIndex,
visibility: Visibility,
span: SourceSpan,
}
#[derive(Debug, Clone)]
pub struct ItemDef {
id: GlobalItemIndex,
visibility: Visibility,
span: SourceSpan,
}
#[derive(Debug, Clone)]
pub struct UseDecl {
owner: ModuleIndex,
alias: String,
kind: ImportKind,
visibility: Visibility,
target: Span<Arc<Path>>,
span: SourceSpan,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ResolvedUse {
Module(ModuleIndex),
Item(GlobalItemIndex),
}
#[derive(Debug, Default, Clone)]
pub struct ResolvedImports {
imports: BTreeMap<(ModuleIndex, String), ResolvedUse>,
}
impl ResolvedImports {
#[inline]
pub fn get(&self, owner: ModuleIndex, alias: &str) -> Option<ResolvedUse> {
self.imports.get(&(owner, alias.to_string())).copied()
}
}
impl NamespaceGraph {
pub fn build(linker: &Linker) -> Result<Self, LinkerError> {
let mut modules_by_path = BTreeMap::new();
for module in linker.modules.iter() {
if modules_by_path.insert(module.path().clone(), module.id()).is_some() {
return Err(LinkerError::DuplicateModule { path: module.path().clone() });
}
}
let mut graph = Self {
modules: linker
.modules
.iter()
.map(|module| ModuleNode::from_link_module(module, linker))
.collect::<Result<Vec<_>, _>>()?,
modules_by_path,
};
graph.connect_submodule_edges(linker)?;
graph.validate_source_module_declarations()?;
Ok(graph)
}
#[inline]
pub fn find_module_index(&self, path: &Path) -> Option<ModuleIndex> {
self.modules_by_path.get(path).copied()
}
#[inline]
pub fn module(&self, id: ModuleIndex) -> &ModuleNode {
&self.modules[id.as_usize()]
}
#[cfg(test)]
fn num_modules(&self) -> usize {
self.modules.len()
}
pub fn reachable_from_root(&self, root: ModuleIndex) -> Vec<ModuleIndex> {
let mut reachable = BTreeSet::new();
let mut stack = vec![root];
while let Some(module_index) = stack.pop() {
if !reachable.insert(module_index) {
continue;
}
for edge in self.module(module_index).submodules.values() {
if edge.visibility.is_public() {
stack.push(edge.child);
}
}
}
reachable.into_iter().collect()
}
pub fn resolve_imports(&self, linker: &Linker) -> Result<ResolvedImports, LinkerError> {
let mut imports = ResolvedImports::default();
let mut pending = self
.modules
.iter()
.flat_map(|module| module.imports.values())
.collect::<Vec<_>>();
while !pending.is_empty() {
let mut next = Vec::new();
let mut progress = false;
for import in pending {
if matches!(import.kind(), ImportKind::Module) && import.visibility().is_public() {
return Err(LinkerError::ModuleReExport {
span: import.span(),
source_file: source_file(linker.source_manager.as_ref(), import.span()),
path: import.target().inner().clone(),
});
}
let resolved = match self.resolve_import_decl(import, &imports, linker) {
Ok(resolved) => resolved,
Err(err @ LinkerError::UndefinedSymbol { .. }) => {
next.push(import);
let _ = err;
continue;
},
Err(err) => return Err(err),
};
self.validate_resolved_import(import, resolved, linker)?;
imports.imports.insert((import.owner(), import.alias().to_string()), resolved);
progress = true;
}
if next.is_empty() {
break;
}
if !progress {
if let Some(import) = next.iter().copied().find(|import| {
self.public_import_dependency(import).is_some_and(|dependency| {
next.iter().any(|candidate| candidate.key() == dependency)
})
}) {
return Err(LinkerError::ImportReExportCycle {
span: import.span(),
source_file: source_file(linker.source_manager.as_ref(), import.span()),
path: import.target().inner().clone(),
});
}
let import = next[0];
return self.resolve_import_decl(import, &imports, linker).map(|_| imports);
}
pending = next;
}
Ok(imports)
}
fn resolve_import_decl(
&self,
import: &UseDecl,
imports: &ResolvedImports,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
self.resolve_import_target(
import.owner(),
import.alias(),
import.target().as_deref(),
imports,
linker,
)
}
fn validate_resolved_import(
&self,
import: &UseDecl,
resolved: ResolvedUse,
linker: &Linker,
) -> Result<(), LinkerError> {
match (import.kind(), resolved) {
(ImportKind::Module, ResolvedUse::Module(_)) => Ok(()),
(ImportKind::Module, ResolvedUse::Item(item)) => {
Err(LinkerError::InvalidModuleImportTarget {
span: import.span(),
source_file: source_file(linker.source_manager.as_ref(), import.span()),
path: item_path(linker, item),
})
},
(ImportKind::Item, ResolvedUse::Item(item)) => {
if import.visibility().is_public()
&& linker[item.module].path().is_kernel_path()
&& linker[item].is_procedure()
&& import.owner != item.module
{
let span = import.span();
let source_file = source_file(linker.source_manager.as_ref(), span);
Err(LinkerError::InvalidReExportOfKernelSyscall {
span,
source_file,
path: import.target().inner().clone(),
})
} else {
Ok(())
}
},
(ImportKind::Item, ResolvedUse::Module(id)) => {
Err(LinkerError::InvalidItemImportTarget {
span: import.span(),
source_file: source_file(linker.source_manager.as_ref(), import.span()),
path: self.module(id).path.clone(),
})
},
}
}
fn public_import_dependency(&self, import: &UseDecl) -> Option<(ModuleIndex, String)> {
let path = import.target().as_deref();
let (name, parent_path) = path.split_last()?;
let parent = self.find_import_target_parent(import.owner(), parent_path)?;
let dependency = self.module(parent).import(name)?;
dependency.visibility().is_public().then(|| dependency.key())
}
fn find_import_target_parent(&self, owner: ModuleIndex, path: &Path) -> Option<ModuleIndex> {
let (first, rest) = path.split_first()?;
if first == "self" {
return if rest.is_empty() {
Some(owner)
} else {
self.find_self_relative_module_index(owner, rest)
};
}
self.find_global_module_index(path)
}
fn find_self_relative_module_index(
&self,
owner: ModuleIndex,
path: &Path,
) -> Option<ModuleIndex> {
let mut current = owner;
let mut remaining = path;
while let Some((component, rest)) = remaining.split_first() {
let edge = self.module(current).submodule(component)?;
current = edge.child();
remaining = rest;
}
Some(current)
}
fn resolve_import_target(
&self,
owner: ModuleIndex,
current_alias: &str,
path: Span<&Path>,
imports: &ResolvedImports,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
let Some((first, rest)) = path.split_first() else {
return Err(undefined_symbol(linker, path));
};
if first == "self" {
if rest.is_empty() {
return Err(undefined_symbol(linker, path));
}
let resolved =
self.resolve_self_relative_path(owner, rest, path.span(), imports, linker)?;
return self.validate_import_target_module(owner, resolved, path, linker);
}
let owner_module = self.module(owner);
if !path.is_absolute()
&& let Some(import) = owner_module.import(first)
&& import.alias() != current_alias
{
return Err(LinkerError::ImportTargetUsesImport {
span: path.span(),
source_file: source_file(linker.source_manager.as_ref(), path.span()),
path: path.into_inner().to_path_buf().into_boxed_path().into(),
alias: first.to_string(),
});
}
match self.resolve_global_path(owner, path.into_inner(), path.span(), Some(imports), linker)
{
Ok(resolved) => self.validate_import_target_module(owner, resolved, path, linker),
Err(LinkerError::UndefinedSymbol { .. }) => Err(undefined_symbol(linker, path)),
Err(err) => Err(err),
}
}
fn validate_import_target_module(
&self,
owner: ModuleIndex,
resolved: ResolvedUse,
path: Span<&Path>,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
let ResolvedUse::Module(module) = resolved else {
return Ok(resolved);
};
if module == owner {
return Err(LinkerError::SelfReferentialImport {
span: path.span(),
source_file: source_file(linker.source_manager.as_ref(), path.span()),
path: path.into_inner().to_path_buf().into_boxed_path().into(),
});
}
if self.module(module).parent() == Some(owner) {
return Err(LinkerError::ImportTargetIsLocalSubmodule {
span: path.span(),
source_file: source_file(linker.source_manager.as_ref(), path.span()),
path: self.module(module).path.clone(),
});
}
Ok(resolved)
}
pub fn resolve_code_path(
&self,
owner: ModuleIndex,
path: Span<&Path>,
imports: &ResolvedImports,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
if path.is_absolute() {
return self.resolve_global_path(
owner,
path.into_inner(),
path.span(),
Some(imports),
linker,
);
}
let Some((first, rest)) = path.split_first() else {
return Err(undefined_symbol(linker, path));
};
if first == "self" {
if rest.is_empty() {
return Err(undefined_symbol(linker, path));
}
return self.resolve_self_relative_path(owner, rest, path.span(), imports, linker);
}
let owner_module = self.module(owner);
if rest.is_empty() {
if let Some(item) = owner_module.item(first) {
self.ensure_item_visible(owner, item, path.span(), linker)?;
return Ok(ResolvedUse::Item(item.id()));
}
if let Some(resolved) = imports.get(owner, first) {
return Ok(resolved);
}
if let Some(edge) = owner_module.submodule(first) {
self.ensure_submodule_visible(owner, owner, edge, path.span(), linker)?;
return Ok(ResolvedUse::Module(edge.child()));
}
} else {
if let Some(item) = owner_module.item(first) {
return Err(SymbolResolutionError::invalid_sub_path(
path.span(),
item.span(),
linker.source_manager.as_ref(),
)
.into());
}
if let Some(resolved) = imports.get(owner, first) {
return match resolved {
ResolvedUse::Module(module) => self.resolve_path_from_module(
owner,
module,
rest,
path.span(),
imports,
linker,
),
ResolvedUse::Item(item) => Err(SymbolResolutionError::invalid_sub_path(
path.span(),
linker[item.module][item.index].name().span(),
linker.source_manager.as_ref(),
)
.into()),
};
}
if let Some(edge) = owner_module.submodule(first) {
self.ensure_submodule_visible(owner, owner, edge, path.span(), linker)?;
return self.resolve_path_from_module(
owner,
edge.child(),
rest,
path.span(),
imports,
linker,
);
}
}
if rest.is_empty() {
Err(undefined_symbol(linker, path))
} else {
Err(LinkerError::InvalidRelativePath {
span: path.span(),
source_file: source_file(linker.source_manager.as_ref(), path.span()),
path: path.into_inner().to_path_buf().into_boxed_path().into(),
})
}
}
fn resolve_self_relative_path(
&self,
owner: ModuleIndex,
path: &Path,
span: SourceSpan,
imports: &ResolvedImports,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
self.resolve_path_from_module(owner, owner, path, span, imports, linker)
}
fn resolve_path_from_module(
&self,
owner: ModuleIndex,
module: ModuleIndex,
path: &Path,
span: SourceSpan,
imports: &ResolvedImports,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
let mut current = module;
let mut remaining = path;
loop {
let Some((component, rest)) = remaining.split_first() else {
return Ok(ResolvedUse::Module(current));
};
let module = self.module(current);
if rest.is_empty() {
if let Some(item) = module.item(component) {
self.ensure_item_visible(owner, item, span, linker)?;
return Ok(ResolvedUse::Item(item.id()));
}
if let Some(edge) = module.submodule(component) {
self.ensure_submodule_visible(owner, current, edge, span, linker)?;
return Ok(ResolvedUse::Module(edge.child()));
}
if let Some(import) = module.import(component)
&& import.visibility().is_public()
&& let Some(resolved) = imports.get(current, component)
{
return Ok(resolved);
}
return Err(undefined_symbol_from_path(linker, span, path));
}
if let Some(edge) = module.submodule(component) {
self.ensure_submodule_visible(owner, current, edge, span, linker)?;
current = edge.child();
remaining = rest;
continue;
}
if let Some(item) = module.item(component) {
return Err(SymbolResolutionError::invalid_sub_path(
span,
item.span(),
linker.source_manager.as_ref(),
)
.into());
}
return Err(undefined_symbol_from_path(linker, span, path));
}
}
fn resolve_global_path(
&self,
owner: ModuleIndex,
path: &Path,
span: SourceSpan,
imports: Option<&ResolvedImports>,
linker: &Linker,
) -> Result<ResolvedUse, LinkerError> {
if let Some(module) = self.find_global_module_index(path) {
self.ensure_module_visible(owner, module, span, linker)?;
return Ok(ResolvedUse::Module(module));
}
let Some((name, parent_path)) = path.split_last() else {
return Err(undefined_symbol_from_path(linker, span, path));
};
if parent_path.is_empty() && !parent_path.is_absolute() {
return Err(undefined_symbol_from_path(linker, span, path));
}
let Some(parent) = self.find_global_module_index(parent_path) else {
if let Some(err) = self.invalid_global_subpath_error(path, span, linker) {
return Err(err);
}
return Err(undefined_symbol_from_path(linker, span, path));
};
self.ensure_module_visible(owner, parent, span, linker)?;
let module = self.module(parent);
if let Some(item) = module.item(name) {
self.ensure_item_visible(owner, item, span, linker)?;
return Ok(ResolvedUse::Item(item.id()));
}
if let Some(edge) = module.submodule(name) {
self.ensure_submodule_visible(owner, parent, edge, span, linker)?;
return Ok(ResolvedUse::Module(edge.child()));
}
if let Some(imports) = imports
&& let Some(import) = module.import(name)
&& import.visibility().is_public()
&& let Some(resolved) = imports.get(parent, name)
{
return Ok(resolved);
}
Err(undefined_symbol_from_path(linker, span, path))
}
fn invalid_global_subpath_error(
&self,
path: &Path,
span: SourceSpan,
linker: &Linker,
) -> Option<LinkerError> {
let mut prefix = PathBuf::with_capacity(path.byte_len());
if path.is_absolute() {
prefix.push_component("::");
}
let mut remaining = path;
while let Some((component, rest)) = remaining.split_first() {
prefix.push_component(component);
if let Some(module) = self.find_global_module_index(prefix.as_path())
&& let Some((next, _)) = rest.split_first()
&& let Some(item) = self.module(module).item(next)
{
return Some(
SymbolResolutionError::invalid_sub_path(
span,
item.span(),
linker.source_manager.as_ref(),
)
.into(),
);
}
remaining = rest;
}
None
}
fn find_global_module_index(&self, path: &Path) -> Option<ModuleIndex> {
self.find_module_index(path)
.or_else(|| {
path.is_absolute().then(|| self.find_module_index(path.to_relative())).flatten()
})
.or_else(|| {
if path.is_absolute() {
None
} else {
path.to_absolute()
.ok()
.and_then(|absolute| self.find_module_index(absolute.as_ref()))
}
})
}
fn ensure_module_visible(
&self,
owner: ModuleIndex,
module: ModuleIndex,
span: SourceSpan,
linker: &Linker,
) -> Result<(), LinkerError> {
let mut child = module;
while let Some(parent) = self.module(child).parent() {
let edge = self
.module(parent)
.submodules
.values()
.find(|edge| edge.child == child)
.expect("child parent edge must exist");
self.ensure_submodule_visible(owner, parent, edge, span, linker)?;
child = parent;
}
Ok(())
}
fn ensure_submodule_visible(
&self,
owner: ModuleIndex,
parent: ModuleIndex,
edge: &ModuleEdge,
span: SourceSpan,
linker: &Linker,
) -> Result<(), LinkerError> {
if edge.visibility().is_public() || self.is_module_in_scope_of(owner, parent) {
return Ok(());
}
let child = self.module(edge.child());
let defined_source_file = source_file(linker.source_manager.as_ref(), edge.span());
let source_file = source_file(linker.source_manager.as_ref(), span);
Err(LinkerError::PrivateSubmodule {
span,
source_file,
module: child.path.clone(),
defined: Some(
RelatedLabel::advice("the referenced submodule is private")
.with_labeled_span(edge.span(), "the referenced submodule is private")
.with_source_file(defined_source_file),
),
})
}
fn is_module_in_scope_of(&self, module: ModuleIndex, ancestor: ModuleIndex) -> bool {
let mut current = Some(module);
while let Some(id) = current {
if id == ancestor {
return true;
}
current = self.module(id).parent();
}
false
}
fn ensure_item_visible(
&self,
owner: ModuleIndex,
item: &ItemDef,
span: SourceSpan,
linker: &Linker,
) -> Result<(), LinkerError> {
if owner == item.id().module || item.visibility().is_public() {
return Ok(());
}
Err(SymbolResolutionError::private_symbol(
span,
item.span(),
linker.source_manager.as_ref(),
)
.into())
}
fn connect_submodule_edges(&mut self, linker: &Linker) -> Result<(), LinkerError> {
for parent in linker.modules.iter() {
let parent_id = parent.id();
for decl in parent.submodules() {
let name = decl.name.as_str();
if self.modules[parent_id.as_usize()].contains_member(name) {
return Err(name_conflict(linker, parent, name, decl.name.span(), "submodule"));
}
let child_path = parent.path().join(&decl.name);
let child = self.find_module_index(child_path.as_path()).ok_or_else(|| {
LinkerError::UndefinedModule {
span: decl.name.span(),
source_file: source_file(linker.source_manager.as_ref(), decl.name.span()),
path: child_path.into_boxed_path().into(),
}
})?;
let edge = ModuleEdge {
name: name.to_string(),
child,
visibility: decl.visibility,
span: decl.name.span(),
};
self.modules[parent_id.as_usize()].submodules.insert(edge.name.clone(), edge);
let child_node = &mut self.modules[child.as_usize()];
child_node.parent.get_or_insert(parent_id);
}
}
Ok(())
}
fn validate_source_module_declarations(&self) -> Result<(), LinkerError> {
for module in self.modules.iter().filter(|module| module.source == ModuleSource::Ast) {
let Some((name, parent_path)) = module.path.split_last() else {
continue;
};
if parent_path.is_empty() {
continue;
}
let Some(parent_id) = self.find_module_index(parent_path) else {
continue;
};
let parent = self.module(parent_id);
match parent.submodule(name) {
Some(edge) if edge.child == module.id => (),
_ => {
return Err(LinkerError::UndeclaredSubmodule {
path: module.path.clone(),
parent: parent.path.clone(),
name: name.to_string(),
});
},
}
}
Ok(())
}
}
impl ModuleNode {
fn from_link_module(module: &super::LinkModule, linker: &Linker) -> Result<Self, LinkerError> {
let mut node = Self {
id: module.id(),
path: module.path().clone(),
source: module.source(),
parent: None,
items: BTreeMap::default(),
submodules: BTreeMap::default(),
imports: BTreeMap::default(),
};
for (index, symbol) in module.symbols().enumerate() {
let name = symbol.name().as_str().to_string();
let span = symbol.name().span();
if node.contains_member(&name) {
return Err(name_conflict(linker, module, &name, span, "item"));
}
node.items.insert(
name,
ItemDef {
id: module.id() + ItemIndex::new(index),
visibility: symbol.visibility(),
span,
},
);
}
for import in module.imports() {
let name = import.local_name().as_str().to_string();
let span = import.local_name().span();
if node.contains_member(&name) {
return Err(name_conflict(linker, module, &name, span, "import"));
}
node.imports.insert(
name.clone(),
UseDecl {
owner: module.id(),
alias: name,
kind: import.kind(),
visibility: import.visibility(),
target: import.target_path(),
span: import.span(),
},
);
}
Ok(node)
}
fn contains_member(&self, name: &str) -> bool {
self.items.contains_key(name)
|| self.imports.contains_key(name)
|| self.submodules.contains_key(name)
}
#[inline]
pub fn parent(&self) -> Option<ModuleIndex> {
self.parent
}
#[inline]
pub fn item(&self, name: &str) -> Option<&ItemDef> {
self.items.get(name)
}
#[inline]
pub fn submodule(&self, name: &str) -> Option<&ModuleEdge> {
self.submodules.get(name)
}
#[inline]
pub fn import(&self, name: &str) -> Option<&UseDecl> {
self.imports.get(name)
}
}
impl ModuleEdge {
#[inline]
pub fn child(&self) -> ModuleIndex {
self.child
}
#[inline]
pub fn visibility(&self) -> Visibility {
self.visibility
}
#[inline]
pub fn span(&self) -> SourceSpan {
self.span
}
}
impl ItemDef {
#[inline]
pub fn id(&self) -> GlobalItemIndex {
self.id
}
#[inline]
pub fn visibility(&self) -> Visibility {
self.visibility
}
#[inline]
pub fn span(&self) -> SourceSpan {
self.span
}
}
impl UseDecl {
#[inline]
pub fn key(&self) -> (ModuleIndex, String) {
(self.owner, self.alias.clone())
}
#[inline]
pub fn owner(&self) -> ModuleIndex {
self.owner
}
#[inline]
pub fn alias(&self) -> &str {
&self.alias
}
#[inline]
pub fn visibility(&self) -> Visibility {
self.visibility
}
#[inline]
pub fn kind(&self) -> ImportKind {
self.kind
}
#[inline]
pub fn target(&self) -> &Span<Arc<Path>> {
&self.target
}
#[inline]
pub fn span(&self) -> SourceSpan {
self.span
}
}
fn name_conflict(
linker: &Linker,
module: &super::LinkModule,
name: &str,
span: SourceSpan,
kind: &'static str,
) -> LinkerError {
LinkerError::NamespaceNameConflict {
span,
source_file: source_file(linker.source_manager.as_ref(), span),
module: module.path().clone(),
name: name.to_string(),
kind,
}
}
fn undefined_symbol(linker: &Linker, path: Span<&Path>) -> LinkerError {
undefined_symbol_from_path(linker, path.span(), path.into_inner())
}
fn undefined_symbol_from_path(linker: &Linker, span: SourceSpan, path: &Path) -> LinkerError {
LinkerError::UndefinedSymbol {
span,
source_file: source_file(linker.source_manager.as_ref(), span),
path: path.to_path_buf().into_boxed_path().into(),
}
}
fn item_path(linker: &Linker, item: GlobalItemIndex) -> Arc<Path> {
linker[item.module].path().join(linker[item.module][item.index].name()).into()
}
fn source_file(
source_manager: &dyn SourceManager,
span: SourceSpan,
) -> Option<Arc<miden_assembly_syntax::debuginfo::SourceFile>> {
source_manager.get(span.source_id()).ok()
}
#[cfg(test)]
mod tests {
use alloc::{boxed::Box, sync::Arc};
use miden_assembly_syntax::{
Parse, Path,
debuginfo::{DefaultSourceManager, SourceLanguage, SourceManager, Span},
};
use super::*;
fn parse_module(
source_manager: Arc<dyn SourceManager>,
name: &str,
source: &str,
) -> Box<miden_assembly_syntax::ast::Module> {
source_manager
.load(SourceLanguage::Masm, name.into(), source.to_string())
.parse(false, source_manager)
.expect("module should parse")
}
#[test]
fn namespace_graph_records_items_imports_and_public_submodule_edges() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace ::root
pub mod child
use external::module as external
pub proc entry
push.1
end
"#,
);
let mut child = parse_module(
source_manager.clone(),
"child.masm",
r#"
namespace ::root::child
pub const VALUE = 1
"#,
);
let mut linker = Linker::new(source_manager);
let root_id = linker.link_module(&mut root).expect("root link should succeed");
let child_id = linker.link_module(&mut child).expect("child link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
assert_eq!(graph.num_modules(), 2);
assert_eq!(graph.find_module_index(Path::new("::root")), Some(root_id));
assert_eq!(graph.find_module_index(Path::new("::root::child")), Some(child_id));
assert_eq!(graph.module(root_id).submodule("child").unwrap().child(), child_id);
assert_eq!(graph.module(child_id).parent(), Some(root_id));
assert!(graph.module(root_id).item("entry").is_some());
assert!(graph.module(root_id).import("external").is_some());
assert!(graph.module(child_id).item("VALUE").is_some());
}
#[test]
fn namespace_graph_rejects_declared_missing_child_module() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace ::root
pub mod missing
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut root).expect("root link should succeed");
let err = NamespaceGraph::build(&linker).expect_err("missing child should fail");
assert!(matches!(err, LinkerError::UndefinedModule { .. }));
}
#[test]
fn namespace_graph_rejects_undeclared_child_module() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace ::root
"#,
);
let mut child = parse_module(
source_manager.clone(),
"child.masm",
r#"
namespace ::root::child
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut root).expect("root link should succeed");
linker.link_module(&mut child).expect("child link should succeed");
let err = NamespaceGraph::build(&linker).expect_err("undeclared child should fail");
assert!(matches!(err, LinkerError::UndeclaredSubmodule { .. }));
}
#[test]
fn namespace_graph_resolves_module_and_item_imports_independently() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut imported = parse_module(
source_manager.clone(),
"imported.masm",
r#"
namespace lib::mod
pub const VALUE = 1
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
use lib::mod
use {VALUE} from lib::mod
"#,
);
let mut linker = Linker::new(source_manager);
let imported_id = linker.link_module(&mut imported).expect("imported link should succeed");
let consumer_id = linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let imports = graph.resolve_imports(&linker).expect("imports should resolve");
assert_eq!(imports.get(consumer_id, "mod"), Some(ResolvedUse::Module(imported_id)));
assert!(matches!(imports.get(consumer_id, "VALUE"), Some(ResolvedUse::Item(_))));
}
#[test]
fn namespace_graph_resolves_code_paths_through_imported_modules() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut imported = parse_module(
source_manager.clone(),
"imported.masm",
r#"
namespace lib::mod
pub const VALUE = 1
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
use lib::mod
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut imported).expect("imported link should succeed");
let consumer_id = linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let imports = graph.resolve_imports(&linker).expect("imports should resolve");
let resolved = graph
.resolve_code_path(
consumer_id,
Span::unknown(Path::new("mod::VALUE")),
&imports,
&linker,
)
.expect("code path should resolve through imported module");
assert!(matches!(resolved, ResolvedUse::Item(_)));
}
#[test]
fn namespace_graph_resolves_absolute_code_paths_globally() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut imported = parse_module(
source_manager.clone(),
"imported.masm",
r#"
namespace real::mod
pub const VALUE = 1
"#,
);
let mut global = parse_module(
source_manager.clone(),
"global.masm",
r#"
namespace lib
pub const VALUE = 2
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
use real::mod as lib
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut imported).expect("imported link should succeed");
let global_id = linker.link_module(&mut global).expect("global link should succeed");
let consumer_id = linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let imports = graph.resolve_imports(&linker).expect("imports should resolve");
let resolved = graph
.resolve_code_path(
consumer_id,
Span::unknown(Path::new("::lib::VALUE")),
&imports,
&linker,
)
.expect("absolute code path should resolve globally");
assert!(matches!(resolved, ResolvedUse::Item(gid) if gid.module == global_id));
}
#[test]
fn namespace_graph_resolves_absolute_code_paths_to_public_item_reexports() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut dep = parse_module(
source_manager.clone(),
"dep.masm",
r#"
namespace dep
pub const VALUE = 1
"#,
);
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace root
pub use {VALUE as ALIAS} from dep
"#,
);
let mut linker = Linker::new(source_manager);
let dep_id = linker.link_module(&mut dep).expect("dep link should succeed");
let root_id = linker.link_module(&mut root).expect("root link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let imports = graph.resolve_imports(&linker).expect("imports should resolve");
let resolved = graph
.resolve_code_path(
root_id,
Span::unknown(Path::new("::root::ALIAS")),
&imports,
&linker,
)
.expect("absolute code path should resolve to public item re-export");
assert!(matches!(resolved, ResolvedUse::Item(gid) if gid.module == dep_id));
}
#[test]
fn namespace_graph_rejects_private_submodule_import() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace root
mod child
"#,
);
let mut child = parse_module(
source_manager.clone(),
"child.masm",
r#"
namespace root::child
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
use root::child
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut root).expect("root link should succeed");
linker.link_module(&mut child).expect("child link should succeed");
linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let err = graph.resolve_imports(&linker).expect_err("private submodule should fail");
assert!(matches!(err, LinkerError::PrivateSubmodule { .. }));
}
#[test]
fn namespace_graph_rejects_module_reexport() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace root
pub mod child
"#,
);
let mut child = parse_module(
source_manager.clone(),
"child.masm",
r#"
namespace root::child
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
pub use {child} from root
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut root).expect("root link should succeed");
linker.link_module(&mut child).expect("child link should succeed");
linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let err = graph.resolve_imports(&linker).expect_err("module re-export should fail");
assert!(matches!(err, LinkerError::InvalidItemImportTarget { .. }));
}
#[test]
fn namespace_graph_rejects_imports_through_other_imports() {
let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let mut root = parse_module(
source_manager.clone(),
"root.masm",
r#"
namespace root
pub mod child
"#,
);
let mut child = parse_module(
source_manager.clone(),
"child.masm",
r#"
namespace root::child
pub const VALUE = 1
"#,
);
let mut consumer = parse_module(
source_manager.clone(),
"consumer.masm",
r#"
namespace app
use root::child
use {VALUE} from child
"#,
);
let mut linker = Linker::new(source_manager);
linker.link_module(&mut root).expect("root link should succeed");
linker.link_module(&mut child).expect("child link should succeed");
linker.link_module(&mut consumer).expect("consumer link should succeed");
let graph = NamespaceGraph::build(&linker).expect("namespace graph should build");
let err = graph.resolve_imports(&linker).expect_err("import chaining should fail");
assert!(matches!(err, LinkerError::ImportTargetUsesImport { .. }));
}
}