Skip to content

Commit 51b9ceb

Browse files
Sdoba16LesterEvSe
authored andcommitted
Added C3 algorithm
1 parent d81073f commit 51b9ceb

2 files changed

Lines changed: 259 additions & 2 deletions

File tree

src/driver/c3_linearization.rs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// TODO: Remove this once the code is actively used.
2+
#![allow(dead_code)]
3+
use std::{collections::HashMap, fmt};
4+
5+
use crate::driver::DependencyGraph;
6+
7+
impl DependencyGraph {
8+
fn c3_linearize(&self) -> Result<Vec<usize>, C3Error> {
9+
self.linearize_module(0)
10+
}
11+
12+
fn linearize_module(&self, root: usize) -> Result<Vec<usize>, C3Error> {
13+
let mut memo = HashMap::new();
14+
let mut visiting = Vec::new();
15+
16+
self.linearize_rec(root, &mut memo, &mut visiting)
17+
}
18+
19+
/// Core recursive DFS for C3 linearization.
20+
///
21+
/// - **Memoization (`memo`):** Prevents exponential blowup in diamond dependencies by calculating shared modules exactly once.
22+
/// - **Cycle Detection (`visiting`):** Prevents infinite loops by returning an error if the current path repeats a module (e.g., A -> B -> A).
23+
///
24+
/// # Returns
25+
/// - `Ok(Vec<usize>)`: The deterministic, top-down ordered sequence of dependencies for the module.
26+
/// - `Err(C3Error)`: If a circular dependency or unresolvable conflict is detected.
27+
fn linearize_rec(
28+
&self,
29+
module: usize,
30+
memo: &mut HashMap<usize, Vec<usize>>,
31+
visiting: &mut Vec<usize>,
32+
) -> Result<Vec<usize>, C3Error> {
33+
if let Some(result) = memo.get(&module) {
34+
return Ok(result.clone());
35+
}
36+
37+
if let Some(cycle_start) = visiting.iter().position(|&m| m == module) {
38+
return Err(C3Error::CycleDetected(
39+
visiting[cycle_start..]
40+
.iter()
41+
.map(|&id| self.modules[id].source.str_name())
42+
.collect(),
43+
));
44+
}
45+
46+
visiting.push(module);
47+
48+
let parents = self
49+
.dependencies
50+
.get(&module)
51+
.map_or(&[] as &[usize], |v| v.as_slice());
52+
53+
let mut seqs: Vec<Vec<usize>> = Vec::with_capacity(parents.len() + 1);
54+
55+
for &parent in parents {
56+
seqs.push(self.linearize_rec(parent, memo, visiting)?);
57+
}
58+
59+
if !parents.is_empty() {
60+
seqs.push(parents.to_vec());
61+
}
62+
63+
let mut result = vec![module];
64+
let merged = c3_merge(seqs).map_err(|conflicts| C3Error::InconsistentLinearization {
65+
module: self.modules[module].source.str_name(),
66+
conflicts: self.format_conflict_names(conflicts),
67+
})?;
68+
69+
result.extend(merged);
70+
71+
visiting.pop();
72+
memo.insert(module, result.clone());
73+
74+
Ok(result)
75+
}
76+
77+
/// Helper to convert raw usize conflict sequences into readable module names
78+
fn format_conflict_names(&self, conflicts: Vec<Vec<usize>>) -> Vec<Vec<String>> {
79+
conflicts
80+
.into_iter()
81+
.map(|seq| {
82+
seq.into_iter()
83+
.map(|id| self.modules[id].source.str_name())
84+
.collect()
85+
})
86+
.collect()
87+
}
88+
}
89+
90+
/// Merges a list of sequences (parent linearizations) into a single sequence.
91+
/// The algorithm ensures that the local precedence order of each sequence is preserved.
92+
fn c3_merge(seqs: Vec<Vec<usize>>) -> Result<Vec<usize>, Vec<Vec<usize>>> {
93+
// Convert to a collection of slices. This allows us to "remove" the head
94+
// by simply advancing the slice pointer by 1, which is entirely zero-cost.
95+
let mut slices: Vec<&[usize]> = seqs.iter().map(AsRef::as_ref).collect();
96+
let mut result = Vec::new();
97+
98+
loop {
99+
slices.retain(|s| !s.is_empty());
100+
if slices.is_empty() {
101+
return Ok(result);
102+
}
103+
104+
let candidate = slices
105+
.iter()
106+
.map(|s| s[0])
107+
.find(|&head| !slices.iter().any(|s| s[1..].contains(&head)));
108+
109+
let Some(head) = candidate else {
110+
let conflicts = slices.into_iter().map(|s| s.to_vec()).collect();
111+
return Err(conflicts);
112+
};
113+
114+
result.push(head);
115+
for seq in &mut slices {
116+
if seq.first() == Some(&head) {
117+
*seq = &seq[1..];
118+
}
119+
}
120+
}
121+
}
122+
123+
#[derive(Debug)]
124+
enum C3Error {
125+
CycleDetected(Vec<String>),
126+
/// Error for inconsistent MRO.
127+
/// This can happen if the dependency graph has a shape that makes the
128+
/// order of parent classes ambiguous.
129+
/// Example: A depends on B and C, and B also depends on C.
130+
/// The linearization of A is A + merge(linearization(B), linearization(C), [B, C]).
131+
/// If B appears before C in one parent's linearization but C appears before B
132+
/// in another's, the merge will fail.
133+
InconsistentLinearization {
134+
module: String,
135+
conflicts: Vec<Vec<String>>,
136+
},
137+
}
138+
139+
impl fmt::Display for C3Error {
140+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141+
match self {
142+
C3Error::CycleDetected(cycle) => {
143+
write!(f, "Circular dependency detected: {:?}", cycle.join(" -> "))
144+
}
145+
C3Error::InconsistentLinearization { module, conflicts } => {
146+
writeln!(f, "Inconsistent resolution order for module '{}'", module)?;
147+
writeln!(
148+
f,
149+
"The compiler could not resolve the following conflicting import constraints:"
150+
)?;
151+
152+
// Loop through the matrix and print each conflicting sequence
153+
for conflict in conflicts {
154+
writeln!(f, " [{}]", conflict.join(", "))?;
155+
}
156+
157+
write!(
158+
f,
159+
"Try reordering your `use` statements to avoid cross-wiring."
160+
)
161+
}
162+
}
163+
}
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use crate::driver::tests::setup_graph;
169+
170+
use super::*;
171+
172+
#[test]
173+
fn test_c3_simple_import() {
174+
let (graph, ids, _dir) = setup_graph(vec![
175+
("main.simf", "use lib::math::some_func;"),
176+
("libs/lib/math.simf", ""),
177+
]);
178+
179+
let order = graph.c3_linearize().unwrap();
180+
181+
let root_id = ids["main"];
182+
let math_id = ids["math"];
183+
184+
// Assuming linearization order: Dependent (Root) -> Dependency (Math)
185+
assert_eq!(order, vec![root_id, math_id]);
186+
}
187+
188+
#[test]
189+
fn test_c3_diamond_dependency_deduplication() {
190+
// Setup:
191+
// root -> imports A, B
192+
// A -> imports Common
193+
// B -> imports Common
194+
// Expected: Common loaded ONLY ONCE.
195+
196+
let (graph, ids, _dir) = setup_graph(vec![
197+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
198+
("libs/lib/A.simf", "use lib::Common::dummy1;"),
199+
("libs/lib/B.simf", "use lib::Common::dummy2;"),
200+
("libs/lib/Common.simf", ""),
201+
]);
202+
203+
let order = graph.c3_linearize().unwrap();
204+
205+
// Verify order using IDs from the helper map
206+
let main_id = ids["main"];
207+
let a_id = ids["A"];
208+
let b_id = ids["B"];
209+
let common_id = ids["Common"];
210+
211+
// The current list is ordered top-down. The calling function will reverse this to process 'common' first and 'main' last.
212+
assert_eq!(order, vec![main_id, a_id, b_id, common_id]);
213+
}
214+
215+
#[test]
216+
fn test_c3_detects_cycle() {
217+
let (graph, _, _dir) = setup_graph(vec![
218+
("main.simf", "use lib::A::entry;"),
219+
("libs/lib/A.simf", "use lib::B::func;"),
220+
("libs/lib/B.simf", "use lib::A::func;"),
221+
]);
222+
223+
let order = graph.c3_linearize();
224+
assert!(matches!(order, Err(C3Error::CycleDetected { .. })));
225+
}
226+
227+
#[test]
228+
fn test_c3_inconsistent_linearization() {
229+
// main declares parents in the order [A, B].
230+
// But B itself depends on A, so B's linearization is [B, A].
231+
//
232+
// True C3 must merge:
233+
// [A]
234+
// [B, A]
235+
// [A, B] <- the raw parent list
236+
//
237+
// That merge has no valid head, so this must be rejected as inconsistent.
238+
let (graph, _, _dir) = setup_graph(vec![
239+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
240+
("libs/lib/A.simf", ""),
241+
("libs/lib/B.simf", "use lib::A::foo;"),
242+
]);
243+
244+
let order = graph.c3_linearize();
245+
246+
assert!(
247+
matches!(order, Err(C3Error::InconsistentLinearization { .. })),
248+
"expected inconsistent linearization, got {order:?}"
249+
);
250+
}
251+
}

src/driver/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod c3_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)