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

17
Cargo.lock generated
View File

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

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

View File

@ -37,6 +37,31 @@ pub struct Module<Info, Definitions> {
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<Item = &UntypedDefinition> {
self.definitions.iter()
}
pub fn into_definitions(self) -> impl Iterator<Item = UntypedDefinition> {
self.definitions.into_iter()
}
}
pub type TypedDefinition = Definition<Arc<Type>, TypedExpr, String, String>;
pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>;
@ -500,7 +525,7 @@ pub struct Span {
impl From<Span> 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())
}
}