Skip to content

Commit 571453c

Browse files
committed
feat: implement DependencyGraph inside driver.rs file and change parse trait to the SourceFile
1 parent f609987 commit 571453c

5 files changed

Lines changed: 382 additions & 18 deletions

File tree

src/driver/mod.rs

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
use std::collections::{HashMap, VecDeque};
2+
use std::path::PathBuf;
3+
use std::sync::Arc;
4+
5+
use crate::error::{Error, ErrorCollector, RichError, Span};
6+
use crate::parse::{self, ParseFromStrWithErrors};
7+
use crate::resolution::{CanonPath, DependencyMap, SourceFile};
8+
9+
/// Represents a single, isolated file in the SimplicityHL project.
10+
/// In this architecture, a file and a module are the exact same thing.
11+
#[derive(Debug, Clone)]
12+
struct Module {
13+
source: SourceFile,
14+
/// The completely parsed program for this specific file.
15+
/// it contains all the functions, aliases, and imports defined inside the file.
16+
parsed_program: parse::Program,
17+
}
18+
19+
/// An Intermediate Representation that helps transform isolated files into a global program.
20+
///
21+
/// While an AST only understands a single file, the `DependencyGraph` links multiple
22+
/// ASTs together into a Directed Acyclic Graph (DAG). This DAG is then used to build
23+
/// one convenient `Program` struct for the semantic analyzer can easily process.
24+
///
25+
/// This structure provides the global context necessary to solve high-level compiler
26+
/// problems, including:
27+
/// * **Cross-Module Resolution:** Allowing the compiler to traverse edges and verify
28+
/// that imported symbols, functions, and types actually exist in other files.
29+
/// * **Topological Sorting:** Guaranteeing that modules are analyzed and compiled in
30+
/// the strictly correct mathematical order (e.g., analyzing module `B` before module
31+
/// `A` if `A` depends on `B`).
32+
/// * **Cycle Detection:** Preventing infinite compiler loops by ensuring no circular
33+
/// imports exist before heavy semantic processing begins.
34+
pub struct DependencyGraph {
35+
/// Implements the Arena Pattern to act as the sole, centralized owner of all parsed modules.
36+
///
37+
/// In C++ or Java, a graph would typically link dependencies using direct memory
38+
/// pointers (e.g., `List<Module*>`). In Rust, doing this requires either
39+
/// lifetimes or performance-heavy reference counting (`Rc<RefCell<T>>`).
40+
///
41+
/// Using a flat `Vec` as a memory arena is the idiomatic Rust solution.
42+
modules: Vec<Module>,
43+
44+
/// The configuration environment.
45+
/// Used to resolve external library dependencies and invoke their associated functions.
46+
dependency_map: Arc<DependencyMap>,
47+
48+
/// Fast lookup: `CanonPath` -> Module ID.
49+
/// A reverse index mapping absolute file paths to their internal IDs.
50+
/// This solves the duplication problem, ensuring each file is only parsed once.
51+
lookup: HashMap<CanonPath, usize>,
52+
53+
/// Fast lookup: Module ID -> `CanonPath`.
54+
/// A direct index mapping internal IDs back to their absolute file paths.
55+
/// This serves as the exact inverse of the `lookup` map.
56+
paths: Vec<CanonPath>,
57+
58+
/// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports.
59+
///
60+
/// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing).
61+
/// The Value (`Vec<usize>`) is a list of IDs of the "Child" modules it relies on.
62+
///
63+
/// Example: If `main.simf` (ID: 0) has `use lib::math;` (ID: 1) and `use lib::io;` (ID: 2),
64+
/// this map will contain: `{ 0: [1, 2] }`.
65+
dependencies: HashMap<usize, Vec<usize>>,
66+
}
67+
68+
impl DependencyGraph {
69+
/// Initializes a new `ProjectGraph` by parsing the root program and discovering all dependencies.
70+
///
71+
/// Performs a BFS to recursively parse `use` statements,
72+
/// building a DAG of the project's modules.
73+
///
74+
/// # Arguments
75+
///
76+
/// * `root_source` - The `SourceFile` representing the entry point of the project.
77+
/// * `dependency_map` - The context-aware mapping rules used to resolve external imports.
78+
/// * `root_program` - A reference to the already-parsed AST of the root file.
79+
/// * `handler` - The diagnostics collector used to record resolution and parsing errors.
80+
///
81+
/// # Returns
82+
///
83+
/// * `Ok(Some(Self))` - If the entire project graph was successfully resolved and parsed.
84+
/// * `Ok(None)` - If the graph traversal completed, but one or more modules contained
85+
/// errors (which have been safely logged into the `handler`).
86+
///
87+
/// # Errors
88+
///
89+
/// This function will return an `Err(String)` only for critical internal compiler errors
90+
/// (e.g., if a provided `SourceFile` is unexpectedly missing its underlying file path).
91+
pub fn new(
92+
root_source: SourceFile,
93+
dependency_map: Arc<DependencyMap>,
94+
root_program: &parse::Program,
95+
handler: &mut ErrorCollector,
96+
) -> Result<Option<Self>, String> {
97+
let root_name = if let Some(root_name) = root_source.name() {
98+
CanonPath::canonicalize(root_name)?
99+
} else {
100+
return Err(
101+
"The root_source variable inside the ProjectGraph::new() function has no name"
102+
.to_string(),
103+
);
104+
};
105+
106+
let mut graph = Self {
107+
modules: vec![Module {
108+
source: root_source,
109+
parsed_program: root_program.clone(),
110+
}],
111+
dependency_map,
112+
lookup: HashMap::new(),
113+
paths: vec![root_name.clone()],
114+
dependencies: HashMap::new(),
115+
};
116+
117+
let root_id = 0;
118+
graph.lookup.insert(root_name, root_id);
119+
graph.dependencies.insert(root_id, Vec::new());
120+
121+
let mut queue = VecDeque::new();
122+
queue.push_back(root_id);
123+
124+
while let Some(curr_id) = queue.pop_front() {
125+
let Some(current_module) = graph.modules.get(curr_id) else {
126+
return Err(format!(
127+
"Internal Driver Error: Module ID {} is in the queue but missing from the graph.modules.",
128+
curr_id
129+
));
130+
};
131+
132+
// We need this to report errors inside THIS file.
133+
let importer_source = current_module.source.clone();
134+
135+
let importer_source_name = if let Some(name) = importer_source.name() {
136+
CanonPath::canonicalize(name)?
137+
} else {
138+
return Err(format!(
139+
"The {:?} variable inside the DependencyGraph::new() function has no name",
140+
importer_source
141+
));
142+
};
143+
144+
// PHASE 1: Immutably read from the graph
145+
let valid_imports = Self::resolve_imports(
146+
&current_module.parsed_program,
147+
&importer_source,
148+
importer_source_name,
149+
&graph.dependency_map,
150+
handler,
151+
);
152+
153+
// PHASE 2: Mutate the graph
154+
graph.load_and_parse_dependencies(
155+
curr_id,
156+
valid_imports,
157+
&importer_source,
158+
handler,
159+
&mut queue,
160+
);
161+
}
162+
163+
Ok((!handler.has_errors()).then_some(graph))
164+
}
165+
166+
/// This helper cleanly encapsulates the process of loading source text, parsing it
167+
/// into an `parse::Program`, and combining them so the compiler can easily work with the file.
168+
/// If the file is missing or contains syntax errors, it logs the diagnostic to the
169+
/// `ErrorCollector` and safely returns `None`.
170+
fn parse_and_get_program(
171+
path: &CanonPath,
172+
importer_source: SourceFile,
173+
span: Span,
174+
handler: &mut ErrorCollector,
175+
) -> Option<Module> {
176+
let Ok(content) = std::fs::read_to_string(path.as_path()) else {
177+
let err = RichError::new(Error::FileNotFound(PathBuf::from(path.as_path())), span)
178+
.with_source(importer_source.clone());
179+
180+
handler.push(err);
181+
return None;
182+
};
183+
184+
let mut error_handler = ErrorCollector::new();
185+
let dep_source_file = SourceFile::new(path.as_path(), Arc::from(content.clone()));
186+
187+
let ast = parse::Program::parse_from_str_with_errors(&dep_source_file, &mut error_handler);
188+
189+
if error_handler.has_errors() {
190+
handler.extend_with_handler(dep_source_file, &error_handler);
191+
None
192+
} else {
193+
ast.map(|parsed_program| Module {
194+
source: dep_source_file.clone(),
195+
parsed_program,
196+
})
197+
}
198+
}
199+
200+
/// PHASE 1 OF GRAPH CONSTRUCTION: Resolves all imports inside a single `parse::Program`.
201+
/// Note: This is a specialized helper function designed exclusively for the `DependencyGraph::new()` constructor.
202+
fn resolve_imports(
203+
current_program: &parse::Program,
204+
importer_source: &SourceFile,
205+
importer_source_name: CanonPath,
206+
dependency_map: &DependencyMap,
207+
handler: &mut ErrorCollector,
208+
) -> Vec<(CanonPath, Span)> {
209+
let mut valid_imports = Vec::new();
210+
211+
for elem in current_program.items() {
212+
let parse::Item::Use(use_decl) = elem else {
213+
continue;
214+
};
215+
216+
match dependency_map.resolve_path(importer_source_name.clone(), use_decl) {
217+
Ok(path) => valid_imports.push((path, *use_decl.span())),
218+
Err(err) => handler.push(err.with_source(importer_source.clone())),
219+
}
220+
}
221+
222+
valid_imports
223+
}
224+
225+
/// PHASE 2 OF GRAPH CONSTRUCTION: Loads, parses, and registers new dependencies.
226+
/// Note: This is a specialized helper function designed exclusively for the `DependencyGraph::new()` constructor.
227+
fn load_and_parse_dependencies(
228+
&mut self,
229+
curr_id: usize,
230+
valid_imports: Vec<(CanonPath, Span)>,
231+
importer_source: &SourceFile,
232+
handler: &mut ErrorCollector,
233+
queue: &mut std::collections::VecDeque<usize>,
234+
) {
235+
for (path, import_span) in valid_imports {
236+
if let Some(&existing_id) = self.lookup.get(&path) {
237+
let deps = self.dependencies.entry(curr_id).or_default();
238+
if !deps.contains(&existing_id) {
239+
deps.push(existing_id);
240+
}
241+
continue;
242+
}
243+
244+
let Some(module) =
245+
Self::parse_and_get_program(&path, importer_source.clone(), import_span, handler)
246+
else {
247+
continue;
248+
};
249+
250+
let last_ind = self.modules.len();
251+
self.modules.push(module);
252+
253+
self.lookup.insert(path.clone(), last_ind);
254+
self.paths.push(path.clone());
255+
self.dependencies.entry(curr_id).or_default().push(last_ind);
256+
257+
queue.push_back(last_ind);
258+
}
259+
}
260+
}
261+
262+
#[cfg(test)]
263+
mod tests {
264+
use super::*;
265+
use crate::resolution::tests::canon;
266+
use crate::test_utils::TempWorkspace;
267+
268+
#[test]
269+
fn test_new_bfs_traversal_state() {
270+
// Goal: Verify that a simple chain (main -> a -> b) correctly pushes items
271+
// into the vectors and builds the adjacency list in BFS order.
272+
273+
let ws = TempWorkspace::new("bfs_state");
274+
let mut handler = ErrorCollector::new();
275+
276+
let workspace = canon(&ws.create_dir("workspace"));
277+
278+
let dir_a = canon(&ws.create_dir("workspace/a"));
279+
let dir_b = canon(&ws.create_dir("workspace/b"));
280+
281+
let main_content = "use a::mock_file::mock_item;";
282+
let a_content = "use b::mock_file::mock_item;";
283+
let b_content = "";
284+
285+
let main_file = canon(&ws.create_file("workspace/main.simf", main_content));
286+
let a_file = canon(&ws.create_file("workspace/a/mock_file.simf", a_content));
287+
let b_file = canon(&ws.create_file("workspace/b/mock_file.simf", b_content));
288+
289+
let mut map = DependencyMap::new();
290+
291+
map.insert(workspace.clone(), "a".to_string(), dir_a)
292+
.unwrap();
293+
map.insert(workspace.clone(), "b".to_string(), dir_b)
294+
.unwrap();
295+
let map = Arc::new(map);
296+
297+
let main_source = SourceFile::new(main_file.as_path(), Arc::from(main_content));
298+
let main_program_option =
299+
parse::Program::parse_from_str_with_errors(&main_source, &mut handler);
300+
301+
let Some(main_program) = main_program_option else {
302+
eprintln!("Parser Error in Test Setup: {}", handler);
303+
std::process::exit(1);
304+
};
305+
306+
// Act
307+
let graph_option =
308+
DependencyGraph::new(main_source, map, &main_program, &mut handler).unwrap();
309+
310+
let Some(graph) = graph_option else {
311+
eprintln!("DependencyGraph Error: {}", handler);
312+
std::process::exit(1);
313+
};
314+
315+
// Assert: Size checks
316+
assert_eq!(graph.modules.len(), 3);
317+
assert_eq!(graph.paths.len(), 3);
318+
319+
// Assert: Ensure BFS assigned the IDs in the exact correct order
320+
let main_id = *graph.lookup.get(&main_file).unwrap();
321+
let a_id = *graph.lookup.get(&a_file).unwrap();
322+
let b_id = *graph.lookup.get(&b_file).unwrap();
323+
324+
assert_eq!(main_id, 0);
325+
assert_eq!(a_id, 1);
326+
assert_eq!(b_id, 2);
327+
328+
// Assert: Ensure the Adjacency List (dependencies map) linked them correctly
329+
assert_eq!(
330+
*graph.dependencies.get(&main_id).unwrap(),
331+
vec![a_id],
332+
"Main depends on A"
333+
);
334+
assert_eq!(
335+
*graph.dependencies.get(&a_id).unwrap(),
336+
vec![b_id],
337+
"A depends on B"
338+
);
339+
assert!(
340+
!graph.dependencies.contains_key(&b_id),
341+
"B depends on nothing"
342+
);
343+
}
344+
}

