Extra project utilities in their own crate.
This was currently in the 'cli' crates, but this code is pretty standalone and need not to be mixed with the rest of the cli logic. Ideally, we want the cli crate to be only a thin wrapper over functionality available from the rest of the lib crates.
This commit is contained in:
20
crates/project/Cargo.toml
Normal file
20
crates/project/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "project"
|
||||
description = "Aiken project utilities"
|
||||
version = "0.0.21"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/txpipe/aiken/crates/project"
|
||||
homepage = "https://github.com/txpipe/aiken"
|
||||
license = "Apache-2.0"
|
||||
authors = ["Lucas Rosa <x@rvcas.dev>", "Kasey White <kwhitemsg@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
aiken-lang = { path = "../lang", version = "0.0.20" }
|
||||
miette = { version = "5.3.0", features = ["fancy"] }
|
||||
petgraph = "0.6.2"
|
||||
regex = "1.6.0"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
thiserror = "1.0.37"
|
||||
toml = "0.5.9"
|
||||
walkdir = "2.3.2"
|
||||
3
crates/project/README.md
Normal file
3
crates/project/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Project
|
||||
|
||||
This crate encapsulates the code used to manage Aiken projects. See [crates/cli](../cli) for usage.
|
||||
21
crates/project/src/config.rs
Normal file
21
crates/project/src/config.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(dir: PathBuf) -> io::Result<Config> {
|
||||
let raw_config = fs::read_to_string(dir.join("aiken.toml"))?;
|
||||
|
||||
let config = toml::from_str(&raw_config).unwrap();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
198
crates/project/src/error.rs
Normal file
198
crates/project/src/error.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
io,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use aiken_lang::{error::ParseError, tipo};
|
||||
use miette::{Diagnostic, 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 { error: io::Error, path: PathBuf },
|
||||
|
||||
#[error("cyclical module imports")]
|
||||
ImportCycle { modules: Vec<String> },
|
||||
|
||||
/// Useful for returning many [`Error::Parse`] at once
|
||||
#[error("a list of errors")]
|
||||
List(Vec<Self>),
|
||||
|
||||
#[error("parsing")]
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
|
||||
src: String,
|
||||
|
||||
#[source]
|
||||
error: Box<ParseError>,
|
||||
},
|
||||
|
||||
#[error("type checking")]
|
||||
Type {
|
||||
path: PathBuf,
|
||||
src: String,
|
||||
#[source]
|
||||
error: tipo::error::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn total(&self) -> usize {
|
||||
match self {
|
||||
Error::List(errors) => errors.len(),
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report(&self) {
|
||||
match self {
|
||||
Error::List(errors) => {
|
||||
for error in errors {
|
||||
eprintln!("Error: {:?}", error)
|
||||
}
|
||||
}
|
||||
rest => eprintln!("Error: {:?}", rest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let miette_handler = MietteHandlerOpts::new()
|
||||
// For better support of terminal themes use the ANSI coloring
|
||||
.rgb_colors(RgbColors::Never)
|
||||
// If ansi support is disabled in the config disable the eye-candy
|
||||
.color(true)
|
||||
.unicode(true)
|
||||
.terminal_links(true)
|
||||
.build();
|
||||
|
||||
// Ignore error to prevent format! panics. This can happen if span points at some
|
||||
// inaccessible location, for example by calling `report_error()` with wrong working set.
|
||||
let _ = miette_handler.debug(self, f);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for Error {
|
||||
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||
match self {
|
||||
Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")),
|
||||
Error::FileIo { .. } => None,
|
||||
Error::ImportCycle { .. } => Some(Box::new("aiken::module::cyclical")),
|
||||
Error::List(_) => None,
|
||||
Error::Parse { .. } => Some(Box::new("aiken::parser")),
|
||||
Error::Type { .. } => Some(Box::new("aiken::typecheck")),
|
||||
}
|
||||
}
|
||||
|
||||
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||
match self {
|
||||
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::List(_) => None,
|
||||
Error::Parse { error, .. } => error.kind.help(),
|
||||
Error::Type { error, .. } => error.help(),
|
||||
}
|
||||
}
|
||||
|
||||
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
||||
match self {
|
||||
Error::DuplicateModule { .. } => None,
|
||||
Error::FileIo { .. } => None,
|
||||
Error::ImportCycle { .. } => None,
|
||||
Error::List(_) => None,
|
||||
Error::Parse { error, .. } => error.labels(),
|
||||
Error::Type { error, .. } => error.labels(),
|
||||
}
|
||||
}
|
||||
|
||||
fn source_code(&self) -> Option<&dyn SourceCode> {
|
||||
match self {
|
||||
Error::DuplicateModule { .. } => None,
|
||||
Error::FileIo { .. } => None,
|
||||
Error::ImportCycle { .. } => None,
|
||||
Error::List(_) => None,
|
||||
Error::Parse { src, .. } => Some(src),
|
||||
Error::Type { src, .. } => Some(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, thiserror::Error)]
|
||||
pub enum Warning {
|
||||
#[error("type checking")]
|
||||
Type {
|
||||
path: PathBuf,
|
||||
src: String,
|
||||
#[source]
|
||||
warning: tipo::error::Warning,
|
||||
},
|
||||
}
|
||||
|
||||
impl Diagnostic for Warning {
|
||||
fn source_code(&self) -> Option<&dyn SourceCode> {
|
||||
match self {
|
||||
Warning::Type { src, .. } => Some(src),
|
||||
}
|
||||
}
|
||||
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
||||
match self {
|
||||
Warning::Type { warning, .. } => warning.labels(),
|
||||
}
|
||||
}
|
||||
|
||||
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||
match self {
|
||||
Warning::Type { .. } => Some(Box::new("aiken::typecheck")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Warning {
|
||||
pub fn from_type_warning(warning: tipo::error::Warning, path: PathBuf, src: String) -> Warning {
|
||||
Warning::Type { path, warning, src }
|
||||
}
|
||||
|
||||
pub fn report(&self) {
|
||||
eprintln!("Warning: {:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Warning {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let miette_handler = MietteHandlerOpts::new()
|
||||
// For better support of terminal themes use the ANSI coloring
|
||||
.rgb_colors(RgbColors::Never)
|
||||
// If ansi support is disabled in the config disable the eye-candy
|
||||
.color(true)
|
||||
.unicode(true)
|
||||
.terminal_links(true)
|
||||
.build();
|
||||
|
||||
// Ignore error to prevent format! panics. This can happen if span points at some
|
||||
// inaccessible location, for example by calling `report_error()` with wrong working set.
|
||||
let _ = miette_handler.debug(self, f);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
299
crates/project/src/lib.rs
Normal file
299
crates/project/src/lib.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod module;
|
||||
|
||||
use aiken_lang::{ast::ModuleKind, builtins, tipo::TypeInfo, IdGenerator};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
error::{Error, Warning},
|
||||
module::{CheckedModule, ParsedModule, ParsedModules},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Source {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
pub kind: ModuleKind,
|
||||
}
|
||||
|
||||
pub struct Project {
|
||||
config: Config,
|
||||
defined_modules: HashMap<String, PathBuf>,
|
||||
id_gen: IdGenerator,
|
||||
module_types: HashMap<String, TypeInfo>,
|
||||
root: PathBuf,
|
||||
sources: Vec<Source>,
|
||||
pub warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(config: Config, root: PathBuf) -> Project {
|
||||
let id_gen = IdGenerator::new();
|
||||
|
||||
let mut module_types = HashMap::new();
|
||||
|
||||
module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
|
||||
module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
|
||||
|
||||
Project {
|
||||
config,
|
||||
defined_modules: HashMap::new(),
|
||||
id_gen,
|
||||
module_types,
|
||||
root,
|
||||
sources: vec![],
|
||||
warnings: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> Result<(), Error> {
|
||||
self.compile(true)
|
||||
}
|
||||
|
||||
pub fn check(&mut self) -> Result<(), Error> {
|
||||
self.compile(false)
|
||||
}
|
||||
|
||||
pub fn compile(&mut self, _uplc_gen: bool) -> Result<(), Error> {
|
||||
self.read_source_files()?;
|
||||
|
||||
let parsed_modules = self.parse_sources()?;
|
||||
|
||||
let processing_sequence = parsed_modules.sequence()?;
|
||||
|
||||
let _checked_modules = self.type_check(parsed_modules, processing_sequence)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_source_files(&mut self) -> Result<(), Error> {
|
||||
let lib = self.root.join(format!("src/{}", self.config.name));
|
||||
let scripts = self.root.join("src/scripts");
|
||||
|
||||
self.read_root_lib_file()?;
|
||||
self.aiken_files(&scripts, ModuleKind::Script)?;
|
||||
self.aiken_files(&lib, ModuleKind::Lib)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_root_lib_file(&mut self) -> Result<(), Error> {
|
||||
let root_lib_path = self.root.join(format!("src/{}.ak", self.config.name));
|
||||
|
||||
if root_lib_path.exists() {
|
||||
self.add_module(root_lib_path, &self.root.join("src"), ModuleKind::Lib)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_sources(&mut self) -> Result<ParsedModules, Error> {
|
||||
let mut errors = Vec::new();
|
||||
let mut parsed_modules = HashMap::with_capacity(self.sources.len());
|
||||
|
||||
for Source {
|
||||
path,
|
||||
name,
|
||||
code,
|
||||
kind,
|
||||
} in self.sources.drain(0..)
|
||||
{
|
||||
match aiken_lang::parser::script(&code) {
|
||||
Ok(mut ast) => {
|
||||
// Store the name
|
||||
ast.name = name.clone();
|
||||
|
||||
let module = ParsedModule {
|
||||
kind,
|
||||
ast,
|
||||
code,
|
||||
name,
|
||||
path,
|
||||
package: self.config.name.clone(),
|
||||
};
|
||||
|
||||
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 {
|
||||
errors.push(Error::Parse {
|
||||
path: path.clone(),
|
||||
src: code.clone(),
|
||||
error: Box::new(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(parsed_modules.into())
|
||||
} else {
|
||||
Err(Error::List(errors))
|
||||
}
|
||||
}
|
||||
|
||||
fn type_check(
|
||||
&mut self,
|
||||
mut parsed_modules: ParsedModules,
|
||||
processing_sequence: Vec<String>,
|
||||
) -> Result<Vec<CheckedModule>, Error> {
|
||||
let mut modules = Vec::with_capacity(parsed_modules.len() + 1);
|
||||
|
||||
for name in processing_sequence {
|
||||
if let Some(ParsedModule {
|
||||
name,
|
||||
path,
|
||||
code,
|
||||
kind,
|
||||
// TODO: come back and figure out where to use this
|
||||
package: _package,
|
||||
ast,
|
||||
}) = parsed_modules.remove(&name)
|
||||
{
|
||||
let mut type_warnings = Vec::new();
|
||||
|
||||
let ast = ast
|
||||
.infer(
|
||||
&self.id_gen,
|
||||
kind,
|
||||
&self.config.name,
|
||||
&self.module_types,
|
||||
&mut type_warnings,
|
||||
)
|
||||
.map_err(|error| Error::Type {
|
||||
path: path.clone(),
|
||||
src: code.clone(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
// Register any warnings emitted as type warnings
|
||||
let type_warnings = type_warnings
|
||||
.into_iter()
|
||||
.map(|w| Warning::from_type_warning(w, path.clone(), code.clone()));
|
||||
|
||||
self.warnings.extend(type_warnings);
|
||||
|
||||
// Register the types from this module so they can be imported into
|
||||
// other modules.
|
||||
self.module_types
|
||||
.insert(name.clone(), ast.type_info.clone());
|
||||
|
||||
modules.push(CheckedModule {
|
||||
kind,
|
||||
// extra,
|
||||
name,
|
||||
code,
|
||||
ast,
|
||||
input_path: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
fn aiken_files(&mut self, dir: &Path, kind: ModuleKind) -> Result<(), Error> {
|
||||
let paths = walkdir::WalkDir::new(dir)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.file_type().is_file())
|
||||
.map(|d| d.into_path())
|
||||
.filter(move |d| is_aiken_path(d, dir));
|
||||
|
||||
for path in paths {
|
||||
self.add_module(path, dir, kind)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_module(&mut self, path: PathBuf, dir: &Path, kind: ModuleKind) -> Result<(), Error> {
|
||||
let name = self.module_name(dir, &path, kind);
|
||||
let code = fs::read_to_string(&path).map_err(|error| Error::FileIo {
|
||||
path: path.clone(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
self.sources.push(Source {
|
||||
name,
|
||||
code,
|
||||
kind,
|
||||
path,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn module_name(
|
||||
&self,
|
||||
package_path: &Path,
|
||||
full_module_path: &Path,
|
||||
kind: ModuleKind,
|
||||
) -> String {
|
||||
// ../../{config.name}/module.ak
|
||||
|
||||
// module.ak
|
||||
let mut module_path = full_module_path
|
||||
.strip_prefix(package_path)
|
||||
.expect("Stripping package prefix from module path")
|
||||
.to_path_buf();
|
||||
|
||||
// module
|
||||
module_path.set_extension("");
|
||||
|
||||
// Stringify
|
||||
let name = module_path
|
||||
.to_str()
|
||||
.expect("Module name path to str")
|
||||
.to_string();
|
||||
|
||||
// normalise windows paths
|
||||
let name = name.replace('\\', "/");
|
||||
|
||||
// project_name/module
|
||||
if kind.is_lib() && name != self.config.name {
|
||||
format!("{}/{}", self.config.name, name)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_aiken_path(path: &Path, dir: impl AsRef<Path>) -> bool {
|
||||
use regex::Regex;
|
||||
|
||||
let re = Regex::new(&format!(
|
||||
"^({module}{slash})*{module}\\.ak$",
|
||||
module = "[a-z][_a-z0-9]*",
|
||||
slash = "(/|\\\\)",
|
||||
))
|
||||
.expect("is_aiken_path() RE regex");
|
||||
|
||||
re.is_match(
|
||||
path.strip_prefix(dir)
|
||||
.expect("is_aiken_path(): strip_prefix")
|
||||
.to_str()
|
||||
.expect("is_aiken_path(): to_str"),
|
||||
)
|
||||
}
|
||||
166
crates/project/src/module.rs
Normal file
166
crates/project/src/module.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use aiken_lang::ast::{ModuleKind, TypedModule, UntypedModule};
|
||||
use petgraph::{algo, graph::NodeIndex, Direction, Graph};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ParsedModules {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut 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
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CheckedModule {
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
pub input_path: PathBuf,
|
||||
pub kind: ModuleKind,
|
||||
pub ast: TypedModule,
|
||||
// pub extra: ModuleExtra,
|
||||
}
|
||||
Reference in New Issue
Block a user