use std::path::{Path, PathBuf};
use blue_lang_runtime::pipeline::{check_entry, render, Checking};
use blue_lang_runtime::uses::Entry;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
}
fn corpus() -> Vec<PathBuf> {
blue_lang_pkg::corpus::files(&root())
}
#[test]
fn the_check_stage_reports_no_error_on_the_corpus() {
let files = corpus();
let loader = distribution_loader(&files);
let mut errors = Vec::new();
let mut resolved = 0usize;
for path in &files {
let text = std::fs::read_to_string(path).expect("read");
let checked = check_entry(
Entry {
path: Some(path),
text: &text,
},
&loader,
None,
Checking::WithTests,
)
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
resolved += checked.outcome.stats.names_resolved;
errors.extend(
checked
.outcome
.errors()
.map(|d| render(&checked.program, d)),
);
}
assert!(
files.len() >= 40,
"the walk found {} .b files; the corpus has more than that",
files.len()
);
assert!(
resolved > 100_000,
"only {resolved} names resolved across {} files: the pass is not looking",
files.len()
);
assert!(
errors.is_empty(),
"the check stage reported {} error(s) on correct code:\n{}",
errors.len(),
errors.join("\n")
);
}
#[test]
fn distribution_definitions_that_replace_builtins_are_pinned() {
use blue_lang_check::names::ScopeKind;
use blue_lang_check::Namespace;
let root = root();
let loader = blue_lang_pkg::load_path::LoadPath::new([root.join("bidamas")]);
let src = "use(\"zenbu\")\n";
let checked =
check_entry(Entry::anonymous(src), &loader, None, Checking::Program).expect("check");
let table = &checked.names;
let builtins: Vec<_> = table
.scopes()
.iter()
.filter(|s| {
matches!(
s.namespace,
Namespace::Builtin | Namespace::Macro | Namespace::SpecialForm
)
})
.collect();
let mut replaces_value = Vec::new();
let mut replaces_head = Vec::new();
for scope in table.scopes() {
let Namespace::Bidama(pkg) = &scope.namespace else {
continue;
};
for b in scope.bindings() {
for bs in &builtins {
if let Some(builtin) = bs.get(&b.name) {
let row = format!("{pkg}.{}", b.name);
match builtin.kind {
ScopeKind::Value => replaces_value.push(row),
_ => replaces_head.push(row),
}
}
}
}
}
replaces_value.sort();
assert_eq!(
replaces_head,
Vec::<String>::new(),
"a definition named like a macro or special form cannot be made"
);
assert_eq!(
replaces_value,
[
"kansuu.compose",
"kansuu.flip",
"kansuu.identity",
"kansuu.juxt",
"kansuu.pipe",
"kansuu.tap",
"kazu.abs",
"kazu.max",
"kazu.min",
"retsu.first",
"retsu.interleave",
"retsu.last",
"retsu.partition",
"retsu.rest",
"retsu.zip",
"shuugou.frequencies",
"shuugou.remove",
],
"a bidama definition that replaces a builtin program-wide was added or removed; \
update this list deliberately"
);
}
#[test]
fn the_reach_walk_and_the_name_table_agree_on_free_names() {
use std::collections::BTreeSet;
let files = corpus();
let loader = distribution_loader(&files);
let mut disagreements = Vec::new();
for path in &files {
let text = std::fs::read_to_string(path).expect("read");
let checked = check_entry(
Entry {
path: Some(path),
text: &text,
},
&loader,
None,
Checking::WithTests,
)
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let forms = checked.program.forms();
let table = &checked.names;
let program_defined: BTreeSet<String> = table
.scopes()
.iter()
.filter(|s| {
matches!(
s.namespace,
blue_lang_check::Namespace::File(_) | blue_lang_check::Namespace::Bidama(_)
)
})
.flat_map(|s| s.bindings().map(|b| b.name.clone()))
.collect();
let builtin = |n: &str| {
matches!(n, "defmacro" | "define-typed" | "use" | "legacy_names")
|| table.scopes().iter().any(|s| {
!matches!(
s.namespace,
blue_lang_check::Namespace::File(_) | blue_lang_check::Namespace::Bidama(_)
) && s.get(n).is_some()
})
};
let qualified_bound = |n: &str| {
blue_lang_syntax::qualified(n).is_some_and(|(p, x)| {
(p == "blue" && builtin(x)) || table.bidama(p).is_some_and(|s| s.get(x).is_some())
})
};
let reach: BTreeSet<String> = blue_lang_waku::free_names(forms, &|n| {
builtin(n) || program_defined.contains(n) || qualified_bound(n)
});
let mut table_free: BTreeSet<String> = checked
.resolve()
.references
.iter()
.filter(|r| r.flat == blue_lang_check::names::Target::Unbound)
.map(|r| r.written.clone())
.collect();
table_free.extend(
template_symbols(forms)
.into_iter()
.filter(|n| !builtin(n) && !program_defined.contains(n)),
);
let only_reach: Vec<_> = reach.difference(&table_free).cloned().collect();
let only_table: Vec<_> = table_free
.difference(&reach)
.filter(|n| !template_symbols(forms).contains(*n))
.cloned()
.collect();
if !only_reach.is_empty() || !only_table.is_empty() {
disagreements.push(format!(
"{}: reach only {only_reach:?}, table only {only_table:?}",
path.display()
));
}
}
assert!(
disagreements.is_empty(),
"the two walks disagree:\n{}",
disagreements.join("\n")
);
}
fn template_symbols(forms: &[blue_lang_syntax::Spanned]) -> std::collections::BTreeSet<String> {
use blue_lang_syntax::{Atom, SpannedForm};
fn inside(
f: &blue_lang_syntax::Spanned,
quoted: bool,
out: &mut std::collections::BTreeSet<String>,
) {
match &f.form {
SpannedForm::Atom(Atom::Symbol(s)) if quoted => {
out.insert(s.clone());
}
SpannedForm::List(items) => items.iter().for_each(|i| inside(i, quoted, out)),
SpannedForm::Quasiquote(i) => inside(i, true, out),
SpannedForm::Unquote(i) | SpannedForm::UnquoteSplice(i) => inside(i, false, out),
_ => {}
}
}
let mut out = std::collections::BTreeSet::new();
for f in forms {
inside(f, false, &mut out);
}
out
}
fn distribution_loader(files: &[PathBuf]) -> blue_lang_pkg::load_path::LoadPath {
blue_lang_pkg::corpus::load_path(&root(), files)
}