feat: sort modules and detect cycles
This commit is contained in:
parent
15c774b7d0
commit
ed2ef4fa9b
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod module;
|
||||
pub mod project;
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue