use std::collections::BTreeSet;
use tatara_lisp::{Atom, Sexp};
pub trait Loader {
fn load(&self, name: &str) -> Result<Vec<(String, String)>, String>;
}
pub struct NoLoader;
impl Loader for NoLoader {
fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
Err(format!(
"cannot load bidama \"{name}\": no loader is installed. A program \
that uses packages must run with one — `blue_lang_pkg::LoadPath` \
reads BLUE_PATH, which `nix develop` and the bidama derivations \
populate."
))
}
}
fn use_target(form: &Sexp) -> Option<String> {
let Sexp::List(items) = form else {
return None;
};
let [head, arg] = items.as_slice() else {
return None;
};
match (head, arg) {
(Sexp::Atom(Atom::Symbol(s)), Sexp::Atom(Atom::Str(name))) if s == "use" => {
Some(name.clone())
}
_ => None,
}
}
pub fn is_test_form(form: &Sexp) -> bool {
let Sexp::List(items) = form else {
return false;
};
matches!(items.first(), Some(Sexp::Atom(Atom::Symbol(s))) if s == "deftest")
}
pub fn resolve_uses(forms: Vec<Sexp>, loader: &dyn Loader) -> Result<Vec<Sexp>, String> {
let mut seen = BTreeSet::new();
expand(forms, loader, &mut seen, &[])
}
fn expand(
forms: Vec<Sexp>,
loader: &dyn Loader,
seen: &mut BTreeSet<String>,
chain: &[String],
) -> Result<Vec<Sexp>, String> {
let mut out = Vec::with_capacity(forms.len());
for form in forms {
let Some(name) = use_target(&form) else {
out.push(form);
continue;
};
if !seen.insert(name.clone()) {
continue;
}
let sources = loader.load(&name).map_err(|e| describe(chain, &name, &e))?;
let mut inner_chain = chain.to_vec();
inner_chain.push(name.clone());
for (label, src) in sources {
let parsed = blue_lang_syntax::parse_program(&src)
.map_err(|e| describe(chain, &name, &format!("{label}: {e}")))?;
let parsed = parsed.into_iter().filter(|f| !is_test_form(f)).collect();
out.extend(expand(parsed, loader, seen, &inner_chain)?);
}
}
Ok(out)
}
fn describe(chain: &[String], name: &str, reason: &str) -> String {
if chain.is_empty() {
return reason.to_owned();
}
format!("while loading {} -> {name}: {reason}", chain.join(" -> "))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
struct MemLoader(BTreeMap<&'static str, &'static str>);
impl Loader for MemLoader {
fn load(&self, name: &str) -> Result<Vec<(String, String)>, String> {
self.0
.get(name)
.map(|s| vec![(format!("{name}.b"), (*s).to_owned())])
.ok_or_else(|| format!("no bidama named \"{name}\""))
}
}
fn parse(src: &str) -> Vec<Sexp> {
blue_lang_syntax::parse_program(src).expect("test source must parse")
}
#[test]
fn a_use_is_replaced_by_the_packages_forms() {
let loader = MemLoader(BTreeMap::from([("kazu", "def double(n)\n n * 2\nend")]));
let out = resolve_uses(parse("use(\"kazu\")\ndouble(21)"), &loader).expect("resolves");
assert!(
out.iter().all(|f| super::use_target(f).is_none()),
"a use form survived resolution and would reach the evaluator as \
an unbound function: {out:?}"
);
assert!(
out.len() > 1,
"the package's definitions must be spliced in, not dropped: {out:?}"
);
}
#[test]
fn imports_are_transitive() {
let loader = MemLoader(BTreeMap::from([
("retsu", "use(\"kazu\")\ndef sum2(a, b)\n a + b\nend"),
("kazu", "def double(n)\n n * 2\nend"),
]));
let out = resolve_uses(parse("use(\"retsu\")"), &loader).expect("resolves");
assert!(
out.len() >= 2,
"the transitive dependency did not arrive: {out:?}"
);
}
#[test]
fn a_package_is_loaded_at_most_once() {
let loader = MemLoader(BTreeMap::from([("kazu", "def double(n)\n n * 2\nend")]));
let once = resolve_uses(parse("use(\"kazu\")"), &loader).expect("resolves");
let twice = resolve_uses(parse("use(\"kazu\")\nuse(\"kazu\")"), &loader).expect("resolves");
assert_eq!(
once.len(),
twice.len(),
"importing a package twice duplicated its definitions; two \
importers of one package must share it"
);
}
#[test]
fn a_dependency_cycle_terminates() {
let loader = MemLoader(BTreeMap::from([
("a", "use(\"b\")\ndef fa()\n 1\nend"),
("b", "use(\"a\")\ndef fb()\n 2\nend"),
]));
let out = resolve_uses(parse("use(\"a\")"), &loader).expect("a cycle must resolve");
assert!(!out.is_empty(), "a cycle resolved to nothing: {out:?}");
}
#[test]
fn a_missing_package_names_itself_and_the_chain() {
let loader = MemLoader(BTreeMap::from([("retsu", "use(\"nowhere\")")]));
let err = resolve_uses(parse("use(\"retsu\")"), &loader).expect_err("must fail");
assert!(
err.contains("nowhere"),
"the error must name the missing package: {err}"
);
assert!(
err.contains("retsu"),
"the error must name the import that pulled it in, or the reader \
cannot tell which of their own imports is at fault: {err}"
);
}
#[test]
fn the_default_loader_refuses_by_name() {
let err = resolve_uses(parse("use(\"kazu\")"), &NoLoader).expect_err("must fail");
assert!(
err.contains("kazu"),
"NoLoader must name what was asked for: {err}"
);
}
#[test]
fn an_imported_packages_tests_are_not_inherited() {
let loader = MemLoader(BTreeMap::from([(
"kazu",
"def double(n)\n n * 2\nend\n\ntest \"doubles\"\n assert double(2) == 4\nend",
)]));
let out = resolve_uses(parse("use(\"kazu\")\ndouble(21)"), &loader).expect("resolves");
assert!(
out.iter().all(|f| !super::is_test_form(f)),
"an imported test block survived and would reach the evaluator as \
an unbound `deftest`: {out:?}"
);
assert!(
out.len() >= 2,
"filtering tests also removed the package's definitions: {out:?}"
);
}
#[test]
fn the_entry_programs_own_tests_survive() {
let loader = MemLoader(BTreeMap::new());
let src = "def f(n)\n n\nend\n\ntest \"t\"\n assert f(1) == 1\nend";
let out = resolve_uses(parse(src), &loader).expect("resolves");
assert!(
out.iter().any(|f| super::is_test_form(f)),
"the file's OWN tests were dropped; only imported ones should be: {out:?}"
);
}
#[test]
fn a_program_without_imports_is_unchanged() {
let loader = MemLoader(BTreeMap::new());
let src = parse("def f(n)\n n + 1\nend\nf(1)");
let out = resolve_uses(src.clone(), &loader).expect("resolves");
assert_eq!(format!("{src:?}"), format!("{out:?}"));
}
}