use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use alba_syntax::{File, Span, Spanned};
use crate::error::{CoreError, SourceIdScope};
use crate::eval::{LazyGit, build_project, parse_error_to_core_error};
use crate::model::{Beam, BeamId, Hook, Project, SourceId};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SourceMap {
entries: Vec<(PathBuf, String)>,
}
impl SourceMap {
fn push(&mut self, path: PathBuf, source: String) -> SourceId {
let id = SourceId(self.entries.len());
self.entries.push((path, source));
id
}
pub fn get(&self, id: SourceId) -> Option<(&Path, &str)> {
self.entries
.get(id.0)
.map(|(path, source)| (path.as_path(), source.as_str()))
}
pub fn paths(&self) -> impl Iterator<Item = &Path> {
self.entries.iter().map(|(path, _)| path.as_path())
}
}
#[derive(Debug, thiserror::Error)]
#[error("{error}")]
pub struct LoadError {
#[source]
pub error: CoreError,
pub sources: SourceMap,
}
pub fn load_project(root: &Path) -> Result<(Project, SourceMap), LoadError> {
let mut loader = Loader::default();
match loader.load_file(root, Span::new(0, 0), true) {
Ok((beams, default, hooks)) => {
let project = Project {
beams,
default,
hooks,
};
match crate::graph::validate_graph(&project) {
Ok(()) => Ok((project, loader.sources)),
Err(error) => Err(LoadError {
error,
sources: loader.sources,
}),
}
}
Err(error) => Err(LoadError {
error,
sources: loader.sources,
}),
}
}
type LoadedFile = (Vec<Beam>, Option<Spanned<BeamId>>, Vec<Hook>);
#[derive(Default)]
struct Loader {
sources: SourceMap,
stack: Vec<(PathBuf, PathBuf)>,
loaded: HashMap<PathBuf, LoadedFile>,
git: Option<Arc<LazyGit>>,
}
impl Loader {
fn canonical(path: &Path, error_span: Span) -> Result<PathBuf, CoreError> {
std::fs::canonicalize(path).map_err(|e| unreadable(path, &e, error_span))
}
fn read(path: &Path, error_span: Span) -> Result<String, CoreError> {
std::fs::read_to_string(path).map_err(|e| unreadable(path, &e, error_span))
}
fn load_file(
&mut self,
path: &Path,
error_span: Span,
is_root: bool,
) -> Result<LoadedFile, CoreError> {
let canonical = Self::canonical(path, error_span)?;
if let Some(start) = self.stack.iter().position(|(c, _)| *c == canonical) {
let mut chain: Vec<String> = self.stack[start..]
.iter()
.map(|(_, display)| display.display().to_string())
.collect();
chain.push(path.display().to_string());
return Err(CoreError::new(
format!("import cycle: {}", chain.join(" -> ")),
error_span,
));
}
if let Some(loaded) = self.loaded.get(&canonical) {
return Ok(loaded.clone());
}
let source = Self::read(path, error_span)?;
let import_base = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let beam_dir = beam_dir_for(path, error_span)?;
let git = Arc::clone(
self.git
.get_or_insert_with(|| Arc::new(LazyGit::new(beam_dir.clone()))),
);
let source_id = self.sources.push(path.to_path_buf(), source.clone());
self.stack.push((canonical.clone(), path.to_path_buf()));
let result = self.load_file_body(&source, &import_base, &beam_dir, source_id, git, is_root);
self.stack.pop();
let loaded = result?;
self.loaded.insert(canonical, loaded.clone());
Ok(loaded)
}
fn load_file_body(
&mut self,
source: &str,
import_base: &Path,
beam_dir: &Path,
source_id: SourceId,
git: Arc<LazyGit>,
is_root: bool,
) -> Result<LoadedFile, CoreError> {
let _scope = SourceIdScope::enter(source_id);
let file: File = alba_syntax::parse(source).map_err(parse_error_to_core_error)?;
check_duplicate_aliases(&file)?;
let local = build_project(&file, beam_dir, git, is_root)?;
let mut beams = local.beams;
for import in &file.imports {
let import_path = import_base.join(&import.path.value);
let (mut child_beams, _child_default, _child_hooks) =
self.load_file(&import_path, import.path.span, false)?;
let alias = &import.alias.value;
for beam in &mut child_beams {
beam.id.0 = format!("{alias}:{}", beam.id.0);
for need in &mut beam.needs {
need.value.0 = format!("{alias}:{}", need.value.0);
}
}
beams.extend(child_beams);
}
Ok((beams, local.default, local.hooks))
}
}
fn unreadable(path: &Path, error: &std::io::Error, error_span: Span) -> CoreError {
CoreError::new(
format!("cannot read Beamfile `{}`: {error}", path.display()),
error_span,
)
}
fn beam_dir_for(path: &Path, error_span: Span) -> Result<PathBuf, CoreError> {
let absolute = std::path::absolute(path).map_err(|e| {
CoreError::new(
format!(
"cannot resolve an absolute path for `{}`: {e}",
path.display()
),
error_span,
)
})?;
Ok(absolute
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(".")))
}
fn check_duplicate_aliases(file: &File) -> Result<(), CoreError> {
let mut seen = HashSet::new();
for import in &file.imports {
if !seen.insert(import.alias.value.as_str()) {
return Err(CoreError::new(
format!("duplicate import alias `{}`", import.alias.value),
import.alias.span,
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn beam_dir_for_bare_filename_is_absolute_and_non_empty() {
let dir = beam_dir_for(Path::new("Beamfile"), Span::new(0, 0)).unwrap();
assert!(dir.is_absolute());
assert!(!dir.as_os_str().is_empty());
assert!(!dir.to_string_lossy().starts_with(r"\\?\"));
}
#[test]
fn beam_dir_for_relative_path_is_absolute() {
let dir = beam_dir_for(Path::new("sub/Beamfile"), Span::new(0, 0)).unwrap();
assert!(dir.is_absolute());
assert!(dir.ends_with("sub"));
assert!(!dir.to_string_lossy().starts_with(r"\\?\"));
}
#[test]
fn beam_dir_for_absolute_path_has_no_verbatim_prefix() {
let root = if cfg!(windows) {
PathBuf::from(r"C:\projects\x\Beamfile")
} else {
PathBuf::from("/projects/x/Beamfile")
};
let dir = beam_dir_for(&root, Span::new(0, 0)).unwrap();
assert!(dir.is_absolute());
assert!(!dir.to_string_lossy().starts_with(r"\\?\"));
}
#[test]
fn beam_dir_for_root_level_file_is_still_a_valid_directory() {
let root = if cfg!(windows) {
PathBuf::from(r"C:\Beamfile")
} else {
PathBuf::from("/Beamfile")
};
let dir = beam_dir_for(&root, Span::new(0, 0)).unwrap();
assert!(dir.is_absolute());
assert!(!dir.as_os_str().is_empty());
assert!(!dir.to_string_lossy().starts_with(r"\\?\"));
}
#[test]
fn source_map_lists_every_loaded_file_in_load_order() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("api")).unwrap();
std::fs::write(
dir.path().join("Beamfile"),
"import \"api/Beamfile\" as api\nbeam build { run \"echo root\" }\n",
)
.unwrap();
std::fs::write(
dir.path().join("api").join("Beamfile"),
"beam build { run \"echo api\" }\n",
)
.unwrap();
let (_, sources) = load_project(&dir.path().join("Beamfile")).unwrap();
let paths: Vec<_> = sources.paths().collect();
assert_eq!(paths.len(), 2);
assert!(paths[0].ends_with("Beamfile"));
assert!(paths[1].ends_with(std::path::Path::new("api").join("Beamfile")));
}
}