use tatara_lisp::{Span, Spanned, SpannedForm};
use tatara_lisp_eval::ffi::Arity;
use tatara_lisp_eval::EvalError;
#[must_use]
pub fn describe(e: &EvalError, forms: &[Spanned]) -> String {
match e {
EvalError::ArityMismatch {
fn_name,
expected,
got,
at,
} => {
let name = callee_at(forms, *at)
.or_else(|| (!fn_name.starts_with('<')).then(|| fn_name.to_string()));
let who = name.map_or_else(|| "this function".to_string(), |n| format!("`{n}`"));
format!("{who} expects {}, got {got}", arguments(expected))
}
other => other.short_message(),
}
}
#[must_use]
pub fn arguments(arity: &Arity) -> String {
let n = |k: usize| {
if k == 1 {
"1 argument".to_string()
} else {
format!("{k} arguments")
}
};
match *arity {
Arity::Exact(k) => n(k),
Arity::AtLeast(k) => format!("at least {}", n(k)),
Arity::Range(lo, hi) => format!("{lo} to {}", n(hi)),
Arity::Any => "any number of arguments".to_string(),
}
}
fn callee_at(forms: &[Spanned], at: Span) -> Option<String> {
if at.is_synthetic() {
return None;
}
forms.iter().find_map(|f| find(f, at))
}
fn find(node: &Spanned, at: Span) -> Option<String> {
if node.span.start > at.start || node.span.end < at.end {
return None;
}
match &node.form {
SpannedForm::List(items) => {
if node.span == at {
if let Some(h) = items.first().and_then(Spanned::as_symbol) {
return Some(h.to_string());
}
}
items.iter().find_map(|i| find(i, at))
}
SpannedForm::Quote(i)
| SpannedForm::Quasiquote(i)
| SpannedForm::Unquote(i)
| SpannedForm::UnquoteSplice(i) => find(i, at),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arity_reads_as_words() {
assert_eq!(arguments(&Arity::Exact(2)), "2 arguments");
assert_eq!(arguments(&Arity::Exact(1)), "1 argument");
assert_eq!(arguments(&Arity::AtLeast(1)), "at least 1 argument");
assert_eq!(arguments(&Arity::Range(1, 3)), "1 to 3 arguments");
}
#[test]
fn a_wrong_argument_count_names_the_function() {
let err = crate::pipeline::run("def add(a, b)\n a + b\nend\n\nadd(1, 2, 3)\n")
.expect_err("three arguments to a two-parameter def");
assert_eq!(
err.to_string(),
"runtime error: <anonymous>:5:1: `add` expects 2 arguments, got 3"
);
}
#[test]
fn a_lambda_bound_to_a_name_is_named_by_the_call() {
let err = crate::pipeline::run("f = fn(a) a end\nf(1, 2)\n").expect_err("arity");
assert_eq!(
err.to_string(),
"runtime error: <anonymous>:2:1: `f` expects 1 argument, got 2"
);
}
}