Skip to main content

ddx_core/
constructors.rs

1// SPDX-FileCopyrightText: 2026 Alexander Merose <al@merose.com> & ddx Authors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Smart constructors for building derivative `sqlparser::ast::Expr` trees.
6//!
7//! These own three correctness properties, not just algebraic tidiness
8//! (design.md §3.2):
9//!
10//! 1. **0/1-folding** — the JAX-`Zero`-tangent equivalent. Structurally-zero
11//!    terms are dropped and dead product branches short-circuit, keeping output
12//!    compact. This is a *stated* NULL-semantics convention (folding
13//!    `0 * (NULL-valued expr)` to `0`), documented and tested, not silent (F11).
14//! 2. **Numeric-type policy** — [`div`] forces floating-point division by
15//!    casting its numerator to `DOUBLE`, so `grad(x/y, y)` on integer columns
16//!    does not silently truncate (integer `/` differs across engines). Literals
17//!    are emitted with an explicit decimal point (F4).
18//! 3. **Precedence-safe construction** — composite operands are wrapped in
19//!    `Expr::Nested` exactly when the operator precedence requires it, because
20//!    `sqlparser`'s `Display` for a binary op emits no precedence parentheses.
21//!    Without this, a *constructed* `mul(add(a,b), c)` displays as `a + b * c`
22//!    and reparses as the wrong expression — a wrong number in valid SQL (G1).
23
24use sqlparser::ast::helpers::attached_token::AttachedToken;
25use sqlparser::ast::{
26    BinaryOperator, CaseWhen, CastKind, DataType, ExactNumberInfo, Expr, Function, FunctionArg,
27    FunctionArgExpr, FunctionArgumentList, FunctionArguments, Ident, ObjectName, ObjectNamePart,
28    UnaryOperator, Value,
29};
30
31use crate::error::{DiffError, Result};
32
33// ---------------------------------------------------------------------------
34// Literals and constant inspection
35// ---------------------------------------------------------------------------
36
37/// Format a *finite, non-negative* `f64` as the digits of a SQL numeric literal,
38/// always with a decimal point (or exponent) so it reads as floating-point.
39/// Negativity is represented structurally by `num` (as a unary minus), not in
40/// the digits — so this never emits a leading `-`.
41fn format_f64(v: f64) -> String {
42    debug_assert!(
43        v.is_finite() && v >= 0.0,
44        "format_f64 expects a finite, non-negative value (got {v})"
45    );
46    let s = format!("{v}");
47    if s.contains(['.', 'e', 'E']) {
48        s
49    } else {
50        format!("{s}.0")
51    }
52}
53
54/// A bare numeric-literal expression for the finite, non-negative value `v`.
55fn raw_num(v: f64) -> Expr {
56    Expr::Value(Value::Number(format_f64(v), false).with_empty_span())
57}
58
59/// A numeric literal expression for the finite value `v` (e.g. `1.0`, `2.0`,
60/// `0.6931471805599453`).
61///
62/// A negative value is emitted as a *unary minus* applied to the magnitude
63/// (`-1.0` ⇒ `UnaryOp{Minus, 1.0}`) — exactly the AST shape `sqlparser` produces
64/// when it parses `-1.0`. This is what makes the §5 round-trip invariant
65/// (`reparse(render(d)) == d` modulo `Nested`) hold for negative literals too;
66/// emitting a `Value("-1.0")` would reparse to a `UnaryOp` and break it
67/// (round-3 review #46).
68///
69/// `v` must be finite. For a *compile-time-known* finite constant (`0`, `1`,
70/// `2`, `ln 2`, …) call `num` directly. For any value *computed from user input*
71/// — which can overflow to `inf` or produce `NaN` — call [`finite_num`] instead,
72/// which fails loud rather than emit an invalid `inf`/`NaN` literal (#33). This
73/// is why `num` is not part of the public `build` surface: external callers get
74/// the checked [`finite_num`], so a non-finite value can never silently become
75/// `inf.0` in a release build.
76pub(crate) fn num(v: f64) -> Expr {
77    debug_assert!(v.is_finite(), "num expects a finite value (got {v})");
78    if v < 0.0 {
79        Expr::UnaryOp {
80            op: UnaryOperator::Minus,
81            expr: Box::new(raw_num(-v)),
82        }
83    } else {
84        // `+0.0` and `-0.0` both land here (`-0.0 < 0.0` is false); normalize so
85        // `-0.0` never renders as the literal `-0.0`.
86        raw_num(if v == 0.0 { 0.0 } else { v })
87    }
88}
89
90/// A numeric literal for a value that *might not be finite* — the checked
91/// counterpart of `num`. Emits the literal if `v` is finite, else a typed
92/// [`DiffError::NotImplemented`]: a non-finite value has no valid SQL literal
93/// (`inf`/`NaN` are not numbers), so a derivative that would carry one must fail
94/// loud, never emit invalid SQL (#33). Use this for every value derived from
95/// user input or an arithmetic that can overflow (e.g. `ln(base)`, an
96/// out-of-range exponent). This is the single seam through which computed
97/// constants become literals.
98pub fn finite_num(v: f64) -> Result<Expr> {
99    if v.is_finite() {
100        Ok(num(v))
101    } else {
102        Err(DiffError::NotImplemented(format!(
103            "cannot emit a non-finite derivative constant ({v}); a non-finite \
104             value has no valid SQL literal"
105        )))
106    }
107}
108
109/// The constant `0.0` — the derivative of anything independent of `wrt`.
110pub fn zero() -> Expr {
111    num(0.0)
112}
113
114/// The constant `1.0` — the derivative of `wrt` itself.
115pub fn one() -> Expr {
116    num(1.0)
117}
118
119/// The `f64` value of a numeric literal expression, if it is one.
120///
121/// Sees through a single `Expr::Nested` wrapper so folding still recognizes a
122/// parenthesized literal.
123pub fn as_const(e: &Expr) -> Option<f64> {
124    match e {
125        Expr::Value(v) => match &v.value {
126            Value::Number(s, _) => s.parse::<f64>().ok(),
127            _ => None,
128        },
129        Expr::Nested(inner) => as_const(inner),
130        // `sqlparser` parses a negative literal `-2` as `UnaryOp{Minus,
131        // Value("2")}`, not `Value("-2")` — so a negated constant must be seen
132        // through here, or the `power` rule misclassifies a constant exponent
133        // like `-2` as variable and wrongly rejects `power(x, -2)`.
134        Expr::UnaryOp {
135            op: UnaryOperator::Minus,
136            expr,
137        } => as_const(expr).map(|v| -v),
138        Expr::UnaryOp {
139            op: UnaryOperator::Plus,
140            expr,
141        } => as_const(expr),
142        // A cast of a constant to a *numeric* type is still that constant.
143        //
144        // This is not a hypothetical tidiness: an engine's own type coercion
145        // injects these. DataFusion's `TypeCoercion` runs before ddx's analyzer
146        // rule sees the marker, so `power(x, 3)` arrives as
147        // `power(CAST(x AS DOUBLE), CAST(3 AS DOUBLE))`. Without this arm the
148        // exponent reads as variable, and the flagship `power(x, 3)` case is
149        // rejected with a message claiming the exponent depends on the
150        // differentiation variable — a wrong diagnosis for a supported case.
151        //
152        // Restricted to numeric targets on purpose: `CAST(1 AS VARCHAR)` is the
153        // string `'1'`, not the number, and must not fold to a numeric constant.
154        // Same shape as the negated-literal case above, different wrapper.
155        Expr::Cast {
156            expr, data_type, ..
157        } if crate::engine::is_numeric_type(data_type) => as_const(expr),
158        _ => None,
159    }
160}
161
162/// True if `e` is a numeric literal exactly equal to zero.
163pub fn is_zero(e: &Expr) -> bool {
164    matches!(as_const(e), Some(v) if v == 0.0)
165}
166
167/// True if `e` is a numeric literal exactly equal to one.
168pub fn is_one(e: &Expr) -> bool {
169    matches!(as_const(e), Some(v) if v == 1.0)
170}
171
172// ---------------------------------------------------------------------------
173// Precedence-safe assembly (G1)
174// ---------------------------------------------------------------------------
175
176/// Binding-precedence of an expression's *top* operator, higher = binds tighter.
177/// Self-delimiting forms (literals, identifiers, function calls, `CAST`,
178/// already-`Nested`) are atoms and never need wrapping.
179fn precedence(e: &Expr) -> u8 {
180    match e {
181        Expr::BinaryOp { op, .. } => match op {
182            BinaryOperator::Plus | BinaryOperator::Minus => 10,
183            BinaryOperator::Multiply | BinaryOperator::Divide | BinaryOperator::Modulo => 20,
184            _ => 20,
185        },
186        Expr::UnaryOp {
187            op: UnaryOperator::Minus,
188            ..
189        } => 30,
190        _ => 100,
191    }
192}
193
194/// Wrap `e` in `Expr::Nested` iff its precedence is below `threshold`
195/// (`strict`) or at-or-below it (`!strict`).
196fn wrap(e: Expr, threshold: u8, strict: bool) -> Expr {
197    let needs = if strict {
198        precedence(&e) < threshold
199    } else {
200        precedence(&e) <= threshold
201    };
202    if needs {
203        Expr::Nested(Box::new(e))
204    } else {
205        e
206    }
207}
208
209/// `left op right`, parenthesizing operands only where precedence demands it.
210///
211/// The two sides are not symmetric, because every operator here is **left**
212/// associative:
213///
214/// * *Left* operand — parenthesize only when it binds strictly looser. An equal
215///   precedence needs nothing, because that is the direction the parser already
216///   associates: `(a / b) * c` reprints as `a / b * c` and reparses unchanged.
217/// * *Right* operand — parenthesize whenever it binds as tightly **or** looser.
218///   At equal precedence, dropping the parentheses re-associates the tree:
219///   `a * (b / c)` reprints as `a * b / c`, which reparses as `(a * b) / c`.
220///
221/// The right-hand rule deliberately does not care whether `op` commutes. That
222/// was the earlier test, and it was the wrong question: `*` commutes, but
223/// `a * (b / c)` still re-associates, because what re-associates it is the
224/// *other* operator sharing its precedence level. Commutativity would only
225/// matter if the reparse produced the same operands under the same operator.
226///
227/// The consequence is real rather than cosmetic. `a * (b / c)` and `(a * b) / c`
228/// agree in exact arithmetic but not in floating point, and they diverge without
229/// limit where `c` is near zero — so a dropped pair of parentheses turns into a
230/// wrong number in valid SQL, which is exactly what this function exists to
231/// prevent. It costs a few redundant parentheses on same-precedence chains
232/// (`a * (b * c)`), which is the right trade.
233fn binary(left: Expr, op: BinaryOperator, right: Expr) -> Expr {
234    let p = match op {
235        BinaryOperator::Plus | BinaryOperator::Minus => 10,
236        _ => 20,
237    };
238    Expr::BinaryOp {
239        left: Box::new(wrap(left, p, true)),
240        op,
241        right: Box::new(wrap(right, p, false)),
242    }
243}
244
245/// Wrap `e` in a `CAST(... AS DOUBLE)`. Self-delimiting, so it never needs
246/// precedence parentheses as an operand.
247pub fn cast_double(e: Expr) -> Expr {
248    Expr::Cast {
249        kind: CastKind::Cast,
250        expr: Box::new(e),
251        data_type: DataType::Double(ExactNumberInfo::None),
252        array: false,
253        format: None,
254    }
255}
256
257// ---------------------------------------------------------------------------
258// The folding builders
259// ---------------------------------------------------------------------------
260
261/// `a + b`, dropping a structurally-zero operand.
262pub fn add(a: Expr, b: Expr) -> Expr {
263    if is_zero(&a) {
264        b
265    } else if is_zero(&b) {
266        a
267    } else {
268        binary(a, BinaryOperator::Plus, b)
269    }
270}
271
272/// `a - b`, dropping a zero right operand and turning `0 - b` into `-b`.
273pub fn sub(a: Expr, b: Expr) -> Expr {
274    if is_zero(&b) {
275        a
276    } else if is_zero(&a) {
277        neg(b)
278    } else {
279        binary(a, BinaryOperator::Minus, b)
280    }
281}
282
283/// `a * b`, folding `0 * _ = 0` and `1 * b = b` (and the mirror cases).
284pub fn mul(a: Expr, b: Expr) -> Expr {
285    if is_zero(&a) || is_zero(&b) {
286        zero()
287    } else if is_one(&a) {
288        b
289    } else if is_one(&b) {
290        a
291    } else {
292        binary(a, BinaryOperator::Multiply, b)
293    }
294}
295
296/// `a / b`, folding `0 / _ = 0` and `a / 1 = a`.
297///
298/// When a real division is emitted, the numerator is cast to `DOUBLE` so the
299/// division is floating-point on every engine — SQL integer division truncates
300/// on some and not others, which would make `grad(x/y, y)` on a `BIGINT`
301/// column silently wrong (F4). Casting one operand promotes the whole division;
302/// casting the *numerator* (not the result) is essential — `CAST(a/b AS DOUBLE)`
303/// would truncate before the cast.
304pub fn div(a: Expr, b: Expr) -> Expr {
305    if is_zero(&a) {
306        zero()
307    } else if is_one(&b) {
308        a
309    } else {
310        binary(cast_double(a), BinaryOperator::Divide, b)
311    }
312}
313
314/// `-a`, folding `-0 = 0` and `-(-e) = e`, and parenthesizing a binary operand
315/// (`-(a + b)`, `-(a / b)`), since unary minus binds tighter than either.
316pub fn neg(a: Expr) -> Expr {
317    if is_zero(&a) {
318        return zero();
319    }
320    match a {
321        // Double negation cancels. This is not just simplification: without it,
322        // `neg(neg(e))` renders as two adjacent minus tokens `--e`, which SQL
323        // parses as a line comment — a silently-wrong result in valid-looking
324        // SQL (e.g. d/dx(-cos(x)) = sin(x) would emit `--sin(x)`).
325        Expr::UnaryOp {
326            op: UnaryOperator::Minus,
327            expr,
328        } => *expr,
329        other => Expr::UnaryOp {
330            op: UnaryOperator::Minus,
331            expr: Box::new(wrap(other, 30, true)),
332        },
333    }
334}
335
336/// `e * e`.
337pub fn square(e: Expr) -> Expr {
338    mul(e.clone(), e)
339}
340
341/// The mathematical sign of `u` as a portable `CASE`, pinning `sign(0) = 0` on
342/// every engine: `CASE WHEN u > 0 THEN 1.0 WHEN u < 0 THEN -1.0 ELSE 0.0 END`.
343///
344/// This is the derivative factor for `abs` (`d/du |u| = sign(u)`). It avoids the
345/// engine-specific builtins — DuckDB has only `sign`, DataFusion only `signum`,
346/// and the two disagree at `0` (`signum(0) = 1`) — so the emitted derivative is
347/// both portable across the target engines and *actually* pins the documented
348/// kink convention `abs'(0) = 0` (design.md §5, F12), which a bare `signum(u)`
349/// call did not.
350pub fn sign(u: Expr) -> Expr {
351    let compare = |op: BinaryOperator| Expr::BinaryOp {
352        left: Box::new(u.clone()),
353        op,
354        right: Box::new(zero()),
355    };
356    Expr::Case {
357        case_token: AttachedToken::empty(),
358        end_token: AttachedToken::empty(),
359        operand: None,
360        conditions: vec![
361            CaseWhen {
362                condition: compare(BinaryOperator::Gt),
363                result: one(),
364            },
365            CaseWhen {
366                condition: compare(BinaryOperator::Lt),
367                result: num(-1.0),
368            },
369        ],
370        else_result: Some(Box::new(zero())),
371    }
372}
373
374// ---------------------------------------------------------------------------
375// Function-call construction (for the outer factors of chain-rule terms)
376// ---------------------------------------------------------------------------
377
378/// Build an unqualified scalar function call `name(args...)`.
379pub fn func(name: &str, args: Vec<Expr>) -> Expr {
380    Expr::Function(Function {
381        name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new(name))]),
382        uses_odbc_syntax: false,
383        parameters: FunctionArguments::None,
384        args: FunctionArguments::List(FunctionArgumentList {
385            duplicate_treatment: None,
386            args: args
387                .into_iter()
388                .map(|e| FunctionArg::Unnamed(FunctionArgExpr::Expr(e)))
389                .collect(),
390            clauses: vec![],
391        }),
392        filter: None,
393        null_treatment: None,
394        over: None,
395        within_group: vec![],
396    })
397}
398
399/// `f(x)` — a unary call, the common case for chain-rule outer derivatives.
400pub fn func1(name: &str, x: Expr) -> Expr {
401    func(name, vec![x])
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn as_const_sees_through_a_numeric_cast() {
410        // An engine's own type coercion wraps literals in casts before ddx ever
411        // sees the expression — DataFusion turns `power(x, 3)` into
412        // `power(CAST(x AS DOUBLE), CAST(3 AS DOUBLE))`. If `as_const` misses
413        // that, the `power` rule misreads a constant exponent as variable and
414        // rejects a supported case with a wrong diagnosis.
415        assert_eq!(as_const(&cast_double(num(3.0))), Some(3.0));
416        // Nested inside the cast, and negated, still constant.
417        assert_eq!(as_const(&cast_double(neg(num(2.0)))), Some(-2.0));
418    }
419
420    #[test]
421    fn as_const_refuses_a_non_numeric_cast() {
422        // `CAST(1 AS VARCHAR)` is the string '1', not the number — folding it to
423        // a numeric constant would be a silent type confusion.
424        let to_text = Expr::Cast {
425            kind: CastKind::Cast,
426            expr: Box::new(num(1.0)),
427            data_type: DataType::Varchar(None),
428            array: false,
429            format: None,
430        };
431        assert_eq!(as_const(&to_text), None);
432    }
433
434    #[test]
435    fn right_operand_of_equal_precedence_is_parenthesized() {
436        // The rendered text must re-associate to the *same* tree. `*` and `/`
437        // share a precedence level and associate left, so a `/` on the right of
438        // a `*` needs parentheses even though `*` itself commutes.
439        let a = || Expr::Identifier(Ident::new("a"));
440        let b = || Expr::Identifier(Ident::new("b"));
441        let c = || Expr::Identifier(Ident::new("c"));
442
443        assert_eq!(
444            mul(a(), div(b(), c())).to_string(),
445            "a * (CAST(b AS DOUBLE) / c)"
446        );
447        assert_eq!(mul(a(), mul(b(), c())).to_string(), "a * (b * c)");
448        assert_eq!(add(a(), sub(b(), c())).to_string(), "a + (b - c)");
449        assert_eq!(sub(a(), sub(b(), c())).to_string(), "a - (b - c)");
450
451        // The left side is the direction the parser already associates, so it
452        // needs nothing added — and must not grow spurious parentheses.
453        assert_eq!(mul(mul(a(), b()), c()).to_string(), "a * b * c");
454        assert_eq!(sub(sub(a(), b()), c()).to_string(), "a - b - c");
455
456        // A looser-binding right operand still gets wrapped, as before.
457        assert_eq!(mul(a(), add(b(), c())).to_string(), "a * (b + c)");
458    }
459
460    #[test]
461    fn folds_additive_zero() {
462        assert_eq!(add(one(), zero()).to_string(), "1.0");
463        assert_eq!(add(zero(), one()).to_string(), "1.0");
464    }
465
466    #[test]
467    fn folds_multiplicative_identity_and_zero() {
468        assert_eq!(mul(one(), num(3.0)).to_string(), "3.0");
469        assert_eq!(mul(num(3.0), one()).to_string(), "3.0");
470        assert_eq!(mul(zero(), num(3.0)).to_string(), "0.0");
471    }
472
473    #[test]
474    fn sub_zero_left_is_negation() {
475        assert_eq!(
476            sub(zero(), Expr::Identifier(Ident::new("b"))).to_string(),
477            "-b"
478        );
479    }
480
481    #[test]
482    fn precedence_wrapping_is_semantic_not_cosmetic() {
483        // (a+b)*c must keep its parentheses under Display (G1). Without the
484        // Nested wrap this would render "a + b * c" and reparse wrongly.
485        let a = Expr::Identifier(Ident::new("a"));
486        let b = Expr::Identifier(Ident::new("b"));
487        let c = Expr::Identifier(Ident::new("c"));
488        let e = mul(add(a, b), c);
489        assert_eq!(e.to_string(), "(a + b) * c");
490    }
491
492    #[test]
493    fn non_commutative_right_operand_is_parenthesized() {
494        let a = Expr::Identifier(Ident::new("a"));
495        let b = Expr::Identifier(Ident::new("b"));
496        let c = Expr::Identifier(Ident::new("c"));
497        // a - (b + c) must keep parentheses; a - b + c would be wrong.
498        assert_eq!(sub(a, add(b, c)).to_string(), "a - (b + c)");
499    }
500
501    #[test]
502    fn div_casts_numerator_to_double() {
503        let x = Expr::Identifier(Ident::new("x"));
504        let y = Expr::Identifier(Ident::new("y"));
505        // Forces float division; integer x/y would otherwise truncate (F4).
506        assert_eq!(div(x, y).to_string(), "CAST(x AS DOUBLE) / y");
507    }
508
509    #[test]
510    fn div_by_one_folds_without_cast() {
511        let x = Expr::Identifier(Ident::new("x"));
512        assert_eq!(div(x, one()).to_string(), "x");
513    }
514
515    #[test]
516    fn num_emits_negatives_as_unary_minus() {
517        // A negative literal must match sqlparser's parse shape (UnaryOp{Minus,
518        // magnitude}) so derivatives round-trip; the rendered text is still
519        // `-2.0` / `-0.5`.
520        assert!(matches!(num(-2.0), Expr::UnaryOp { .. }));
521        assert_eq!(num(-2.0).to_string(), "-2.0");
522        assert_eq!(num(-0.5).to_string(), "-0.5");
523        assert_eq!(num(0.0).to_string(), "0.0"); // incl. -0.0 normalization
524        assert_eq!(num(-0.0).to_string(), "0.0");
525    }
526
527    #[test]
528    fn finite_num_rejects_non_finite_values() {
529        // The checked emission seam: finite values pass, inf/NaN fail loud
530        // (never a silent `inf.0`/`NaN.0` token). This is the public `build`
531        // surface, so external callers cannot emit invalid SQL in release.
532        assert_eq!(finite_num(2.0).unwrap().to_string(), "2.0");
533        assert!(finite_num(f64::INFINITY).is_err());
534        assert!(finite_num(f64::NEG_INFINITY).is_err());
535        assert!(finite_num(f64::NAN).is_err());
536    }
537}