use crate::ast::{BinOp, Builtin, Expr, ExprLit, UnOp};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonDifferentiable {
pub construct: String,
pub path: String,
}
fn lit_f(v: f64) -> Expr {
Expr::Lit(ExprLit::Float(v))
}
pub fn differentiate(e: &Expr, wrt: &str) -> Result<Expr, NonDifferentiable> {
diff_at(e, wrt, String::new())
}
fn diff_at(e: &Expr, wrt: &str, path: String) -> Result<Expr, NonDifferentiable> {
match e {
Expr::Lit(ExprLit::Int(_)) | Expr::Lit(ExprLit::Float(_)) => Ok(lit_f(0.0)),
Expr::Lit(ExprLit::Bool(_)) => Err(NonDifferentiable {
construct: "bool literal".into(),
path,
}),
Expr::Lit(ExprLit::Str(_)) => Err(NonDifferentiable {
construct: "string literal".into(),
path,
}),
Expr::Ref(name) => Ok(if name == wrt { lit_f(1.0) } else { lit_f(0.0) }),
Expr::Unary(UnOp::Neg, inner) => Ok(Expr::Unary(
UnOp::Neg,
Box::new(diff_at(inner, wrt, format!("{path}.arg"))?),
)),
Expr::Unary(UnOp::Not, _) => Err(NonDifferentiable {
construct: "logical not".into(),
path,
}),
Expr::Binary(op, l, r) => {
let dl = || diff_at(l, wrt, format!("{path}.lhs"));
let dr = || diff_at(r, wrt, format!("{path}.rhs"));
match op {
BinOp::Add => Ok(Expr::Binary(BinOp::Add, Box::new(dl()?), Box::new(dr()?))),
BinOp::Sub => Ok(Expr::Binary(BinOp::Sub, Box::new(dl()?), Box::new(dr()?))),
BinOp::Mul => Ok(Expr::Binary(
BinOp::Add,
Box::new(Expr::Binary(BinOp::Mul, Box::new(dl()?), r.clone())),
Box::new(Expr::Binary(BinOp::Mul, l.clone(), Box::new(dr()?))),
)),
BinOp::Div => Ok(Expr::Binary(
BinOp::Div,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Binary(BinOp::Mul, Box::new(dl()?), r.clone())),
Box::new(Expr::Binary(BinOp::Mul, l.clone(), Box::new(dr()?))),
)),
Box::new(Expr::Binary(BinOp::Mul, r.clone(), r.clone())),
)),
BinOp::Mod => Err(NonDifferentiable {
construct: "mod".into(),
path,
}),
BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
Err(NonDifferentiable {
construct: "comparison".into(),
path,
})
}
BinOp::And | BinOp::Or => Err(NonDifferentiable {
construct: "logical connective".into(),
path,
}),
}
}
Expr::Call(Builtin::AsFloat, args) if args.len() == 1 => {
diff_at(&args[0], wrt, format!("{path}.arg"))
}
Expr::Call(b, _) => Err(NonDifferentiable {
construct: format!("builtin {}", b.surface()),
path,
}),
Expr::Field(_, name) => Err(NonDifferentiable {
construct: format!("field access .{name}"),
path,
}),
Expr::Index(_, _) => Err(NonDifferentiable {
construct: "index access".into(),
path,
}),
}
}
fn is_lit(e: &Expr, v: f64) -> bool {
match e {
Expr::Lit(ExprLit::Float(f)) => *f == v,
Expr::Lit(ExprLit::Int(i)) => *i as f64 == v,
_ => false,
}
}
fn as_num(e: &Expr) -> Option<f64> {
match e {
Expr::Lit(ExprLit::Float(f)) => Some(*f),
Expr::Lit(ExprLit::Int(i)) => Some(*i as f64),
_ => None,
}
}
pub fn simplify(e: Expr) -> Expr {
let simplified = simplify_once(e);
simplified
}
fn simplify_once(e: Expr) -> Expr {
match e {
Expr::Unary(UnOp::Neg, inner) => {
let inner = simplify_once(*inner);
match as_num(&inner) {
Some(v) => lit_f(-v),
None => Expr::Unary(UnOp::Neg, Box::new(inner)),
}
}
Expr::Binary(op, l, r) => {
let l = simplify_once(*l);
let r = simplify_once(*r);
if let (Some(a), Some(b)) = (as_num(&l), as_num(&r)) {
let folded = match op {
BinOp::Add => Some(a + b),
BinOp::Sub => Some(a - b),
BinOp::Mul => Some(a * b),
BinOp::Div if b != 0.0 => Some(a / b),
_ => None,
};
if let Some(v) = folded {
return lit_f(v);
}
}
match op {
BinOp::Add if is_lit(&l, 0.0) => r,
BinOp::Add if is_lit(&r, 0.0) => l,
BinOp::Sub if is_lit(&r, 0.0) => l,
BinOp::Mul if is_lit(&l, 0.0) || is_lit(&r, 0.0) => lit_f(0.0),
BinOp::Mul if is_lit(&l, 1.0) => r,
BinOp::Mul if is_lit(&r, 1.0) => l,
BinOp::Div if is_lit(&l, 0.0) => lit_f(0.0),
BinOp::Div if is_lit(&r, 1.0) => l,
_ => Expr::Binary(op, Box::new(l), Box::new(r)),
}
}
Expr::Call(b, args) => Expr::Call(b, args.into_iter().map(simplify_once).collect()),
other => other,
}
}
pub fn grad(e: &Expr, wrt: &str) -> Result<Expr, NonDifferentiable> {
differentiate(e, wrt).map(simplify)
}
#[cfg(test)]
mod tests {
use super::*;
fn r(name: &str) -> Expr {
Expr::Ref(name.to_string())
}
fn mul(a: Expr, b: Expr) -> Expr {
Expr::Binary(BinOp::Mul, Box::new(a), Box::new(b))
}
fn add(a: Expr, b: Expr) -> Expr {
Expr::Binary(BinOp::Add, Box::new(a), Box::new(b))
}
#[test]
fn constants_and_refs() {
assert!(matches!(grad(&lit_f(7.0), "x").unwrap(), Expr::Lit(ExprLit::Float(v)) if v == 0.0));
assert!(matches!(grad(&r("x"), "x").unwrap(), Expr::Lit(ExprLit::Float(v)) if v == 1.0));
assert!(matches!(grad(&r("y"), "x").unwrap(), Expr::Lit(ExprLit::Float(v)) if v == 0.0));
}
#[test]
fn product_rule_with_simplification() {
let e = mul(r("x"), r("x"));
let d = grad(&e, "x").unwrap();
match d {
Expr::Binary(BinOp::Add, l, rr) => {
assert!(matches!(*l, Expr::Ref(ref n) if n == "x"));
assert!(matches!(*rr, Expr::Ref(ref n) if n == "x"));
}
other => panic!("expected x + x, got {other:?}"),
}
}
#[test]
fn linear_combination() {
let e = add(mul(lit_f(3.0), r("x")), r("y"));
assert!(matches!(grad(&e, "x").unwrap(), Expr::Lit(ExprLit::Float(v)) if v == 3.0));
assert!(matches!(grad(&e, "y").unwrap(), Expr::Lit(ExprLit::Float(v)) if v == 1.0));
}
#[test]
fn quotient_rule_shape() {
let e = Expr::Binary(BinOp::Div, Box::new(r("x")), Box::new(r("y")));
let d = grad(&e, "x").unwrap();
match d {
Expr::Binary(BinOp::Div, num, den) => {
assert!(matches!(*num, Expr::Ref(ref n) if n == "y"));
assert!(matches!(*den, Expr::Binary(BinOp::Mul, _, _)));
}
other => panic!("expected y/(y·y), got {other:?}"),
}
}
#[test]
fn chain_through_nesting_and_as_float() {
let inner = mul(add(r("x"), lit_f(2.0)), r("x"));
let e = Expr::Call(Builtin::AsFloat, vec![inner]);
let d = grad(&e, "x").unwrap();
assert!(matches!(d, Expr::Binary(BinOp::Add, _, _)), "{d:?}");
}
#[test]
fn refusals_name_construct_and_position() {
let e = Expr::Call(Builtin::Length, vec![r("s")]);
let err = grad(&e, "x").unwrap_err();
assert!(err.construct.contains("builtin"), "{err:?}");
let e = add(r("x"), Expr::Binary(BinOp::Mod, Box::new(r("a")), Box::new(r("b"))));
let err = grad(&e, "x").unwrap_err();
assert_eq!(err.construct, "mod");
assert_eq!(err.path, ".rhs");
let e = Expr::Binary(BinOp::Lt, Box::new(r("x")), Box::new(lit_f(1.0)));
assert!(grad(&e, "x").is_err());
}
#[test]
fn differential_closure_grad_of_grad_is_well_defined() {
let e = mul(mul(r("x"), r("x")), r("x"));
let d1 = grad(&e, "x").unwrap();
let d2 = grad(&d1, "x").unwrap();
assert!(grad(&d2, "x").is_ok(), "closure holds at every order");
}
#[test]
fn simplifier_is_deterministic_and_idempotent() {
let e = add(mul(lit_f(0.0), r("x")), mul(r("y"), lit_f(1.0)));
let s1 = simplify(e.clone());
let s2 = simplify(s1.clone());
assert_eq!(format!("{s1:?}"), format!("{s2:?}"), "fixpoint");
assert!(matches!(s1, Expr::Ref(ref n) if n == "y"));
}
}