Skip to content

Commit e4183d9

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

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed

src/driver/c3_linearization.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
let mut result = vec![module];
60+
let merged = c3_merge(seqs).map_err(|conflicts| C3Error::InconsistentLinearization {
61+
module: self.modules[module].source.str_name(),
62+
conflicts: self.format_conflict_names(conflicts),
63+
})?;
64+
65+
result.extend(merged);
66+
67+
visiting.pop();
68+
memo.insert(module, result.clone());
69+
70+
Ok(result)
71+
}
72+
73+
/// Helper to convert raw usize conflict sequences into readable module names
74+
fn format_conflict_names(&self, conflicts: Vec<Vec<usize>>) -> Vec<Vec<String>> {
75+
conflicts
76+
.into_iter()
77+
.map(|seq| {
78+
seq.into_iter()
79+
.map(|id| self.modules[id].source.str_name())
80+
.collect()
81+
})
82+
.collect()
83+
}
84+
}
85+
86+
/// Merges a list of sequences (parent linearizations) into a single sequence.
87+
/// The algorithm ensures that the local precedence order of each sequence is preserved.
88+
fn c3_merge(seqs: Vec<Vec<usize>>) -> Result<Vec<usize>, Vec<Vec<usize>>> {
89+
// Convert to a collection of slices. This allows us to "remove" the head
90+
// by simply advancing the slice pointer by 1, which is entirely zero-cost.
91+
let mut slices: Vec<&[usize]> = seqs.iter().map(AsRef::as_ref).collect();
92+
let mut result = Vec::new();
93+
94+
loop {
95+
slices.retain(|s| !s.is_empty());
96+
if slices.is_empty() {
97+
return Ok(result);
98+
}
99+
100+
let candidate = slices
101+
.iter()
102+
.map(|s| s[0])
103+
.find(|&head| !slices.iter().any(|s| s[1..].contains(&head)));
104+
105+
let Some(head) = candidate else {
106+
let conflicts = slices.into_iter().map(|s| s.to_vec()).collect();
107+
return Err(conflicts);
108+
};
109+
110+
result.push(head);
111+
for seq in &mut slices {
112+
if seq.first() == Some(&head) {
113+
*seq = &seq[1..];
114+
}
115+
}
116+
}
117+
}
118+
119+
#[derive(Debug)]
120+
enum C3Error {
121+
CycleDetected(Vec<String>),
122+
/// Error for inconsistent MRO.
123+
/// This can happen if the dependency graph has a shape that makes the
124+
/// order of parent classes ambiguous.
125+
/// Example: A depends on B and C, and B also depends on C.
126+
/// The linearization of A is A + merge(linearization(B), linearization(C), [B, C]).
127+
/// If B appears before C in one parent's linearization but C appears before B
128+
/// in another's, the merge will fail.
129+
InconsistentLinearization {
130+
module: String,
131+
conflicts: Vec<Vec<String>>,
132+
},
133+
}
134+
135+
impl fmt::Display for C3Error {
136+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137+
match self {
138+
C3Error::CycleDetected(cycle) => {
139+
write!(f, "Circular dependency detected: {:?}", cycle.join(" -> "))
140+
}
141+
C3Error::InconsistentLinearization { module, conflicts } => {
142+
writeln!(f, "Inconsistent resolution order for module '{}'", module)?;
143+
writeln!(
144+
f,
145+
"The compiler could not resolve the following conflicting import constraints:"
146+
)?;
147+
148+
// Loop through the matrix and print each conflicting sequence
149+
for conflict in conflicts {
150+
writeln!(f, " [{}]", conflict.join(", "))?;
151+
}
152+
153+
write!(
154+
f,
155+
"Try reordering your `use` statements to avoid cross-wiring."
156+
)
157+
}
158+
}
159+
}
160+
}
161+
162+
#[cfg(test)]
163+
mod tests {
164+
use crate::driver::tests::setup_graph;
165+
166+
use super::*;
167+
168+
#[test]
169+
fn test_c3_simple_import() {
170+
let (graph, ids, _dir) = setup_graph(
171+
"c3_simple_import",
172+
vec![
173+
("main.simf", "use lib::math::some_func;"),
174+
("libs/lib/math.simf", ""),
175+
],
176+
);
177+
178+
let order = graph.c3_linearize().unwrap();
179+
180+
let root_id = ids["main"];
181+
let math_id = ids["math"];
182+
183+
// Assuming linearization order: Dependent (Root) -> Dependency (Math)
184+
assert_eq!(order, vec![root_id, math_id]);
185+
}
186+
187+
#[test]
188+
fn test_c3_diamond_dependency_deduplication() {
189+
// Setup:
190+
// root -> imports A, B
191+
// A -> imports Common
192+
// B -> imports Common
193+
// Expected: Common loaded ONLY ONCE.
194+
195+
let (graph, ids, _dir) = setup_graph(
196+
"c3",
197+
vec![
198+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
199+
("libs/lib/A.simf", "use lib::Common::dummy1;"),
200+
("libs/lib/B.simf", "use lib::Common::dummy2;"),
201+
("libs/lib/Common.simf", ""),
202+
],
203+
);
204+
205+
let order = graph.c3_linearize().unwrap();
206+
207+
// Verify order using IDs from the helper map
208+
let main_id = ids["main"];
209+
let a_id = ids["A"];
210+
let b_id = ids["B"];
211+
let common_id = ids["Common"];
212+
213+
// The current list is ordered top-down. The calling function will reverse this to process 'common' first and 'main' last.
214+
assert_eq!(order, vec![main_id, a_id, b_id, common_id]);
215+
}
216+
217+
#[test]
218+
fn test_c3_detects_cycle() {
219+
let (graph, _, _dir) = setup_graph(
220+
"c3",
221+
vec![
222+
("main.simf", "use lib::A::entry;"),
223+
("libs/lib/A.simf", "use lib::B::func;"),
224+
("libs/lib/B.simf", "use lib::A::func;"),
225+
],
226+
);
227+
228+
let order = graph.c3_linearize();
229+
matches!(order, Err(C3Error::CycleDetected(_)));
230+
}
231+
}

src/driver/mod.rs

Lines changed: 6 additions & 0 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
}

0 commit comments

Comments
 (0)