Skip to content

Commit 4b0b834

Browse files
committed
feat: add linearize and build functionality to the driver, fix Error enum a bit
1 parent 9ca524c commit 4b0b834

4 files changed

Lines changed: 335 additions & 24 deletions

File tree

src/driver/linearization.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
//! Because we do not need to enforce strict local precedence, a standard post-order
77
//! DFS is a better option.
88
9-
// TODO: Remove this once the code is actively used.
10-
#![allow(dead_code)]
119
use std::collections::HashSet;
1210
use std::fmt;
1311

@@ -16,7 +14,7 @@ use crate::driver::DependencyGraph;
1614
/// This is a core component of the [`DependencyGraph`].
1715
impl DependencyGraph {
1816
/// Returns the deterministic, BOTTOM-UP load order of dependencies.
19-
pub fn linearize(&self) -> Result<Vec<usize>, LinearizationError> {
17+
pub(crate) fn linearize(&self) -> Result<Vec<usize>, LinearizationError> {
2018
let mut visited = HashSet::new();
2119
let mut visiting = Vec::new();
2220
let mut order = Vec::new();

src/driver/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,34 @@
1+
//! The `driver` module is responsible for module resolution and dependency management.
2+
//!
3+
//! Our compiler operates in a strict pipeline: `Lexer -> Parser -> Driver -> AST`.
4+
//! While the Parser only understands a single file at a time, the Driver processes
5+
//! multiple files, resolves their dependencies, and converts them into a unified
6+
//! structure ready for final AST construction.
7+
//!
8+
//! # Architecture
9+
//!
10+
//! ## Dependency Graph & Linearization
11+
//!
12+
//! The driver parses the root file and recursively discovers all imported modules
13+
//! to build a Directed Acyclic Graph (DAG) of the project's dependencies. Because
14+
//! the final AST requires a flat array of items, the driver applies a deterministic
15+
//! linearization strategy to this DAG. This safely flattens the multi-file project
16+
//! into a single, logically ordered sequence, strictly enforcing visibility rules
17+
//! and preventing duplicate imports.
18+
//!
19+
//! ## Project Structure & Entry Point
20+
//!
21+
//! SimplicityHL does not define a "project root" directory. Instead, the compiler
22+
//! relies on a single entry point: the file passed as the first positional argument.
23+
//! This file must contain the `main` function, which serves as the program's
24+
//! starting point.
25+
//!
26+
//! External libraries are explicitly linked using the `--dep` flag. The driver
27+
//! resolves and parses these external files relative to the entry point during
28+
//! the dependency graph construction.
29+
130
mod linearization;
31+
mod resolve_order;
232

333
use std::collections::{HashMap, HashSet, VecDeque};
434
use std::path::PathBuf;

src/driver/resolve_order.rs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
use std::collections::{BTreeSet, HashMap};
2+
use std::sync::Arc;
3+
4+
use crate::driver::DependencyGraph;
5+
use crate::error::{Error, ErrorCollector, RichError, Span};
6+
use crate::impl_eq_hash;
7+
use crate::parse::{self, Visibility};
8+
use crate::resolution::CanonPath;
9+
10+
/// The final, flattened representation of a SimplicityHL program.
11+
///
12+
/// This struct holds the fully resolved sequence of items, paths, and scope
13+
/// resolutions, ready to be passed to the next stage of the compiler.
14+
#[derive(Clone, Debug)]
15+
pub struct Program {
16+
/// The linear sequence of compiled items (`Functions`, `TypeAliases`, etc.).
17+
items: Arc<[parse::Item]>,
18+
19+
/// The files that make up this program, along with their scoping rules.
20+
files: Arc<[ResolvedFile]>,
21+
22+
span: Span,
23+
}
24+
25+
impl Program {
26+
pub fn items(&self) -> &[parse::Item] {
27+
&self.items
28+
}
29+
30+
pub fn files(&self) -> &[ResolvedFile] {
31+
&self.files
32+
}
33+
34+
pub fn span(&self) -> &Span {
35+
&self.span
36+
}
37+
}
38+
39+
impl_eq_hash!(Program; items, files);
40+
41+
/// Represents a single source file alongside its resolved scoping and visibility rules.
42+
#[derive(Clone, Debug)]
43+
pub struct ResolvedFile {
44+
path: CanonPath,
45+
46+
/// The set of resolved item names available within this file's scope.
47+
// Use BTreeSet instead of HashMap for the impl_eq_hash! macro.
48+
resolutions: BTreeSet<Arc<str>>,
49+
}
50+
51+
impl ResolvedFile {
52+
pub fn path(&self) -> &CanonPath {
53+
&self.path
54+
}
55+
56+
pub fn resolutions(&self) -> &BTreeSet<Arc<str>> {
57+
&self.resolutions
58+
}
59+
}
60+
61+
impl_eq_hash!(ResolvedFile; path, resolutions);
62+
63+
/// This is a core component of the [`DependencyGraph`].
64+
impl DependencyGraph {
65+
/// Resolves the dependency graph and constructs the final AST program.
66+
pub fn linearize_and_build(
67+
&self,
68+
handler: &mut ErrorCollector,
69+
) -> Result<Option<Program>, String> {
70+
match self.linearize() {
71+
Ok(order) => Ok(self.build_program(&order, handler)),
72+
Err(err) => Err(err.to_string()),
73+
}
74+
}
75+
76+
/// Constructs the unified AST for the entire program.
77+
fn build_program(&self, order: &[usize], handler: &mut ErrorCollector) -> Option<Program> {
78+
let mut items: Vec<parse::Item> = Vec::new();
79+
let mut resolutions: Vec<HashMap<Arc<str>, Visibility>> =
80+
vec![HashMap::new(); self.modules.len()];
81+
82+
for &source_id in order {
83+
let module = &self.modules[source_id];
84+
let source = &module.source;
85+
86+
for elem in module.parsed_program.items() {
87+
// Handle Uses (Early Continue flattens the nesting)
88+
if let parse::Item::Use(use_decl) = elem {
89+
let resolve_path =
90+
match self.dependency_map.resolve_path(source.name(), use_decl) {
91+
Ok(path) => path,
92+
Err(err) => {
93+
handler.push(err.with_source(source.clone()));
94+
continue;
95+
}
96+
};
97+
98+
let ind = self.lookup[&resolve_path];
99+
let use_decl_items = match use_decl.items() {
100+
parse::UseItems::Single(elem) => std::slice::from_ref(elem),
101+
parse::UseItems::List(elems) => elems.as_slice(),
102+
};
103+
104+
for item in use_decl_items {
105+
if let Err(err) = Self::process_use_item(
106+
&mut resolutions,
107+
source_id,
108+
ind,
109+
Arc::from(item.as_inner()),
110+
use_decl,
111+
) {
112+
handler.push(err.with_source(source.clone()));
113+
}
114+
}
115+
continue;
116+
}
117+
118+
// Handle Types & Functions
119+
let (name, vis) = match elem {
120+
parse::Item::TypeAlias(a) => (a.name().as_inner(), a.visibility()),
121+
parse::Item::Function(f) => (f.name().as_inner(), f.visibility()),
122+
123+
// Safe to skip: `Use` items are handled earlier in the loop, and `Module` currently has no functionality.
124+
parse::Item::Module | parse::Item::Use(_) => continue,
125+
};
126+
127+
items.push(elem.clone());
128+
resolutions[source_id].insert(Arc::from(name), vis.clone());
129+
}
130+
}
131+
132+
(!handler.has_errors()).then(|| Program {
133+
items: items.into(),
134+
files: construct_resolved_file_array(&self.paths, &resolutions),
135+
span: *self.modules[0].parsed_program.as_ref(),
136+
})
137+
}
138+
139+
/// Processes a single imported item during the module resolution phase.
140+
///
141+
/// # Arguments
142+
///
143+
/// * `resolutions` - A mutable slice of hash maps, where each index corresponds to a module's ID and holds its resolved items and their visibilities.
144+
/// * `source_id` - The `usize` identifier of the destination source.
145+
/// * `ind` - The unique identifier (`usize`) of the source module being imported *from*.
146+
/// * `name` - The specific item name (`Arc<str>`) being imported from the source.
147+
/// * `use_decl` - The AST node of the `use` statement. This dictates the visibility of the newly imported item in the destination module.
148+
///
149+
/// # Returns
150+
///
151+
/// Returns `None` on success. Returns `Some(RichError)` if:
152+
/// * [`Error::UnresolvedItem`]: The target `name` does not exist in the source module (`ind`).
153+
/// * [`Error::PrivateItem`]: The target exists in the source module, but its visibility is expl
154+
fn process_use_item(
155+
resolutions: &mut [HashMap<Arc<str>, Visibility>],
156+
source_id: usize,
157+
ind: usize,
158+
name: Arc<str>,
159+
use_decl: &parse::UseDecl,
160+
) -> Result<(), RichError> {
161+
let span = *use_decl.span();
162+
163+
let visibility = resolutions[ind]
164+
.get(&name)
165+
.ok_or_else(|| RichError::new(Error::UnresolvedItem(name.to_string()), span))?;
166+
167+
if matches!(visibility, parse::Visibility::Private) {
168+
return Err(RichError::new(Error::PrivateItem(name.to_string()), span));
169+
}
170+
171+
resolutions[source_id].insert(name, use_decl.visibility().clone());
172+
Ok(())
173+
}
174+
}
175+
176+
fn construct_resolved_file_array(
177+
paths: &[CanonPath],
178+
resolutions: &[HashMap<Arc<str>, Visibility>],
179+
) -> Arc<[ResolvedFile]> {
180+
let mut result = Vec::with_capacity(paths.len());
181+
182+
for i in 0..paths.len() {
183+
let file_resolutions: BTreeSet<Arc<str>> = resolutions[i].keys().cloned().collect();
184+
185+
result.push(ResolvedFile {
186+
path: paths[i].clone(),
187+
resolutions: file_resolutions,
188+
});
189+
}
190+
191+
result.into()
192+
}
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use crate::driver::tests::setup_graph;
197+
198+
use super::*;
199+
200+
#[test]
201+
fn test_local_definitions_visibility() {
202+
// main.simf defines a private function and a public function.
203+
// Expected: Both should appear in the scope with correct visibility.
204+
205+
let (graph, ids, _dir) = setup_graph(vec![(
206+
"main.simf",
207+
"fn private_fn() {} pub fn public_fn() {}",
208+
)]);
209+
210+
let mut error_handler = ErrorCollector::new();
211+
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
212+
213+
let Some(program) = program_option else {
214+
panic!("{}", error_handler);
215+
};
216+
217+
let root_id = ids["main"];
218+
let resolutions = &program.files[root_id].resolutions;
219+
220+
resolutions
221+
.get(&Arc::from("private_fn"))
222+
.expect("private_fn missing");
223+
224+
resolutions
225+
.get(&Arc::from("public_fn"))
226+
.expect("public_fn missing");
227+
}
228+
229+
#[test]
230+
fn test_pub_use_propagation() {
231+
// Scenario: Re-exporting.
232+
// 1. A.simf defines `pub fn foo`.
233+
// 2. B.simf imports it and re-exports it via `pub use`.
234+
// 3. main.simf imports it from B.
235+
// Expected: B's scope must contain `foo` marked as Public.
236+
237+
let (graph, ids, _dir) = setup_graph(vec![
238+
("libs/lib/A.simf", "pub fn foo() {}"),
239+
("libs/lib/B.simf", "pub use lib::A::foo;"),
240+
("main.simf", "use lib::B::foo;"),
241+
]);
242+
243+
let mut error_handler = ErrorCollector::new();
244+
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
245+
246+
let Some(program) = program_option else {
247+
panic!("{}", error_handler);
248+
};
249+
250+
let id_b = ids["B"];
251+
let id_root = ids["main"];
252+
253+
// Check B's scope
254+
program.files[id_b]
255+
.resolutions
256+
.get(&Arc::from("foo"))
257+
.expect("foo missing in B");
258+
259+
// Check Root's scope
260+
program.files[id_root]
261+
.resolutions
262+
.get(&Arc::from("foo"))
263+
.expect("foo missing in Root");
264+
}
265+
266+
#[test]
267+
fn test_private_import_encapsulation_error() {
268+
// Scenario: Access violation.
269+
// 1. A.simf defines `pub fn foo`.
270+
// 2. B.simf imports it via `use` (Private import).
271+
// 3. main.simf tries to import `foo` from B.
272+
// Expected: Error, because B did not re-export foo.
273+
274+
let (graph, _ids, _dir) = setup_graph(vec![
275+
("libs/lib/A.simf", "pub fn foo() {}"),
276+
("libs/lib/B.simf", "use lib::A::foo;"), // <--- Private binding!
277+
("main.simf", "use lib::B::foo;"), // <--- Should fail
278+
]);
279+
280+
let mut error_handler = ErrorCollector::new();
281+
let program_option = graph.linearize_and_build(&mut error_handler).unwrap();
282+
283+
assert!(
284+
program_option.is_none(),
285+
"Build should fail and return None when importing a private binding"
286+
);
287+
assert!(error_handler
288+
.to_string()
289+
.contains(&"Item `foo` is private".to_string()));
290+
}
291+
}

0 commit comments

Comments
 (0)