mod error;
mod graph;
mod v006;
mod v01x;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
pub use error::LoadError;
pub use rustyfi_syntax::RustyfiVersion;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum LoadMode {
#[default]
Legacy,
Envelopes {
deps: Option<PathBuf>,
},
}
#[derive(Default)]
pub struct LoadOptions {
pub lib_root: Option<PathBuf>,
pub fallback_roots: Vec<PathBuf>,
pub version: RustyfiVersion,
pub mode: LoadMode,
}
impl LoadOptions {
fn roots(&self) -> Vec<&Path> {
self.lib_root
.as_deref()
.into_iter()
.chain(self.fallback_roots.iter().map(|p| p.as_path()))
.collect()
}
}
#[derive(Debug)]
pub enum LoadedCst {
V0_0(rustyfi_syntax::cst::File),
V0_1(rustyfi_syntax::cst_v1::FileV1),
}
impl LoadedCst {
pub fn is_document(&self) -> bool {
match self {
Self::V0_0(f) => f.body.is_some(),
Self::V0_1(f) => matches!(f, rustyfi_syntax::cst_v1::FileV1::Document { .. }),
}
}
fn headers_v006(&self) -> Option<&[rustyfi_syntax::cst::Header]> {
match self {
Self::V0_0(f) => Some(&f.headers),
Self::V0_1(_) => None,
}
}
fn headers_v1(&self) -> Option<&[rustyfi_syntax::cst_v1::HeaderV1]> {
match self {
Self::V0_0(_) => None,
Self::V0_1(f) => Some(match f {
rustyfi_syntax::cst_v1::FileV1::Document { headers, .. }
| rustyfi_syntax::cst_v1::FileV1::Library { headers, .. } => headers,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum FileOrigin {
#[default]
Local,
Envelope { envelope: String, module: String },
}
#[derive(Debug)]
pub struct LoadedFile {
pub path: PathBuf,
pub cst: LoadedCst,
pub origin: FileOrigin,
pub version: RustyfiVersion,
}
#[derive(Debug)]
pub struct LoadedProgram {
pub files: Vec<LoadedFile>,
}
pub fn load(entry: &Path, opts: &LoadOptions) -> Result<LoadedProgram, LoadError> {
if !opts.version.is_implemented() {
return Err(LoadError::UnsupportedVersion {
requested: opts.version,
supported: RustyfiVersion::supported().to_vec(),
});
}
match &opts.mode {
LoadMode::Legacy => load_legacy(entry, opts),
LoadMode::Envelopes { deps } => {
if !matches!(opts.version, RustyfiVersion::V0_1) {
return Err(LoadError::InvalidModeVersion {
version: opts.version,
});
}
v01x::open_doc::load(entry, deps.as_deref(), opts)
}
}
}
fn resolve_legacy_header(
header: &rustyfi_syntax::cst::Header,
dir: &Path,
from: &Path,
opts: &LoadOptions,
) -> Result<Option<PathBuf>, LoadError> {
Ok(Some(match header {
rustyfi_syntax::cst::Header::Import(tok) => {
v006::resolve::resolve_import(dir, &tok.content).map_err(|searched| {
LoadError::UnresolvedImport {
name: tok.content.clone(),
from: from.to_path_buf(),
searched,
}
})?
}
rustyfi_syntax::cst::Header::Require(tok) => {
v006::resolve::resolve_require(&opts.roots(), &tok.content, opts.version)
.map_err(|searched| LoadError::UnresolvedRequire {
name: tok.content.clone(),
searched,
})?
}
rustyfi_syntax::cst::Header::Stage(_) => return Ok(None),
}))
}
fn load_legacy(entry: &Path, opts: &LoadOptions) -> Result<LoadedProgram, LoadError> {
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 version_of: HashMap<u32, RustyfiVersion> = HashMap::new();
let mut require_targets: HashSet<u32> = HashSet::new();
let mut require_v01_targets: HashSet<u32> = HashSet::new();
let mut import_parent_version: HashMap<u32, RustyfiVersion> = 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_version = match opts.version {
RustyfiVersion::V0_1 if id != entry_id => rustyfi_syntax::sniff_version(&src)
.unwrap_or(
if require_targets.contains(&id)
&& path.to_string_lossy().contains("/dist/packages/")
{
RustyfiVersion::V0_0
} else {
import_parent_version
.get(&id)
.copied()
.unwrap_or(RustyfiVersion::V0_1)
},
),
RustyfiVersion::V0_0 if id != entry_id => rustyfi_syntax::sniff_version(&src)
.unwrap_or(if require_v01_targets.contains(&id) {
RustyfiVersion::V0_1
} else {
import_parent_version
.get(&id)
.copied()
.unwrap_or(RustyfiVersion::V0_0)
}),
other => other,
};
let cst: LoadedCst = match file_version {
RustyfiVersion::V0_0 => {
LoadedCst::V0_0(rustyfi_syntax::parse_file(&src).map_err(|source| {
LoadError::Parse {
path: path.clone(),
source,
}
})?)
}
RustyfiVersion::V0_1 => {
LoadedCst::V0_1(rustyfi_syntax::parse_file_v1(&src).map_err(|source| {
LoadError::Parse {
path: path.clone(),
source,
}
})?)
}
other => unreachable!(
"RustyfiVersion::is_implemented() admitted {other} but load()'s \
parse dispatch has no arm for it"
),
};
version_of.insert(id, file_version);
if id == entry_id {
if !cst.is_document() {
return Err(LoadError::LibraryAsEntry { path });
}
} else if cst.is_document() {
return Err(LoadError::DocumentAsDependency { path });
}
let dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let mut resolved_deps: Vec<(PathBuf, bool)> = Vec::new();
if let Some(headers) = cst.headers_v006() {
for header in headers {
let is_require = matches!(header, rustyfi_syntax::cst::Header::Require(_));
if let Some(resolved) = resolve_legacy_header(header, &dir, &path, opts)? {
resolved_deps.push((resolved, is_require));
}
}
} else if let Some(headers) = cst.headers_v1() {
use rustyfi_syntax::cst_v1::HeaderV1;
for header in headers {
match header {
HeaderV1::Legacy(h) => {
let is_require = matches!(h, rustyfi_syntax::cst::Header::Require(_));
if let Some(resolved) = resolve_legacy_header(h, &dir, &path, opts)? {
resolved_deps.push((resolved, is_require));
}
}
HeaderV1::UsePackage { .. } | HeaderV1::UseOf { .. } | HeaderV1::Use { .. } => {
return Err(LoadError::EnvelopeHeaderUnderLegacy {
header: header.display_name(),
from: path.clone(),
});
}
}
}
}
let mut deps = Vec::new();
for (resolved, is_require) in resolved_deps {
let dep_canon = canonicalize(&resolved)?;
let is_corpus_target = is_require && is_dist_packages_target(&dep_canon);
let is_v01_corpus_target = is_require && is_dist_v01_packages_target(&dep_canon);
let dep_id = alloc_id(dep_canon, &mut next_id, &mut id_of, &mut path_of);
if is_corpus_target {
require_targets.insert(dep_id);
}
if is_v01_corpus_target {
require_v01_targets.insert(dep_id);
}
if !is_require {
import_parent_version.entry(dep_id).or_insert(file_version);
}
deps.push(dep_id);
worklist.push(dep_id);
}
adjacency.insert(id, deps);
cst_of.insert(id, cst);
}
let order = graph::header_order_toposort(&adjacency, entry_id).map_err(|chain_ids| {
LoadError::Cycle {
chain: graph::chain_to_paths(&chain_ids, &path_of),
}
})?;
let files = 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: version_of
.remove(&id)
.expect("every graph node id was version-tagged before toposort"),
})
.collect();
Ok(LoadedProgram { files })
}
pub(crate) fn canonicalize(path: &Path) -> Result<PathBuf, LoadError> {
std::fs::canonicalize(path).map_err(|source| LoadError::Io {
path: path.to_path_buf(),
source,
})
}
fn is_dist_packages_target(path: &Path) -> bool {
let comps: Vec<_> = path.components().collect();
comps
.windows(2)
.any(|w| w[0].as_os_str() == "dist" && w[1].as_os_str() == "packages")
}
fn is_dist_v01_packages_target(path: &Path) -> bool {
let comps: Vec<_> = path.components().collect();
comps
.windows(2)
.any(|w| w[0].as_os_str() == "dist-v01" && w[1].as_os_str() == "packages")
}
pub(crate) fn alloc_id(
path: PathBuf,
next_id: &mut u32,
id_of: &mut HashMap<PathBuf, u32>,
path_of: &mut HashMap<u32, PathBuf>,
) -> u32 {
if let Some(&id) = id_of.get(&path) {
return id;
}
let id = *next_id;
*next_id += 1;
id_of.insert(path.clone(), id);
path_of.insert(id, path);
id
}