Skip to content

Commit 77e449a

Browse files
committed
feat: implement ProjectGraph inside driver.rs file and change parse trait to the SourceFile
1 parent f609987 commit 77e449a

4 files changed

Lines changed: 359 additions & 17 deletions

File tree

src/driver.rs

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

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)