-
Notifications
You must be signed in to change notification settings - Fork 33
Implement ProjectGraph and foundational driver architecture
#271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LesterEvSe
wants to merge
1
commit into
BlockstreamResearch:dev/imports
Choose a base branch
from
LesterEvSe:feature/simple-driver
base: dev/imports
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+226
−12
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| use std::collections::{HashMap, VecDeque}; | ||
| use std::path::PathBuf; | ||
| use std::sync::Arc; | ||
|
|
||
| use crate::error::{Error, ErrorCollector, RichError, Span}; | ||
| use crate::parse::{self, ParseFromStrWithErrors}; | ||
| use crate::resolution::{CanonPath, DependencyMap, SourceFile}; | ||
|
|
||
| /// Represents a single, isolated file in the SimplicityHL project. | ||
| /// In this architecture, a file and a module are the exact same thing. | ||
| #[derive(Debug, Clone)] | ||
| pub struct Module { | ||
| pub source: SourceFile, | ||
| /// The completely parsed program for this specific file. | ||
| /// it contains all the functions, aliases, and imports defined inside the file. | ||
| pub parsed_program: parse::Program, | ||
| } | ||
|
|
||
| /// The Dependency Graph itself. | ||
| pub struct ProjectGraph { | ||
| /// Arena Pattern: the data itself lives here. | ||
| /// A flat vector guarantees that module data is stored contiguously in memory. | ||
| #[expect(dead_code)] | ||
| pub(self) modules: Vec<Module>, | ||
|
|
||
| /// The configuration environment. | ||
| /// Used to resolve external library dependencies and invoke their associated functions. | ||
| pub dependency_map: Arc<DependencyMap>, | ||
|
|
||
| /// Fast lookup: `CanonPath` -> Module ID. | ||
| /// A reverse index mapping absolute file paths to their internal IDs. | ||
| /// This solves the duplication problem, ensuring each file is only parsed once. | ||
| pub lookup: HashMap<CanonPath, usize>, | ||
|
|
||
| /// Fast lookup: Module ID -> `CanonPath`. | ||
| /// A direct index mapping internal IDs back to their absolute file paths. | ||
| /// This serves as the exact inverse of the `lookup` map. | ||
| pub paths: Arc<[CanonPath]>, | ||
|
|
||
| /// The Adjacency List: Defines the Directed acyclic Graph (DAG) of imports. | ||
| /// | ||
| /// The Key (`usize`) is the ID of a "Parent" module (the file doing the importing). | ||
| /// The Value (`Vec<usize>`) is a list of IDs of the "Child" modules it relies on. | ||
| /// | ||
| /// Example: If `main.simf` (ID: 0) has `use lib::math;` (ID: 1) and `use lib::io;` (ID: 2), | ||
| /// this map will contain: `{ 0: [1, 2] }`. | ||
| pub dependencies: HashMap<usize, Vec<usize>>, | ||
| } | ||
|
|
||
| impl ProjectGraph { | ||
| /// This helper cleanly encapsulates the process of loading source text, parsing it | ||
| /// into an `parse::Program`, and combining them so the compiler can easily work with the file. | ||
| /// If the file is missing or contains syntax errors, it logs the diagnostic to the | ||
| /// `ErrorCollector` and safely returns `None`. | ||
| fn parse_and_get_program( | ||
| path: &CanonPath, | ||
| importer_source: SourceFile, | ||
| span: Span, | ||
| handler: &mut ErrorCollector, | ||
| ) -> Option<Module> { | ||
| let Ok(content) = std::fs::read_to_string(path.as_path()) else { | ||
| let err = RichError::new(Error::FileNotFound(PathBuf::from(path.as_path())), span) | ||
| .with_source(importer_source.clone()); | ||
|
|
||
| handler.push(err); | ||
| return None; | ||
| }; | ||
|
|
||
| let dep_source_file = SourceFile::new(path.as_path(), Arc::from(content.clone())); | ||
|
|
||
| parse::Program::parse_from_str_with_errors(&dep_source_file, handler).map( | ||
| |parsed_program| Module { | ||
| source: dep_source_file, | ||
| parsed_program, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| /// Initializes a new `ProjectGraph` by parsing the root program and discovering all dependencies. | ||
| /// | ||
| /// Performs a BFS to recursively parse `use` statements, | ||
| /// building a DAG of the project's modules. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `root_source` - The `SourceFile` representing the entry point of the project. | ||
| /// * `dependency_map` - The context-aware mapping rules used to resolve external imports. | ||
| /// * `root_program` - A reference to the already-parsed AST of the root file. | ||
| /// * `handler` - The diagnostics collector used to record resolution and parsing errors. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// * `Ok(Some(Self))` - If the entire project graph was successfully resolved and parsed. | ||
| /// * `Ok(None)` - If the graph traversal completed, but one or more modules contained | ||
| /// errors (which have been safely logged into the `handler`). | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// This function will return an `Err(String)` only for critical internal compiler errors | ||
| /// (e.g., if a provided `SourceFile` is unexpectedly missing its underlying file path). | ||
| pub fn new( | ||
| root_source: SourceFile, | ||
| dependency_map: Arc<DependencyMap>, | ||
| root_program: &parse::Program, | ||
| handler: &mut ErrorCollector, | ||
| ) -> Result<Option<Self>, String> { | ||
| let root_name = if let Some(root_name) = root_source.name() { | ||
| CanonPath::canonicalize(root_name)? | ||
| } else { | ||
| return Err( | ||
| "The root_source variable inside the ProjectGraph::new() function has no name" | ||
| .to_string(), | ||
| ); | ||
| }; | ||
|
|
||
| let mut modules: Vec<Module> = vec![Module { | ||
| source: root_source, | ||
| parsed_program: root_program.clone(), | ||
| }]; | ||
|
|
||
| let mut lookup: HashMap<CanonPath, usize> = HashMap::new(); | ||
| let mut paths: Vec<CanonPath> = vec![root_name.clone()]; | ||
| let mut dependencies: HashMap<usize, Vec<usize>> = HashMap::new(); | ||
|
|
||
| let root_id = 0; | ||
| lookup.insert(root_name, root_id); | ||
| dependencies.insert(root_id, Vec::new()); | ||
|
|
||
| // Implementation of the standard BFS algorithm with memoization and queue | ||
| let mut queue = VecDeque::new(); | ||
| queue.push_back(root_id); | ||
|
|
||
| while let Some(curr_id) = queue.pop_front() { | ||
| // We need this to report errors inside THIS file. | ||
| let importer_source = modules[curr_id].source.clone(); | ||
| let importer_source_name = if let Some(name) = importer_source.name() { | ||
| CanonPath::canonicalize(name)? | ||
| } else { | ||
| return Err(format!( | ||
| "The {:?} variable inside the ProjectGraph::new() function has no name", | ||
| importer_source | ||
| )); | ||
| }; | ||
|
|
||
| let current_program = &modules[curr_id].parsed_program; | ||
|
|
||
| // Lists to separate valid logic from errors | ||
| let mut valid_imports: Vec<(CanonPath, Span)> = Vec::new(); | ||
| let mut resolution_errors: Vec<RichError> = Vec::new(); | ||
|
|
||
| // PHASE 1: Resolve Imports | ||
| for elem in current_program.items() { | ||
| if let parse::Item::Use(use_decl) = elem { | ||
| match dependency_map.resolve_path(importer_source_name.clone(), use_decl) { | ||
| Ok(path) => valid_imports.push((path, *use_decl.span())), | ||
| Err(err) => { | ||
| resolution_errors.push(err.with_source(importer_source.clone())) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // PHASE 2: Load and Parse Dependencies | ||
| for (path, import_span) in valid_imports { | ||
| if let Some(&existing_id) = lookup.get(&path) { | ||
| let deps = dependencies.entry(curr_id).or_default(); | ||
| if !deps.contains(&existing_id) { | ||
| deps.push(existing_id); | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| let Some(module) = ProjectGraph::parse_and_get_program( | ||
| &path, | ||
| importer_source.clone(), | ||
| import_span, | ||
| handler, | ||
| ) else { | ||
| continue; | ||
| }; | ||
|
|
||
| let last_ind = modules.len(); | ||
| modules.push(module); | ||
|
|
||
| lookup.insert(path.clone(), last_ind); | ||
| paths.push(path); | ||
| dependencies.entry(curr_id).or_default().push(last_ind); | ||
|
|
||
| queue.push_back(last_ind); | ||
| } | ||
| } | ||
|
|
||
| Ok(if handler.has_errors() { | ||
| None | ||
| } else { | ||
| Some(Self { | ||
| modules, | ||
| dependency_map, | ||
| lookup, | ||
| paths: paths.into(), | ||
| dependencies, | ||
| }) | ||
| }) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.