src/error.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ impl ErrorCollector {
421421
Self { errors: Vec::new() }
422422
}
423423

424-
/// Exten existing errors with specific `RichError`.
424+
/// Extend existing errors with specific `RichError`.
425425
/// We assume that `RichError` contains `SourceFile`.
426426
pub fn push(&mut self, error: RichError) {
427427
self.errors.push(error);
@@ -437,6 +437,11 @@ impl ErrorCollector {
437437
self.errors.extend(new_errors);
438438
}
439439

440+
/// The same idea applies to the `extend()` function.
441+
pub fn extend_with_handler(&mut self, source: SourceFile, handler: &ErrorCollector) {
442+
self.extend(source, handler.errors.iter().cloned());
443+
}
444+
440445
pub fn get(&self) -> &[RichError] {
441446
&self.errors
442447
}

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod compile;
66
pub mod debug;
77
#[cfg(feature = "docs")]
88
pub mod docs;
9+
pub mod driver;
910
pub mod dummy_env;
1011
pub mod error;
1112
pub mod jet;
@@ -37,6 +38,7 @@ pub use simplicity::elements;
3738
use crate::debug::DebugSymbols;
3839
use crate::error::{ErrorCollector, WithContent};
3940
use crate::parse::ParseFromStrWithErrors;
41+
use crate::resolution::SourceFile;
4042
pub use crate::types::ResolvedType;
4143
pub use crate::value::Value;
4244
pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues};
@@ -58,8 +60,10 @@ impl TemplateProgram {
5860
/// The string is not a valid SimplicityHL program.
5961
pub fn new<Str: Into<Arc<str>>>(s: Str) -> Result<Self, String> {
6062
let file = s.into();
63+
let source = SourceFile::anonymous(file.clone());
6164
let mut error_handler = ErrorCollector::new();
62-
let parse_program = parse::Program::parse_from_str_with_errors(&file, &mut error_handler);
65+
let parse_program = parse::Program::parse_from_str_with_errors(&source, &mut error_handler);
66+
6367
if let Some(program) = parse_program {
6468
let ast_program = ast::Program::analyze(&program).with_content(Arc::clone(&file))?;
6569
Ok(Self {

0 commit comments

Comments
 (0)