use std::fmt::{self, Debug};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Symbol(i32);
impl Symbol {
#[must_use]
#[inline]
pub const fn value(self) -> i32 {
self.0
}
#[must_use]
#[inline]
pub(crate) const fn from_raw(id: i32) -> Self {
Self(id)
}
}
impl From<Symbol> for i32 {
#[inline]
fn from(symbol: Symbol) -> Self {
symbol.0
}
}
impl fmt::Display for Symbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "sym{}", self.0)
}
}
#[must_use]
#[inline]
pub(crate) fn finite_or_zero(v: f32) -> f32 {
if v.is_finite() { v } else { 0.0 }
}
pub trait FunctionSet: Send + Sync + Debug {
fn num_functions(&self) -> usize;
fn arity(&self, symbol: Symbol) -> usize;
fn max_arity(&self) -> usize;
fn apply(&self, symbol: Symbol, args: &[f32]) -> f32;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ArithmeticFunctionSet;
impl ArithmeticFunctionSet {
pub const ARITIES: [usize; 8] = [2, 2, 2, 2, 1, 1, 1, 0];
}
impl FunctionSet for ArithmeticFunctionSet {
fn num_functions(&self) -> usize {
Self::ARITIES.len()
}
fn arity(&self, symbol: Symbol) -> usize {
usize::try_from(symbol.value())
.ok()
.and_then(|i| Self::ARITIES.get(i).copied())
.unwrap_or(0)
}
fn max_arity(&self) -> usize {
2
}
fn apply(&self, symbol: Symbol, args: &[f32]) -> f32 {
let a = args.first().copied().unwrap_or(0.0);
let b = args.get(1).copied().unwrap_or(0.0);
match symbol.value() {
0 => a + b,
1 => a - b,
2 => a * b,
3 => {
if b.abs() < 1e-6 {
a
} else {
a / b
}
}
4 => a.sin(),
5 => a.cos(),
6 => a.tanh(),
7 => 1.0,
_ => 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arithmetic_apply_matches_inline_cgp_match() {
let fs = ArithmeticFunctionSet;
let (a, b) = (0.7_f32, 0.25_f32);
let inline = |func: usize, a: f32, b: f32| -> f32 {
match func {
0 => a + b,
1 => a - b,
2 => a * b,
3 => {
if b.abs() < 1e-6 {
a
} else {
a / b
}
}
4 => a.sin(),
5 => a.cos(),
6 => a.tanh(),
7 => 1.0,
_ => 0.0,
}
};
for func in 0..8 {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let sym = Symbol::from_raw(func as i32);
let arity = fs.arity(sym);
let arg_buf = [a, b];
let got = fs.apply(sym, &arg_buf[..arity]);
let want = inline(func, a, b);
approx::assert_relative_eq!(got, want, epsilon = 1e-7);
}
}
#[test]
fn apply_handles_short_args_without_panic() {
let fs = ArithmeticFunctionSet;
approx::assert_relative_eq!(fs.apply(Symbol::from_raw(0), &[]), 0.0, epsilon = 1e-7);
approx::assert_relative_eq!(fs.apply(Symbol::from_raw(1), &[5.0]), 5.0, epsilon = 1e-7);
}
#[test]
fn apply_propagates_non_finite_arguments() {
let fs = ArithmeticFunctionSet;
assert!(fs.apply(Symbol::from_raw(0), &[f32::NAN, 1.0]).is_nan());
assert!(
fs.apply(Symbol::from_raw(2), &[f32::INFINITY, 2.0])
.is_infinite()
);
}
#[test]
fn protected_div_guards_small_denominator() {
let fs = ArithmeticFunctionSet;
let got = fs.apply(Symbol::from_raw(3), &[3.0, 1e-9]);
approx::assert_relative_eq!(got, 3.0, epsilon = 1e-7);
}
#[test]
fn out_of_range_opcode_is_inert() {
let fs = ArithmeticFunctionSet;
assert_eq!(fs.arity(Symbol::from_raw(99)), 0);
assert_eq!(fs.arity(Symbol::from_raw(-1)), 0);
approx::assert_relative_eq!(fs.apply(Symbol::from_raw(99), &[]), 0.0, epsilon = 1e-7);
}
#[test]
fn arities_and_counts() {
let fs = ArithmeticFunctionSet;
assert_eq!(fs.num_functions(), 8);
assert_eq!(fs.max_arity(), 2);
assert_eq!(fs.arity(Symbol::from_raw(0)), 2);
assert_eq!(fs.arity(Symbol::from_raw(6)), 1);
assert_eq!(fs.arity(Symbol::from_raw(7)), 0);
}
}