Skip to content

Commit 874e4a5

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

4 files changed

Lines changed: 226 additions & 12 deletions

File tree

src/driver.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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+
pub struct Module {
13+
pub 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+
pub parsed_program: parse::Program,
17+
}
18+
19+
/// The Dependency Graph itself.
20+
pub struct ProjectGraph {
21+
/// Arena Pattern: the data itself lives here.
22+
/// A flat vector guarantees that module data is stored contiguously in memory.
23+
#[expect(dead_code)]
24+
pub(self) modules: Vec<Module>,
25+
26+
/// The configuration environment.
27+
/// Used to resolve external library dependencies and invoke their associated functions.
28+
pub dependency_map: Arc<DependencyMap>,
29+
30+
/// Fast lookup: `CanonPath` -> Module ID.
31+
/// A reverse index mapping absolute file paths to their internal IDs.
32+
/// This solves the duplication problem, ensuring each file is only parsed once.
33+
pub lookup: HashMap<CanonPath, usize>,
34+
35+
/// Fast lookup: Module ID -> `CanonPath`.
36+
/// A direct index mapping internal IDs back to their absolute file paths.
37+
/// This serves as the exact inverse of the `lookup` map.
38+
pub paths: Arc<[CanonPath]>,
39+
40+
/// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports.
41+
///
42+
/// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing).
43+
/// The Value (`Vec<usize>`) is a list of IDs of the "Child" modules it relies on.
44+
///
45+
/// Example: If `main.simf` (ID: 0) has `use lib::math;` (ID: 1) and `use lib::io;` (ID: 2),
46+
/// this map will contain: `{ 0: [1, 2] }`.
47+
pub dependencies: HashMap<usize, Vec<usize>>,
48+
}
49+
50+
impl ProjectGraph {
51+
/// This helper cleanly encapsulates the process of loading source text, parsing it
52+
/// into an `parse::Program`, and combining them so the compiler can easily work with the file.
53+
/// If the file is missing or contains syntax errors, it logs the diagnostic to the
54+
/// `ErrorCollector` and safely returns `None`.
55+
fn parse_and_get_program(
56+
path: &CanonPath,
57+
importer_source: SourceFile,
58+
span: Span,
59+
handler: &mut ErrorCollector,
60+
) -> Option<Module> {
61+
let Ok(content) = std::fs::read_to_string(path.as_path()) else {
62+
let err = RichError::new(Error::FileNotFound(PathBuf::from(path.as_path())), span)
63+
.with_source(importer_source.clone());
64+
65+
handler.push(err);
66+
return None;
67+
};
68+
69+
let dep_source_file = SourceFile::new(path.as_path(), Arc::from(content.clone()));
70+
71+
parse::Program::parse_from_str_with_errors(&dep_source_file, handler).map(
72+
|parsed_program| Module {
73+
source: dep_source_file,
74+
parsed_program,
75+
},
76+
)
77+
}
78+
79+
/// Initializes a new `ProjectGraph` by parsing the root program and discovering all dependencies.
80+
///
81+
/// Performs a BFS to recursively parse `use` statements,
82+
/// building a DAG of the project's modules.
83+
///
84+
/// # Arguments
85+
///
86+
/// * `root_source` - The `SourceFile` representing the entry point of the project.
87+
/// * `dependency_map` - The context-aware mapping rules used to resolve external imports.
88+
/// * `root_program` - A reference to the already-parsed AST of the root file.
89+
/// * `handler` - The diagnostics collector used to record resolution and parsing errors.
90+
///
91+
/// # Returns
92+
///
93+
/// * `Ok(Some(Self))` - If the entire project graph was successfully resolved and parsed.
94+
/// * `Ok(None)` - If the graph traversal completed, but one or more modules contained
95+
/// errors (which have been safely logged into the `handler`).
96+
///
97+
/// # Errors
98+
///
99+
/// This function will return an `Err(String)` only for critical internal compiler errors
100+
/// (e.g., if a provided `SourceFile` is unexpectedly missing its underlying file path).
101+
pub fn new(
102+
root_source: SourceFile,
103+
dependency_map: Arc<DependencyMap>,
104+
root_program: &parse::Program,
105+
handler: &mut ErrorCollector,
106+
) -> Result<Option<Self>, String> {
107+
let root_name = if let Some(root_name) = root_source.name() {
108+
CanonPath::canonicalize(root_name)?
109+
} else {
110+
return Err(
111+
"The root_source variable inside the ProjectGraph::new() function has no name"
112+
.to_string(),
113+
);
114+
};
115+
116+
let mut modules: Vec<Module> = vec![Module {
117+
source: root_source,
118+
parsed_program: root_program.clone(),
119+
}];
120+
121+
let mut lookup: HashMap<CanonPath, usize> = HashMap::new();
122+
let mut paths: Vec<CanonPath> = vec![root_name.clone()];
123+
let mut dependencies: HashMap<usize, Vec<usize>> = HashMap::new();
124+
125+
let root_id = 0;
126+
lookup.insert(root_name, root_id);
127+
dependencies.insert(root_id, Vec::new());
128+
129+
// Implementation of the standard BFS algorithm with memoization and queue
130+
let mut queue = VecDeque::new();
131+
queue.push_back(root_id);
132+
133+
while let Some(curr_id) = queue.pop_front() {
134+
// We need this to report errors inside THIS file.
135+
let importer_source = modules[curr_id].source.clone();
136+
let importer_source_name = if let Some(name) = importer_source.name() {
137+
CanonPath::canonicalize(name)?
138+
} else {
139+
return Err(format!(
140+
"The {:?} variable inside the ProjectGraph::new() function has no name",
141+
importer_source
142+
));
143+
};
144+
145+
let current_program = &modules[curr_id].parsed_program;
146+
147+
// Lists to separate valid logic from errors
148+
let mut valid_imports: Vec<(CanonPath, Span)> = Vec::new();
149+
let mut resolution_errors: Vec<RichError> = Vec::new();
150+
151+
// PHASE 1: Resolve Imports
152+
for elem in current_program.items() {
153+
if let parse::Item::Use(use_decl) = elem {
154+
match dependency_map.resolve_path(importer_source_name.clone(), use_decl) {
155+
Ok(path) => valid_imports.push((path, *use_decl.span())),
156+
Err(err) => {
157+
resolution_errors.push(err.with_source(importer_source.clone()))
158+
}
159+
}
160+
}
161+
}
162+
163+
// PHASE 2: Load and Parse Dependencies
164+
for (path, import_span) in valid_imports {
165+
if let Some(&existing_id) = lookup.get(&path) {
166+
let deps = dependencies.entry(curr_id).or_default();
167+
if !deps.contains(&existing_id) {
168+
deps.push(existing_id);
169+
}
170+
continue;
171+
}
172+
173+
let Some(module) = ProjectGraph::parse_and_get_program(
174+
&path,
175+
importer_source.clone(),
176+
import_span,
177+
handler,
178+
) else {
179+
continue;
180+
};
181+
182+
let last_ind = modules.len();
183+
modules.push(module);
184+
185+
lookup.insert(path.clone(), last_ind);
186+
paths.push(path);
187+
dependencies.entry(curr_id).or_default().push(last_ind);
188+
189+
queue.push_back(last_ind);
190+
}
191+
}
192+
193+
Ok(if handler.has_errors() {
194+
None
195+
} else {
196+
Some(Self {
197+
modules,
198+
dependency_map,
199+
lookup,
200+
paths: paths.into(),
201+
dependencies,
202+
})
203+
})
204+
}
205+
}

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 {

src/parse.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,10 @@ pub trait ParseFromStr: Sized {
10331033
/// Trait for parsing with collection of errors.
10341034
pub trait ParseFromStrWithErrors: Sized {
10351035
/// Parse a value from the string `s` with Errors.
1036-
fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option<Self>;
1036+
fn parse_from_str_with_errors(
1037+
source: &SourceFile,
1038+
handler: &mut ErrorCollector,
1039+
) -> Option<Self>;
10371040
}
10381041

10391042
/// Trait for generating parsers of themselves.
@@ -1077,10 +1080,13 @@ impl<A: ChumskyParse + std::fmt::Debug> ParseFromStr for A {
10771080
}
10781081

10791082
impl<A: ChumskyParse + std::fmt::Debug> ParseFromStrWithErrors for A {
1080-
fn parse_from_str_with_errors(s: &str, handler: &mut ErrorCollector) -> Option<Self> {
1083+
fn parse_from_str_with_errors(
1084+
source: &SourceFile,
1085+
handler: &mut ErrorCollector,
1086+
) -> Option<Self> {
1087+
let s = &source.content().to_string();
10811088
let (tokens, lex_errs) = crate::lexer::lex(s);
10821089

1083-
let source = SourceFile::anonymous(Arc::from(s));
10841090
handler.extend(source.clone(), lex_errs);
10851091
let tokens = tokens?;
10861092

@@ -1093,7 +1099,7 @@ impl<A: ChumskyParse + std::fmt::Debug> ParseFromStrWithErrors for A {
10931099
)
10941100
.into_output_errors();
10951101

1096-
handler.extend(source, parse_errs);
1102+
handler.extend(source.clone(), parse_errs);
10971103

10981104
// TODO: We should return parsed result if we found errors, but because analyzing in `ast` module
10991105
// is not handling poisoned tree right now, we don't return parsed result
@@ -2428,8 +2434,9 @@ mod test {
24282434
#[test]
24292435
fn test_double_colon() {
24302436
let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }";
2437+
let source = SourceFile::anonymous(Arc::from(input));
24312438
let mut error_handler = ErrorCollector::new();
2432-
let parse_program = Program::parse_from_str_with_errors(input, &mut error_handler);
2439+
let parse_program = Program::parse_from_str_with_errors(&source, &mut error_handler);
24332440

24342441
assert!(parse_program.is_none());
24352442
assert!(ErrorCollector::to_string(&error_handler).contains("Expected '::', found ':'"));
@@ -2438,8 +2445,9 @@ mod test {
24382445
#[test]
24392446
fn test_double_double_colon() {
24402447
let input = "fn main() { let pk: Pubkey = witnes::::PK; }";
2448+
let source = SourceFile::anonymous(Arc::from(input));
24412449
let mut error_handler = ErrorCollector::new();
2442-
let parse_program = Program::parse_from_str_with_errors(input, &mut error_handler);
2450+
let parse_program = Program::parse_from_str_with_errors(&source, &mut error_handler);
24432451

24442452
assert!(parse_program.is_none());
24452453
assert!(ErrorCollector::to_string(&error_handler).contains("Expected ';', found '::'"));

src/resolution.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,15 @@ use crate::parse::UseDecl;
88
/// Powers error reporting by mapping compiler diagnostics to the specific file.
99
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
1010
pub struct SourceFile {
11-
/// The name or path of the source file (e.g., "./simf/main.simf").
11+
/// The path of the source file (e.g., "./src/main.simf").
1212
name: Option<Arc<Path>>,
1313
/// The actual text content of the source file.
1414
content: Arc<str>,
1515
}
1616

1717
impl From<(&Path, &str)> for SourceFile {
1818
fn from((name, content): (&Path, &str)) -> Self {
19-
Self {
20-
name: Some(Arc::from(name)),
21-
content: Arc::from(content),
22-
}
19+
Self::new(name, Arc::from(content))
2320
}
2421
}
2522

0 commit comments

Comments
 (0)