use std::collections::HashMap;
use super::lockfile::{integrity_of, LockEntry, Lockfile};
use super::{parse_version, version_satisfies, FileResolver, ImportSpec, ModuleId};
use crate::error::Diagnostic;
use crate::eval::value::Value;
use crate::eval::Interpreter;
use crate::lexer::Lexer;
use crate::parser::ast::ImportSource;
use crate::parser::Parser;
use crate::source::SourceCache;
use crate::span::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LoadState {
Loading,
Loaded,
}
pub struct Loader<'a, 'r> {
pub cache: &'a SourceCache,
pub resolver: &'r dyn FileResolver,
pub lock: Lockfile,
pub frozen: bool,
pub hermetic: bool,
pub diags: Vec<Diagnostic>,
state: HashMap<ModuleId, LoadState>,
values: HashMap<ModuleId, Value<'a>>,
chain: Vec<ModuleId>,
}
fn rt(code: &'static str, msg: impl Into<String>, span: Span) -> Diagnostic {
Diagnostic::error(code, msg, span, "while loading modules")
}
impl<'a, 'r> Loader<'a, 'r> {
pub fn new(cache: &'a SourceCache, resolver: &'r dyn FileResolver) -> Self {
Loader {
cache,
resolver,
lock: Lockfile::default(),
frozen: false,
hermetic: false,
diags: Vec::new(),
state: HashMap::new(),
values: HashMap::new(),
chain: Vec::new(),
}
}
pub fn eval_entry(
&mut self,
interp: &mut Interpreter<'a>,
spec: &ImportSpec<'_>,
) -> Result<Value<'a>, Diagnostic> {
let dummy = Span::new(0, 0, 0);
let id = self
.resolver
.resolve(spec, None)
.map_err(|e| rt("E0404", e, dummy))?;
self.eval_module_rec(interp, id, dummy, true)
}
fn eval_module_rec(
&mut self,
interp: &mut Interpreter<'a>,
id: ModuleId,
err_span: Span,
is_root: bool,
) -> Result<Value<'a>, Diagnostic> {
match self.state.get(&id) {
Some(LoadState::Loaded) => return Ok(self.values[&id].clone()),
Some(LoadState::Loading) => {
let mut chain: Vec<String> = self.chain.iter().map(ModuleId::to_string).collect();
chain.push(id.to_string());
return Err(rt(
"E0401",
format!("cyclic import: {}", chain.join(" -> ")),
err_span,
));
}
None => {}
}
self.state.insert(id.clone(), LoadState::Loading);
self.chain.push(id.clone());
let result = self.eval_module_inner(interp, &id, err_span, is_root);
self.chain.pop();
match &result {
Ok(v) => {
self.state.insert(id.clone(), LoadState::Loaded);
self.values.insert(id, v.clone());
}
Err(_) => {
self.state.remove(&id);
}
}
result
}
fn eval_module_inner(
&mut self,
interp: &mut Interpreter<'a>,
id: &ModuleId,
err_span: Span,
is_root: bool,
) -> Result<Value<'a>, Diagnostic> {
let text = self
.resolver
.load(id)
.map_err(|e| rt("E0404", format!("cannot load module '{id}': {e}"), err_span))?;
self.verify_lock(id, &text, err_span)?;
let (source_id, src) = self.cache.add(id.to_string(), text);
let toks = Lexer::new(src, source_id).tokenize()?;
let module = match Parser::new(toks).parse_module() {
Ok(m) => m,
Err(mut ds) => {
let first = ds.remove(0);
self.diags.extend(ds);
return Err(first);
}
};
let mut analysis = crate::analysis::analyze_with(&module, is_root, self.hermetic);
if let Some(pos) = analysis
.iter()
.position(|d| d.severity == crate::error::Severity::Error)
{
let first = analysis.remove(pos);
self.diags.extend(analysis);
return Err(first);
}
self.diags.extend(analysis);
let mut resolved: Vec<(&str, Value<'a>)> = Vec::with_capacity(module.imports.len());
for imp in &module.imports {
let spec = match &imp.source {
ImportSource::File(p) => ImportSpec::File(p),
ImportSource::Registry { path, version } => ImportSpec::Registry { path, version },
};
let child_id = self.resolve_with_lock(&spec, Some(id), imp.span)?;
let value = self.eval_module_rec(interp, child_id, imp.span, false)?;
resolved.push((imp.alias, value));
}
for (alias, value) in resolved {
interp.provide_module(alias, value);
}
let saved_root = interp.current_root;
interp.current_root = is_root;
let result = interp.eval_module(&module);
interp.current_root = saved_root;
result
}
fn resolve_with_lock(
&mut self,
spec: &ImportSpec<'_>,
importer: Option<&ModuleId>,
span: Span,
) -> Result<ModuleId, Diagnostic> {
if let ImportSpec::Registry { path, version } = spec {
let request = parse_version(version)
.ok_or_else(|| rt("E0404", format!("malformed version '{version}'"), span))?;
if let Some(entry) = self.lock.entries.get(*path) {
if parse_version(&entry.version)
.is_some_and(|locked| version_satisfies(&request, &locked))
{
return Ok(ModuleId::Registry {
path: path.to_string(),
version: entry.version.clone(),
});
}
}
if self.frozen {
return Err(rt(
"E0403",
format!("--frozen: aura.lock has no entry satisfying '{path}@{version}'"),
span,
));
}
}
self.resolver
.resolve(spec, importer)
.map_err(|e| rt("E0404", e, span))
}
fn verify_lock(&mut self, id: &ModuleId, text: &str, span: Span) -> Result<(), Diagnostic> {
let ModuleId::Registry { path, version } = id else {
return Ok(());
};
let hash = integrity_of(text);
match self.lock.entries.get(path) {
Some(entry) if entry.version == *version => {
let expected = crate::vfs::lockfile::recompute_like(&entry.integrity, text);
if entry.integrity != expected {
return Err(rt(
"E0402",
format!(
"integrity mismatch for '{path}@v{version}': lock has {}, got {expected}",
entry.integrity
),
span,
));
}
if expected != hash && !self.frozen {
self.lock.entries.insert(
path.clone(),
LockEntry {
version: version.clone(),
integrity: hash,
},
);
self.lock.dirty = true;
}
}
_ => {
if self.frozen {
return Err(rt(
"E0403",
format!("--frozen: '{path}@v{version}' is not in aura.lock"),
span,
));
}
self.lock.entries.insert(
path.clone(),
LockEntry {
version: version.clone(),
integrity: hash,
},
);
self.lock.dirty = true;
}
}
Ok(())
}
}