Skip to content

Commit a49865f

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

File tree

2 files changed

+330
-2
lines changed

2 files changed

+330
-2
lines changed

src/driver/c3_linearization.rs

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
//! Dependency-Driven C3 Linearization Engine
2+
//!
3+
//! This module implements a variant of the C3 Linearization algorithm optimized
4+
//! specifically for module dependency resolution rather than Object-Oriented
5+
//! Method Resolution Order (MRO).
6+
//!
7+
//! # Architectural Note: Strict vs. Dependency-Driven C3
8+
//! Standard C3 Linearization (used in Python and Solidity) strictly enforces
9+
//! *local precedence* by appending the direct parent list to the merge sequences.
10+
//! If a developer's local import order contradicts the physical dependency graph
11+
//! (e.g., they import `A` before `B`, but `B` depends on `A`), strict C3 deadlocks
12+
//! and throws an error to prevent method-overriding paradoxes.
13+
//!
14+
//! Because SimplicityHL module imports rely on namespacing/aliasing rather than
15+
//! inheritance and method overriding, enforcing strict local precedence is unnecessary
16+
//! and creates a rigid user experience. This implementation deliberately omits the
17+
//! local precedence constraint.
18+
//!
19+
//! # Rationale: Why keep C3 instead of refactoring to pure DFS?
20+
//!
21+
//! While cycle detection and topological sorting could theoretically be handled by a
22+
//! simpler DFS, we must consider the long-term impact on the SimplicityHL ecosystem
23+
//! and whether strict `InconsistentLinearization` errors will be necessary in the future.
24+
//!
25+
//! When deciding how to handle this, we faced two architectural paths:
26+
//!
27+
//! 1. **Refactor to DFS:** We strip out C3 now. If Simplicity Devs start writing
28+
//! libraries, and we later discover a complex resolution bug that forces us to
29+
//! revert back to C3, the sudden change in resolution rules could break existing
30+
//! libraries across the ecosystem. This is a highly undesirable risk.
31+
//!
32+
//! 2. **Retain a Relaxed C3 (Chosen Path):** We keep the robust C3 merge engine
33+
//! but weaken its strict local-precedence restrictions to mimic DFS behavior.
34+
//! This allows current tests to pass and provides a forgiving user experience.
35+
//! If the relaxed rules are enough, great. If strict C3 constraints are needed
36+
//! later, the architectural foundation is already in place, preventing a massive
37+
//! and destructive migration for developers.
38+
//!
39+
//! By prioritizing the physical graph structure over arbitrary `use` statement
40+
//! ordering, this "Dependency-Driven" approach provides the following guarantees:
41+
//!
42+
//! 1. **Diamond Deduplication:** Shared dependencies are evaluated and loaded exactly once.
43+
//! 2. **Cycle Detection:** Circular dependencies (`A -> B -> A`) are caught and cleanly rejected.
44+
//! 3. **Order Forgiveness:** Contradictory local import statements are safely auto-corrected
45+
//! to satisfy the structural requirements of the Directed Acyclic Graph.
46+
47+
// TODO: Remove this once the code is actively used.
48+
#![allow(dead_code)]
49+
use std::{collections::HashMap, fmt};
50+
51+
use crate::driver::DependencyGraph;
52+
53+
/// This is a core component of the [`DependencyGraph`](super::DependencyGraph).
54+
impl DependencyGraph {
55+
/// Returns the deterministic, BOTTOM-UP load order of dependencies.
56+
fn c3_linearize(&self) -> Result<Vec<usize>, C3Error> {
57+
let mut order = self.linearize_module(0)?;
58+
order.reverse();
59+
Ok(order)
60+
}
61+
62+
fn linearize_module(&self, root: usize) -> Result<Vec<usize>, C3Error> {
63+
let mut memo = HashMap::new();
64+
let mut visiting = Vec::new();
65+
66+
self.linearize_rec(root, &mut memo, &mut visiting)
67+
}
68+
69+
/// Core recursive DFS for C3 linearization.
70+
///
71+
/// - **Memoization (`memo`):** Prevents exponential blowup in diamond dependencies by calculating shared modules exactly once.
72+
/// - **Cycle Detection (`visiting`):** Prevents infinite loops by returning an error if the current path repeats a module (e.g., A -> B -> A).
73+
///
74+
/// # Returns
75+
/// - `Ok(Vec<usize>)`: The deterministic, top-down ordered sequence of dependencies for the module.
76+
/// - `Err(C3Error)`: If a circular dependency or unresolvable conflict is detected.
77+
fn linearize_rec(
78+
&self,
79+
module: usize,
80+
memo: &mut HashMap<usize, Vec<usize>>,
81+
visiting: &mut Vec<usize>,
82+
) -> Result<Vec<usize>, C3Error> {
83+
if let Some(result) = memo.get(&module) {
84+
return Ok(result.clone());
85+
}
86+
87+
if let Some(cycle_start) = visiting.iter().position(|&m| m == module) {
88+
return Err(C3Error::CycleDetected(
89+
visiting[cycle_start..]
90+
.iter()
91+
.map(|&id| self.modules[id].source.str_name())
92+
.collect(),
93+
));
94+
}
95+
96+
visiting.push(module);
97+
98+
let parents = self
99+
.dependencies
100+
.get(&module)
101+
.map_or(&[] as &[usize], |v| v.as_slice());
102+
103+
let mut seqs: Vec<Vec<usize>> = Vec::with_capacity(parents.len() + 1);
104+
105+
for &parent in parents {
106+
seqs.push(self.linearize_rec(parent, memo, visiting)?);
107+
}
108+
109+
let mut result = vec![module];
110+
let merged = c3_merge(seqs).map_err(|conflicts| C3Error::InconsistentLinearization {
111+
module: self.modules[module].source.str_name(),
112+
conflicts: self.format_conflict_names(conflicts),
113+
})?;
114+
115+
result.extend(merged);
116+
117+
visiting.pop();
118+
memo.insert(module, result.clone());
119+
120+
Ok(result)
121+
}
122+
123+
/// Helper to convert raw usize conflict sequences into readable module names
124+
fn format_conflict_names(&self, conflicts: Vec<Vec<usize>>) -> Vec<Vec<String>> {
125+
conflicts
126+
.into_iter()
127+
.map(|seq| {
128+
seq.into_iter()
129+
.map(|id| self.modules[id].source.str_name())
130+
.collect()
131+
})
132+
.collect()
133+
}
134+
}
135+
136+
/// Merges a list of sequences (parent linearizations) into a single sequence.
137+
/// The algorithm ensures that the local precedence order of each sequence is preserved.
138+
fn c3_merge(seqs: Vec<Vec<usize>>) -> Result<Vec<usize>, Vec<Vec<usize>>> {
139+
// Convert to a collection of slices. This allows us to "remove" the head
140+
// by simply advancing the slice pointer by 1, which is entirely zero-cost.
141+
let mut slices: Vec<&[usize]> = seqs.iter().map(AsRef::as_ref).collect();
142+
let mut result = Vec::new();
143+
144+
loop {
145+
slices.retain(|s| !s.is_empty());
146+
if slices.is_empty() {
147+
return Ok(result);
148+
}
149+
150+
let candidate = slices
151+
.iter()
152+
.map(|s| s[0])
153+
.find(|&head| !slices.iter().any(|s| s[1..].contains(&head)));
154+
155+
let Some(head) = candidate else {
156+
let conflicts = slices.into_iter().map(|s| s.to_vec()).collect();
157+
return Err(conflicts);
158+
};
159+
160+
result.push(head);
161+
for seq in &mut slices {
162+
if seq.first() == Some(&head) {
163+
*seq = &seq[1..];
164+
}
165+
}
166+
}
167+
}
168+
169+
#[derive(Debug)]
170+
enum C3Error {
171+
CycleDetected(Vec<String>),
172+
/// Error for inconsistent MRO.
173+
/// This can happen if the dependency graph has a shape that makes the
174+
/// order of parent classes ambiguous.
175+
/// Example: A depends on B and C, and B also depends on C.
176+
/// The linearization of A is A + merge(linearization(B), linearization(C), [B, C]).
177+
/// If B appears before C in one parent's linearization but C appears before B
178+
/// in another's, the merge will fail.
179+
InconsistentLinearization {
180+
module: String,
181+
conflicts: Vec<Vec<String>>,
182+
},
183+
}
184+
185+
impl fmt::Display for C3Error {
186+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187+
match self {
188+
C3Error::CycleDetected(cycle) => {
189+
write!(f, "Circular dependency detected: {:?}", cycle.join(" -> "))
190+
}
191+
C3Error::InconsistentLinearization { module, conflicts } => {
192+
writeln!(f, "Inconsistent resolution order for module '{}'", module)?;
193+
writeln!(
194+
f,
195+
"The compiler could not resolve the following conflicting import constraints:"
196+
)?;
197+
198+
// Loop through the matrix and print each conflicting sequence
199+
for conflict in conflicts {
200+
writeln!(f, " [{}]", conflict.join(", "))?;
201+
}
202+
203+
write!(
204+
f,
205+
"Try reordering your `use` statements to avoid cross-wiring."
206+
)
207+
}
208+
}
209+
}
210+
}
211+
212+
#[cfg(test)]
213+
mod tests {
214+
use crate::driver::tests::setup_graph;
215+
216+
use super::*;
217+
218+
#[test]
219+
fn test_c3_simple_import() {
220+
let (graph, ids, _dir) = setup_graph(vec![
221+
("main.simf", "use lib::math::some_func;"),
222+
("libs/lib/math.simf", ""),
223+
]);
224+
225+
let order = graph.c3_linearize().unwrap();
226+
227+
let root_id = ids["main"];
228+
let math_id = ids["math"];
229+
230+
assert_eq!(order, vec![math_id, root_id]);
231+
}
232+
233+
#[test]
234+
fn test_c3_diamond_dependency_deduplication() {
235+
// Setup:
236+
// root -> imports A, B
237+
// A -> imports Common
238+
// B -> imports Common
239+
// Expected: Common loaded ONLY ONCE.
240+
241+
let (graph, ids, _dir) = setup_graph(vec![
242+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
243+
("libs/lib/A.simf", "use lib::Common::dummy1;"),
244+
("libs/lib/B.simf", "use lib::Common::dummy2;"),
245+
("libs/lib/Common.simf", ""),
246+
]);
247+
248+
let order = graph.c3_linearize().unwrap();
249+
250+
// Verify order using IDs from the helper map
251+
let main_id = ids["main"];
252+
let a_id = ids["A"];
253+
let b_id = ids["B"];
254+
let common_id = ids["Common"];
255+
256+
assert_eq!(order, vec![common_id, b_id, a_id, main_id]);
257+
}
258+
259+
#[test]
260+
fn test_c3_detects_cycle() {
261+
let (graph, _, _dir) = setup_graph(vec![
262+
("main.simf", "use lib::A::entry;"),
263+
("libs/lib/A.simf", "use lib::B::func;"),
264+
("libs/lib/B.simf", "use lib::A::func;"),
265+
]);
266+
267+
let order = graph.c3_linearize();
268+
assert!(matches!(order, Err(C3Error::CycleDetected { .. })));
269+
}
270+
271+
#[test]
272+
fn test_c3_inconsistent_linearization() {
273+
// Setup:
274+
// `main` declares imports in the local order: A, then B.
275+
// However, B internally depends on A.
276+
//
277+
// Under STRICT C3 Linearization:
278+
// This would fail and return `C3Error::InconsistentLinearization`.
279+
// The algorithm would deadlock because `main` demands A before B,
280+
// but the physical graph demands B wraps A.
281+
//
282+
// Under our DEPENDENCY-DRIVEN C3 Linearization:
283+
// We forgive the local import order of `main`. The algorithm prioritizes
284+
// the physical graph structure over the arbitrary order of `use` statements,
285+
// successfully auto-correcting the load order to prevent compiler crashes.
286+
let (graph, ids, _dir) = setup_graph(vec![
287+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
288+
("libs/lib/A.simf", ""),
289+
("libs/lib/B.simf", "use lib::A::foo;"),
290+
]);
291+
292+
let order = graph
293+
.c3_linearize()
294+
.expect("valid dependency DAG should linearize successfully");
295+
296+
let main_id = ids["main"];
297+
let a_id = ids["A"];
298+
let b_id = ids["B"];
299+
300+
assert_eq!(order, vec![a_id, b_id, main_id]);
301+
}
302+
303+
#[test]
304+
fn test_c3_allows_valid_parent_chain() {
305+
// main imports A, then B; B itself imports A.
306+
let (graph, ids, _dir) = setup_graph(vec![
307+
("main.simf", "use lib::A::foo; use lib::B::bar;"),
308+
("libs/lib/A.simf", ""),
309+
("libs/lib/B.simf", "use lib::A::foo;"),
310+
]);
311+
312+
let order = graph
313+
.c3_linearize()
314+
.expect("valid dependency DAG should linearize successfully");
315+
316+
let main_id = ids["main"];
317+
let a_id = ids["A"];
318+
let b_id = ids["B"];
319+
320+
assert_eq!(order, vec![a_id, b_id, main_id]);
321+
}
322+
}

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)