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 `CASE` that behaves identically on every
342/// engine:
343///
344/// ```text
345/// CASE WHEN u > 0 THEN 1.0
346/// WHEN u < 0 THEN -1.0
347/// WHEN u = 0 THEN 0.0
348/// ELSE CAST(NULL AS DOUBLE) END
349/// ```
350///
351/// This is the derivative factor for `abs` (`d/du |u| = sign(u)`), and every
352/// part of the shape is doing work:
353///
354/// * **Not an engine builtin.** DuckDB offers only `sign`, DataFusion only
355/// `signum`, so neither name is portable — and both answer `1` at zero, where
356/// ddx pins `abs'(0) = 0`. `|u|` has no derivative at `0`; the symmetric point
357/// is chosen because it is the only one that keeps `sign` odd, as `abs` is
358/// even. A builtin would silently break that pin on whichever engine had it.
359/// * **`u = 0` is a stated branch, not the `ELSE`.** SQL comparisons are
360/// three-valued: against NULL they are NULL, not false. A NULL input therefore
361/// answers none of the comparisons, so an `ELSE 0.0` would report a *zero
362/// derivative* for a row that has no value — indistinguishable from a
363/// parameter that genuinely does not move, which for a gradient is the
364/// difference between a gap in the data and a converged weight.
365/// * **The `ELSE` is a typed NULL,** which carries the missing value through and
366/// also fixes the expression's result type at `DOUBLE`. Left as bare literals
367/// the branches are decimals, and DuckDB types the whole `CASE`
368/// `DECIMAL(2,1)` — so this one derivative would arrive in a different type,
369/// with different arithmetic under it, from every other derivative ddx emits.
370pub fn sign(u: Expr) -> Expr {
371 let compare = |op: BinaryOperator| Expr::BinaryOp {
372 left: Box::new(u.clone()),
373 op,
374 right: Box::new(zero()),
375 };
376 Expr::Case {
377 case_token: AttachedToken::empty(),
378 end_token: AttachedToken::empty(),
379 operand: None,
380 conditions: vec![
381 CaseWhen {
382 condition: compare(BinaryOperator::Gt),
383 result: one(),
384 },
385 CaseWhen {
386 condition: compare(BinaryOperator::Lt),
387 result: num(-1.0),
388 },
389 // The kink, stated rather than left to `ELSE`. See the doc comment:
390 // this is what keeps `ELSE` meaning "no comparison answered".
391 CaseWhen {
392 condition: compare(BinaryOperator::Eq),
393 result: zero(),
394 },
395 ],
396 // Reached only by a NULL input, and typed so the CASE is DOUBLE.
397 else_result: Some(Box::new(cast_double(Expr::Value(
398 Value::Null.with_empty_span(),
399 )))),
400 }
401}
402
403// ---------------------------------------------------------------------------
404// Function-call construction (for the outer factors of chain-rule terms)
405// ---------------------------------------------------------------------------
406
407/// Build an unqualified scalar function call `name(args...)`.
408pub fn func(name: &str, args: Vec<Expr>) -> Expr {
409 Expr::Function(Function {
410 name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new(name))]),
411 uses_odbc_syntax: false,
412 parameters: FunctionArguments::None,
413 args: FunctionArguments::List(FunctionArgumentList {
414 duplicate_treatment: None,
415 args: args
416 .into_iter()
417 .map(|e| FunctionArg::Unnamed(FunctionArgExpr::Expr(e)))
418 .collect(),
419 clauses: vec![],
420 }),
421 filter: None,
422 null_treatment: None,
423 over: None,
424 within_group: vec![],
425 })
426}
427
428/// `f(x)` — a unary call, the common case for chain-rule outer derivatives.
429pub fn func1(name: &str, x: Expr) -> Expr {
430 func(name, vec![x])
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn as_const_sees_through_a_numeric_cast() {
439 // An engine's own type coercion wraps literals in casts before ddx ever
440 // sees the expression — DataFusion turns `power(x, 3)` into
441 // `power(CAST(x AS DOUBLE), CAST(3 AS DOUBLE))`. If `as_const` misses
442 // that, the `power` rule misreads a constant exponent as variable and
443 // rejects a supported case with a wrong diagnosis.
444 assert_eq!(as_const(&cast_double(num(3.0))), Some(3.0));
445 // Nested inside the cast, and negated, still constant.
446 assert_eq!(as_const(&cast_double(neg(num(2.0)))), Some(-2.0));
447 }
448
449 #[test]
450 fn as_const_refuses_a_non_numeric_cast() {
451 // `CAST(1 AS VARCHAR)` is the string '1', not the number — folding it to
452 // a numeric constant would be a silent type confusion.
453 let to_text = Expr::Cast {
454 kind: CastKind::Cast,
455 expr: Box::new(num(1.0)),
456 data_type: DataType::Varchar(None),
457 array: false,
458 format: None,
459 };
460 assert_eq!(as_const(&to_text), None);
461 }
462
463 #[test]
464 fn right_operand_of_equal_precedence_is_parenthesized() {
465 // The rendered text must re-associate to the *same* tree. `*` and `/`
466 // share a precedence level and associate left, so a `/` on the right of
467 // a `*` needs parentheses even though `*` itself commutes.
468 let a = || Expr::Identifier(Ident::new("a"));
469 let b = || Expr::Identifier(Ident::new("b"));
470 let c = || Expr::Identifier(Ident::new("c"));
471
472 assert_eq!(
473 mul(a(), div(b(), c())).to_string(),
474 "a * (CAST(b AS DOUBLE) / c)"
475 );
476 assert_eq!(mul(a(), mul(b(), c())).to_string(), "a * (b * c)");
477 assert_eq!(add(a(), sub(b(), c())).to_string(), "a + (b - c)");
478 assert_eq!(sub(a(), sub(b(), c())).to_string(), "a - (b - c)");
479
480 // The left side is the direction the parser already associates, so it
481 // needs nothing added — and must not grow spurious parentheses.
482 assert_eq!(mul(mul(a(), b()), c()).to_string(), "a * b * c");
483 assert_eq!(sub(sub(a(), b()), c()).to_string(), "a - b - c");
484
485 // A looser-binding right operand still gets wrapped, as before.
486 assert_eq!(mul(a(), add(b(), c())).to_string(), "a * (b + c)");
487 }
488
489 #[test]
490 fn folds_additive_zero() {
491 assert_eq!(add(one(), zero()).to_string(), "1.0");
492 assert_eq!(add(zero(), one()).to_string(), "1.0");
493 }
494
495 #[test]
496 fn folds_multiplicative_identity_and_zero() {
497 assert_eq!(mul(one(), num(3.0)).to_string(), "3.0");
498 assert_eq!(mul(num(3.0), one()).to_string(), "3.0");
499 assert_eq!(mul(zero(), num(3.0)).to_string(), "0.0");
500 }
501
502 #[test]
503 fn sub_zero_left_is_negation() {
504 assert_eq!(
505 sub(zero(), Expr::Identifier(Ident::new("b"))).to_string(),
506 "-b"
507 );
508 }
509
510 #[test]
511 fn precedence_wrapping_is_semantic_not_cosmetic() {
512 // (a+b)*c must keep its parentheses under Display (G1). Without the
513 // Nested wrap this would render "a + b * c" and reparse wrongly.
514 let a = Expr::Identifier(Ident::new("a"));
515 let b = Expr::Identifier(Ident::new("b"));
516 let c = Expr::Identifier(Ident::new("c"));
517 let e = mul(add(a, b), c);
518 assert_eq!(e.to_string(), "(a + b) * c");
519 }
520
521 #[test]
522 fn non_commutative_right_operand_is_parenthesized() {
523 let a = Expr::Identifier(Ident::new("a"));
524 let b = Expr::Identifier(Ident::new("b"));
525 let c = Expr::Identifier(Ident::new("c"));
526 // a - (b + c) must keep parentheses; a - b + c would be wrong.
527 assert_eq!(sub(a, add(b, c)).to_string(), "a - (b + c)");
528 }
529
530 #[test]
531 fn div_casts_numerator_to_double() {
532 let x = Expr::Identifier(Ident::new("x"));
533 let y = Expr::Identifier(Ident::new("y"));
534 // Forces float division; integer x/y would otherwise truncate (F4).
535 assert_eq!(div(x, y).to_string(), "CAST(x AS DOUBLE) / y");
536 }
537
538 #[test]
539 fn div_by_one_folds_without_cast() {
540 let x = Expr::Identifier(Ident::new("x"));
541 assert_eq!(div(x, one()).to_string(), "x");
542 }
543
544 #[test]
545 fn num_emits_negatives_as_unary_minus() {
546 // A negative literal must match sqlparser's parse shape (UnaryOp{Minus,
547 // magnitude}) so derivatives round-trip; the rendered text is still
548 // `-2.0` / `-0.5`.
549 assert!(matches!(num(-2.0), Expr::UnaryOp { .. }));
550 assert_eq!(num(-2.0).to_string(), "-2.0");
551 assert_eq!(num(-0.5).to_string(), "-0.5");
552 assert_eq!(num(0.0).to_string(), "0.0"); // incl. -0.0 normalization
553 assert_eq!(num(-0.0).to_string(), "0.0");
554 }
555
556 #[test]
557 fn finite_num_rejects_non_finite_values() {
558 // The checked emission seam: finite values pass, inf/NaN fail loud
559 // (never a silent `inf.0`/`NaN.0` token). This is the public `build`
560 // surface, so external callers cannot emit invalid SQL in release.
561 assert_eq!(finite_num(2.0).unwrap().to_string(), "2.0");
562 assert!(finite_num(f64::INFINITY).is_err());
563 assert!(finite_num(f64::NEG_INFINITY).is_err());
564 assert!(finite_num(f64::NAN).is_err());
565 }
566}