|
| 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