diff --git a/src/driver/linearization.rs b/src/driver/linearization.rs new file mode 100644 index 0000000..8b9f32a --- /dev/null +++ b/src/driver/linearization.rs @@ -0,0 +1,184 @@ +//! Computes the bottom-up load order of dependencies using a DFS. +//! +//! # Architectural Note: Why pure DFS instead of C3? +//! Unlike OOP languages that require C3 Linearization to resolve complex method +//! overriding (MRO), SimplicityHL module imports rely on namespacing and aliasing. +//! Because we do not need to enforce strict local precedence, a standard post-order +//! DFS is a better option. + +// TODO: Remove this once the code is actively used. +#![allow(dead_code)] +use std::collections::HashSet; +use std::fmt; + +use crate::driver::DependencyGraph; + +/// This is a core component of the [`DependencyGraph`]. +impl DependencyGraph { + /// Returns the deterministic, BOTTOM-UP load order of dependencies. + pub fn linearize(&self) -> Result, LinearizationError> { + let mut visited = HashSet::new(); + let mut visiting = Vec::new(); + let mut order = Vec::new(); + + self.dfs_linearize(0, &mut visited, &mut visiting, &mut order)?; + + Ok(order) + } + + /// Core recursive Post-Order DFS for topological sorting. + /// + /// - **Visited Set (`visited`):** Prevents processing shared dependencies multiple times (solves diamonds). + /// - **Cycle Detection (`visiting`):** Tracks the current path stack to catch infinite loops. + /// - **Order List (`order`):** Accumulates the deterministic load order bottom-up. + fn dfs_linearize( + &self, + module: usize, + visited: &mut HashSet, + visiting: &mut Vec, + order: &mut Vec, + ) -> Result<(), LinearizationError> { + // If we have already fully processed this module, skip it (Diamond Deduplication) + if visited.contains(&module) { + return Ok(()); + } + + if let Some(cycle_start) = visiting.iter().position(|&m| m == module) { + return Err(LinearizationError::CycleDetected( + visiting[cycle_start..] + .iter() + .map(|&id| self.modules[id].source.str_name()) + .collect(), + )); + } + + visiting.push(module); + + let parents = self + .dependencies + .get(&module) + .map_or(&[] as &[usize], |v| v.as_slice()); + + for &parent in parents { + self.dfs_linearize(parent, visited, visiting, order)?; + } + + visiting.pop(); + visited.insert(module); + order.push(module); + + Ok(()) + } +} + +#[derive(Debug)] +pub enum LinearizationError { + /// Raised when a circular dependency (e.g., A -> B -> A) is detected. + CycleDetected(Vec), +} + +impl fmt::Display for LinearizationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LinearizationError::CycleDetected(cycle) => { + write!(f, "Circular dependency detected: {:?}", cycle.join(" -> ")) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::driver::tests::setup_graph; + + use super::*; + + #[test] + fn test_linearize_simple_import() { + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::math::some_func;"), + ("libs/lib/math.simf", ""), + ]); + + let order = graph.linearize().unwrap(); + + let root_id = ids["main"]; + let math_id = ids["math"]; + + assert_eq!(order, vec![math_id, root_id]); + } + + #[test] + fn test_linearize_diamond_dependency_deduplication() { + // Setup: + // root -> imports A, B + // A -> imports Common + // B -> imports Common + // Expected: Common loaded ONLY ONCE. + + let (graph, ids, _dir) = 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", ""), + ]); + + let order = graph.linearize().unwrap(); + + // Verify order using IDs from the helper map + let main_id = ids["main"]; + let a_id = ids["A"]; + let b_id = ids["B"]; + let common_id = ids["Common"]; + + assert!( + order == vec![common_id, b_id, a_id, main_id] + || order == vec![common_id, a_id, b_id, main_id] + ); + } + + #[test] + fn test_linearize_detects_cycle() { + let (graph, _, _dir) = 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 order = graph.linearize(); + assert!(matches!( + order, + Err(LinearizationError::CycleDetected { .. }) + )); + } + + #[test] + fn test_linearize_allows_conflicting_nested_import_order() { + // A imports X then Y, while B imports Y then X. + // This DAG is still valid because neither X nor Y depends on the other. + let (graph, ids, _dir) = setup_graph(vec![ + ("main.simf", "use lib::A::foo; use lib::B::bar;"), + ("libs/lib/A.simf", "use lib::X::foo; use lib::Y::bar;"), + ("libs/lib/B.simf", "use lib::Y::baz; use lib::X::qux;"), + ("libs/lib/X.simf", ""), + ("libs/lib/Y.simf", ""), + ]); + + let order = graph + .linearize() + .expect("valid dependency DAG should linearize successfully"); + + let main_id = ids["main"]; + let a_id = ids["A"]; + let b_id = ids["B"]; + let x_id = ids["X"]; + let y_id = ids["Y"]; + + assert!( + order == vec![x_id, y_id, a_id, b_id, main_id] + || order == vec![y_id, x_id, a_id, b_id, main_id] + || order == vec![x_id, y_id, b_id, a_id, main_id] + || order == vec![y_id, x_id, b_id, a_id, main_id] + ); + } +} diff --git a/src/driver/mod.rs b/src/driver/mod.rs index c326883..34227d4 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -1,3 +1,5 @@ +mod linearization; + use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; use std::sync::Arc; @@ -46,6 +48,10 @@ impl CanonSourceFile { &self.name } + pub fn str_name(&self) -> String { + self.name.as_path().display().to_string() + } + pub fn content(&self) -> Arc { self.content.clone() } @@ -326,7 +332,7 @@ mod tests { /// 3. `TempWorkspace`: The temporary directory instance. This must be kept in scope by the caller so /// the OS doesn't delete the files before the test finishes. /// 4. `ErrorCollector`: The handler containing any logged errors, useful fo - fn setup_graph_raw( + pub(crate) fn setup_graph_raw( files: Vec<(&str, &str)>, ) -> ( Option, @@ -414,7 +420,7 @@ mod tests { /// /// This function will immediately panic and print the collected errors /// to standard error if the parser or graph builder encounters any issues. - fn setup_graph( + pub(crate) fn setup_graph( files: Vec<(&str, &str)>, ) -> (DependencyGraph, HashMap, TempWorkspace) { let (graph_option, file_ids, ws, handler) = setup_graph_raw(files);