diff --git a/Cargo.lock b/Cargo.lock index 9ec40a63..fd9ce8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,7 @@ dependencies = [ "pallas-crypto", "pallas-primitives", "pallas-traverse", + "petgraph", "regex", "serde", "serde_json", @@ -302,6 +303,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flat-rs" version = "0.0.21" @@ -704,6 +711,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f" +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "ppv-lite86" version = "0.2.16" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9d95fe93..19e34dc7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -27,3 +27,4 @@ ignore = "0.4.18" regex = "1.6.0" miette = { version = "5.3.0", features = ["fancy"] } thiserror = "1.0.37" +petgraph = "0.6.2" diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index e56b64b1..ce7ae86e 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -10,8 +10,16 @@ use miette::{EyreContext, LabeledSpan, MietteHandlerOpts, RgbColors, SourceCode} #[allow(dead_code)] #[derive(thiserror::Error)] pub enum Error { + #[error("duplicate module {module}")] + DuplicateModule { + module: String, + first: PathBuf, + second: PathBuf, + }, #[error("file operation failed")] - FileIo { path: PathBuf, error: io::Error }, + FileIo { error: io::Error, path: PathBuf }, + #[error("cyclical module imports")] + ImportCycle { modules: Vec }, #[error("failed to parse")] Parse { path: PathBuf, @@ -48,32 +56,47 @@ impl Debug for Error { impl miette::Diagnostic for Error { fn code<'a>(&'a self) -> Option> { match self { - Error::Parse { .. } => Some(Box::new("aiken::parser".to_string())), - Error::FileIo { .. } => None, - Error::List(_) => None, - } - } - - fn source_code(&self) -> Option<&dyn SourceCode> { - match self { - Error::Parse { src, .. } => Some(src), - Error::FileIo { .. } => None, - Error::List(_) => None, - } - } - - fn labels(&self) -> Option + '_>> { - match self { - Error::Parse { error, .. } => error.labels(), + Error::DuplicateModule { .. } => Some(Box::new("aiken::project::duplicate_module")), Error::FileIo { .. } => None, + Error::ImportCycle { .. } => Some(Box::new("aiken::project::cyclical_import")), + Error::Parse { .. } => Some(Box::new("aiken::parser")), Error::List(_) => None, } } fn help<'a>(&'a self) -> Option> { match self { - Error::Parse { error, .. } => error.kind.help(), + Error::DuplicateModule { first, second, .. } => Some(Box::new(format!( + "rename either {} or {}", + first.display(), + second.display() + ))), Error::FileIo { .. } => None, + Error::ImportCycle { modules } => Some(Box::new(format!( + "try moving the shared code to a separate module that the others can depend on\n- {}", + modules.join("\n- ") + ))), + Error::Parse { error, .. } => error.kind.help(), + Error::List(_) => None, + } + } + + fn labels(&self) -> Option + '_>> { + match self { + Error::DuplicateModule { .. } => None, + Error::FileIo { .. } => None, + Error::ImportCycle { .. } => None, + Error::Parse { error, .. } => error.labels(), + Error::List(_) => None, + } + } + + fn source_code(&self) -> Option<&dyn SourceCode> { + match self { + Error::DuplicateModule { .. } => None, + Error::FileIo { .. } => None, + Error::ImportCycle { .. } => None, + Error::Parse { src, .. } => Some(src), Error::List(_) => None, } } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 37fc93f1..77ec66a1 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,3 +1,4 @@ pub mod config; pub mod error; +pub mod module; pub mod project; diff --git a/crates/cli/src/module.rs b/crates/cli/src/module.rs new file mode 100644 index 00000000..cd814f9d --- /dev/null +++ b/crates/cli/src/module.rs @@ -0,0 +1,151 @@ +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, + path::PathBuf, +}; + +use aiken_lang::ast::{ModuleKind, UntypedModule}; +use petgraph::{algo, graph::NodeIndex, Direction, Graph}; + +use crate::error::Error; + +#[derive(Debug)] +#[allow(dead_code)] +pub struct ParsedModule { + pub path: PathBuf, + pub name: String, + pub code: String, + pub kind: ModuleKind, + pub package: String, + pub ast: UntypedModule, + // extra: ModuleExtra, +} + +impl ParsedModule { + pub fn deps_for_graph(&self) -> (String, Vec) { + let name = self.name.clone(); + + let deps: Vec<_> = self + .ast + .dependencies() + .into_iter() + .map(|(dep, _span)| dep) + .collect(); + + (name, deps) + } +} + +pub struct ParsedModules(HashMap); + +impl ParsedModules { + pub fn sequence(&self) -> Result, Error> { + let inputs = self + .0 + .values() + .map(|m| m.deps_for_graph()) + .collect::)>>(); + + let capacity = inputs.len(); + + let mut graph = Graph::<(), ()>::with_capacity(capacity, capacity * 5); + + // TODO: maybe use a bimap? + let mut indices = HashMap::with_capacity(capacity); + let mut values = HashMap::with_capacity(capacity); + + for (value, _) in &inputs { + let index = graph.add_node(()); + + indices.insert(value.clone(), index); + + values.insert(index, value.clone()); + } + + for (value, deps) in inputs { + if let Some(from_index) = indices.get(&value) { + let deps = deps.into_iter().filter_map(|dep| indices.get(&dep)); + + for to_index in deps { + graph.add_edge(*from_index, *to_index, ()); + } + } + } + + match algo::toposort(&graph, None) { + Ok(sequence) => { + let sequence = sequence + .iter() + .filter_map(|i| values.remove(i)) + .rev() + .collect(); + + Ok(sequence) + } + Err(cycle) => { + let origin = cycle.node_id(); + + let mut path = vec![]; + + find_cycle(origin, origin, &graph, &mut path, &mut HashSet::new()); + + let modules = path + .iter() + .filter_map(|index| values.remove(index)) + .collect(); + + Err(Error::ImportCycle { modules }) + } + } + } +} + +impl From> for ParsedModules { + fn from(parsed_modules: HashMap) -> Self { + ParsedModules(parsed_modules) + } +} + +impl From for HashMap { + fn from(parsed_modules: ParsedModules) -> Self { + parsed_modules.0 + } +} + +impl Deref for ParsedModules { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn find_cycle( + origin: NodeIndex, + parent: NodeIndex, + graph: &petgraph::Graph<(), ()>, + path: &mut Vec, + seen: &mut HashSet, +) -> bool { + seen.insert(parent); + + for node in graph.neighbors_directed(parent, Direction::Outgoing) { + if node == origin { + path.push(node); + + return true; + } + + if seen.contains(&node) { + continue; + } + + if find_cycle(origin, node, graph, path, seen) { + path.push(node); + + return true; + } + } + + false +} diff --git a/crates/cli/src/project.rs b/crates/cli/src/project.rs index 059f3e43..b1539406 100644 --- a/crates/cli/src/project.rs +++ b/crates/cli/src/project.rs @@ -4,9 +4,13 @@ use std::{ path::{Path, PathBuf}, }; -use aiken_lang::ast::{ModuleKind, UntypedModule}; +use aiken_lang::ast::ModuleKind; -use crate::{config::Config, error::Error}; +use crate::{ + config::Config, + error::Error, + module::{ParsedModule, ParsedModules}, +}; #[derive(Debug)] pub struct Source { @@ -16,22 +20,11 @@ pub struct Source { pub kind: ModuleKind, } -#[derive(Debug)] -#[allow(dead_code)] -struct ParsedModule { - path: PathBuf, - name: String, - code: String, - kind: ModuleKind, - package: String, - ast: UntypedModule, - // extra: ModuleExtra, -} - pub struct Project { config: Config, root: PathBuf, sources: Vec, + defined_modules: HashMap, } impl Project { @@ -40,18 +33,21 @@ impl Project { config, root, sources: vec![], + defined_modules: HashMap::new(), } } pub fn build(&mut self) -> Result<(), Error> { self.read_source_files()?; - self.parse_sources()?; + let parsed_modules = self.parse_sources()?; + + let processing_sequence = parsed_modules.sequence()?; Ok(()) } - fn parse_sources(&mut self) -> Result, Error> { + fn parse_sources(&mut self) -> Result { let mut errors = Vec::new(); let mut parsed_modules = HashMap::with_capacity(self.sources.len()); @@ -73,10 +69,21 @@ impl Project { code, name, path, - package: "".to_string(), + package: self.config.name.clone(), }; - let _ = parsed_modules.insert(module.name.clone(), module); + if let Some(first) = self + .defined_modules + .insert(module.name.clone(), module.path.clone()) + { + return Err(Error::DuplicateModule { + module: module.name.clone(), + first, + second: module.path, + }); + } + + parsed_modules.insert(module.name.clone(), module); } Err(errs) => { for error in errs { @@ -91,7 +98,7 @@ impl Project { } if errors.is_empty() { - Ok(parsed_modules) + Ok(parsed_modules.into()) } else { Err(Error::List(errors)) } diff --git a/crates/lang/src/ast.rs b/crates/lang/src/ast.rs index 99cd267b..94118902 100644 --- a/crates/lang/src/ast.rs +++ b/crates/lang/src/ast.rs @@ -37,6 +37,31 @@ pub struct Module { pub kind: ModuleKind, } +impl UntypedModule { + pub fn dependencies(&self) -> Vec<(String, Span)> { + self.definitions() + .flat_map(|def| { + if let Definition::Use { + location, module, .. + } = def + { + Some((module.join("/"), *location)) + } else { + None + } + }) + .collect() + } + + pub fn definitions(&self) -> impl Iterator { + self.definitions.iter() + } + + pub fn into_definitions(self) -> impl Iterator { + self.definitions.into_iter() + } +} + pub type TypedDefinition = Definition, TypedExpr, String, String>; pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>; @@ -500,7 +525,7 @@ pub struct Span { impl From for miette::SourceSpan { fn from(span: Span) -> Self { - Self::new(span.start.into(), span.end.into()) + Self::new(span.start.into(), (span.end - span.start).into()) } }