use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use rustyfi_syntax::cst_v1::{FileV1, HeaderV1};
use rustyfi_syntax::RustyfiVersion;
use crate::v01x::{closed, deps as deps_mod, envelope};
use crate::{
alloc_id, canonicalize, graph, FileOrigin, LoadError, LoadOptions, LoadedCst, LoadedFile,
LoadedProgram,
};
const CANDIDATE_EXTS: [&str; 2] = [".satyh", ".satyg"];
pub(crate) fn load(
entry: &Path,
deps: Option<&Path>,
_opts: &LoadOptions,
) -> Result<LoadedProgram, LoadError> {
let deps_config = deps.map(deps_mod::load).transpose()?;
let used_as_map = deps_config
.as_ref()
.map(|c| deps_mod::make_used_as_map(&c.explicit_dependencies));
let mut prefix: Vec<LoadedFile> = Vec::new();
if let Some(config) = &deps_config {
for spec in closed::sort_envelopes(config)? {
let read = envelope::read(&spec.path)?;
let sorted = closed::sort_modules(read.sources)?;
for source in sorted {
prefix.push(LoadedFile {
path: source.path,
cst: LoadedCst::V0_1(source.file),
origin: FileOrigin::Envelope {
envelope: spec.name.clone(),
module: source.module_name,
},
version: RustyfiVersion::V0_1,
});
}
}
}
let entry_canon = canonicalize(entry)?;
let mut next_id: u32 = 0;
let mut id_of: HashMap<PathBuf, u32> = HashMap::new();
let mut path_of: HashMap<u32, PathBuf> = HashMap::new();
let mut cst_of: HashMap<u32, LoadedCst> = HashMap::new();
let mut adjacency: HashMap<u32, Vec<u32>> = HashMap::new();
let mut processed: HashSet<u32> = HashSet::new();
let entry_id = alloc_id(entry_canon, &mut next_id, &mut id_of, &mut path_of);
let mut worklist = vec![entry_id];
while let Some(id) = worklist.pop() {
if processed.contains(&id) {
continue;
}
processed.insert(id);
let path = path_of[&id].clone();
let src = std::fs::read_to_string(&path).map_err(|source| LoadError::Io {
path: path.clone(),
source,
})?;
let file = rustyfi_syntax::parse_file_v1(&src).map_err(|source| LoadError::Parse {
path: path.clone(),
source,
})?;
let is_document = matches!(file, FileV1::Document { .. });
if id == entry_id {
if !is_document {
return Err(LoadError::LibraryAsEntry { path });
}
} else if is_document {
return Err(LoadError::DocumentAsDependency { path });
}
let dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let headers: &[HeaderV1] = match &file {
FileV1::Document { headers, .. } | FileV1::Library { headers, .. } => headers,
};
let mut resolved_deps: Vec<PathBuf> = Vec::new();
for header in headers {
match header {
HeaderV1::UseOf { relpath, .. } => {
let resolved = resolve_use_of(&dir, &relpath.body).ok_or_else(|| {
LoadError::UnresolvedUseOf {
relpath: relpath.body.clone(),
from: path.clone(),
searched: candidates(&dir, &relpath.body),
}
})?;
resolved_deps.push(resolved);
}
HeaderV1::UsePackage { path: modpath, .. } => {
let head = modpath.head_name();
match &used_as_map {
Some(map) => {
if !map.contains_key(&head) {
return Err(LoadError::UnknownPackageDependency {
module: head,
from: path.clone(),
});
}
}
None => {
return Err(LoadError::PackageDependencyUnresolved {
module: head,
from: path.clone(),
});
}
}
}
HeaderV1::Use { path: modpath, .. } => {
return Err(LoadError::BareUseOutsidePackage {
module: modpath.head_name(),
from: path.clone(),
});
}
HeaderV1::Legacy(_) => {
return Err(LoadError::LegacyHeaderUnderEnvelopes {
header: header.display_name(),
from: path.clone(),
});
}
}
}
let mut dep_ids = Vec::new();
for resolved in resolved_deps {
let dep_canon = canonicalize(&resolved)?;
let dep_id = alloc_id(dep_canon, &mut next_id, &mut id_of, &mut path_of);
dep_ids.push(dep_id);
worklist.push(dep_id);
}
adjacency.insert(id, dep_ids);
cst_of.insert(id, LoadedCst::V0_1(file));
}
let order = graph::toposort(&adjacency).map_err(|chain_ids| LoadError::Cycle {
chain: graph::chain_to_paths(&chain_ids, &path_of),
})?;
let mut files = prefix;
files.extend(order.into_iter().map(|id| {
LoadedFile {
path: path_of[&id].clone(),
cst: cst_of
.remove(&id)
.expect("every graph node id was parsed before toposort"),
origin: FileOrigin::Local,
version: RustyfiVersion::V0_1,
}
}));
Ok(LoadedProgram { files })
}
fn candidates(dir: &Path, rel: &str) -> Vec<PathBuf> {
if CANDIDATE_EXTS.iter().any(|ext| rel.ends_with(ext)) {
vec![dir.join(rel)]
} else {
CANDIDATE_EXTS
.iter()
.map(|ext| dir.join(format!("{rel}{ext}")))
.collect()
}
}
fn resolve_use_of(dir: &Path, rel: &str) -> Option<PathBuf> {
candidates(dir, rel).into_iter().find(|c| c.is_file())
}