use tatara_lisp::Sexp;
use tatara_lisp_eval::Value;
use blue_lang_check::{NameTable, Namespace};
use crate::erase::erase_types;
use crate::inputs::Inputs;
use crate::uses::Entry;
#[derive(Debug, thiserror::Error)]
pub enum RunError {
#[error("parse error: {0}")]
Parse(String),
#[error("{} error(s) in the check stage:\n{}", .0.len(), .0.join("\n"))]
Types(Vec<String>),
#[error("the emitted tatara-lisp could not be read back: {0}")]
Lower(String),
#[error("runtime error: {0}")]
Eval(Box<crate::observe::Fault>),
#[error("import error: {0}")]
Import(String),
}
#[derive(Debug)]
pub struct Run {
pub value: Value,
pub visited: usize,
pub typed_decls: usize,
pub seams: usize,
}
pub fn parse(src: &str) -> Result<Vec<Sexp>, RunError> {
parse_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
}
pub fn parse_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, RunError> {
blue_lang_syntax::parse_program_with_depth(src, max_depth)
.map_err(|e| RunError::Parse(e.to_string()))
}
pub fn parse_tree_with_depth(
src: &str,
max_depth: usize,
) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
blue_lang_syntax::parse_program_tree_with_depth(src, max_depth)
.map_err(|e| RunError::Parse(e.to_string()))
}
pub fn parse_tree(src: &str) -> Result<Vec<blue_lang_syntax::Spanned>, RunError> {
parse_tree_with_depth(src, blue_lang_syntax::MAX_EXPR_DEPTH)
}
pub fn run(src: &str) -> Result<Run, RunError> {
run_with_inputs(src, Inputs::new())
}
pub fn run_with_inputs(src: &str, inputs: Inputs) -> Result<Run, RunError> {
run_with_loader(src, inputs, &crate::uses::NoLoader)
}
pub fn run_with_loader(
src: &str,
inputs: Inputs,
loader: &dyn crate::uses::Loader,
) -> Result<Run, RunError> {
run_in_surface(Entry::anonymous(src), inputs, loader, None)
}
pub fn run_in_surface(
entry: Entry<'_>,
inputs: Inputs,
loader: &dyn crate::uses::Loader,
surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
) -> Result<Run, RunError> {
let mut interp = crate::interpreter_hostless();
crate::inputs::install_input_primitives(&mut interp, inputs);
let prepared = prepare(entry, loader, surface, &builtin_names(&interp))?;
let mut value = eval_prepared(&mut interp, &prepared, &mut ())?;
if let Some(v) = call_main(&mut interp, &prepared, &mut ())? {
value = v;
}
Ok(Run {
value,
visited: prepared.visited,
typed_decls: prepared.typed_decls,
seams: prepared.seams,
})
}
pub struct Observed {
pub definitions: std::sync::Arc<crate::observe::Definitions>,
pub outcome: Result<Run, RunError>,
}
pub fn run_observed(
entry: Entry<'_>,
inputs: Inputs,
loader: &dyn crate::uses::Loader,
surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
watch: impl FnOnce(
&std::sync::Arc<crate::observe::Definitions>,
) -> std::sync::Arc<dyn crate::observe::CallObserver>,
) -> Result<Observed, RunError> {
let mut interp = crate::interpreter_hostless();
crate::inputs::install_input_primitives(&mut interp, inputs);
let prepared = prepare(entry, loader, surface, &builtin_names(&interp))?;
let definitions = std::sync::Arc::new(crate::observe::Definitions::of(
&prepared.program,
&prepared.erased,
));
interp.set_call_observer(Some(watch(&definitions)));
let outcome = eval_prepared(&mut interp, &prepared, &mut ())
.and_then(|value| Ok(call_main(&mut interp, &prepared, &mut ())?.unwrap_or(value)))
.map(|value| Run {
value,
visited: prepared.visited,
typed_decls: prepared.typed_decls,
seams: prepared.seams,
});
interp.set_call_observer(None);
Ok(Observed {
definitions,
outcome,
})
}
pub(crate) struct Prepared {
program: crate::uses::ResolvedProgram,
erased: Vec<tatara_lisp::Spanned>,
visited: usize,
typed_decls: usize,
seams: usize,
}
impl Prepared {
pub(crate) fn erased(&self) -> &[tatara_lisp::Spanned] {
&self.erased
}
pub(crate) fn entry_namespace(&self) -> Namespace {
let first = (0..self.program.forms().len())
.find(|i| self.program.owner_of(*i) == Some(crate::uses::ResolvedProgram::ENTRY));
match first {
Some(i) => namespace_of(&self.program, i),
None => match self
.program
.file(crate::uses::ResolvedProgram::ENTRY)
.and_then(|f| f.package.clone())
{
Some(p) => Namespace::Bidama(p),
None => Namespace::File(String::new()),
},
}
}
}
pub(crate) fn prepare(
entry: Entry<'_>,
loader: &dyn crate::uses::Loader,
surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
builtins: &NameTable,
) -> Result<Prepared, RunError> {
let mut program = parse_and_resolve(entry, loader, surface)?;
program.retain(|f| !crate::uses::is_test_form(f));
let (outcome, names) = check_stage(&program, builtins, false);
if !outcome.ok() {
return Err(RunError::Types(
outcome
.errors()
.map(|d| render(&program, d))
.collect(),
));
}
let erased = erase_types(&lower(&program, &names));
Ok(Prepared {
program,
erased,
visited: outcome.stats.visited,
typed_decls: outcome.stats.typed_decls,
seams: outcome.seams.len(),
})
}
pub(crate) fn parse_and_resolve(
entry: Entry<'_>,
loader: &dyn crate::uses::Loader,
surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
) -> Result<crate::uses::ResolvedProgram, RunError> {
let forms = match surface {
Some(pack) => blue_lang_syntax::parse_program_tree_in(entry.text, pack)
.map_err(|e| RunError::Parse(e.to_string()))?,
None => loader
.parse(entry.text)
.map_err(|e| RunError::Parse(e.to_string()))?,
};
crate::uses::resolve_uses(forms, entry, loader).map_err(RunError::Import)
}
pub(crate) fn check_stage(
program: &crate::uses::ResolvedProgram,
builtins: &NameTable,
with_tests: bool,
) -> (blue_lang_check::Outcome, NameTable) {
use crate::uses::ResolvedProgram;
let forms = program.forms();
let is_entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
let mut outcome = blue_lang_check::check_program(forms);
let table = program_names(program, builtins);
let whole = |i: usize| with_tests && is_entry(i);
let (names, resolved) = blue_lang_check::names::check_names_whole(
forms,
&table,
&|i| namespace_of(program, i),
&is_entry,
&whole,
);
let stored = program
.file(ResolvedProgram::ENTRY)
.is_some_and(|f| f.path.is_some());
outcome.diagnostics.extend(names);
outcome.stats.names_resolved = resolved;
if stored {
let script = program
.file(ResolvedProgram::ENTRY)
.is_some_and(|f| f.package.is_none());
let found = blue_lang_check::form_rules::check(forms, &is_entry, script);
let silenced: Vec<tatara_lisp::Span> = found
.iter()
.filter(|d| d.code == blue_lang_check::Code::B0038)
.map(|d| d.span)
.collect();
outcome
.diagnostics
.retain(|d| !(d.code == blue_lang_check::Code::B0002 && silenced.contains(&d.span)));
outcome.diagnostics.extend(found);
}
let mut waivers = Vec::new();
for file in program.files() {
let spans: Vec<(usize, tatara_lisp::Span)> = forms
.iter()
.enumerate()
.filter(|(i, _)| program.owner_of(*i) == Some(file.id))
.map(|(i, f)| (i, f.span))
.collect();
let (found, malformed) = blue_lang_check::waiver::collect(&file.text, &spans);
waivers.extend(found);
outcome.diagnostics.extend(malformed);
}
outcome.waived = blue_lang_check::waiver::apply(&mut outcome.diagnostics, waivers, &is_entry);
if !stored {
outcome.diagnostics.retain(|d| {
!matches!(
d.code,
blue_lang_check::Code::B0023
| blue_lang_check::Code::B0036
| blue_lang_check::Code::B0037
)
});
}
let (enforced, census): (Vec<_>, Vec<_>) = std::mem::take(&mut outcome.diagnostics)
.into_iter()
.partition(|d| d.code.rule().active());
outcome.diagnostics = enforced;
outcome.census = census;
outcome
.diagnostics
.sort_by_key(|d| (d.top_level, d.span.start, d.code));
(outcome, table)
}
#[must_use]
pub fn program_names(program: &crate::uses::ResolvedProgram, builtins: &NameTable) -> NameTable {
use blue_lang_check::Binding;
let mut table = NameTable::new();
table.add_program(program.forms(), |i| namespace_of(program, i));
let builtin_macro = |head: &str| {
builtins
.scopes()
.iter()
.any(|s| s.namespace == Namespace::Macro && s.get(head).is_some())
};
let mut expander: Option<tatara_lisp_eval::Interpreter<()>> = None;
for (i, form) in program.forms().iter().enumerate() {
let Some(head) = form
.as_list()
.and_then(|items| items.first())
.and_then(tatara_lisp::Spanned::as_symbol)
else {
continue;
};
if !builtin_macro(head) {
continue;
}
let interp = expander.get_or_insert_with(crate::interpreter_hostless);
let Ok(expanded) = interp.fully_expand(form, &mut ()) else {
continue;
};
let nullary = blue_lang_syntax::scope::nullary_definition(&expanded);
for (name, span, kind) in blue_lang_check::names::definitions_of(&expanded) {
table.define(
namespace_of(program, i),
Binding {
nullary: nullary == Some(name.as_str()),
name,
kind,
span: Some(span),
top_level: Some(i),
},
);
}
}
for scope in builtins.scopes() {
table.push(scope.clone());
}
table.spell_bare_like(builtins);
let form_file: Vec<usize> = (0..program.forms().len())
.map(|i| program.owner_of(i).map_or(usize::MAX, |f| f.index()))
.collect();
let mut imports: std::collections::BTreeMap<usize, blue_lang_check::names::FileImports> =
std::collections::BTreeMap::new();
for (file, import) in program.imports() {
let entry = imports.entry(file.index()).or_default();
entry.add(&import.package, import.names.iter().map(|(n, _)| n.clone()));
let at = import.span.start;
entry.use_at = Some(entry.use_at.map_or(at, |a| a.min(at)));
}
let facade_uses =
|pkg: &str,
imports: &std::collections::BTreeMap<usize, blue_lang_check::names::FileImports>| {
let defines = table.bidama(pkg).is_some_and(|s| !s.is_empty());
if defines {
return std::collections::BTreeSet::new();
}
program
.files()
.iter()
.filter(|f| f.package.as_deref() == Some(pkg))
.filter_map(|f| imports.get(&f.id.index()))
.flat_map(|i| i.uses.iter().cloned())
.collect::<std::collections::BTreeSet<String>>()
};
loop {
let mut grew = false;
let files: Vec<usize> = imports.keys().copied().collect();
for f in files {
let used: Vec<String> = imports[&f].uses.iter().cloned().collect();
for p in used {
for member in facade_uses(&p, &imports) {
if let Some(i) = imports.get_mut(&f) {
if i.uses.insert(member.clone()) {
i.via.insert(member, p.clone());
grew = true;
}
}
}
}
}
if !grew {
break;
}
}
let mut pkg_uses: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
std::collections::BTreeMap::new();
for (file, import) in program.imports() {
if let Some(p) = program.file(*file).and_then(|f| f.package.clone()) {
pkg_uses
.entry(p)
.or_default()
.insert(import.package.clone());
}
}
table.attach_files(form_file, imports);
table.attach_package_uses(pkg_uses);
table.attach_needs(program.needs().clone());
let canonical = |p: &std::path::Path| {
std::fs::canonicalize(p)
.unwrap_or_else(|_| p.to_path_buf())
.display()
.to_string()
};
let mut file_paths = std::collections::BTreeMap::new();
let mut pkg_roots = std::collections::BTreeMap::new();
for f in program.files() {
if let Some(path) = &f.path {
file_paths.insert(f.id.index(), canonical(path));
if let (Some(pkg), Some(root)) =
(&f.package, path.parent().and_then(std::path::Path::parent))
{
pkg_roots
.entry(pkg.clone())
.or_insert_with(|| canonical(root));
}
}
}
table.attach_paths(file_paths, pkg_roots);
let versions = program.versions().clone();
table.apply_legacy(program.forms(), &|i| namespace_of(program, i), &|p| {
versions.get(p).cloned()
});
table
}
#[must_use]
pub fn lower(
program: &crate::uses::ResolvedProgram,
names: &NameTable,
) -> Vec<tatara_lisp::Spanned> {
lower_with(program, names, true)
}
#[must_use]
pub fn lower_with(
program: &crate::uses::ResolvedProgram,
names: &NameTable,
simplify_chains: bool,
) -> Vec<tatara_lisp::Spanned> {
let mut resolved = blue_lang_check::names::resolve_program(program.forms(), names, &|i| {
namespace_of(program, i)
});
let simplified = if simplify_chains {
chain_simplified(program.forms(), &resolved)
} else {
None
};
let forms: &[tatara_lisp::Spanned] = simplified.as_deref().unwrap_or(program.forms());
if simplified.is_some() {
resolved =
blue_lang_check::names::resolve_program(forms, names, &|i| namespace_of(program, i));
}
resolved
.resolved_tree(forms, blue_lang_check::names::Rule::Namespaced)
.into_iter()
.zip(forms)
.enumerate()
.map(|(i, (resolved, written))| {
if crate::uses::use_target(written).is_some()
|| blue_lang_syntax::scope::legacy_target(written).is_some()
{
tatara_lisp::Spanned::new(written.span, tatara_lisp::SpannedForm::Nil)
} else {
let file = program.owner_of(i).and_then(|id| program.file(id));
match file {
Some(f) => locate_raises(&resolved, &raise_site(f)),
None => resolved,
}
}
})
.collect()
}
fn chain_simplified(
forms: &[tatara_lisp::Spanned],
resolved: &blue_lang_check::names::Resolved,
) -> Option<Vec<tatara_lisp::Spanned>> {
let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> = resolved
.respellings
.iter()
.filter(|(_, r)| matches!(r.respell, blue_lang_check::respell::Respell::Chain(_)))
.cloned()
.collect();
if respell.is_empty() {
return None;
}
let bare = std::collections::HashSet::new();
let canon = Canon {
bare: &bare,
respell: &respell,
};
Some(forms.iter().map(|f| canon.form(f)).collect())
}
fn raise_site(file: &crate::uses::SourceFile) -> impl Fn(tatara_lisp::Span) -> String + '_ {
move |span| {
let (line, _) =
blue_lang_syntax::Span::line_col(&file.text, span.start.min(file.text.len()));
let shown = file
.path
.as_ref()
.map_or_else(|| "<source>".to_string(), |p| p.display().to_string());
format!("{shown}:{line}")
}
}
fn locate_raises(
form: &tatara_lisp::Spanned,
site: &dyn Fn(tatara_lisp::Span) -> String,
) -> tatara_lisp::Spanned {
use tatara_lisp::{Atom, Spanned, SpannedForm};
let inner = match &form.form {
SpannedForm::List(items) => {
let mut out: Vec<Spanned> = items.iter().map(|i| locate_raises(i, site)).collect();
let raising = items.first().and_then(Spanned::as_symbol) == Some("raise")
&& (3..=4).contains(&items.len());
if raising {
out[0] = Spanned::new(
items[0].span,
SpannedForm::Atom(Atom::Symbol(crate::errors::RAISE_AT.to_string())),
);
out.insert(
1,
Spanned::new(items[0].span, SpannedForm::Atom(Atom::Str(site(form.span)))),
);
}
SpannedForm::List(out)
}
SpannedForm::Quote(_) | SpannedForm::Quasiquote(_) => return form.clone(),
other => other.clone(),
};
Spanned::new(form.span, inner)
}
#[must_use]
pub fn namespace_of(program: &crate::uses::ResolvedProgram, i: usize) -> Namespace {
let file = program.owner_of(i).and_then(|id| program.file(id));
match file {
Some(f) => match &f.package {
Some(p) => Namespace::Bidama(p.clone()),
None => Namespace::File(
f.path
.as_ref()
.map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string()),
),
},
None => Namespace::File("<unknown file>".to_string()),
}
}
pub const HARNESS_NAMES: &[&str] = &[blue_lang_syntax::LOWERED_ASSERT];
#[must_use]
pub fn builtin_names<H: 'static>(interp: &tatara_lisp_eval::Interpreter<H>) -> NameTable {
use blue_lang_check::{Binding, Scope, ScopeKind};
use tatara_lisp_eval::HeadBinding;
let mut special = Scope::new(Namespace::SpecialForm);
let mut macros = Scope::new(Namespace::Macro);
let mut values = Scope::new(Namespace::Builtin);
let mut names: Vec<std::sync::Arc<str>> = interp.reserved_head_names().into_iter().collect();
names.extend(
crate::inputs::INPUT_PRIMITIVES
.iter()
.filter(|n| interp.resolve_head(n).is_some())
.map(|n| std::sync::Arc::from(*n)),
);
for name in names {
let (scope, kind) = match interp.resolve_head(&name) {
Some(HeadBinding::SpecialForm) => (&mut special, ScopeKind::SpecialForm),
Some(HeadBinding::Macro) => (&mut macros, ScopeKind::Macro),
_ => (&mut values, ScopeKind::Value),
};
let nullary = kind == ScopeKind::Value
&& interp.lookup_global(&name).is_some_and(|v| {
nullary_value(&v)
|| (crate::docs::doc_of(&name).is_some_and(|d| d.signature == &*name)
&& matches!(
v,
tatara_lisp_eval::Value::NativeFn(_)
| tatara_lisp_eval::Value::Closure(_)
))
});
scope.bind(Binding {
name: name.to_string(),
kind,
span: None,
top_level: None,
nullary,
});
}
let mut table = NameTable::new();
let mut harness = Scope::new(Namespace::Harness);
for n in HARNESS_NAMES {
harness.bind(Binding {
name: (*n).to_string(),
kind: ScopeKind::Value,
span: None,
top_level: None,
nullary: false,
});
}
table.push(harness);
table.push(special);
table.push(macros);
table.push(values);
table.spell_bare_only(
crate::docs::NAMES
.iter()
.filter(|d| d.signature == d.name)
.map(|d| d.name.to_string())
.collect(),
);
table
}
#[must_use]
pub fn nullary_value(value: &tatara_lisp_eval::Value) -> bool {
match value {
tatara_lisp_eval::Value::NativeFn(f) => {
matches!(f.arity, tatara_lisp_eval::ffi::Arity::Exact(0))
}
tatara_lisp_eval::Value::Closure(c) => c.params.is_empty() && c.rest.is_none(),
_ => false,
}
}
#[must_use]
pub fn render(program: &crate::uses::ResolvedProgram, d: &blue_lang_check::Diagnostic) -> String {
let head = d.to_string();
let mut out = program.locate(d.top_level, d.span, &head).to_string();
if let Some(help) = &d.help {
out.push_str("\n help: ");
out.push_str(help);
}
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Checking {
Program,
WithTests,
}
pub struct Checked {
pub program: crate::uses::ResolvedProgram,
pub outcome: blue_lang_check::Outcome,
pub names: NameTable,
}
impl Checked {
#[must_use]
pub fn erased(&self) -> Vec<tatara_lisp::Spanned> {
erase_types(&lower(&self.program, &self.names))
}
#[must_use]
pub fn evaluable(&self) -> Vec<tatara_lisp::Sexp> {
lower(&self.program, &self.names)
.iter()
.map(tatara_lisp::Spanned::to_sexp)
.collect()
}
#[must_use]
pub fn resolve(&self) -> blue_lang_check::names::Resolved {
blue_lang_check::names::resolve_program(self.program.forms(), &self.names, &|i| {
namespace_of(&self.program, i)
})
}
}
impl Checked {
#[must_use]
pub fn call_explicit_forms(&self) -> Vec<tatara_lisp::Spanned> {
self.resolve().call_explicit_tree(self.program.forms())
}
#[must_use]
pub fn canonical_entry(&self, src: &str) -> Option<Vec<tatara_lisp::Spanned>> {
use crate::uses::ResolvedProgram;
let program = &self.program;
let entry: Vec<&tatara_lisp::Spanned> = program
.forms()
.iter()
.enumerate()
.filter(|(i, _)| program.owner_of(*i) == Some(ResolvedProgram::ENTRY))
.map(|(_, f)| f)
.collect();
let written = blue_lang_syntax::parse_program_tree(src).ok()?;
let same = written.len() == entry.len()
&& written
.iter()
.zip(&entry)
.all(|(w, e)| w.to_sexp() == e.to_sexp() && w.span == e.span);
if !same {
return None;
}
let resolved = self.resolve();
let bare: std::collections::HashSet<usize> = resolved
.parenthesized_nullary_calls()
.into_iter()
.filter(|r| program.owner_of(r.top_level) == Some(ResolvedProgram::ENTRY))
.map(blue_lang_check::names::Reference::node_address)
.collect();
let waived: Vec<tatara_lisp::Span> = self
.outcome
.waived
.iter()
.map(|w| w.diagnostic.span)
.collect();
let respell: std::collections::HashMap<usize, blue_lang_check::respell::Respelling> =
resolved
.respellings
.iter()
.filter(|(_, r)| !waived.contains(&r.span))
.cloned()
.collect();
let canon = Canon {
bare: &bare,
respell: &respell,
};
let forms: Vec<tatara_lisp::Spanned> = entry.into_iter().map(|f| canon.form(f)).collect();
let script = program
.file(ResolvedProgram::ENTRY)
.is_some_and(|f| f.package.is_none());
let forms = if script {
blue_lang_check::form_rules::without_prefix(&forms).unwrap_or(forms)
} else {
forms
};
let forms = blue_lang_check::form_rules::without_main_guard(&forms).unwrap_or(forms);
Some(blue_lang_check::form_rules::eta_reduced(&forms).unwrap_or(forms))
}
}
struct Canon<'a> {
bare: &'a std::collections::HashSet<usize>,
respell: &'a std::collections::HashMap<usize, blue_lang_check::respell::Respelling>,
}
impl Canon<'_> {
fn form(&self, form: &tatara_lisp::Spanned) -> tatara_lisp::Spanned {
use tatara_lisp::{Spanned, SpannedForm};
let inner = match &form.form {
SpannedForm::List(items) => match items.as_slice() {
[head] if self.bare.contains(&(std::ptr::from_ref(head) as usize)) => {
return Spanned::new(form.span, head.form.clone());
}
[head, ..]
if self
.respell
.contains_key(&(std::ptr::from_ref(head) as usize)) =>
{
let r = &self.respell[&(std::ptr::from_ref(head) as usize)];
let walked: Vec<Spanned> = items.iter().map(|i| self.form(i)).collect();
return blue_lang_check::respell::apply(form, &walked, r);
}
_ => SpannedForm::List(items.iter().map(|i| self.form(i)).collect()),
},
SpannedForm::Quote(i) => SpannedForm::Quote(Box::new(self.form(i))),
SpannedForm::Quasiquote(i) => SpannedForm::Quasiquote(Box::new(self.form(i))),
SpannedForm::Unquote(i) => SpannedForm::Unquote(Box::new(self.form(i))),
SpannedForm::UnquoteSplice(i) => SpannedForm::UnquoteSplice(Box::new(self.form(i))),
other => other.clone(),
};
Spanned::new(form.span, inner)
}
}
pub fn check_entry(
entry: Entry<'_>,
loader: &dyn crate::uses::Loader,
surface: Option<&blue_lang_syntax::yakugo::Yakugo>,
checking: Checking,
) -> Result<Checked, RunError> {
let mut program = parse_and_resolve(entry, loader, surface)?;
if checking == Checking::Program {
program.retain(|f| !crate::uses::is_test_form(f));
}
let mut interp = crate::interpreter_hostless();
crate::inputs::install_input_primitives(&mut interp, Inputs::new());
let builtins = builtin_names(&interp);
let (outcome, names) = check_stage(&program, &builtins, checking == Checking::WithTests);
Ok(Checked {
program,
outcome,
names,
})
}
#[must_use]
pub fn spelling_code(spelling: blue_lang_syntax::Spelling) -> blue_lang_check::Code {
use blue_lang_check::Code;
use blue_lang_syntax::Spelling;
match spelling {
Spelling::LogicalSymbol => Code::B0025,
Spelling::Unless => Code::B0026,
Spelling::ElseIf => Code::B0027,
Spelling::CallForm => Code::B0028,
Spelling::KeywordRocket => Code::B0029,
Spelling::ParameterList => Code::B0030,
Spelling::OperatorSpelling => Code::B0031,
Spelling::TrailingComma => Code::B0032,
Spelling::NegatedComparison => Code::B0034,
Spelling::ElseNil => Code::B0035,
Spelling::TryCatch => Code::B0037,
Spelling::Apply => Code::B0042,
}
}
#[must_use]
pub fn form_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
match blue_lang_fmt::format_source_lossless(text) {
Ok(formatted) => form_diagnostics_against(text, &formatted),
Err(_) => Vec::new(),
}
}
#[must_use]
pub fn form_diagnostics_against(text: &str, formatted: &str) -> Vec<blue_lang_check::Diagnostic> {
use blue_lang_check::{Code, Diagnostic};
if formatted.trim_end() == text.trim_end() {
return Vec::new();
}
let help = "`blue fmt --write` writes it; `blue run`, `blue test` and `blue check` rewrite a writable file before compiling it";
let mut out: Vec<Diagnostic> = blue_lang_syntax::spelling_notes(text)
.unwrap_or_default()
.into_iter()
.map(|n| {
let message = if n.canonical.is_empty() {
format!("`{}` is not written", n.written)
} else {
format!("`{}` is written `{}`", n.written, n.canonical)
};
Diagnostic::new(spelling_code(n.spelling), message, n.span)
.at_top_level(0)
.with_help(help)
})
.collect();
out.extend(
literal_diagnostics(text)
.into_iter()
.map(|d| d.with_help(help)),
);
if out.is_empty() {
let at = text
.lines()
.zip(formatted.lines())
.position(|(a, b)| a != b)
.unwrap_or_else(|| text.lines().count().min(formatted.lines().count()));
let start: usize = text.lines().take(at).map(|l| l.len() + 1).sum();
let start = start.min(text.len());
let end = text[start..].find('\n').map_or(text.len(), |e| start + e);
out.push(
Diagnostic::new(
Code::B0024,
format!("line {} is not laid out as blue writes it", at + 1),
tatara_lisp::Span::new(start, end),
)
.at_top_level(0)
.with_help(help),
);
}
out.sort_by_key(|d| (d.span.start, d.code));
out
}
fn literal_diagnostics(text: &str) -> Vec<blue_lang_check::Diagnostic> {
use blue_lang_syntax::{Atom, TokenKind};
let Ok(toks) = blue_lang_syntax::lex(text) else {
return Vec::new();
};
toks.iter()
.filter_map(|t| {
let atom = match &t.kind {
TokenKind::Int(v) => Atom::Int(*v),
TokenKind::Float(v) => Atom::Float(*v),
TokenKind::Str(v) => Atom::Str(v.clone()),
_ => return None,
};
let written = text.get(t.span.start..t.span.end)?;
let canonical = blue_lang_fmt::render_literal(&atom);
(written != canonical).then(|| {
blue_lang_check::Diagnostic::new(
blue_lang_check::Code::B0033,
format!("`{written}` is written `{canonical}`"),
t.span,
)
.at_top_level(0)
})
})
.collect()
}
#[must_use]
pub fn syntax_diagnostic(text: &str) -> Option<blue_lang_check::Diagnostic> {
let e = blue_lang_syntax::parse_program_tree(text).err()?;
Some(
blue_lang_check::Diagnostic::new(blue_lang_check::Code::B0006, e.message, e.span)
.at_top_level(0),
)
}
#[must_use]
pub fn display_keys(message: &str) -> String {
let mut out = String::with_capacity(message.len());
let mut parts = message.split('`');
if let Some(first) = parts.next() {
out.push_str(first);
}
for (i, part) in parts.enumerate() {
out.push('`');
if i % 2 == 0 {
out.push_str(&crate::observe::display_key(part));
} else {
out.push_str(part);
}
}
out
}
pub(crate) fn eval_prepared<H: 'static>(
interp: &mut tatara_lisp_eval::Interpreter<H>,
prepared: &Prepared,
host: &mut H,
) -> Result<Value, RunError> {
let Prepared {
program, erased, ..
} = prepared;
let mut value = Value::Nil;
for (top_level, form) in erased.iter().enumerate() {
value = interp.eval_top_form(form, host).map_err(|e| {
RunError::Eval(Box::new(crate::observe::Fault::of(
program,
erased,
top_level,
&e,
&interp.take_trace(),
)))
})?;
}
Ok(value)
}
pub const MAIN: &str = "main";
#[must_use]
pub fn entry_main(program: &crate::uses::ResolvedProgram) -> Option<(usize, tatara_lisp::Span)> {
use crate::uses::ResolvedProgram;
let stored = program
.file(ResolvedProgram::ENTRY)
.is_some_and(|f| f.path.is_some());
if !stored {
return None;
}
program.forms().iter().enumerate().find_map(|(i, f)| {
if program.owner_of(i) != Some(ResolvedProgram::ENTRY) {
return None;
}
let items = f.as_list()?;
let sig = items.get(1)?.as_list()?;
let named_main = matches!(
items.first().and_then(tatara_lisp::Spanned::as_symbol),
Some("define" | "define-typed")
) && sig.len() == 1
&& sig[0].as_symbol() == Some(MAIN);
named_main.then_some((i, sig[0].span))
})
}
pub(crate) fn call_main<H: 'static>(
interp: &mut tatara_lisp_eval::Interpreter<H>,
prepared: &Prepared,
host: &mut H,
) -> Result<Option<Value>, RunError> {
let Some((top_level, span)) = entry_main(&prepared.program) else {
return Ok(None);
};
let ns = namespace_of(&prepared.program, top_level);
let call = tatara_lisp::Spanned::new(
span,
tatara_lisp::SpannedForm::List(vec![tatara_lisp::Spanned::new(
span,
tatara_lisp::SpannedForm::Atom(tatara_lisp::Atom::Symbol(blue_lang_check::names::key(
&ns, MAIN,
))),
)]),
);
interp.eval_top_form(&call, host).map(Some).map_err(|e| {
RunError::Eval(Box::new(crate::observe::Fault::of(
&prepared.program,
&prepared.erased,
top_level,
&e,
&interp.take_trace(),
)))
})
}
#[cfg(test)]
mod position_tests {
use super::*;
use crate::uses::{Entry, Loader};
use std::collections::BTreeMap;
use std::path::Path;
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}\""))
}
}
const BAKUHATSU: &str = "\
def tsukawanai_a()
1
end
def tsukawanai_b()
2
end
# waive B0001: the fixture needs a raise at runtime
def bakuhatsu()
kore_wa_sonzai_shinai()
end
bakuhatsu";
const KOTAE: &str = "def kotae()\n 42\nend";
const BAKUHATSU2: &str =
"# waive B0001: a raise at runtime\ndef yobu()\n kore_wa_sonzai_shinai()\nend";
fn loader() -> MemLoader {
MemLoader(BTreeMap::from([
("kotae", KOTAE),
("bakuhatsu", BAKUHATSU),
("bakuhatsu2", BAKUHATSU2),
]))
}
fn run_named(path: &str, src: &str) -> RunError {
run_in_surface(
Entry {
path: Some(Path::new(path)),
text: src,
},
Inputs::new(),
&loader(),
None,
)
.expect_err("the fixture must raise")
}
const TWO_IMPORTS: &str =
"use(\"kotae\")\n# waive B0015: kotae loads first on purpose\nuse(\"bakuhatsu\")\n";
#[test]
fn a_raise_inside_an_imported_package_names_that_package() {
let err = run_named("entry.b", TWO_IMPORTS);
assert_eq!(
err.to_string(),
"runtime error: bakuhatsu.b:11:3: unbound symbol `kore_wa_sonzai_shinai`\n \
at bakuhatsu::bakuhatsu (bakuhatsu.b:11:3)\n \
at <top level> (bakuhatsu.b:14:1)"
);
}
#[test]
fn line_11_column_3_of_the_fixture_is_the_failing_call() {
let line = BAKUHATSU.lines().nth(10).expect("line 11 exists");
assert_eq!(line, " kore_wa_sonzai_shinai()");
assert_eq!(
&line[2..],
"kore_wa_sonzai_shinai()",
"column 3 (1-indexed) is where the call starts"
);
let offset = BAKUHATSU.find("kore_wa_sonzai_shinai").expect("in fixture");
assert!(
offset > KOTAE.len(),
"kotae.b could answer for byte {offset}"
);
assert!(
offset > TWO_IMPORTS.len(),
"the entry file could answer for byte {offset}"
);
}
#[test]
fn a_raise_in_the_entry_file_names_the_entry_file() {
let err = run_named(
"honmono.b",
"def f()\n 1\nend\n\n# waive B0001: a raise at runtime\nnani_mo_nai()\n",
);
assert_eq!(
err.to_string(),
"runtime error: honmono.b:6:1: unbound symbol `nani_mo_nai`"
);
}
#[test]
fn a_raise_inside_an_annotated_def_still_names_its_line() {
let err = run_named(
"chuu.b",
"# waive B0001: a raise at runtime\ndef f(a: Int) -> Int\n mada_nai(a)\nend\n\nf(1)\n",
);
assert_eq!(
err.to_string(),
"runtime error: chuu.b:3:3: unbound symbol `mada_nai`\n \
at f (chuu.b:3:3)\n \
at <top level> (chuu.b:6:1)"
);
}
#[test]
fn a_raise_in_a_callee_from_another_file_names_the_callees_file() {
let err = run_named(
"yobidashi.b",
"use(\"bakuhatsu2\")\n# pad: 47's in range\nbakuhatsu2::yobu\n",
);
assert_eq!(
err.to_string(),
"runtime error: bakuhatsu2.b:3:3: unbound symbol `kore_wa_sonzai_shinai`\n \
at bakuhatsu2::yobu (bakuhatsu2.b:3:3)\n \
at <top level> (yobidashi.b:3:1)"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn int(src: &str) -> i64 {
match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
Value::Int(v) => v,
other => panic!("{src:?} produced {other:?}"),
}
}
#[test]
fn annotating_buys_analysis_and_changes_nothing_else() {
let plain = run("def add(a, b)\n a + b\nend\nadd(2, 3)").expect("plain");
let typed = run("def add(a: Int, b: Int) -> Int\n a + b\nend\nadd(2, 3)").expect("typed");
assert!(matches!(plain.value, Value::Int(5)));
assert!(
matches!(typed.value, Value::Int(5)),
"the annotated program must compute the same answer"
);
assert_eq!(plain.visited, 0, "no annotations means no analysis");
assert!(
typed.visited > 0,
"an annotation must actually buy analysis, not just decorate"
);
assert_eq!(plain.typed_decls, 0);
assert_eq!(typed.typed_decls, 1);
}
#[test]
fn a_type_error_is_reported_and_the_program_does_not_run() {
let err = run("def add(a: Int, b: Int) -> Str\n a + b\nend\nadd(1, 2)")
.expect_err("a declared Str return from an Int body must be rejected");
assert!(
matches!(err, RunError::Types(ref d) if !d.is_empty()),
"expected type diagnostics, got {err}"
);
}
#[test]
fn the_same_program_without_annotations_runs() {
assert_eq!(int("def add(a, b)\n a + b\nend\nadd(1, 2)"), 3);
}
#[test]
fn a_parse_error_is_reported_as_one() {
assert!(matches!(run("def (").unwrap_err(), RunError::Parse(_)));
}
#[test]
fn a_runtime_error_is_reported_as_one() {
let err = run("throw(error(\"boom\"))").expect_err("a raise");
assert!(matches!(err, RunError::Eval(_)), "got {err}");
}
#[test]
fn the_pipeline_reaches_both_runtime_layers() {
assert_eq!(int("6 % 3"), 0);
assert_eq!(int("7 % 3"), 1);
assert_eq!(int("2 + 3 * 4"), 14);
}
#[test]
fn the_deleted_round_trip_agreed_with_the_direct_lowering() {
let corpus = [
"def add(a, b)\n a + b\nend\nadd(2, 3)",
"def fact(n)\n if n < 2\n 1\n else\n n * fact(n - 1)\n end\nend\nfact(5)",
"def f(a, b)\n c = a + b\n c * 2\nend\nf(1, 2)",
"defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)",
"\"a string with spaces, a ( and a )\"",
"def g(a: Int) -> Int\n a + 1\nend\ng(1)",
"6 % 3",
"1.5 + 2.25",
];
for src in corpus {
let erased = crate::to_sexps(&erase_types(&parse_tree(src).expect("parse")));
let direct: Vec<Sexp> = crate::lower_to_spanned(&erased)
.iter()
.map(tatara_lisp::Spanned::to_sexp)
.collect();
assert_eq!(direct, erased, "the direct lowering must be the identity");
let text = erased
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
let round_tripped: Vec<Sexp> = tatara_lisp::read_spanned(&text)
.unwrap_or_else(|e| panic!("{src:?}: the old path could not read back: {e:?}"))
.iter()
.map(tatara_lisp::Spanned::to_sexp)
.collect();
assert_eq!(
round_tripped, erased,
"{src:?}: the old print-and-reparse path changed the tree"
);
}
}
#[test]
fn the_round_trip_is_not_the_identity_in_general() {
let tree = Sexp::List(vec![
Sexp::Atom(tatara_lisp::Atom::Symbol("f".into())),
Sexp::Atom(tatara_lisp::Atom::Symbol("a b".into())),
]);
let text = tree.to_string();
let back: Vec<Sexp> = tatara_lisp::read_spanned(&text)
.expect("it reads back cleanly — that IS the problem")
.iter()
.map(tatara_lisp::Spanned::to_sexp)
.collect();
assert_ne!(
back,
vec![tree.clone()],
"if print-then-read became inverse over symbols, the class would be \
closed upstream and this test should be deleted rather than relaxed"
);
let direct: Vec<Sexp> = crate::lower_to_spanned(std::slice::from_ref(&tree))
.iter()
.map(tatara_lisp::Spanned::to_sexp)
.collect();
assert_eq!(direct, vec![tree]);
}
}
#[cfg(test)]
mod macro_tests {
use super::*;
fn int(src: &str) -> i64 {
match run(src).unwrap_or_else(|e| panic!("{src:?}: {e}")).value {
Value::Int(v) => v,
other => panic!("{src:?} produced {other:?}"),
}
}
#[test]
fn a_macro_expands_and_runs() {
assert_eq!(
int("defmacro double(x)\n quote\n unquote(x) + unquote(x)\n end\nend\ndouble(21)"),
42
);
}
#[test]
fn a_macro_operates_on_syntax_not_values() {
assert_eq!(
int("defmacro sq(e)\n quote\n unquote(e) * unquote(e)\n end\nend\nsq(2 + 3)"),
25,
"the argument form `2 + 3` must be substituted twice"
);
}
#[test]
fn a_runaway_macro_fails_the_compilation_rather_than_the_process() {
let err =
run("defmacro forever(x)\n quote\n forever(unquote(x))\n end\nend\nforever(1)")
.expect_err("a self-referential macro must be rejected");
let msg = err.to_string();
assert!(
msg.contains("forever") && msg.contains("expansion"),
"the error must name the macro and the limit it hit: {msg}"
);
assert!(
msg.contains("<anonymous>:6:1"),
"and must place the call that ran away — line 6 is `forever(1)`: {msg}"
);
}
}
#[cfg(test)]
mod input_tests {
use super::*;
use crate::inputs::{Declaration, Inputs};
const SCHEMA: &[u8] = b"3";
fn with_schema(src: &str) -> Result<Run, RunError> {
let hash = Inputs::hash_of(SCHEMA);
let mut inputs = Inputs::new();
inputs
.bind(
&Declaration {
name: "schema".to_string(),
hash,
},
SCHEMA.to_vec(),
)
.expect("bind");
run_with_inputs(src, inputs)
}
fn decl_line() -> String {
let mut s = String::from("definput(\"schema\", \"");
s.push_str(&Inputs::hash_of(SCHEMA));
s.push_str("\")\n");
s
}
#[test]
fn a_macro_can_read_a_declared_build_input() {
let src = decl_line() + "input(\"schema\")";
let out = with_schema(&src).expect("run");
assert!(
matches!(out.value, Value::Str(ref s) if &**s == "3"),
"got {:?}",
out.value
);
}
#[test]
fn an_undeclared_input_is_an_error() {
let err = with_schema("input(\"not_declared\")").expect_err("must fail");
let msg = err.to_string();
assert!(msg.contains("not_declared"), "must name it: {msg}");
assert!(msg.contains("definput"), "and say how to declare it: {msg}");
}
#[cfg(not(feature = "sys"))]
#[test]
fn there_is_no_ambient_file_read() {
for attempt in [
"read_file(\"/etc/passwd\")",
"File(\"/etc/passwd\")",
"slurp(\"/etc/passwd\")",
"open(\"/etc/passwd\")",
] {
let err = with_schema(attempt).expect_err("must not resolve");
assert!(
err.to_string().contains("unbound"),
"{attempt} must be UNBOUND — a capability removed by absence, \
not guarded by a check: {err}"
);
}
}
#[cfg(feature = "sys")]
#[test]
fn sys_read_file_is_the_trusted_cli_only_exception() {
let err = with_schema("definitely_not_a_primitive(\"x\")").expect_err("must not resolve");
assert!(err.to_string().contains("unbound"), "{err}");
assert!(
with_schema("read_file(\"/etc/passwd\")").is_ok(),
"with `sys` on, read_file is the trusted CLI surface"
);
let out = with_schema("input(\"schema\")").expect("run");
assert!(
matches!(out.value, Value::Str(ref s) if &**s == "3"),
"input() still binds beside the sys surface: {:?}",
out.value
);
}
#[test]
fn a_declared_input_with_no_bytes_supplied_fails() {
let src = decl_line() + "input(\"schema\")";
assert!(run(&src).is_err(), "no bytes were supplied");
}
}
#[cfg(test)]
mod tier2_tests {
use super::*;
use crate::inputs::{Declaration, Inputs};
#[test]
fn a_macro_generates_code_from_a_schema() {
let schema = b"7";
let mut inputs = Inputs::new();
inputs
.bind(
&Declaration {
name: "arity".to_string(),
hash: Inputs::hash_of(schema),
},
schema.to_vec(),
)
.expect("bind");
let mut src = String::from("definput(\"arity\", \"");
src.push_str(&Inputs::hash_of(schema));
src.push_str("\")\n");
src.push_str(
"defmacro from_schema()\n quote\n unquote(to_int(input(\"arity\")))\n end\nend\n\
from_schema() * 6",
);
let out = run_with_inputs(&src, inputs).expect("run");
assert!(
matches!(out.value, Value::Int(42)),
"the schema's 7 must reach the generated code: got {:?}",
out.value
);
}
}