feat: sort modules and detect cycles
This commit is contained in:
parent
15c774b7d0
commit
ed2ef4fa9b
|
@ -61,6 +61,7 @@ dependencies = [
|
||||||
"pallas-crypto",
|
"pallas-crypto",
|
||||||
"pallas-primitives",
|
"pallas-primitives",
|
||||||
"pallas-traverse",
|
"pallas-traverse",
|
||||||
|
"petgraph",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -302,6 +303,12 @@ dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flat-rs"
|
name = "flat-rs"
|
||||||
version = "0.0.21"
|
version = "0.0.21"
|
||||||
|
@ -704,6 +711,16 @@ version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9b0efd3ba03c3a409d44d60425f279ec442bcf0b9e63ff4e410da31c8b0f69f"
|
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]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
|
@ -27,3 +27,4 @@ ignore = "0.4.18"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
miette = { version = "5.3.0", features = ["fancy"] }
|
miette = { version = "5.3.0", features = ["fancy"] }
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
petgraph = "0.6.2"
|
||||||
|
|
|
@ -10,8 +10,16 @@ use miette::{EyreContext, LabeledSpan, MietteHandlerOpts, RgbColors, SourceCode}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(thiserror::Error)]
|
#[derive(thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[error("duplicate module {module}")]
|
||||||
|
DuplicateModule {
|
||||||
|
module: String,
|
||||||
|
first: PathBuf,
|
||||||
|
second: PathBuf,
|
||||||
|
},
|
||||||
#[error("file operation failed")]
|
#[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")]
|
#[error("failed to parse")]
|
||||||
Parse {
|
Parse {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
@ -48,32 +56,47 @@ impl Debug for Error {
|
||||||
impl miette::Diagnostic for Error {
|
impl miette::Diagnostic for Error {
|
||||||
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||||
match self {
|
match self {
|
||||||
Error::Parse { .. } => Some(Box::new("aiken::parser".to_string())),
|
Error::DuplicateModule { .. } => Some(Box::new("aiken::project::duplicate_module")),
|
||||||
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::FileIo { .. } => None,
|
Error::FileIo { .. } => None,
|
||||||
|
Error::ImportCycle { .. } => Some(Box::new("aiken::project::cyclical_import")),
|
||||||
|
Error::Parse { .. } => Some(Box::new("aiken::parser")),
|
||||||
Error::List(_) => None,
|
Error::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||||
match self {
|
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::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,
|
Error::List(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod module;
|
||||||
pub mod project;
|
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},
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
|
@ -16,22 +20,11 @@ pub struct Source {
|
||||||
pub kind: ModuleKind,
|
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 {
|
pub struct Project {
|
||||||
config: Config,
|
config: Config,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
sources: Vec<Source>,
|
sources: Vec<Source>,
|
||||||
|
defined_modules: HashMap<String, PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
|
@ -40,18 +33,21 @@ impl Project {
|
||||||
config,
|
config,
|
||||||
root,
|
root,
|
||||||
sources: vec![],
|
sources: vec![],
|
||||||
|
defined_modules: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&mut self) -> Result<(), Error> {
|
pub fn build(&mut self) -> Result<(), Error> {
|
||||||
self.read_source_files()?;
|
self.read_source_files()?;
|
||||||
|
|
||||||
self.parse_sources()?;
|
let parsed_modules = self.parse_sources()?;
|
||||||
|
|
||||||
|
let processing_sequence = parsed_modules.sequence()?;
|
||||||
|
|
||||||
Ok(())
|
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 errors = Vec::new();
|
||||||
let mut parsed_modules = HashMap::with_capacity(self.sources.len());
|
let mut parsed_modules = HashMap::with_capacity(self.sources.len());
|
||||||
|
|
||||||
|
@ -73,10 +69,21 @@ impl Project {
|
||||||
code,
|
code,
|
||||||
name,
|
name,
|
||||||
path,
|
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) => {
|
Err(errs) => {
|
||||||
for error in errs {
|
for error in errs {
|
||||||
|
@ -91,7 +98,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
Ok(parsed_modules)
|
Ok(parsed_modules.into())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::List(errors))
|
Err(Error::List(errors))
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,31 @@ pub struct Module<Info, Definitions> {
|
||||||
pub kind: ModuleKind,
|
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 TypedDefinition = Definition<Arc<Type>, TypedExpr, String, String>;
|
||||||
pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>;
|
pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>;
|
||||||
|
|
||||||
|
@ -500,7 +525,7 @@ pub struct Span {
|
||||||
|
|
||||||
impl From<Span> for miette::SourceSpan {
|
impl From<Span> for miette::SourceSpan {
|
||||||
fn from(span: Span) -> Self {
|
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