use std::collections::HashMap;
use std::f64::consts::{LN_10, LN_2};
use std::sync::Arc;
use sqlparser::ast::{
BinaryOperator, DataType, Expr, Function, FunctionArg, FunctionArgExpr, FunctionArguments,
ObjectNamePart, UnaryOperator,
};
use crate::colref::{ColRef, IdentCasing, Match};
use crate::constructors::{
add, as_const, div, finite_num, func, func1, is_zero, mul, neg, num, one, sign, square, sub,
zero,
};
use crate::error::{DiffError, Result};
const SUPPORTED: &str = "ddx differentiates the operators + - * /, unary calls to \
sin/cos/tan/asin/acos/atan/exp/ln/log2/log10/sqrt/sinh/cosh/tanh/abs, power(...) with a \
constant base or exponent, casts to a numeric type, and column/literal leaves";
pub type Rule = Arc<dyn Fn(&Expr) -> Result<Expr> + Send + Sync>;
#[derive(Clone)]
pub struct RuleRegistry {
unary: HashMap<String, Rule>,
}
impl Default for RuleRegistry {
fn default() -> Self {
Self::new()
}
}
fn rule(f: impl Fn(&Expr) -> Expr + Send + Sync + 'static) -> Rule {
Arc::new(move |u| Ok(f(u)))
}
impl RuleRegistry {
pub fn new() -> Self {
let mut unary: HashMap<String, Rule> = HashMap::new();
unary.insert("sin".into(), rule(|u| func1("cos", u.clone())));
unary.insert("cos".into(), rule(|u| neg(func1("sin", u.clone()))));
unary.insert(
"tan".into(),
rule(|u| div(one(), square(func1("cos", u.clone())))),
);
unary.insert(
"asin".into(),
rule(|u| div(one(), func1("sqrt", sub(one(), square(u.clone()))))),
);
unary.insert(
"acos".into(),
rule(|u| neg(div(one(), func1("sqrt", sub(one(), square(u.clone())))))),
);
unary.insert(
"atan".into(),
rule(|u| div(one(), add(one(), square(u.clone())))),
);
unary.insert("exp".into(), rule(|u| func1("exp", u.clone())));
unary.insert("ln".into(), rule(|u| div(one(), u.clone())));
unary.insert(
"log2".into(),
rule(|u| div(one(), mul(u.clone(), num(LN_2)))),
);
unary.insert(
"log10".into(),
rule(|u| div(one(), mul(u.clone(), num(LN_10)))),
);
unary.insert(
"sqrt".into(),
rule(|u| div(one(), mul(num(2.0), func1("sqrt", u.clone())))),
);
unary.insert("sinh".into(), rule(|u| func1("cosh", u.clone())));
unary.insert("cosh".into(), rule(|u| func1("sinh", u.clone())));
unary.insert(
"tanh".into(),
rule(|u| sub(one(), square(func1("tanh", u.clone())))),
);
unary.insert("abs".into(), rule(|u| sign(u.clone())));
RuleRegistry { unary }
}
pub fn register(&mut self, name: &str, rule: Rule) {
self.unary.insert(name.to_ascii_lowercase(), rule);
}
fn lookup(&self, name: &str) -> Option<&Rule> {
self.unary.get(name)
}
}
type Leaf<'a> = dyn Fn(&ColRef) -> Result<Expr> + 'a;
pub fn differentiate(
expr: &Expr,
wrt: &ColRef,
casing: IdentCasing,
reg: &RuleRegistry,
) -> Result<Expr> {
let leaf = |c: &ColRef| match c.classify(wrt, casing) {
Match::Is => Ok(one()),
Match::Not => Ok(zero()),
Match::Ambiguous => Err(DiffError::AmbiguousColumn(format!(
"occurrence of `{}` cannot be matched against differentiation \
variable `{}` — fully qualify it",
c.display(),
wrt.display()
))),
};
linearize(expr, &leaf, reg)
}
pub fn jvp(
expr: &Expr,
seeds: &[(ColRef, Expr)],
casing: IdentCasing,
reg: &RuleRegistry,
) -> Result<Expr> {
let leaf = |c: &ColRef| {
for (col, tangent) in seeds {
match c.classify(col, casing) {
Match::Is => return Ok(tangent.clone()),
Match::Ambiguous => {
return Err(DiffError::AmbiguousColumn(format!(
"occurrence of `{}` cannot be matched against seeded \
column `{}` — fully qualify it",
c.display(),
col.display()
)))
}
Match::Not => continue,
}
}
Ok(zero())
};
linearize(expr, &leaf, reg)
}
fn linearize(expr: &Expr, leaf: &Leaf, reg: &RuleRegistry) -> Result<Expr> {
match expr {
Expr::Identifier(_) | Expr::CompoundIdentifier(_) => {
let cr = ColRef::from_expr(expr)
.ok_or_else(|| DiffError::Internal("column expr yielded no ColRef".into()))?;
leaf(&cr)
}
Expr::Value(_) => Ok(zero()),
Expr::Nested(inner) => linearize(inner, leaf, reg),
Expr::Cast {
kind,
expr: inner,
data_type,
array,
format,
} => {
if !is_numeric_type(data_type) {
return Err(DiffError::NotImplemented(format!(
"differentiation through a cast to non-numeric type `{data_type}` \
is not supported"
)));
}
let du = linearize(inner, leaf, reg)?;
Ok(Expr::Cast {
kind: kind.clone(),
expr: Box::new(du),
data_type: data_type.clone(),
array: *array,
format: format.clone(),
})
}
Expr::UnaryOp {
op: UnaryOperator::Minus,
expr: inner,
} => Ok(neg(linearize(inner, leaf, reg)?)),
Expr::UnaryOp {
op: UnaryOperator::Plus,
expr: inner,
} => linearize(inner, leaf, reg),
Expr::BinaryOp { left, op, right } => linearize_binary(left, op, right, leaf, reg),
Expr::Function(f) => linearize_function(f, leaf, reg),
other => Err(DiffError::NotImplemented(format!(
"this expression cannot be differentiated: `{other}`. {SUPPORTED}; CASE, \
comparisons, subqueries, window functions, and string/temporal expressions are \
not differentiable"
))),
}
}
fn linearize_binary(
left: &Expr,
op: &BinaryOperator,
right: &Expr,
leaf: &Leaf,
reg: &RuleRegistry,
) -> Result<Expr> {
let da = linearize(left, leaf, reg)?;
let db = linearize(right, leaf, reg)?;
match op {
BinaryOperator::Plus => Ok(add(da, db)),
BinaryOperator::Minus => Ok(sub(da, db)),
BinaryOperator::Multiply => Ok(add(mul(da, right.clone()), mul(left.clone(), db))),
BinaryOperator::Divide => {
let numerator = sub(mul(da, right.clone()), mul(left.clone(), db));
Ok(div(numerator, square(right.clone())))
}
other => Err(DiffError::NotImplemented(format!(
"the operator `{other}` is not differentiable. {SUPPORTED}"
))),
}
}
fn linearize_function(f: &Function, leaf: &Leaf, reg: &RuleRegistry) -> Result<Expr> {
let name = simple_func_name(f).ok_or_else(|| {
DiffError::NotImplemented(format!(
"cannot differentiate the call `{f}`: only an unqualified function name has a \
differentiation rule (a schema-qualified or otherwise complex name is left alone)"
))
})?;
let args = positional_args(f).ok_or_else(|| {
DiffError::NotImplemented(format!(
"function `{name}` has non-positional arguments, which are not differentiable"
))
})?;
if name == "power" || name == "pow" {
return linearize_power(&name, &args, leaf, reg);
}
if args.len() != 1 {
return Err(DiffError::NotImplemented(format!(
"no differentiation rule for `{name}` with {} arguments: the built-in function \
rules are unary, and `power` is the only two-argument rule",
args.len()
)));
}
let u = args[0];
let du = linearize(u, leaf, reg)?;
if is_zero(&du) {
return Ok(zero());
}
let outer = reg.lookup(&name).ok_or_else(|| {
DiffError::NotImplemented(format!(
"no differentiation rule for function `{name}`. {SUPPORTED}. Register a custom \
rule with `Ddx::register(\"{name}\", ...)`"
))
})?(u)?;
Ok(mul(outer, du))
}
fn linearize_power(name: &str, args: &[&Expr], leaf: &Leaf, reg: &RuleRegistry) -> Result<Expr> {
if args.len() != 2 {
return Err(DiffError::NotImplemented(format!(
"{name}() expects exactly two arguments"
)));
}
let base = args[0];
let exponent = args[1];
match (as_const(base), as_const(exponent)) {
(_, Some(c)) => {
let dbase = linearize(base, leaf, reg)?;
if is_zero(&dbase) {
return Ok(zero());
}
let outer = mul(
finite_num(c)?,
func("power", vec![base.clone(), finite_num(c - 1.0)?]),
);
Ok(mul(outer, dbase))
}
(Some(a), None) => {
let dexp = linearize(exponent, leaf, reg)?;
if is_zero(&dexp) {
return Ok(zero());
}
let outer = mul(
func("power", vec![base.clone(), exponent.clone()]),
finite_num(a.ln())?,
);
Ok(mul(outer, dexp))
}
(None, None) => Err(DiffError::NotImplemented(
"cannot differentiate `power(base, exponent)` when both the base and the exponent \
depend on the differentiation variable; ddx handles it only when one side is a \
constant (e.g. `power(x, 2)` or `power(2, x)`). For the general u^v case, rewrite \
it as `exp(exponent * ln(base))` when the base is positive"
.into(),
)),
}
}
fn simple_func_name(f: &Function) -> Option<String> {
match f.name.0.as_slice() {
[ObjectNamePart::Identifier(id)] => Some(id.value.to_ascii_lowercase()),
_ => None,
}
}
pub(crate) fn positional_args(f: &Function) -> Option<Vec<&Expr>> {
match &f.args {
FunctionArguments::List(list) => {
let mut out = Vec::with_capacity(list.args.len());
for arg in &list.args {
match arg {
FunctionArg::Unnamed(FunctionArgExpr::Expr(e)) => out.push(e),
_ => return None,
}
}
Some(out)
}
_ => None,
}
}
fn is_numeric_type(dt: &DataType) -> bool {
matches!(
dt,
DataType::Numeric(_)
| DataType::Decimal(_)
| DataType::BigNumeric(_)
| DataType::BigDecimal(_)
| DataType::Dec(_)
| DataType::Float(_)
| DataType::FloatUnsigned(_)
| DataType::Float4
| DataType::Float32
| DataType::Float64
| DataType::Real
| DataType::RealUnsigned
| DataType::Float8
| DataType::Double(_)
| DataType::DoubleUnsigned(_)
| DataType::DoublePrecision
| DataType::DoublePrecisionUnsigned
| DataType::TinyInt(_)
| DataType::TinyIntUnsigned(_)
| DataType::UTinyInt
| DataType::Int2(_)
| DataType::Int2Unsigned(_)
| DataType::SmallInt(_)
| DataType::SmallIntUnsigned(_)
| DataType::USmallInt
| DataType::MediumInt(_)
| DataType::MediumIntUnsigned(_)
| DataType::Int(_)
| DataType::Int4(_)
| DataType::Int8(_)
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::Int128
| DataType::Int256
| DataType::Integer(_)
| DataType::IntUnsigned(_)
| DataType::Int4Unsigned(_)
| DataType::IntegerUnsigned(_)
| DataType::HugeInt
| DataType::UHugeInt
| DataType::UInt8
| DataType::UInt16
| DataType::UInt32
| DataType::UInt64
| DataType::UInt128
| DataType::UInt256
| DataType::BigInt(_)
| DataType::BigIntUnsigned(_)
| DataType::UBigInt
| DataType::Int8Unsigned(_)
| DataType::Signed
| DataType::SignedInteger
| DataType::Unsigned
| DataType::UnsignedInteger
)
}