use std::collections::HashMap;
use std::path::PathBuf;
use rustyfi_syntax::cst_v1::{FileV1, HeaderV1};
use crate::error::LoadError;
use crate::graph;
use crate::v01x::deps::{DepsConfig, EnvelopeSpec};
use crate::v01x::envelope::EnvelopeSource;
pub(crate) fn sort_modules(sources: Vec<EnvelopeSource>) -> Result<Vec<EnvelopeSource>, LoadError> {
let mut id_of_module: HashMap<String, u32> = HashMap::new();
let mut path_of: HashMap<u32, PathBuf> = HashMap::new();
for (i, source) in sources.iter().enumerate() {
let id = i as u32;
if let Some(&prev_id) = id_of_module.get(&source.module_name) {
return Err(LoadError::FileModuleNameConflict {
module: source.module_name.clone(),
prev: path_of[&prev_id].clone(),
path: source.path.clone(),
});
}
id_of_module.insert(source.module_name.clone(), id);
path_of.insert(id, source.path.clone());
}
let mut adjacency: HashMap<u32, Vec<u32>> = HashMap::new();
for (i, source) in sources.iter().enumerate() {
let id = i as u32;
let mut deps: Vec<u32> = Vec::new();
for header in headers_of(&source.file) {
match header {
HeaderV1::Use { path: modpath, .. } => {
let target = modpath.head_name();
match id_of_module.get(&target) {
Some(&dep_id) => deps.push(dep_id),
None => {
return Err(LoadError::FileModuleNotFound {
module: target,
from: source.path.clone(),
});
}
}
}
HeaderV1::UsePackage { .. } => {}
HeaderV1::UseOf { path: modpath, .. } => {
return Err(LoadError::UseOfInsidePackage {
module: modpath.head_name(),
from: source.path.clone(),
});
}
HeaderV1::Legacy(_) => {
return Err(LoadError::LegacyHeaderUnderEnvelopes {
header: header.display_name(),
from: source.path.clone(),
});
}
}
}
adjacency.insert(id, deps);
}
let order = graph::toposort(&adjacency).map_err(|chain_ids| LoadError::Cycle {
chain: graph::chain_to_paths(&chain_ids, &path_of),
})?;
let mut by_id: HashMap<u32, EnvelopeSource> = sources
.into_iter()
.enumerate()
.map(|(i, s)| (i as u32, s))
.collect();
Ok(order
.into_iter()
.map(|id| {
by_id
.remove(&id)
.expect("every source id was interned before toposort")
})
.collect())
}
pub(crate) fn sort_envelopes(deps: &DepsConfig) -> Result<Vec<&EnvelopeSpec>, LoadError> {
let mut id_of_name: HashMap<String, u32> = HashMap::new();
let mut name_of: HashMap<u32, String> = HashMap::new();
let mut specs: Vec<&EnvelopeSpec> = Vec::new();
for spec in &deps.envelopes {
if spec.test_only {
continue;
}
if id_of_name.contains_key(&spec.name) {
return Err(LoadError::EnvelopeNameConflict {
name: spec.name.clone(),
});
}
let id = specs.len() as u32;
id_of_name.insert(spec.name.clone(), id);
name_of.insert(id, spec.name.clone());
specs.push(spec);
}
let mut adjacency: HashMap<u32, Vec<u32>> = HashMap::new();
for (i, spec) in specs.iter().enumerate() {
let id = i as u32;
let mut dep_ids: Vec<u32> = Vec::new();
for dep in &spec.dependencies {
match id_of_name.get(&dep.name) {
Some(&dep_id) => dep_ids.push(dep_id),
None => {
return Err(LoadError::DependencyOnUnknownEnvelope {
depending: spec.name.clone(),
depended: dep.name.clone(),
});
}
}
}
adjacency.insert(id, dep_ids);
}
let order =
graph::toposort(&adjacency).map_err(|chain_ids| LoadError::CyclicEnvelopeDependency {
chain: chain_ids.iter().map(|id| name_of[id].clone()).collect(),
})?;
Ok(order.into_iter().map(|id| specs[id as usize]).collect())
}
fn headers_of(file: &FileV1) -> &[HeaderV1] {
match file {
FileV1::Document { headers, .. } | FileV1::Library { headers, .. } => headers,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v01x::deps::EnvelopeDependency;
fn source(path: &str, src: &str) -> EnvelopeSource {
let file = rustyfi_syntax::parse_file_v1(src)
.unwrap_or_else(|e| panic!("{path}: parse failed: {e}"));
let module_name = match &file {
FileV1::Library { name, .. } => name.name.clone(),
FileV1::Document { .. } => panic!("{path}: expected a library"),
};
EnvelopeSource {
path: PathBuf::from(path),
file,
module_name,
}
}
fn spec(name: &str, deps: &[&str]) -> EnvelopeSpec {
EnvelopeSpec {
name: name.to_string(),
path: PathBuf::from(format!("/synthetic/{name}/rustyfi-envelope.yaml")),
dependencies: deps
.iter()
.map(|d| EnvelopeDependency {
name: d.to_string(),
used_as: "Dep".to_string(),
})
.collect(),
test_only: false,
}
}
fn module_names(sorted: &[EnvelopeSource]) -> Vec<String> {
sorted.iter().map(|s| s.module_name.clone()).collect()
}
fn envelope_names(sorted: &[&EnvelopeSpec]) -> Vec<String> {
sorted.iter().map(|s| s.name.clone()).collect()
}
#[test]
fn sort_modules_orders_bare_use_dependency_first() {
let sources = vec![
source("c.satyh", "use B\nmodule C = struct\nval c = 1\nend"),
source("b.satyh", "use A\nmodule B = struct\nval b = 1\nend"),
source("a.satyh", "module A = struct\nval a = 1\nend"),
];
let sorted = sort_modules(sources).expect("acyclic module graph sorts");
assert_eq!(module_names(&sorted), vec!["A", "B", "C"]);
}
#[test]
fn sort_modules_no_edges_keeps_all() {
let sources = vec![
source("a.satyh", "module A = struct\nval a = 1\nend"),
source("b.satyh", "module B = struct\nval b = 1\nend"),
];
let sorted = sort_modules(sources).expect("independent modules sort");
assert_eq!(sorted.len(), 2);
}
#[test]
fn sort_modules_duplicate_module_name_conflicts() {
let sources = vec![
source("a.satyh", "module A = struct\nval a = 1\nend"),
source("a2.satyh", "module A = struct\nval a = 2\nend"),
];
match sort_modules(sources) {
Err(LoadError::FileModuleNameConflict { module, .. }) => assert_eq!(module, "A"),
other => panic!("expected FileModuleNameConflict, got {other:?}"),
}
}
#[test]
fn sort_modules_unknown_bare_use_is_not_found() {
let sources = vec![source(
"a.satyh",
"use Missing\nmodule A = struct\nval a = 1\nend",
)];
match sort_modules(sources) {
Err(LoadError::FileModuleNotFound { module, .. }) => assert_eq!(module, "Missing"),
other => panic!("expected FileModuleNotFound, got {other:?}"),
}
}
#[test]
fn sort_modules_use_of_inside_package_rejected() {
let sources = vec![source(
"a.satyh",
"use B of `./b`\nmodule A = struct\nval a = 1\nend",
)];
match sort_modules(sources) {
Err(LoadError::UseOfInsidePackage { module, .. }) => assert_eq!(module, "B"),
other => panic!("expected UseOfInsidePackage, got {other:?}"),
}
}
#[test]
fn sort_modules_cycle_detected() {
let sources = vec![
source("a.satyh", "use B\nmodule A = struct\nval a = 1\nend"),
source("b.satyh", "use A\nmodule B = struct\nval b = 1\nend"),
];
match sort_modules(sources) {
Err(LoadError::Cycle { chain }) => assert!(chain.len() >= 2, "chain: {chain:?}"),
other => panic!("expected Cycle, got {other:?}"),
}
}
#[test]
fn sort_modules_use_package_is_a_no_op() {
let sources = vec![source(
"a.satyh",
"use package X\nmodule A = struct\nval a = 1\nend",
)];
let sorted = sort_modules(sources).expect("use package is a no-op in the closed graph");
assert_eq!(module_names(&sorted), vec!["A"]);
}
#[test]
fn sort_envelopes_orders_dependency_first() {
let cfg = DepsConfig {
envelopes: vec![spec("B", &["A"]), spec("A", &[])],
explicit_dependencies: vec![],
explicit_test_dependencies: vec![],
};
let sorted = sort_envelopes(&cfg).expect("acyclic envelope graph sorts");
assert_eq!(envelope_names(&sorted), vec!["A", "B"]);
}
#[test]
fn sort_envelopes_skips_test_only() {
let mut t = spec("T", &[]);
t.test_only = true;
let cfg = DepsConfig {
envelopes: vec![spec("A", &[]), t],
explicit_dependencies: vec![],
explicit_test_dependencies: vec![],
};
let sorted = sort_envelopes(&cfg).expect("test-only envelope is skipped");
assert_eq!(envelope_names(&sorted), vec!["A"]);
}
#[test]
fn sort_envelopes_duplicate_name_conflicts() {
let cfg = DepsConfig {
envelopes: vec![spec("A", &[]), spec("A", &[])],
explicit_dependencies: vec![],
explicit_test_dependencies: vec![],
};
match sort_envelopes(&cfg) {
Err(LoadError::EnvelopeNameConflict { name }) => assert_eq!(name, "A"),
other => panic!("expected EnvelopeNameConflict, got {other:?}"),
}
}
#[test]
fn sort_envelopes_unknown_dependency() {
let cfg = DepsConfig {
envelopes: vec![spec("A", &["Nope"])],
explicit_dependencies: vec![],
explicit_test_dependencies: vec![],
};
match sort_envelopes(&cfg) {
Err(LoadError::DependencyOnUnknownEnvelope {
depending,
depended,
}) => {
assert_eq!(depending, "A");
assert_eq!(depended, "Nope");
}
other => panic!("expected DependencyOnUnknownEnvelope, got {other:?}"),
}
}
#[test]
fn sort_envelopes_cycle_detected() {
let cfg = DepsConfig {
envelopes: vec![spec("A", &["B"]), spec("B", &["A"])],
explicit_dependencies: vec![],
explicit_test_dependencies: vec![],
};
match sort_envelopes(&cfg) {
Err(LoadError::CyclicEnvelopeDependency { chain }) => {
assert!(chain.len() >= 2, "chain: {chain:?}")
}
other => panic!("expected CyclicEnvelopeDependency, got {other:?}"),
}
}
}