Skip to content

Commit 105ea6f

Browse files
Sdoba16LesterEvSe
authored andcommitted
feat: Added linearization algorithm
1 parent d81073f commit 105ea6f

File tree

2 files changed

+192
-2
lines changed

2 files changed

+192
-2
lines changed

src/driver/linearization.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//! Computes the bottom-up load order of dependencies using a DFS.
2+
//!
3+
//! # Architectural Note: Why pure DFS instead of C3?
4+
//! Unlike OOP languages that require C3 Linearization to resolve complex method
5+
//! overriding (MRO), SimplicityHL module imports rely on namespacing and aliasing.
6+
//! Because we do not need to enforce strict local precedence, a standard post-order
7+
//! DFS is a better option.
8+
9+
// TODO: Remove this once the code is actively used.
10+
#![allow(dead_code)]
11+
use std::collections::HashSet;
12+
use std::fmt;
13+
14+
use crate::driver::DependencyGraph;
15+
16+
/// This is a core component of the [`DependencyGraph`](super::DependencyGraph).
17+
impl DependencyGraph {
18+
/// Returns the deterministic, BOTTOM-UP load order of dependencies.
19+
pub fn linearize(&self) -> Result<Vec<usize>, LinearizationError> {
20+
let mut visited = HashSet::new();
21+
let mut visiting = Vec::new();
22+
let mut order = Vec::new();
23+
24+
self.dfs_linearize(0, &mut visited, &mut visiting, &mut order)?;
25+
26+
Ok(order)
27+
}
28+
29+
/// Core recursive Post-Order DFS for topological sorting.
30+
///
31+
/// - **Visited Set (`visited`):** Prevents processing shared dependencies multiple times (solves diamonds).
32+
/// - **Cycle Detection (`visiting`):** Tracks the current path stack to catch infinite loops.
33+
/// - **Order List (`order`):** Accumulates the deterministic load order bottom-up.
34+
fn dfs_linearize(
35+
&self,
36+
module: usize,
37+
visited: &mut HashSet<usize>,
38+
visiting: &mut Vec<usize>,
39+
order: &mut Vec<usize>,
40+
) -> Result<(), LinearizationError> {
41+
// If we have already fully processed this module, skip it (Diamond Deduplication)
42+
if visited.contains(&module) {
43+
return Ok(());
44+
}
45+
46+
if let Some(cycle_start) = visiting.iter().position(|&m| m == module) {
47+
return Err(LinearizationError::CycleDetected(
48+
visiting[cycle_start..]
49+
.iter()
50+
.map(|&id| self.modules[id].source.str_name())
51+
.collect(),
52+
));
53+
}
54+
55+
visiting.push(module);
56+
57+
let parents = self
58+
.dependencies
59+
.get(&module)
60+
.map_or(&[] as &[usize], |v| v.as_slice());
61+
62+
for &parent in parents {
63+
self.dfs_linearize(parent, visited, visiting, order)?;
64+
}
65+
66+
visiting.pop();
67+
visited.insert(module);
68+
order.push(module);
69+
70+
Ok(())
71+
}
72+
}
73+
74+
#[derive(Debug)]
75+
pub enum LinearizationError {
76+
/// Raised when a circular dependency (e.g., A -> B -> A) is detected.
77+
CycleDetected(Vec<String>),
78+
}
79+
80+
impl fmt::Display for LinearizationError {
81+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82+
match self {
83+
LinearizationError::CycleDetected(cycle) => {
84+
write!(f, "Circular dependency detected: {:?}", cycle.join(" -> "))
85+
}
86+
}
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use crate::driver::tests::setup_graph;
93+
94+
use super::*;
95+
96+
#[test]
97+
fn test_linearize_simple_import() {
98+
let (graph, ids, _dir) = setup_graph(vec![
99+
("main.simf", "use lib::math::some_func;"),
100+
("libs/lib/math.simf", ""),
101+
]);
102+
103+
let order = graph.linearize().unwrap();
104+
105+
let root_id = ids["main"];
106+
let math_id = ids["math"];
107+
108+
assert_eq!(order, vec![math_id, root_id]);
109+
}
110+
111+
#[test]
112+
fn test_linearize_diamond_dependency_deduplication() {
113+
// Setup:
114+
// root -> imports A, B
115+
// A -> imports Common
116+
// B -> imports Common
117+
// Expected: Common loaded ONLY ONCE.
118+
119+
let (graph, ids, _dir) = setup_graph(vec![
120+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
121+
("libs/lib/A.simf", "use lib::Common::dummy1;"),
122+
("libs/lib/B.simf", "use lib::Common::dummy2;"),
123+
("libs/lib/Common.simf", ""),
124+
]);
125+
126+
let order = graph.linearize().unwrap();
127+
128+
// Verify order using IDs from the helper map
129+
let main_id = ids["main"];
130+
let a_id = ids["A"];
131+
let b_id = ids["B"];
132+
let common_id = ids["Common"];
133+
134+
assert!(
135+
order == vec![common_id, b_id, a_id, main_id]
136+
|| order == vec![common_id, a_id, b_id, main_id]
137+
);
138+
}
139+
140+
#[test]
141+
fn test_linearize_detects_cycle() {
142+
let (graph, _, _dir) = setup_graph(vec![
143+
("main.simf", "use lib::A::entry;"),
144+
("libs/lib/A.simf", "use lib::B::func;"),
145+
("libs/lib/B.simf", "use lib::A::func;"),
146+
]);
147+
148+
let order = graph.linearize();
149+
assert!(matches!(
150+
order,
151+
Err(LinearizationError::CycleDetected { .. })
152+
));
153+
}
154+
155+
#[test]
156+
fn test_linearize_allows_conflicting_nested_import_order() {
157+
// A imports X then Y, while B imports Y then X.
158+
// This DAG is still valid because neither X nor Y depends on the other.
159+
let (graph, ids, _dir) = setup_graph(vec![
160+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
161+
("libs/lib/A.simf", "use lib::X::foo; use lib::Y::bar;"),
162+
("libs/lib/B.simf", "use lib::Y::baz; use lib::X::qux;"),
163+
("libs/lib/X.simf", ""),
164+
("libs/lib/Y.simf", ""),
165+
]);
166+
167+
let order = graph
168+
.linearize()
169+
.expect("valid dependency DAG should linearize successfully");
170+
171+
let main_id = ids["main"];
172+
let a_id = ids["A"];
173+
let b_id = ids["B"];
174+
let x_id = ids["X"];
175+
let y_id = ids["Y"];
176+
177+
assert!(
178+
order == vec![x_id, y_id, a_id, b_id, main_id]
179+
|| order == vec![y_id, x_id, a_id, b_id, main_id]
180+
|| order == vec![x_id, y_id, b_id, a_id, main_id]
181+
|| order == vec![y_id, x_id, b_id, a_id, main_id]
182+
);
183+
}
184+
}

src/driver/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod linearization;
2+
13
use std::collections::{HashMap, HashSet, VecDeque};
24
use std::path::PathBuf;
35
use std::sync::Arc;
@@ -46,6 +48,10 @@ impl CanonSourceFile {
4648
&self.name
4749
}
4850

51+
pub fn str_name(&self) -> String {
52+
self.name.as_path().display().to_string()
53+
}
54+
4955
pub fn content(&self) -> Arc<str> {
5056
self.content.clone()
5157
}
@@ -326,7 +332,7 @@ mod tests {
326332
/// 3. `TempWorkspace`: The temporary directory instance. This must be kept in scope by the caller so
327333
/// the OS doesn't delete the files before the test finishes.
328334
/// 4. `ErrorCollector`: The handler containing any logged errors, useful fo
329-
fn setup_graph_raw(
335+
pub(crate) fn setup_graph_raw(
330336
files: Vec<(&str, &str)>,
331337
) -> (
332338
Option<DependencyGraph>,
@@ -414,7 +420,7 @@ mod tests {
414420
///
415421
/// This function will immediately panic and print the collected errors
416422
/// to standard error if the parser or graph builder encounters any issues.
417-
fn setup_graph(
423+
pub(crate) fn setup_graph(
418424
files: Vec<(&str, &str)>,
419425
) -> (DependencyGraph, HashMap<String, usize>, TempWorkspace) {
420426
let (graph_option, file_ids, ws, handler) = setup_graph_raw(files);

0 commit comments

Comments
 (0)