mod linearization;
pub(crate) mod resolve_order;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::Arc;
use chumsky::container::Container;
use crate::error::{Error, ErrorCollector, RichError, Span};
use crate::parse::{self, ParseFromStrWithErrors};
use crate::resolution::{CanonPath, DependencyMap, SourceFile};
pub use crate::driver::resolve_order::{FileScoped, Program, SymbolTable};
pub(crate) const MAIN_STR: &str = "main";
pub(crate) const MAIN_MODULE: usize = 0;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CanonSourceFile {
name: CanonPath,
content: Arc<str>,
}
impl TryFrom<SourceFile> for CanonSourceFile {
type Error = String;
fn try_from(source: SourceFile) -> Result<Self, Self::Error> {
let name = if let Some(root_name) = source.name() {
CanonPath::canonicalize(root_name)?
} else {
return Err(
"Cannot canonicalize the SourceFile because it is missing a file name.".to_string(),
);
};
Ok(CanonSourceFile {
name,
content: source.content(),
})
}
}
impl CanonSourceFile {
pub fn new(name: CanonPath, content: Arc<str>) -> Self {
Self { name, content }
}
pub fn name(&self) -> &CanonPath {
&self.name
}
pub fn str_name(&self) -> String {
self.name.as_path().display().to_string()
}
pub fn content(&self) -> Arc<str> {
self.content.clone()
}
}
#[derive(Debug, Clone)]
struct Module {
source: CanonSourceFile,
parsed_program: parse::Program,
}
pub(crate) struct DependencyGraph {
modules: Vec<Module>,
dependency_map: Arc<DependencyMap>,
lookup: HashMap<CanonPath, usize>,
paths: Vec<CanonPath>,
dependencies: HashMap<usize, Vec<usize>>,
}
impl DependencyGraph {
pub fn new(
root_source: SourceFile,
dependency_map: Arc<DependencyMap>,
root_program: &parse::Program,
handler: &mut ErrorCollector,
) -> Result<Option<Self>, String> {
let root_canon_source = CanonSourceFile::try_from(root_source)?;
let mut graph = Self {
modules: vec![Module {
source: root_canon_source.clone(),
parsed_program: root_program.clone(),
}],
dependency_map,
lookup: HashMap::new(),
paths: vec![root_canon_source.name().clone()],
dependencies: HashMap::new(),
};
graph
.lookup
.insert(root_canon_source.name().clone(), MAIN_MODULE);
graph.dependencies.insert(MAIN_MODULE, Vec::new());
let mut queue = VecDeque::new();
queue.push_back(MAIN_MODULE);
let mut inalid_imports = HashSet::new();
while let Some(curr_id) = queue.pop_front() {
let Some(current_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_module.source.clone();
let valid_imports = Self::resolve_imports(
¤t_module.parsed_program,
&importer_source,
&graph.dependency_map,
handler,
);
graph.load_and_parse_dependencies(
curr_id,
valid_imports,
&mut inalid_imports,
&importer_source,
handler,
&mut queue,
);
}
Ok((!handler.has_errors()).then_some(graph))
}
fn parse_and_get_program(
path: &CanonPath,
importer_source: &CanonSourceFile,
span: Span,
handler: &mut ErrorCollector,
) -> Option<Module> {
let Ok(content) = std::fs::read_to_string(path.as_path()) else {
let err = RichError::new(Error::FileNotFound(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(), &mut error_handler);
if error_handler.has_errors() {
handler.extend_with_handler(source, &error_handler);
None
} else {
ast.map(|parsed_program| Module {
source,
parsed_program,
})
}
}
fn resolve_imports(
current_program: &parse::Program,
importer_source: &CanonSourceFile,
dependency_map: &DependencyMap,
handler: &mut ErrorCollector,
) -> Vec<(CanonPath, Span)> {
let mut valid_imports = Vec::new();
for elem in current_program.items() {
let parse::Item::Use(use_decl) = elem else {
continue;
};
match dependency_map.resolve_path(importer_source.name(), use_decl) {
Ok(path) => valid_imports.push((path, *use_decl.span())),
Err(err) => handler.push(err.with_source(importer_source.clone())),
}
}
valid_imports
}
fn load_and_parse_dependencies(
&mut self,
curr_id: usize,
valid_imports: Vec<(CanonPath, Span)>,
inalid_imports: &mut HashSet<CanonPath>,
importer_source: &CanonSourceFile,
handler: &mut ErrorCollector,
queue: &mut VecDeque<usize>,
) {
for (path, import_span) in valid_imports {
if inalid_imports.contains(&path) {
continue;
}
if let Some(&existing_id) = self.lookup.get(&path) {
let deps = self.dependencies.entry(curr_id).or_default();
if !deps.contains(&existing_id) {
deps.push(existing_id);
}
continue;
}
let Some(module) =
Self::parse_and_get_program(&path, importer_source, import_span, handler)
else {
inalid_imports.push(path);
continue;
};
let last_ind = self.modules.len();
self.modules.push(module);
self.lookup.insert(path.clone(), last_ind);
self.paths.push(path.clone());
self.dependencies.entry(curr_id).or_default().push(last_ind);
queue.push_back(last_ind);
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::resolution::tests::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 mut map = DependencyMap::new();
map.insert(workspace_dir.clone(), "lib".to_string(), lib_dir)
.expect("Failed to insert dependency map");
let map = Arc::new(map);
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_source = SourceFile::from(main_canon_source);
let main_program_option =
parse::Program::parse_from_str_with_errors(main_source.clone(), &mut handler);
let Some(main_program) = main_program_option else {
return (None, HashMap::new(), ws, handler);
};
let graph_option =
DependencyGraph::new(main_source, map, &main_program, &mut handler).unwrap();
let mut file_ids = HashMap::new();
if let Some(ref graph) = graph_option {
for (path, id) in &graph.lookup {
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 lib::Common::dummy1;"),
("libs/lib/B.simf", "use lib::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 lib::B::func;"),
("libs/lib/B.simf", "use lib::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 lib::B::mock_item;"),
("libs/lib/B.simf", ""),
]);
assert_eq!(graph.modules.len(), 3);
assert_eq!(graph.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");
}
}