use std::fmt;
use crate::errors::AlkahestError;
pub fn known_func_arity(name: &str) -> Option<(usize, usize)> {
let one = Some((1, 1));
match name {
"sin" | "cos" | "tan" | "sinh" | "cosh" | "tanh" | "asin" | "acos" | "atan" | "asinh"
| "acosh" | "atanh" | "exp" | "log" | "ln" | "sqrt" => one,
"abs" | "sign" | "floor" | "ceil" | "round" | "heaviside" | "diracdelta" | "conjugate"
| "re" | "im" | "arg" => one,
"erf" | "erfc" | "gamma" | "digamma" | "trigamma" | "lambert_w" | "bessel_j0"
| "bessel_j1" | "EllipticK" | "Ei" | "li" | "Si" | "Ci" | "Shi" | "Chi" | "fresnels"
| "fresnelc" | "dilog" => one,
"EllipticE" => Some((1, 2)),
"EllipticF" | "atan2" => Some((2, 2)),
"EllipticPi" => Some((3, 3)),
"min" | "max" => Some((2, usize::MAX)),
_ => None,
}
}
pub fn func_arity_ok(name: &str, n: usize) -> bool {
match known_func_arity(name) {
Some((lo, hi)) => (lo..=hi).contains(&n),
None => true,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FuncArityError {
name: String,
got: usize,
min: usize,
max: usize,
}
impl FuncArityError {
pub fn check(name: &str, got: usize) -> Result<(), FuncArityError> {
match known_func_arity(name) {
Some((min, max)) if !(min..=max).contains(&got) => Err(FuncArityError {
name: name.to_string(),
got,
min,
max,
}),
_ => Ok(()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn got(&self) -> usize {
self.got
}
pub fn expected(&self) -> (usize, usize) {
(self.min, self.max)
}
pub fn expected_text(&self) -> String {
if self.max == usize::MAX {
format!("at least {}", self.min)
} else if self.min == self.max {
format!("{}", self.min)
} else if self.max == self.min + 1 {
format!("{} or {}", self.min, self.max)
} else {
format!("{} to {}", self.min, self.max)
}
}
}
impl fmt::Display for FuncArityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let plural = if self.min == 1 && self.max == 1 {
"argument"
} else {
"arguments"
};
write!(
f,
"{} takes {} {plural}, got {}",
self.name,
self.expected_text(),
self.got
)
}
}
impl std::error::Error for FuncArityError {}
impl AlkahestError for FuncArityError {
fn code(&self) -> &'static str {
"E-POOL-002"
}
fn remediation(&self) -> Option<&'static str> {
Some(
"a built-in function name has a fixed number of arguments; \
use a different name for a user-defined function of another arity",
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_covers_the_registered_primitives() {
let reg = crate::primitive::PrimitiveRegistry::dispatch_registry();
for row in reg.coverage_report().rows {
assert!(
known_func_arity(&row.name).is_some(),
"registered primitive `{}` has no arity entry",
row.name
);
}
}
#[test]
fn check_accepts_and_refuses() {
assert!(FuncArityError::check("sin", 1).is_ok());
assert!(FuncArityError::check("f", 0).is_ok());
assert!(FuncArityError::check("f", 7).is_ok());
assert!(FuncArityError::check("EllipticE", 2).is_ok());
assert!(FuncArityError::check("min", 5).is_ok());
let e = FuncArityError::check("sqrt", 0).unwrap_err();
assert_eq!(e.code(), "E-POOL-002");
assert_eq!(e.to_string(), "sqrt takes 1 argument, got 0");
let e = FuncArityError::check("EllipticE", 3).unwrap_err();
assert_eq!(e.to_string(), "EllipticE takes 1 or 2 arguments, got 3");
let e = FuncArityError::check("max", 1).unwrap_err();
assert_eq!(e.to_string(), "max takes at least 2 arguments, got 1");
}
}
#[cfg(test)]
mod wrong_arity_sweep {
use super::known_func_arity;
use crate::deriv::log::DerivedExpr;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
const NAMES: &[&str] = &[
"sin",
"cos",
"tan",
"sinh",
"cosh",
"tanh",
"asin",
"acos",
"atan",
"asinh",
"acosh",
"atanh",
"exp",
"log",
"ln",
"sqrt",
"abs",
"sign",
"floor",
"ceil",
"round",
"heaviside",
"diracdelta",
"conjugate",
"re",
"im",
"arg",
"erf",
"erfc",
"gamma",
"digamma",
"trigamma",
"lambert_w",
"bessel_j0",
"bessel_j1",
"EllipticK",
"EllipticE",
"EllipticF",
"EllipticPi",
"Ei",
"li",
"Si",
"Ci",
"Shi",
"Chi",
"fresnels",
"fresnelc",
"dilog",
"atan2",
"min",
"max",
];
type Op<'a> = (&'static str, Box<dyn Fn() + 'a>);
fn ops(pool: &ExprPool, e: ExprId, x: ExprId) -> Vec<Op<'_>> {
let zero = pool.integer(0_i32);
vec![
("str", Box::new(move || drop(pool.display(e).to_string()))),
(
"latex",
Box::new(move || drop(crate::kernel::render_latex(e, pool))),
),
(
"unicode",
Box::new(move || drop(crate::kernel::render_unicode(e, pool))),
),
(
"diff",
Box::new(move || drop(crate::diff::diff(e, x, pool))),
),
(
"grad",
Box::new(move || drop(crate::diff::grad(e, &[x], pool))),
),
(
"simplify",
Box::new(move || drop(crate::simplify::simplify(e, pool))),
),
(
"simplify_egraph",
Box::new(move || drop(crate::simplify::simplify_egraph(e, pool))),
),
(
"integrate",
Box::new(move || drop(crate::integrate::integrate(e, x, pool))),
),
(
"series",
Box::new(move || drop(crate::calculus::series(e, x, zero, 4, pool))),
),
(
"limit",
Box::new(move || {
drop(crate::calculus::limit(
e,
x,
zero,
crate::calculus::LimitDirection::Plus,
pool,
))
}),
),
(
"to_lean",
Box::new(move || {
drop(crate::lean::emit_lean_expr(&DerivedExpr::new(e), pool));
if let Ok(d) = crate::diff::diff(e, x, pool) {
drop(crate::lean::emit_lean_expr(&d, pool));
}
}),
),
(
"eval",
Box::new(move || {
let env: HashMap<ExprId, f64> = [(x, 0.5)].into_iter().collect();
drop(crate::eval::eval_f64(e, pool, &env));
let _ = crate::jit::eval_interp(e, &env, pool);
}),
),
]
}
#[test]
fn no_operation_panics_on_a_wrong_arity_builtin() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut failures = Vec::new();
for &name in NAMES {
let (lo, hi) = known_func_arity(name).expect(name);
for n in 0..=4usize {
if (lo..=hi).contains(&n) {
continue;
}
let e = pool.intern(ExprData::Func {
name: name.to_string(),
args: vec![x; n],
});
let wrapped = pool.add(vec![pool.mul(vec![pool.integer(2_i32), e]), x]);
for target in [e, wrapped] {
for (op, run) in ops(&pool, target, x) {
if catch_unwind(AssertUnwindSafe(run)).is_err() {
failures.push(format!("{op}({name}/{n})"));
}
}
}
}
}
assert!(failures.is_empty(), "panicked: {}", failures.join(", "));
}
}