#![allow(rustdoc::private_intra_doc_links)]
mod linearization;
mod resolve_order;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
use crate::error::{Error, ErrorCollector, RichError, 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, Default)]
pub struct SourceMap {
ids: HashMap<CanonPath, usize>,
paths: Vec<CanonPath>,
}
impl SourceMap {
fn new(root_source: CanonSourceFile) -> Self {
let mut ids = HashMap::new();
ids.insert(root_source.name().clone(), MAIN_MODULE);
Self {
ids,
paths: vec![root_source.name().clone()],
}
}
fn insert(&mut self, path: CanonPath) -> usize {
let id = self.paths.len();
self.ids.insert(path.clone(), id);
self.paths.push(path);
id
}
pub fn get_id(&self, path: &CanonPath) -> Option<usize> {
self.ids.get(path).copied()
}
pub fn get_path(&self, id: usize) -> Option<&CanonPath> {
self.paths.get(id)
}
pub fn iter(&self) -> impl Iterator<Item = (&CanonPath, &usize)> {
self.ids.iter()
}
}
pub(crate) struct DependencyGraph {
modules: Vec<SourceModule>,
dependency_map: Arc<DependencyMap>,
source_map: SourceMap,
use_cache: HashMap<parse::UseDecl, ResolvedUse>,
dependencies: HashMap<usize, Vec<usize>>,
}
impl DependencyGraph {
pub fn new(
root_source: CanonSourceFile,
dependency_map: Arc<DependencyMap>,
root_program: &parse::Program,
handler: &mut ErrorCollector,
unstable_features: &UnstableFeatures,
) -> Result<Option<Self>, String> {
let mut graph = Self {
modules: vec![SourceModule {
source: root_source.clone(),
program: root_program.clone(),
}],
dependency_map,
source_map: SourceMap::new(root_source),
use_cache: HashMap::new(),
dependencies: HashMap::new(),
};
graph.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) = graph.modules.get(curr_id) else {
return Err(format!(
"Internal Driver Error: Module ID {} is in the queue but missing from the graph.modules.",
curr_id
));
};
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,
&graph.dependency_map,
&mut use_cache,
handler,
);
let mut ctx = LoadContext {
invalid_imports: &mut invalid_imports,
handler,
queue: &mut queue,
unstable_features,
};
graph.load_and_parse_dependencies(¤t, valid_imports, &mut ctx);
}
graph.use_cache = use_cache;
Ok((!handler.has_errors()).then_some(graph))
}
fn parse_and_get_source_module(
path: &CanonPath,
importer_source: &CanonSourceFile,
span: Span,
handler: &mut ErrorCollector,
unstable_features: &UnstableFeatures,
) -> Option<SourceModule> {
let Ok(content) = std::fs::read_to_string(path.as_path()) else {
let err = RichError::new(
Error::FileNotFound {
filename: PathBuf::from(path.as_path()),
},
span,
)
.with_source(importer_source.clone());
handler.push(err);
return None;
};
let mut error_handler = ErrorCollector::new();
let source = CanonSourceFile::new(path.clone(), Arc::from(content));
let ast = parse::Program::parse_from_str_with_errors(
source.clone(),
unstable_features,
&mut error_handler,
);
if error_handler.has_errors() {
handler.extend_with_handler(source, &error_handler);
None
} else {
ast.map(|program| SourceModule { source, program })
}
}
fn resolve_imports(
current_program: &parse::Program,
current_module: &CurrentModule,
dependency_map: &DependencyMap,
use_cache: &mut HashMap<parse::UseDecl, ResolvedUse>,
handler: &mut ErrorCollector,
) -> Vec<(CanonPath, Span)> {
let mut ctx = ImportContext {
current: current_module.clone(),
dependency_map,
use_cache,
handler,
};
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.source_map.get_id(&path) {
let deps = self.dependencies.entry(current.id).or_default();
if !deps.contains(&existing_id) {
deps.push(existing_id);
}
continue;
}
let Some(module) = Self::parse_and_get_source_module(
&path,
¤t.source,
import_span,
ctx.handler,
ctx.unstable_features,
) else {
let _ = ctx.invalid_imports.insert(path);
continue;
};
let new_id = self.source_map.insert(path.clone());
self.modules.push(module);
self.dependencies
.entry(current.id)
.or_default()
.push(new_id);
ctx.queue.push_back(new_id);
}
}
pub fn source_map(&self) -> &SourceMap {
&self.source_map
}
}
struct ImportContext<'a> {
current: CurrentModule,
dependency_map: &'a DependencyMap,
use_cache: &'a mut HashMap<parse::UseDecl, ResolvedUse>,
handler: &'a mut ErrorCollector,
}
impl<'a> ImportContext<'a> {
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.handler
.push(err.with_source(self.current.source.clone()));
return None;
}
};
let mut use_decl = use_decl.clone();
let span = *use_decl.span();
use_decl.set_file_id(self.current.id);
let result: (CanonPath, Span) = (resolved.path.clone(), span);
if let Some(old_value) = self.use_cache.insert(use_decl, resolved) {
let msg = format!(
"Reevaluated an existing use_decl. Old value was: {:?}",
old_value
);
let err = RichError::new(Error::Internal { msg }, span)
.with_source(self.current.source.clone());
self.handler.push(err);
}
Some(result)
}
fn process_item(&mut self, item: &parse::Item, valid_imports: &mut Vec<(CanonPath, Span)>) {
match item {
parse::Item::Use(use_decl) => {
if let Some(import) = self.resolve_single(use_decl) {
valid_imports.push(import);
}
}
parse::Item::Module(module) => {
for item in module.items() {
self.process_item(item, valid_imports);
}
}
parse::Item::TypeAlias(_) | parse::Item::Function(_) | parse::Item::Ignored => {}
}
}
}
struct LoadContext<'a> {
invalid_imports: &'a mut HashSet<CanonPath>,
handler: &'a mut ErrorCollector,
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,
ErrorCollector,
) {
let ws = TempWorkspace::new("graph");
let mut handler = ErrorCollector::new();
let workspace_dir = canon(&ws.create_dir("workspace"));
let lib_dir = canon(&ws.create_dir("workspace/libs/lib"));
let map =
Arc::new(build_map(&workspace_dir, &[(&workspace_dir, "lib", &lib_dir)]).unwrap());
let mut root_file_path = None;
let mut root_content = String::new();
for (path, content) in files {
let full_path = format!("workspace/{}", path);
let created_file = canon(&ws.create_file(&full_path, content));
if path == "main.simf" {
root_file_path = Some(created_file);
root_content = content.to_string();
}
}
let root_p = root_file_path.expect("main.simf must be defined in file list");
let main_canon_source = CanonSourceFile::new(root_p, Arc::from(root_content));
let main_program_option = parse::Program::parse_from_str_with_errors(
main_canon_source.clone(),
&UnstableFeatures::all(),
&mut handler,
);
let Some(main_program) = main_program_option else {
return (None, HashMap::new(), ws, handler);
};
let graph_option = DependencyGraph::new(
main_canon_source,
map,
&main_program,
&mut handler,
&UnstableFeatures::all(),
)
.unwrap();
let mut file_ids = HashMap::new();
if let Some(ref graph) = graph_option {
for (path, id) in graph.source_map.iter() {
let file_stem = path
.as_path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
file_ids.insert(file_stem, *id);
}
}
(graph_option, file_ids, ws, handler)
}
pub(crate) fn setup_graph(
files: Vec<(&str, &str)>,
) -> (DependencyGraph, HashMap<String, usize>, TempWorkspace) {
let (graph_option, file_ids, ws, handler) = setup_graph_raw(files);
let Some(graph) = graph_option else {
panic!(
"Parser or DependencyGraph Error in Test Setup:\n{}",
handler
);
};
(graph, file_ids, ws)
}
#[test]
fn test_simple_import() {
let (graph, ids, _ws) = 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) = 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) = 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, handler) =
setup_graph_raw(vec![("main.simf", "use unknown::library::item;")]);
assert!(
graph_option.is_none(),
"Graph unexpectedly succeeded despite having an unknown import!"
);
assert!(
handler.has_errors(),
"The ErrorCollector should have logged an error about the unmapped import"
);
}
#[test]
fn test_new_bfs_traversal_state() {
let (graph, ids, _ws) = 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.source_map.paths.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");
}
}