feat: sort modules and detect cycles

This commit is contained in:
rvcas
2022-10-13 15:29:08 -04:00
committed by Lucas
parent 15c774b7d0
commit ed2ef4fa9b
7 changed files with 264 additions and 39 deletions

View File

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

View File

@@ -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<String> },
#[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<Box<dyn Display + 'a>> {
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<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
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<Box<dyn Display + 'a>> {
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<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
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,
}
}

View File

@@ -1,3 +1,4 @@
pub mod config;
pub mod error;
pub mod module;
pub mod project;

151
crates/cli/src/module.rs Normal file
View File

@@ -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<String>) {
let name = self.name.clone();
let deps: Vec<_> = self
.ast
.dependencies()
.into_iter()
.map(|(dep, _span)| dep)
.collect();
(name, deps)
}
}
pub struct ParsedModules(HashMap<String, ParsedModule>);
impl ParsedModules {
pub fn sequence(&self) -> Result<Vec<String>, Error> {
let inputs = self
.0
.values()
.map(|m| m.deps_for_graph())
.collect::<Vec<(String, Vec<String>)>>();
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<HashMap<String, ParsedModule>> for ParsedModules {
fn from(parsed_modules: HashMap<String, ParsedModule>) -> Self {
ParsedModules(parsed_modules)
}
}
impl From<ParsedModules> for HashMap<String, ParsedModule> {
fn from(parsed_modules: ParsedModules) -> Self {
parsed_modules.0
}
}
impl Deref for ParsedModules {
type Target = HashMap<String, ParsedModule>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn find_cycle(
origin: NodeIndex,
parent: NodeIndex,
graph: &petgraph::Graph<(), ()>,
path: &mut Vec<NodeIndex>,
seen: &mut HashSet<NodeIndex>,
) -> 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
}

View File

@@ -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<Source>,
defined_modules: HashMap<String, PathBuf>,
}
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<HashMap<String, ParsedModule>, Error> {
fn parse_sources(&mut self) -> Result<ParsedModules, Error> {
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))
}