#![allow(rustdoc::private_intra_doc_links)]
mod linearization;
mod resolve_order;
#[cfg(test)]
mod version_tests;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
use crate::error::{Diagnostic, DiagnosticManager, Error, Span};
use crate::parse::{self, ParseFromStrWithErrors};
use crate::resolution::{DependencyMap, ResolvedUse};
use crate::source::{CanonPath, CanonSourceFile};
use crate::unstable::UnstableFeatures;
pub(crate) const MAIN_STR: &str = "main";
pub(crate) const CRATE_STR: &str = "crate";
pub(crate) const MAIN_MODULE: usize = 0;
#[derive(Debug, Clone)]
struct SourceModule {
source: CanonSourceFile,
program: parse::Program,
}
#[derive(Debug, Clone)]
pub struct SourceMap {
ids: HashMap<CanonPath, usize>,
entries: Vec<CanonSourceFile>,
}
impl SourceMap {
pub(crate) fn with_source(source: CanonSourceFile) -> Self {
let mut ids = HashMap::new();
ids.insert(source.name().clone(), MAIN_MODULE);
Self {
ids,
entries: vec![source],
}
}
pub(crate) fn insert(&mut self, entry: CanonSourceFile) {
let id = self.entries.len();
self.ids.insert(entry.name().clone(), id);
self.entries.push(entry);
debug_assert_eq!(self.ids.len(), self.entries.len());
}
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
pub fn id(&self, path: &CanonPath) -> Option<usize> {
self.ids.get(path).copied()
}
pub fn entry(&self, id: usize) -> Option<&CanonSourceFile> {
self.entries.get(id)
}
pub fn content(&self, id: usize) -> Option<Arc<str>> {
self.entries.get(id).map(|e| e.content().clone())
}
pub fn path(&self, id: usize) -> Option<&CanonPath> {
self.entries.get(id).map(|e| e.name())
}
pub fn iter(&self) -> impl Iterator<Item = (&CanonPath, &usize)> {
self.ids.iter()
}
}
pub(crate) struct DependencyGraph {
modules: HashMap<usize, SourceModule>,
dependency_map: Arc<DependencyMap>,
sources: SourceMap,
use_cache: HashMap<Span, ResolvedUse>,
dependencies: HashMap<usize, Vec<usize>>,
}
impl DependencyGraph {
pub fn build_program(
root_source: CanonSourceFile,
dependency_map: Arc<DependencyMap>,
unstable_features: &UnstableFeatures,
) -> (Option<parse::Program>, DiagnosticManager) {
let (graph, mut diagnostics) =
Self::build_graph(root_source, dependency_map, unstable_features);
let Some(graph) = graph else {
return (None, diagnostics);
};
let program = graph.linearize_and_assemble(&mut diagnostics);
diagnostics.with_sources(graph.sources);
(program, diagnostics)
}
fn build_graph(
root_source: CanonSourceFile,
dependency_map: Arc<DependencyMap>,
unstable_features: &UnstableFeatures,
) -> (Option<Self>, DiagnosticManager) {
let mut diagnostics = DiagnosticManager::default();
let sources = SourceMap::with_source(root_source.clone());
let Some(program) = parse::Program::parse_from_str_with_errors(
MAIN_MODULE,
&root_source.content(),
unstable_features,
&mut diagnostics,
) else {
diagnostics.with_sources(sources);
return (None, diagnostics);
};
let mut modules = HashMap::new();
modules.insert(
MAIN_MODULE,
SourceModule {
source: root_source.clone(),
program,
},
);
let mut graph = Self {
modules,
dependency_map,
sources,
use_cache: HashMap::new(),
dependencies: HashMap::new(),
};
graph.discover_dependencies(&mut diagnostics, unstable_features);
if diagnostics.has_errors() {
diagnostics.with_sources(graph.sources);
(None, diagnostics)
} else {
(Some(graph), diagnostics)
}
}
fn discover_dependencies(
&mut self,
diagnostics: &mut DiagnosticManager,
unstable_features: &UnstableFeatures,
) {
self.dependencies.insert(MAIN_MODULE, Vec::new());
let mut use_cache = HashMap::new();
let mut queue = VecDeque::new();
queue.push_back(MAIN_MODULE);
let mut invalid_imports = HashSet::new();
while let Some(curr_id) = queue.pop_front() {
let Some(current_source_module) = self.modules.get(&curr_id) else {
debug_assert!(
false,
"poisoned invariant broken: id {curr_id} popped without a module"
);
diagnostics.push(Diagnostic::global(Error::Internal {
msg: format!("module id {curr_id} missing from graph; aborting compilation"),
}));
return;
};
let importer_source = current_source_module.source.clone();
let current = CurrentModule {
id: curr_id,
source: importer_source,
};
let valid_imports = Self::resolve_imports(
¤t_source_module.program,
¤t,
&self.dependency_map,
&mut use_cache,
diagnostics,
);
let mut ctx = LoadContext {
invalid_imports: &mut invalid_imports,
diagnostics,
queue: &mut queue,
unstable_features,
};
self.load_and_parse_dependencies(¤t, valid_imports, &mut ctx);
}
self.use_cache = use_cache;
}
fn resolve_imports(
current_program: &parse::Program,
current_module: &CurrentModule,
dependency_map: &DependencyMap,
use_cache: &mut HashMap<Span, ResolvedUse>,
diagnostics: &mut DiagnosticManager,
) -> Vec<(CanonPath, Span)> {
let mut ctx = ImportContext {
current: current_module.clone(),
dependency_map,
use_cache,
diagnostics,
};
let mut valid_imports = Vec::new();
for item in current_program.items() {
ctx.process_item(item, &mut valid_imports);
}
valid_imports
}
fn load_and_parse_dependencies(
&mut self,
current: &CurrentModule,
valid_imports: Vec<(CanonPath, Span)>,
ctx: &mut LoadContext,
) {
for (path, import_span) in valid_imports {
if ctx.invalid_imports.contains(&path) {
continue;
}
if let Some(existing_id) = self.sources.id(&path) {
let deps = self.dependencies.entry(current.id).or_default();
if !deps.contains(&existing_id) {
deps.push(existing_id);
}
continue;
}
let Ok(content) = std::fs::read_to_string(path.as_path()) else {
let err = Diagnostic::new(
Error::FileNotFound {
filename: PathBuf::from(path.as_path()),
},
import_span,
);
ctx.diagnostics.push(err);
let _ = ctx.invalid_imports.insert(path);
continue;
};
let source = CanonSourceFile::new(path.clone(), Arc::from(content));
let new_id = self.sources.len();
self.sources.insert(source.clone());
let Some(parsed_program) = Self::parse_and_get_source_module(
new_id,
&source.content(),
ctx.diagnostics,
ctx.unstable_features,
) else {
let _ = ctx.invalid_imports.insert(path);
continue;
};
let module = SourceModule {
source: source.clone(),
program: parsed_program,
};
self.modules.insert(new_id, module);
self.dependencies
.entry(current.id)
.or_default()
.push(new_id);
ctx.queue.push_back(new_id);
}
}
fn parse_and_get_source_module(
new_id: usize,
content: &str,
diagnostics: &mut DiagnosticManager,
unstable_features: &UnstableFeatures,
) -> Option<parse::Program> {
let before = diagnostics.error_count();
let ast = parse::Program::parse_from_str_with_errors(
new_id,
content,
unstable_features,
diagnostics,
);
if diagnostics.error_count() > before {
return None;
}
ast
}
}
struct ImportContext<'a> {
current: CurrentModule,
dependency_map: &'a DependencyMap,
use_cache: &'a mut HashMap<Span, ResolvedUse>,
diagnostics: &'a mut DiagnosticManager,
}
impl<'a> ImportContext<'a> {
fn process_item(&mut self, item: &parse::Item, valid_imports: &mut Vec<(CanonPath, Span)>) {
match item {
parse::Item::Use(use_decl) => valid_imports.extend(self.resolve_single(use_decl)),
parse::Item::Module(module) => {
for item in module.items() {
self.process_item(item, valid_imports);
}
}
parse::Item::TypeAlias(_)
| parse::Item::Function(_)
| parse::Item::EnumDeclaration(_)
| parse::Item::Ignored => {}
}
}
fn resolve_single(&mut self, use_decl: &parse::UseDecl) -> Option<(CanonPath, Span)> {
let resolved = match self
.dependency_map
.resolve_path_internal(self.current.source.name(), use_decl)
{
Ok(res) => res,
Err(err) => {
self.diagnostics.push(err);
return None;
}
};
let span = *use_decl.span();
let result: (CanonPath, Span) = (resolved.path.clone(), span);
if let Some(old_value) = self.use_cache.insert(span, resolved) {
let msg = format!(
"Reevaluated an existing use_decl. Old value was: {:?}",
old_value
);
let err = Diagnostic::new(Error::Internal { msg }, span);
self.diagnostics.push(err);
}
Some(result)
}
}
struct LoadContext<'a> {
invalid_imports: &'a mut HashSet<CanonPath>,
diagnostics: &'a mut DiagnosticManager,
queue: &'a mut VecDeque<usize>,
unstable_features: &'a UnstableFeatures,
}
#[derive(Debug, Clone)]
struct CurrentModule {
id: usize,
source: CanonSourceFile,
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::resolution::tests::{build_map, canon};
use crate::test_utils::TempWorkspace;
pub(crate) fn setup_graph_raw(
files: Vec<(&str, &str)>,
) -> (
Option<DependencyGraph>,
HashMap<String, usize>,
TempWorkspace,
DiagnosticManager,
) {
let ws = TempWorkspace::new("graph");
let workspace_dir = canon(&ws.create_dir("workspace"));
let lib_dir = canon(&ws.create_dir("workspace/libs/lib"));
let dependency_map =
Arc::new(build_map(&workspace_dir, &[(&workspace_dir, "lib", &lib_dir)]).unwrap());
let mut root_source_opt = None;
for (path, content) in files {
let full_path = format!("workspace/{}", path);
let created = canon(&ws.create_file(&full_path, content));
if path == "main.simf" {
root_source_opt = Some(CanonSourceFile::new(created, Arc::from(content)));
}
}
let root_source = root_source_opt.expect("main.simf must be defined in file list");
let (graph_opt, diagnostics) =
DependencyGraph::build_graph(root_source, dependency_map, &UnstableFeatures::all());
let file_ids = match &graph_opt {
Some(graph) => build_file_ids(&graph.sources),
None => diagnostics
.sources()
.map(build_file_ids)
.unwrap_or_default(),
};
(graph_opt, file_ids, ws, diagnostics)
}
fn build_file_ids(sources: &SourceMap) -> HashMap<String, usize> {
sources
.iter()
.filter_map(|(path, id)| {
let stem = path.as_path().file_stem()?;
Some((stem.to_string_lossy().into_owned(), *id))
})
.collect()
}
pub(crate) fn setup_graph(
files: Vec<(&str, &str)>,
) -> (
DependencyGraph,
HashMap<String, usize>,
TempWorkspace,
DiagnosticManager,
) {
let (graph_option, file_ids, ws, diagnostics) = setup_graph_raw(files);
let Some(graph) = graph_option else {
panic!(
"Parser or DependencyGraph Error in Test Setup:\n{}",
diagnostics
);
};
(graph, file_ids, ws, diagnostics)
}
#[test]
fn test_simple_import() {
let (graph, ids, _ws, _diags) = setup_graph(vec![
("main.simf", "use lib::math::some_func;"),
("libs/lib/math.simf", ""),
]);
assert_eq!(graph.modules.len(), 2, "Should have Root and Math module");
let root_id = ids["main"];
let math_id = ids["math"];
assert!(
graph
.dependencies
.get(&root_id)
.is_some_and(|deps| deps.contains(&math_id)),
"Root (main.simf) should depend on Math (math.simf)"
);
}
#[test]
fn test_diamond_dependency_deduplication() {
let (graph, ids, _ws, _diags) = setup_graph(vec![
("main.simf", "use lib::A::foo; use lib::B::bar;"),
("libs/lib/A.simf", "use crate::Common::dummy1;"),
("libs/lib/B.simf", "use crate::Common::dummy2;"),
("libs/lib/Common.simf", ""),
]);
assert_eq!(
graph.modules.len(),
4,
"Should resolve exactly 4 unique modules"
);
let a_id = ids["A"];
let b_id = ids["B"];
let common_id = ids["Common"];
assert!(
graph
.dependencies
.get(&a_id)
.is_some_and(|deps| deps.contains(&common_id)),
"A should depend on Common"
);
assert!(
graph
.dependencies
.get(&b_id)
.is_some_and(|deps| deps.contains(&common_id)),
"B should depend on Common"
);
}
#[test]
fn test_cyclic_dependency() {
let (graph, ids, _ws, _diags) = setup_graph(vec![
("main.simf", "use lib::A::entry;"),
("libs/lib/A.simf", "use crate::B::func;"),
("libs/lib/B.simf", "use crate::A::func;"),
]);
let a_id = ids["A"];
let b_id = ids["B"];
assert!(
graph
.dependencies
.get(&a_id)
.is_some_and(|deps| deps.contains(&b_id)),
"A should depend on B"
);
assert!(
graph
.dependencies
.get(&b_id)
.is_some_and(|deps| deps.contains(&a_id)),
"B should depend on A"
);
}
#[test]
fn test_fails_on_unmapped_imports() {
let (graph_option, _ids, _ws, diagnostics) =
setup_graph_raw(vec![("main.simf", "use unknown::library::item;")]);
assert!(
graph_option.is_none(),
"Graph unexpectedly succeeded despite having an unknown import!"
);
assert!(
diagnostics.has_errors(),
"The ErrorCollector should have logged an error about the unmapped import"
);
}
#[test]
fn test_new_bfs_traversal_state() {
let (graph, ids, _ws, _diags) = setup_graph(vec![
("main.simf", "use lib::A::mock_item;"),
("libs/lib/A.simf", "use crate::B::mock_item;"),
("libs/lib/B.simf", ""),
]);
assert_eq!(graph.modules.len(), 3);
assert_eq!(graph.sources.len(), 3);
let main_id = ids["main"];
let a_id = ids["A"];
let b_id = ids["B"];
assert_eq!(main_id, 0);
assert_eq!(a_id, 1);
assert_eq!(b_id, 2);
assert_eq!(
*graph.dependencies.get(&main_id).unwrap(),
vec![a_id],
"Main depends on A"
);
assert_eq!(
*graph.dependencies.get(&a_id).unwrap(),
vec![b_id],
"A depends on B"
);
let b_has_no_deps = graph
.dependencies
.get(&b_id)
.map_or(true, |deps| deps.is_empty());
assert!(b_has_no_deps, "B depends on nothing");
}
}