Skip to main content

symplex/calculus/
summation.rs

1//! Symbolic summation and products: Faulhaber, telescoping, hypergeometric,
2//! infinite sums, and closed-form products.
3//!
4//! This module is the backend behind [`Ex::summation`](crate::api::expr::Ex::summation)
5//! and [`Ex::product_over`](crate::api::expr::Ex::product_over).  It works on
6//! `&mut Arena` + `ExprId` and is also used by `eval()` when it encounters a
7//! `Sum` node (via the thin shim in `transforms::sum_eval`).
8//!
9//! # Summation strategies
10//!
11//! For `Σ_{k=lo}^{hi} f(k)` the dispatcher tries, in order:
12//!
13//! 1. **Empty / enumerable ranges** — concrete integer bounds with at most
14//!    [`MAX_ENUMERATION_TERMS`] terms are summed directly (exactly).
15//! 2. **Constant body** — `f` independent of `k` gives `f·(hi − lo + 1)`.
16//! 3. **Rational functions of `k`** — partial fractions, then each pole
17//!    family `c/(k+β)^m` is summed with harmonic numbers / digamma, and
18//!    integer-shifted poles telescope exactly (`Σ 1/(k(k+1)) = 1 − 1/(n+1)`).
19//! 4. **Telescoping** `g(k) − g(k+d)` for two-term bodies.
20//! 5. **Linearity** — sums of terms are summed term-by-term; terms that
21//!    cannot be summed are kept as an unevaluated `Sum`.
22//! 6. **Polynomials in `k`** (any degree, symbolic coefficients allowed) via
23//!    Faulhaber's formula with exact Bernoulli numbers.
24//! 7. **Binomial identities** (`Σ C(n,k) = 2ⁿ`, `Σ k·C(n,k) = n·2ⁿ⁻¹`,
25//!    `Σ C(n,k)² = C(2n,n)`, `Σ C(n,k) xᵏ = (1+x)ⁿ`, …).
26//! 8. **Geometric / arithmetico-geometric** `Σ P(k)·rᵏ` with symbolic `r`
27//!    (a `Piecewise` covers `r = 1`).
28//! 9. **Gosper's algorithm** for hypergeometric terms.
29//!
30//! For infinite upper bounds the engine additionally recognises p-series
31//! (`ζ(2m)` in closed form), alternating p-series (`η`, Dirichlet `β`),
32//! convergent geometric series, and a table of classical power series
33//! (`Σ xᵏ/k! = eˣ`, `Σ (−1)ᵏ x²ᵏ⁺¹/(2k+1)! = sin x`, `Σ xᵏ/k = −ln(1−x)`, …),
34//! and proves divergence where it can.
35//!
36//! Values without an elementary closed form use the dedicated nodes:
37//! `Σ 1/k³ = ζ(3)` (`Zeta`), `Σ (−1)^k/(2k+1)² = G` (`Catalan`).
38
39use num_bigint::BigInt;
40use num_rational::Ratio;
41use num_traits::{One, Signed, ToPrimitive, Zero};
42
43use crate::base::arena::Arena;
44use crate::base::bernoulli::bernoulli;
45use crate::base::node::{ExprId, ExprNode};
46use crate::base::walk;
47use crate::calculus::gosper;
48use crate::poly::Poly;
49use crate::poly::polybridge;
50use crate::transforms::{apart, eval, subs};
51
52type Rat = Ratio<BigInt>;
53
54/// Maximum number of terms that will be summed / multiplied by direct
55/// enumeration when both bounds are concrete integers.
56pub const MAX_ENUMERATION_TERMS: i64 = 1000;
57
58/// Maximum integer shift between two poles that is telescoped explicitly.
59const MAX_TELESCOPE_SHIFT: i64 = 200;
60
61/// Maximum integer shift between Gamma-function arguments that is expanded
62/// into a product of linear factors when normalising product results.
63const MAX_GAMMA_SHIFT: i64 = 60;
64
65// ═══════════════════════════════════════════════════════════════════════════
66// Result type
67// ═══════════════════════════════════════════════════════════════════════════
68
69/// Outcome of a symbolic summation or product.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum SumOutcome {
72    /// A closed form was found.  It may still contain an unevaluated `Sum`
73    /// for terms that could not be summed (partial linearity).
74    Closed(ExprId),
75    /// The sum / product was proven divergent.  `Some(±∞)` when the
76    /// direction is known, `None` for oscillating divergence.
77    Divergent(Option<ExprId>),
78    /// No strategy applied — the caller should keep the formal node.
79    Unevaluated,
80}
81
82// ═══════════════════════════════════════════════════════════════════════════
83// Small helpers
84// ═══════════════════════════════════════════════════════════════════════════
85
86fn rat_i(n: i64) -> Rat {
87    Ratio::from_integer(BigInt::from(n))
88}
89
90fn rat_expr(arena: &mut Arena, r: Rat) -> ExprId {
91    let nid = arena.intern_num(r);
92    arena.intern(ExprNode::Num(nid))
93}
94
95fn depends_on(arena: &Arena, e: ExprId, var: ExprId) -> bool {
96    walk::contains(arena, e, var)
97}
98
99fn as_rat(arena: &Arena, e: ExprId) -> Option<Rat> {
100    arena.as_num(e).cloned()
101}
102
103fn as_i64(arena: &Arena, e: ExprId) -> Option<i64> {
104    arena.as_num(e).and_then(|r| {
105        if r.is_integer() {
106            r.numer().to_i64()
107        } else {
108            None
109        }
110    })
111}
112
113fn eval_rat(arena: &mut Arena, e: ExprId) -> Option<Rat> {
114    let v = eval::eval(arena, e);
115    as_rat(arena, v)
116}
117
118fn mul_all(arena: &mut Arena, factors: &[ExprId]) -> ExprId {
119    match factors.len() {
120        0 => arena.one,
121        1 => factors[0],
122        _ => arena.mul(factors),
123    }
124}
125
126fn add_all(arena: &mut Arena, terms: &[ExprId]) -> ExprId {
127    match terms.len() {
128        0 => arena.zero,
129        1 => terms[0],
130        _ => arena.add(terms),
131    }
132}
133
134fn add_terms(arena: &Arena, e: ExprId) -> Vec<ExprId> {
135    match arena.node(e) {
136        ExprNode::Add(ch) => ch.to_vec(),
137        _ => vec![e],
138    }
139}
140
141fn mul_factors(arena: &Arena, e: ExprId) -> Vec<ExprId> {
142    match arena.node(e) {
143        ExprNode::Mul(ch) => ch.to_vec(),
144        _ => vec![e],
145    }
146}
147
148/// `pow(base, r)` for a rational exponent.
149fn pow_rat(arena: &mut Arena, base: ExprId, r: &Rat) -> ExprId {
150    if r.is_zero() {
151        return arena.one;
152    }
153    if r.is_one() {
154        return base;
155    }
156    let e = rat_expr(arena, r.clone());
157    arena.pow(base, e)
158}
159
160/// `r^n` for rational `r` and integer `n` (exact).
161fn rat_pow_i(r: &Rat, n: i64) -> Rat {
162    let mut acc = Rat::one();
163    let base = if n < 0 { Rat::one() / r } else { r.clone() };
164    for _ in 0..n.unsigned_abs() {
165        acc *= &base;
166    }
167    acc
168}
169
170fn factorial_big(n: u64) -> BigInt {
171    let mut acc = BigInt::one();
172    for i in 2..=n {
173        acc *= BigInt::from(i);
174    }
175    acc
176}
177
178fn binomial_big(n: u64, k: u64) -> BigInt {
179    if k > n {
180        return BigInt::zero();
181    }
182    let mut acc = BigInt::one();
183    for i in 0..k {
184        acc = acc * BigInt::from(n - i) / BigInt::from(i + 1);
185    }
186    acc
187}
188
189/// Put a sum of fractions over a common denominator (no-op for non-sums).
190fn combine_fractions(arena: &mut Arena, e: ExprId) -> ExprId {
191    if matches!(arena.node(e), ExprNode::Add(_)) {
192        let t = polybridge::together(arena, e);
193        eval::eval(arena, t)
194    } else {
195        e
196    }
197}
198
199/// Fractional part `β − ⌊β⌋` of a rational.
200fn frac_part(r: &Rat) -> Rat {
201    r - r.floor()
202}
203
204/// Is `x + c` (as an expression) — builds `x + c` with rational `c`.
205fn add_rat(arena: &mut Arena, x: ExprId, c: &Rat) -> ExprId {
206    if c.is_zero() {
207        return x;
208    }
209    let ce = rat_expr(arena, c.clone());
210    arena.add(&[x, ce])
211}
212
213/// Evaluate `e` and, if the result is a rational number, return it.
214fn const_value(arena: &mut Arena, e: ExprId) -> Option<Rat> {
215    eval_rat(arena, e)
216}
217
218/// `hi − lo + 1`.
219fn range_count(arena: &mut Arena, lo: ExprId, hi: ExprId) -> ExprId {
220    let d = arena.sub(hi, lo);
221    let one = arena.one;
222    arena.add(&[d, one])
223}
224
225/// Extract `(a, b)` such that `e = a·var + b` with rational `a ≠ 0`, `b`.
226fn linear_in(arena: &Arena, e: ExprId, var: ExprId) -> Option<(Rat, Rat)> {
227    let p = polybridge::expr_to_poly(arena, e, var)?;
228    match p.degree() {
229        Some(1) => Some((p.coeff(1), p.coeff(0))),
230        _ => None,
231    }
232}
233
234/// A "structural" zero test: expand + eval, and if that does not reach a
235/// literal zero, fall back to the unified simplifier.
236fn is_zero_expr(arena: &mut Arena, e: ExprId) -> bool {
237    let ex = arena.expand_expr(e);
238    let ev = eval::eval(arena, ex);
239    if arena.is_zero_structural(ev) {
240        return true;
241    }
242    if let Some(r) = as_rat(arena, ev) {
243        return r.is_zero();
244    }
245    let simp = crate::simplify::simplify_engine::unified_simplify(
246        arena,
247        ev,
248        &crate::simplify::simplify_engine::SimplifyOpts::single_pass(),
249    );
250    arena.is_zero_structural(simp.expr)
251}
252
253/// Sign of a k-free expression: `Some(true)` positive, `Some(false)`
254/// negative, `None` unknown / zero.
255fn const_sign(arena: &mut Arena, e: ExprId) -> Option<bool> {
256    if let Some(r) = const_value(arena, e) {
257        if r.is_positive() {
258            return Some(true);
259        }
260        if r.is_negative() {
261            return Some(false);
262        }
263        return None;
264    }
265    let f = crate::transforms::evalf::eval_const_f64(arena, e)?;
266    if f > 0.0 {
267        Some(true)
268    } else if f < 0.0 {
269        Some(false)
270    } else {
271        None
272    }
273}
274
275/// Numeric magnitude test `|e| < 1` for a k-free expression.
276///
277/// Exact for rationals; for other constants uses a 16-digit evaluation
278/// with a safety margin and returns `None` when the value is within
279/// `1e-9` of the unit circle.
280fn abs_less_than_one(arena: &mut Arena, e: ExprId) -> Option<bool> {
281    if let Some(r) = const_value(arena, e) {
282        return Some(r.abs() < Rat::one());
283    }
284    let f = crate::transforms::evalf::eval_const_f64(arena, e)?;
285    if !f.is_finite() {
286        return Some(false);
287    }
288    let a = f.abs();
289    if a < 1.0 - 1e-9 {
290        Some(true)
291    } else if a > 1.0 + 1e-9 {
292        Some(false)
293    } else {
294        None
295    }
296}
297
298fn infinity_of_sign(arena: &Arena, positive: Option<bool>) -> Option<ExprId> {
299    match positive {
300        Some(true) => Some(arena.infinity),
301        Some(false) => Some(arena.neg_infinity),
302        None => None,
303    }
304}
305
306// ═══════════════════════════════════════════════════════════════════════════
307// Bounds
308// ═══════════════════════════════════════════════════════════════════════════
309
310enum Bound {
311    Finite(ExprId),
312    PosInf,
313    NegInf,
314}
315
316fn classify_bound(arena: &Arena, b: ExprId) -> Bound {
317    match arena.node(b) {
318        ExprNode::Infinity => Bound::PosInf,
319        ExprNode::NegInfinity => Bound::NegInf,
320        _ => Bound::Finite(b),
321    }
322}
323
324// ═══════════════════════════════════════════════════════════════════════════
325// Public (crate) entry points
326// ═══════════════════════════════════════════════════════════════════════════
327
328/// Evaluate `Σ_{var=lower}^{upper} body` symbolically.
329///
330/// `lower` / `upper` may be concrete integers, symbolic expressions, or
331/// `Infinity` / `NegInfinity`.  See the module docs for the strategies.
332pub(crate) fn summation(
333    arena: &mut Arena,
334    body: ExprId,
335    var: ExprId,
336    lower: ExprId,
337    upper: ExprId,
338) -> SumOutcome {
339    if !matches!(arena.node(var), ExprNode::Symbol(_)) {
340        return SumOutcome::Unevaluated;
341    }
342    tracing::debug!("summation: dispatching");
343    match (classify_bound(arena, lower), classify_bound(arena, upper)) {
344        (Bound::Finite(lo), Bound::Finite(hi)) => finite_sum(arena, body, var, lo, hi),
345        (Bound::Finite(lo), Bound::PosInf) => infinite_sum(arena, body, var, lo),
346        (Bound::NegInf, Bound::Finite(hi)) => {
347            // k = −j:  Σ_{k=−∞}^{hi} f(k) = Σ_{j=−hi}^{∞} f(−j)
348            let neg_var = arena.neg(var);
349            let reflected = subs::subs(arena, body, var, neg_var);
350            let lo2 = arena.neg(hi);
351            let lo2 = eval::eval(arena, lo2);
352            infinite_sum(arena, reflected, var, lo2)
353        }
354        (Bound::NegInf, Bound::PosInf) => {
355            let zero = arena.zero;
356            let one = arena.one;
357            let right = infinite_sum(arena, body, var, zero);
358            let neg_var = arena.neg(var);
359            let reflected = subs::subs(arena, body, var, neg_var);
360            let left = infinite_sum(arena, reflected, var, one);
361            combine_outcomes(arena, right, left)
362        }
363        _ => SumOutcome::Unevaluated,
364    }
365}
366
367/// Combine two independent partial results by addition.
368fn combine_outcomes(arena: &mut Arena, a: SumOutcome, b: SumOutcome) -> SumOutcome {
369    use SumOutcome::*;
370    match (a, b) {
371        (Closed(x), Closed(y)) => {
372            let s = arena.add(&[x, y]);
373            Closed(eval::eval(arena, s))
374        }
375        (Divergent(d), Closed(_)) | (Closed(_), Divergent(d)) => Divergent(d),
376        (Divergent(Some(x)), Divergent(Some(y))) if x == y => Divergent(Some(x)),
377        (Divergent(_), Divergent(_)) => Divergent(None),
378        _ => Unevaluated,
379    }
380}
381
382// ═══════════════════════════════════════════════════════════════════════════
383// Finite sums
384// ═══════════════════════════════════════════════════════════════════════════
385
386fn finite_sum(arena: &mut Arena, body: ExprId, var: ExprId, lo: ExprId, hi: ExprId) -> SumOutcome {
387    if let (Some(a), Some(b)) = (as_i64(arena, lo), as_i64(arena, hi)) {
388        if b < a {
389            return SumOutcome::Closed(arena.zero);
390        }
391        if b - a < MAX_ENUMERATION_TERMS {
392            tracing::debug!("summation: enumerating {} terms", b - a + 1);
393            return SumOutcome::Closed(enumerate_sum(arena, body, var, a, b));
394        }
395    }
396    match finite_closed(arena, body, var, lo, hi) {
397        Some(id) => SumOutcome::Closed(eval::eval(arena, id)),
398        None => SumOutcome::Unevaluated,
399    }
400}
401
402fn enumerate_sum(arena: &mut Arena, body: ExprId, var: ExprId, a: i64, b: i64) -> ExprId {
403    let mut terms = Vec::with_capacity((b - a + 1) as usize);
404    for k in a..=b {
405        let kk = arena.int(k);
406        let t = subs::subs(arena, body, var, kk);
407        terms.push(eval::eval(arena, t));
408    }
409    let s = add_all(arena, &terms);
410    eval::eval(arena, s)
411}
412
413fn enumerate_product(arena: &mut Arena, body: ExprId, var: ExprId, a: i64, b: i64) -> ExprId {
414    let mut factors = Vec::with_capacity((b - a + 1) as usize);
415    for k in a..=b {
416        let kk = arena.int(k);
417        let t = subs::subs(arena, body, var, kk);
418        factors.push(eval::eval(arena, t));
419    }
420    let p = mul_all(arena, &factors);
421    eval::eval(arena, p)
422}
423
424/// Closed form of a finite sum with (possibly symbolic) bounds.
425fn finite_closed(
426    arena: &mut Arena,
427    body: ExprId,
428    var: ExprId,
429    lo: ExprId,
430    hi: ExprId,
431) -> Option<ExprId> {
432    // 0. Body independent of the summation variable.
433    if !depends_on(arena, body, var) {
434        let n = range_count(arena, lo, hi);
435        return Some(arena.mul(&[body, n]));
436    }
437
438    let node = arena.node(body).clone();
439
440    // 1. Sums: whole-rational, telescoping, linearity.
441    if let ExprNode::Add(ref terms) = node {
442        let terms: Vec<ExprId> = terms.to_vec();
443        if let Some(r) = rational_sum_finite(arena, body, var, lo, hi) {
444            return Some(r);
445        }
446        if let Some(r) = telescoping_finite(arena, &terms, var, lo, hi) {
447            return Some(r);
448        }
449        if let Some(p) = sym_poly_in(arena, body, var) {
450            return Some(faulhaber_sym(arena, &p, lo, hi));
451        }
452        let mut done = Vec::new();
453        let mut failed = Vec::new();
454        for &t in &terms {
455            match finite_closed(arena, t, var, lo, hi) {
456                Some(v) => done.push(v),
457                None => failed.push(t),
458            }
459        }
460        if done.is_empty() {
461            return None;
462        }
463        if !failed.is_empty() {
464            let rest = add_all(arena, &failed);
465            done.push(arena.intern(ExprNode::Sum(rest, var, lo, hi)));
466        }
467        return Some(add_all(arena, &done));
468    }
469
470    // 2. Constant-factor extraction.
471    if let ExprNode::Mul(ref factors) = node {
472        let factors: Vec<ExprId> = factors.to_vec();
473        let (consts, varf): (Vec<ExprId>, Vec<ExprId>) = factors
474            .iter()
475            .copied()
476            .partition(|&f| !depends_on(arena, f, var));
477        if !consts.is_empty() && !varf.is_empty() {
478            let inner = mul_all(arena, &varf);
479            let s = finite_closed(arena, inner, var, lo, hi)?;
480            let mut all = consts;
481            all.push(s);
482            return Some(arena.mul(&all));
483        }
484    }
485
486    // 3. Polynomial in k (symbolic coefficients allowed).
487    if let Some(p) = sym_poly_in(arena, body, var) {
488        return Some(faulhaber_sym(arena, &p, lo, hi));
489    }
490
491    // 4. Rational function of k.
492    if let Some(r) = rational_sum_finite(arena, body, var, lo, hi) {
493        return Some(r);
494    }
495
496    // 5. Binomial identities.
497    if let Some(r) = binomial_sum(arena, body, var, lo, hi) {
498        return Some(r);
499    }
500
501    // 6. Geometric / arithmetico-geometric.
502    if let Some(r) = geometric_poly_finite(arena, body, var, lo, hi) {
503        return Some(r);
504    }
505
506    // 7. Gosper.
507    if let Some(r) = gosper::gosper_sum(arena, body, var, lo, hi) {
508        tracing::debug!("summation: Gosper succeeded");
509        return Some(r);
510    }
511
512    None
513}
514
515// ═══════════════════════════════════════════════════════════════════════════
516// Polynomials in k with symbolic coefficients
517// ═══════════════════════════════════════════════════════════════════════════
518
519/// Decompose `expr` (after expansion) as `Σ c_j·var^j` where each `c_j` is
520/// free of `var`.  Returns `None` if `expr` is not polynomial in `var`.
521pub(crate) fn sym_poly_in(
522    arena: &mut Arena,
523    expr: ExprId,
524    var: ExprId,
525) -> Option<Vec<(usize, ExprId)>> {
526    let expanded = arena.expand_expr(expr);
527    let terms = add_terms(arena, expanded);
528    let mut acc: Vec<(usize, Vec<ExprId>)> = Vec::new();
529    for t in terms {
530        let (power, coeff) = monomial_in(arena, t, var)?;
531        if let Some(slot) = acc.iter_mut().find(|(p, _)| *p == power) {
532            slot.1.push(coeff);
533        } else {
534            acc.push((power, vec![coeff]));
535        }
536    }
537    acc.sort_by_key(|(p, _)| *p);
538    let mut out = Vec::with_capacity(acc.len());
539    for (p, coeffs) in acc {
540        let c = add_all(arena, &coeffs);
541        let c = eval::eval(arena, c);
542        if !arena.is_zero_structural(c) {
543            out.push((p, c));
544        }
545    }
546    Some(out)
547}
548
549/// `t = c·var^p` with `c` free of `var`.
550fn monomial_in(arena: &mut Arena, t: ExprId, var: ExprId) -> Option<(usize, ExprId)> {
551    if !depends_on(arena, t, var) {
552        return Some((0, t));
553    }
554    if t == var {
555        return Some((1, arena.one));
556    }
557    match arena.node(t).clone() {
558        ExprNode::Pow(base, exp) if base == var => {
559            let n = as_i64(arena, exp)?;
560            if n < 0 {
561                return None;
562            }
563            Some((n as usize, arena.one))
564        }
565        ExprNode::Mul(ref factors) => {
566            let mut power = 0usize;
567            let mut consts = Vec::new();
568            let mut seen_var = false;
569            for &f in factors.iter() {
570                if !depends_on(arena, f, var) {
571                    consts.push(f);
572                } else if f == var {
573                    if seen_var {
574                        return None;
575                    }
576                    seen_var = true;
577                    power = 1;
578                } else if let ExprNode::Pow(base, exp) = arena.node(f).clone()
579                    && base == var
580                {
581                    let n = as_i64(arena, exp)?;
582                    if n < 0 || seen_var {
583                        return None;
584                    }
585                    seen_var = true;
586                    power = n as usize;
587                } else {
588                    return None;
589                }
590            }
591            let c = mul_all(arena, &consts);
592            Some((power, c))
593        }
594        _ => None,
595    }
596}
597
598// ═══════════════════════════════════════════════════════════════════════════
599// Faulhaber
600// ═══════════════════════════════════════════════════════════════════════════
601
602/// Coefficients (ascending) of the Faulhaber polynomial
603/// `S_p(n) = Σ_{k=1}^{n} k^p` using `B₁ = +1/2`:
604///
605/// `S_p(n) = 1/(p+1) · Σ_{j=0}^{p} C(p+1, j) · B⁺_j · n^{p+1−j}`.
606pub(crate) fn faulhaber_coefficients(p: usize) -> Poly {
607    let mut coeffs = vec![Rat::zero(); p + 2];
608    let inv = Rat::one() / rat_i(p as i64 + 1);
609    for j in 0..=p {
610        let mut bj = bernoulli(j);
611        if j == 1 {
612            bj = -bj; // B₁⁺ = +1/2
613        }
614        if bj.is_zero() {
615            continue;
616        }
617        let c = Rat::from_integer(binomial_big((p + 1) as u64, j as u64));
618        let power = p + 1 - j;
619        coeffs[power] += &inv * c * bj;
620    }
621    Poly::from_coeffs(coeffs)
622}
623
624/// `Σ_{k=1}^{n} k^p` as an expression in `n`.
625pub(crate) fn faulhaber_from_one(arena: &mut Arena, p: usize, n: ExprId) -> ExprId {
626    let poly = faulhaber_coefficients(p);
627    poly_at(arena, &poly, n)
628}
629
630/// Evaluate a rational-coefficient polynomial at an arbitrary expression.
631fn poly_at(arena: &mut Arena, poly: &Poly, x: ExprId) -> ExprId {
632    let mut terms = Vec::new();
633    for (i, c) in poly.coeffs().iter().enumerate() {
634        if c.is_zero() {
635            continue;
636        }
637        let ce = rat_expr(arena, c.clone());
638        let t = if i == 0 {
639            ce
640        } else {
641            let xi = pow_rat(arena, x, &rat_i(i as i64));
642            arena.mul(&[ce, xi])
643        };
644        terms.push(t);
645    }
646    let s = add_all(arena, &terms);
647    eval::eval(arena, s)
648}
649
650/// `Σ_{k=lo}^{hi} P(k)` for a rational-coefficient polynomial `P`.
651fn faulhaber_poly(arena: &mut Arena, poly: &Poly, lo: ExprId, hi: ExprId) -> ExprId {
652    let sym: Vec<(usize, ExprId)> = poly
653        .coeffs()
654        .iter()
655        .enumerate()
656        .filter(|(_, c)| !c.is_zero())
657        .map(|(i, c)| (i, rat_expr(arena, c.clone())))
658        .collect();
659    faulhaber_sym(arena, &sym, lo, hi)
660}
661
662/// `Σ_{k=lo}^{hi} Σ_j c_j k^j = Σ_j c_j (S_j(hi) − S_j(lo − 1))`.
663fn faulhaber_sym(arena: &mut Arena, poly: &[(usize, ExprId)], lo: ExprId, hi: ExprId) -> ExprId {
664    let lo_is_one = as_i64(arena, lo) == Some(1);
665    let one = arena.one;
666    let lo_m1 = arena.sub(lo, one);
667    let lo_m1 = eval::eval(arena, lo_m1);
668    let mut terms = Vec::new();
669    for &(p, c) in poly {
670        let s_hi = faulhaber_from_one(arena, p, hi);
671        let s = if lo_is_one {
672            s_hi
673        } else {
674            let s_lo = faulhaber_from_one(arena, p, lo_m1);
675            arena.sub(s_hi, s_lo)
676        };
677        terms.push(arena.mul(&[c, s]));
678    }
679    let total = add_all(arena, &terms);
680    let expanded = arena.expand_expr(total);
681    eval::eval(arena, expanded)
682}
683
684// ═══════════════════════════════════════════════════════════════════════════
685// Rational functions of k: partial fractions + harmonic / digamma
686// ═══════════════════════════════════════════════════════════════════════════
687
688/// A pole term `c·(k + β)^(−m)`.
689#[derive(Debug, Clone)]
690struct PoleTerm {
691    c: Rat,
692    beta: Rat,
693    m: u32,
694}
695
696/// Decompose a rational function of `var` (rational coefficients) into a
697/// polynomial part and pole terms.  Returns `None` if `body` is not a
698/// rational function with a non-constant denominator, or has a pole term
699/// that is not of the form `c/(αk+β)^m`.
700fn rational_decompose(
701    arena: &mut Arena,
702    body: ExprId,
703    var: ExprId,
704) -> Option<(Poly, Vec<PoleTerm>)> {
705    let body = combine_fractions(arena, body);
706    let (n, d) = polybridge::as_numer_denom(arena, body);
707    let np = polybridge::expr_to_poly(arena, n, var)?;
708    let dp = polybridge::expr_to_poly(arena, d, var)?;
709    if dp.is_zero() || dp.is_constant() || np.is_zero() {
710        return None;
711    }
712    let decomposed = apart::apart(arena, body, var);
713    let terms = add_terms(arena, decomposed);
714    let mut poly_part = Poly::zero();
715    let mut poles = Vec::new();
716    for t in terms {
717        if let Some(p) = polybridge::expr_to_poly(arena, t, var) {
718            poly_part = &poly_part + &p;
719            continue;
720        }
721        poles.push(as_pole_term(arena, t, var)?);
722    }
723    Some((poly_part, poles))
724}
725
726fn as_pole_term(arena: &mut Arena, t: ExprId, var: ExprId) -> Option<PoleTerm> {
727    let (c, rest) = arena.as_coeff_term(t);
728    let ExprNode::Pow(base, exp) = arena.node(rest).clone() else {
729        return None;
730    };
731    let e = as_i64(arena, exp)?;
732    if e >= 0 {
733        return None;
734    }
735    let m = (-e) as u32;
736    let (alpha, beta0) = linear_in(arena, base, var)?;
737    // c·(αk+β')^(−m) = c·α^(−m)·(k + β'/α)^(−m)
738    let coeff = c * rat_pow_i(&alpha, -(m as i64));
739    Some(PoleTerm {
740        c: coeff,
741        beta: beta0 / alpha,
742        m,
743    })
744}
745
746/// Group poles by `(m, frac(β))`; each group sorted by `β`.
747fn group_poles(poles: &[PoleTerm]) -> Vec<Vec<PoleTerm>> {
748    let mut groups: Vec<Vec<PoleTerm>> = Vec::new();
749    for p in poles {
750        let fp = frac_part(&p.beta);
751        if let Some(g) = groups
752            .iter_mut()
753            .find(|g| g[0].m == p.m && frac_part(&g[0].beta) == fp)
754        {
755            g.push(p.clone());
756        } else {
757            groups.push(vec![p.clone()]);
758        }
759    }
760    for g in &mut groups {
761        g.sort_by(|a, b| a.beta.cmp(&b.beta));
762    }
763    groups
764}
765
766/// `G_m(x)` — the "partial sum function" for `Σ 1/(k+β)^m`:
767/// `harmonic(x)` for integer offsets and `m = 1`, `digamma(x+1)` otherwise.
768fn partial_sum_fn(arena: &mut Arena, x: ExprId, integer_offsets: bool) -> ExprId {
769    if integer_offsets {
770        arena.harmonic(x)
771    } else {
772        let one = arena.one;
773        let x1 = arena.add(&[x, one]);
774        arena.digamma(x1)
775    }
776}
777
778/// `T(N) = Σ_i c_i G_m(N + β_i)` re-expressed relative to the smallest `β`
779/// in the group, so that integer-shifted poles telescope exactly.
780///
781/// Poles more than [`MAX_TELESCOPE_SHIFT`] beyond the smallest `β` are not
782/// expanded term by term; for `m = 1` they keep their own
783/// `harmonic` / `digamma` value instead (still exact, just less compact).
784/// Returns `None` if the group needs a generalised harmonic number
785/// (`m ≥ 2` with non-zero total coefficient, or a far `m ≥ 2` pole).
786fn pole_group_partial(arena: &mut Arena, group: &[PoleTerm], n_expr: ExprId) -> Option<ExprId> {
787    let m = group[0].m;
788    let b0 = group[0].beta.clone();
789    let integer_offsets = b0.is_integer();
790    let mut terms = Vec::new();
791    let mut csum = Rat::zero();
792    let mut far = Vec::new();
793    for p in group {
794        let d = (&p.beta - &b0).to_integer().to_i64()?;
795        if d > MAX_TELESCOPE_SHIFT {
796            far.push(p);
797            continue;
798        }
799        csum += &p.c;
800        for j in 1..=d {
801            let shift = &b0 + rat_i(j);
802            let x = add_rat(arena, n_expr, &shift);
803            let inv = pow_rat(arena, x, &rat_i(-(m as i64)));
804            let ce = rat_expr(arena, p.c.clone());
805            terms.push(arena.mul(&[ce, inv]));
806        }
807    }
808    if !csum.is_zero() {
809        if m != 1 {
810            return None;
811        }
812        let x = add_rat(arena, n_expr, &b0);
813        let g = partial_sum_fn(arena, x, integer_offsets);
814        let ce = rat_expr(arena, csum);
815        terms.push(arena.mul(&[ce, g]));
816    }
817    for p in far {
818        if m != 1 {
819            return None;
820        }
821        let x = add_rat(arena, n_expr, &p.beta);
822        let g = partial_sum_fn(arena, x, integer_offsets);
823        let ce = rat_expr(arena, p.c.clone());
824        terms.push(arena.mul(&[ce, g]));
825    }
826    Some(add_all(arena, &terms))
827}
828
829fn rational_sum_finite(
830    arena: &mut Arena,
831    body: ExprId,
832    var: ExprId,
833    lo: ExprId,
834    hi: ExprId,
835) -> Option<ExprId> {
836    let (poly_part, poles) = rational_decompose(arena, body, var)?;
837    let mut parts = Vec::new();
838    if !poly_part.is_zero() {
839        parts.push(faulhaber_poly(arena, &poly_part, lo, hi));
840    }
841    let one = arena.one;
842    let lo_m1 = arena.sub(lo, one);
843    let lo_m1 = eval::eval(arena, lo_m1);
844    for group in group_poles(&poles) {
845        let t_hi = pole_group_partial(arena, &group, hi)?;
846        let t_lo = pole_group_partial(arena, &group, lo_m1)?;
847        parts.push(arena.sub(t_hi, t_lo));
848    }
849    let total = add_all(arena, &parts);
850    Some(eval::eval(arena, total))
851}
852
853/// Infinite rational sums `Σ_{k=lo}^{∞} N(k)/D(k)`.
854fn rational_sum_infinite(
855    arena: &mut Arena,
856    body: ExprId,
857    var: ExprId,
858    lo: ExprId,
859) -> Option<SumOutcome> {
860    let (poly_part, poles) = rational_decompose(arena, body, var)?;
861    if !poly_part.is_zero() {
862        let lc = poly_part.leading_coeff().cloned().unwrap_or_else(Rat::zero);
863        return Some(SumOutcome::Divergent(infinity_of_sign(
864            arena,
865            Some(lc.is_positive()),
866        )));
867    }
868    // Simple poles: total coefficient must vanish for convergence.
869    let simple_total: Rat = poles.iter().filter(|p| p.m == 1).map(|p| p.c.clone()).sum();
870    if !simple_total.is_zero() {
871        return Some(SumOutcome::Divergent(infinity_of_sign(
872            arena,
873            Some(simple_total.is_positive()),
874        )));
875    }
876    // Integer-offset simple poles: use harmonic numbers only if their
877    // own coefficient sum vanishes (so Euler's γ cancels); otherwise digamma.
878    let int_simple_total: Rat = poles
879        .iter()
880        .filter(|p| p.m == 1 && p.beta.is_integer())
881        .map(|p| p.c.clone())
882        .sum();
883    let use_harmonic = int_simple_total.is_zero();
884
885    let mut parts = Vec::new();
886    for group in group_poles(&poles) {
887        let m = group[0].m;
888        let b0 = group[0].beta.clone();
889        let csum: Rat = group.iter().map(|p| p.c.clone()).sum();
890        if m == 1 {
891            // Σ_{k=lo}^{∞} Σ_i c_i/(k+β_i) = −Σ_i c_i ψ(lo + β_i)   (Σ c_i = 0 overall)
892            // Within the group, ψ(x + d) = ψ(x) + Σ_{j<d} 1/(x+j), so only the
893            // group's total coefficient multiplies a ψ / harmonic value.
894            // Poles shifted by more than MAX_TELESCOPE_SHIFT keep their own ψ.
895            let x0 = add_rat(arena, lo, &b0);
896            let psi = |arena: &mut Arena, x: ExprId, integer: bool| -> ExprId {
897                if use_harmonic && integer {
898                    // ψ(x) = H_{x−1} − γ; γ cancels overall.
899                    let one = arena.one;
900                    let xm1 = arena.sub(x, one);
901                    let xm1 = eval::eval(arena, xm1);
902                    arena.harmonic(xm1)
903                } else {
904                    arena.digamma(x)
905                }
906            };
907            let mut near_sum = Rat::zero();
908            for p in &group {
909                let d = (&p.beta - &b0).to_integer().to_i64()?;
910                if d > MAX_TELESCOPE_SHIFT {
911                    let x = add_rat(arena, lo, &p.beta);
912                    let g = psi(arena, x, p.beta.is_integer());
913                    let ce = rat_expr(arena, -p.c.clone());
914                    parts.push(arena.mul(&[ce, g]));
915                    continue;
916                }
917                near_sum += &p.c;
918                for j in 0..d {
919                    let x = add_rat(arena, x0, &rat_i(j));
920                    let inv = pow_rat(arena, x, &(-Rat::one()));
921                    let ce = rat_expr(arena, -p.c.clone());
922                    parts.push(arena.mul(&[ce, inv]));
923                }
924            }
925            if !near_sum.is_zero() {
926                let g = psi(arena, x0, b0.is_integer());
927                let ce = rat_expr(arena, -near_sum);
928                parts.push(arena.mul(&[ce, g]));
929            }
930        } else if csum.is_zero() {
931            // Pure telescoping: −Σ_i c_i Σ_{j=0}^{d_i−1} (lo + β_0 + j)^(−m)
932            for p in &group {
933                let d = (&p.beta - &b0).to_integer().to_i64()?;
934                if d > MAX_TELESCOPE_SHIFT {
935                    return None;
936                }
937                for j in 0..d {
938                    let shift = &b0 + rat_i(j);
939                    let x = add_rat(arena, lo, &shift);
940                    let inv = pow_rat(arena, x, &rat_i(-(m as i64)));
941                    let ce = rat_expr(arena, -p.c.clone());
942                    parts.push(arena.mul(&[ce, inv]));
943                }
944            }
945        } else {
946            // Each term is a Hurwitz zeta value ζ(m, lo+β).
947            for p in &group {
948                let x = add_rat(arena, lo, &p.beta);
949                let q = eval::eval(arena, x);
950                let qr = as_rat(arena, q)?;
951                let z = hurwitz_zeta_closed(arena, m as usize, &qr)?;
952                let ce = rat_expr(arena, p.c.clone());
953                parts.push(arena.mul(&[ce, z]));
954            }
955        }
956    }
957    let total = add_all(arena, &parts);
958    Some(SumOutcome::Closed(eval::eval(arena, total)))
959}
960
961// ═══════════════════════════════════════════════════════════════════════════
962// Zeta-type constants
963// ═══════════════════════════════════════════════════════════════════════════
964
965/// `ζ(2m) = (−1)^{m+1} B_{2m} (2π)^{2m} / (2·(2m)!)` — returns the rational
966/// factor `r` such that `ζ(2m) = r·π^{2m}`.
967pub(crate) fn zeta_even_rational(m: usize) -> Rat {
968    let two_m = 2 * m;
969    let b = bernoulli(two_m);
970    let sign = if m % 2 == 1 { Rat::one() } else { -Rat::one() };
971    let two_pow = rat_pow_i(&rat_i(2), two_m as i64);
972    let denom = Rat::from_integer(factorial_big(two_m as u64)) * rat_i(2);
973    sign * b * two_pow / denom
974}
975
976/// Euler numbers `E_0, E_2, E_4, …` (E_{2n} for the given `n`).
977pub(crate) fn euler_number(n: usize) -> BigInt {
978    // Σ_{k=0}^{n} C(2n, 2k) E_{2k} = 0 for n ≥ 1, E_0 = 1.
979    let mut e = vec![BigInt::one()];
980    for nn in 1..=n {
981        let mut acc = BigInt::zero();
982        for (k, ek) in e.iter().enumerate() {
983            acc += binomial_big(2 * nn as u64, 2 * k as u64) * ek;
984        }
985        e.push(-acc);
986    }
987    e[n].clone()
988}
989
990/// Exact value of `ζ(p)` for integer `p ≥ 2`.
991///
992/// Even `p` gives the elementary closed form (`ζ(2) = π²/6`, `ζ(4) = π⁴/90`,
993/// …, via Bernoulli numbers); odd `p ≥ 3` has no elementary closed form and
994/// is returned as the `Zeta(p)` node (`ζ(3)` is Apéry's constant).
995/// Returns `None` for `p < 2` (the harmonic series diverges).
996pub(crate) fn zeta_value(arena: &mut Arena, p: usize) -> Option<ExprId> {
997    if p < 2 {
998        return None;
999    }
1000    if p % 2 == 1 {
1001        let pe = arena.int(p as i64);
1002        return Some(arena.zeta(pe));
1003    }
1004    let r = zeta_even_rational(p / 2);
1005    let re = rat_expr(arena, r);
1006    let pi = arena.pi;
1007    let pip = pow_rat(arena, pi, &rat_i(p as i64));
1008    let v = arena.mul(&[re, pip]);
1009    Some(eval::eval(arena, v))
1010}
1011
1012/// Dirichlet eta `η(p) = Σ_{k≥1} (−1)^{k+1}/k^p = (1 − 2^{1−p}) ζ(p)`; `η(1) = ln 2`.
1013fn eta_value(arena: &mut Arena, p: usize) -> Option<ExprId> {
1014    if p == 1 {
1015        let two = arena.int(2);
1016        return Some(arena.ln(two));
1017    }
1018    let z = zeta_value(arena, p)?;
1019    let factor = Rat::one() - rat_pow_i(&rat_i(2), 1 - p as i64);
1020    let fe = rat_expr(arena, factor);
1021    let v = arena.mul(&[fe, z]);
1022    Some(eval::eval(arena, v))
1023}
1024
1025/// Dirichlet beta `β(p) = Σ_{k≥0} (−1)^k/(2k+1)^p`; closed form for odd `p`:
1026/// `β(2m+1) = (−1)^m E_{2m} π^{2m+1} / (4^{m+1} (2m)!)`, and `β(2) = G`
1027/// (Catalan's constant).  Even `p ≥ 4` has no known closed form.
1028fn dirichlet_beta_value(arena: &mut Arena, p: usize) -> Option<ExprId> {
1029    if p == 2 {
1030        return Some(arena.catalan);
1031    }
1032    if p.is_multiple_of(2) {
1033        return None;
1034    }
1035    let m = (p - 1) / 2;
1036    let e = euler_number(m);
1037    let sign = if m.is_multiple_of(2) {
1038        BigInt::one()
1039    } else {
1040        -BigInt::one()
1041    };
1042    let denom = rat_pow_i(&rat_i(4), m as i64 + 1) * Rat::from_integer(factorial_big(2 * m as u64));
1043    let r = Rat::from_integer(sign * e) / denom;
1044    let re = rat_expr(arena, r);
1045    let pi = arena.pi;
1046    let pip = pow_rat(arena, pi, &rat_i(p as i64));
1047    let v = arena.mul(&[re, pip]);
1048    Some(eval::eval(arena, v))
1049}
1050
1051/// Hurwitz zeta `ζ(m, q) = Σ_{k≥0} 1/(k+q)^m` for integer `m ≥ 2` and the
1052/// rational offsets we can handle exactly: positive integers and
1053/// half-integers, with `m` even.
1054fn hurwitz_zeta_closed(arena: &mut Arena, m: usize, q: &Rat) -> Option<ExprId> {
1055    if m < 2 || !q.is_positive() {
1056        return None;
1057    }
1058    if q.is_integer() {
1059        let qi = q.to_integer().to_i64()?;
1060        if qi > MAX_TELESCOPE_SHIFT {
1061            return None;
1062        }
1063        let z = zeta_value(arena, m)?;
1064        let mut acc = Rat::zero();
1065        for j in 1..qi {
1066            acc += rat_pow_i(&rat_i(j), -(m as i64));
1067        }
1068        let ce = rat_expr(arena, -acc);
1069        let v = arena.add(&[z, ce]);
1070        return Some(eval::eval(arena, v));
1071    }
1072    // half-integer q = n + 1/2 with n ≥ 0: ζ(m, 1/2) = (2^m − 1) ζ(m)
1073    let two_q = q * rat_i(2);
1074    if two_q.is_integer() {
1075        let n = ((q - Rat::new(BigInt::one(), BigInt::from(2))).to_integer()).to_i64()?;
1076        if !(0..=MAX_TELESCOPE_SHIFT).contains(&n) {
1077            return None;
1078        }
1079        let z = zeta_value(arena, m)?;
1080        let factor = rat_pow_i(&rat_i(2), m as i64) - Rat::one();
1081        let mut acc = Rat::zero();
1082        for j in 0..n {
1083            let x = rat_i(j) + Rat::new(BigInt::one(), BigInt::from(2));
1084            acc += rat_pow_i(&x, -(m as i64));
1085        }
1086        let fe = rat_expr(arena, factor);
1087        let ce = rat_expr(arena, -acc);
1088        let fz = arena.mul(&[fe, z]);
1089        let v = arena.add(&[fz, ce]);
1090        return Some(eval::eval(arena, v));
1091    }
1092    None
1093}
1094
1095// ═══════════════════════════════════════════════════════════════════════════
1096// Telescoping  g(k) − g(k+d)
1097// ═══════════════════════════════════════════════════════════════════════════
1098
1099/// If `terms = [A, B]` with `B(k+d) = −A(k)` (or vice versa) for some
1100/// `d ∈ {1,2,3}`, returns `(g, d)` such that `body = g(k) − g(k+d)`.
1101fn telescoping_form(arena: &mut Arena, terms: &[ExprId], var: ExprId) -> Option<(ExprId, i64)> {
1102    if terms.len() != 2 {
1103        return None;
1104    }
1105    let (a, b) = (terms[0], terms[1]);
1106    for d in 1..=3i64 {
1107        let de = arena.int(d);
1108        let shifted_var = arena.add(&[var, de]);
1109        let b_shift = subs::subs(arena, b, var, shifted_var);
1110        let test = arena.add(&[a, b_shift]);
1111        if is_zero_expr(arena, test) {
1112            return Some((b, d));
1113        }
1114        let a_shift = subs::subs(arena, a, var, shifted_var);
1115        let test = arena.add(&[b, a_shift]);
1116        if is_zero_expr(arena, test) {
1117            return Some((a, d));
1118        }
1119    }
1120    None
1121}
1122
1123fn telescoping_finite(
1124    arena: &mut Arena,
1125    terms: &[ExprId],
1126    var: ExprId,
1127    lo: ExprId,
1128    hi: ExprId,
1129) -> Option<ExprId> {
1130    let (g, d) = telescoping_form(arena, terms, var)?;
1131    // Σ_{k=lo}^{hi} [g(k) − g(k+d)] = Σ_{j=0}^{d−1} [g(lo+j) − g(hi+1+j)]
1132    let mut parts = Vec::new();
1133    for j in 0..d {
1134        let je = arena.int(j);
1135        let lo_j = arena.add(&[lo, je]);
1136        let hi_j = arena.int(j + 1);
1137        let hi_j = arena.add(&[hi, hi_j]);
1138        let g_lo = subs::subs(arena, g, var, lo_j);
1139        let g_hi = subs::subs(arena, g, var, hi_j);
1140        parts.push(arena.sub(g_lo, g_hi));
1141    }
1142    let total = add_all(arena, &parts);
1143    Some(eval::eval(arena, total))
1144}
1145
1146/// Infinite telescoping: needs `lim_{N→∞} g(N)`.  Only rational-function
1147/// tails are trusted (their limit is computed exactly here).
1148fn telescoping_infinite(
1149    arena: &mut Arena,
1150    terms: &[ExprId],
1151    var: ExprId,
1152    lo: ExprId,
1153) -> Option<SumOutcome> {
1154    let (g, d) = telescoping_form(arena, terms, var)?;
1155    let limit = limit_at_infinity(arena, g, var)?;
1156    let mut parts = Vec::new();
1157    for j in 0..d {
1158        let je = arena.int(j);
1159        let lo_j = arena.add(&[lo, je]);
1160        let g_lo = subs::subs(arena, g, var, lo_j);
1161        parts.push(arena.sub(g_lo, limit));
1162    }
1163    let total = add_all(arena, &parts);
1164    Some(SumOutcome::Closed(eval::eval(arena, total)))
1165}
1166
1167/// Exact `lim_{k→∞} f(k)` for the shapes we can decide without the general
1168/// limit engine: rational functions of `k` (finite limits only), sums of
1169/// such terms, and hypergeometric-type terms whose exact Stirling growth
1170/// analysis shows they tend to zero (`P(k)·r^k` with `|r| < 1`,
1171/// `1/(k+1)!`, `k!/k^k`, …).
1172pub(crate) fn limit_at_infinity(arena: &mut Arena, f: ExprId, var: ExprId) -> Option<ExprId> {
1173    if !depends_on(arena, f, var) {
1174        return Some(f);
1175    }
1176    if let Some(l) = rational_limit_at_infinity(arena, f, var) {
1177        return l.map(|r| rat_expr(arena, r));
1178    }
1179    if let ExprNode::Add(ref terms) = arena.node(f).clone() {
1180        let terms: Vec<ExprId> = terms.to_vec();
1181        let mut limits = Vec::with_capacity(terms.len());
1182        for t in terms {
1183            limits.push(limit_at_infinity(arena, t, var)?);
1184        }
1185        let s = add_all(arena, &limits);
1186        return Some(eval::eval(arena, s));
1187    }
1188    // Constant factors: c · g(k)
1189    if let ExprNode::Mul(ref factors) = arena.node(f).clone() {
1190        let factors: Vec<ExprId> = factors.to_vec();
1191        let (consts, varf): (Vec<ExprId>, Vec<ExprId>) = factors
1192            .iter()
1193            .copied()
1194            .partition(|&g| !depends_on(arena, g, var));
1195        if !consts.is_empty() && !varf.is_empty() {
1196            let inner = mul_all(arena, &varf);
1197            let l = limit_at_infinity(arena, inner, var)?;
1198            let mut all = consts;
1199            all.push(l);
1200            let p = arena.mul(&all);
1201            return Some(eval::eval(arena, p));
1202        }
1203    }
1204    // Growth analysis needs monomial factors: replace every polynomial
1205    // factor `P(k)` (rational coefficients) by its leading term `lc·k^d`,
1206    // which has the same asymptotic growth.
1207    let dominant = dominant_factor_form(arena, f, var);
1208    if crate::calculus::convergence::growth_exponents(arena, dominant, var)
1209        .and_then(|g| g.tends_to_zero())
1210        == Some(true)
1211    {
1212        return Some(arena.zero);
1213    }
1214    None
1215}
1216
1217/// Replace polynomial factors of `f` (with rational coefficients, degree
1218/// ≥ 1) by their leading monomials.  Only the growth rate is preserved.
1219fn dominant_factor_form(arena: &mut Arena, f: ExprId, var: ExprId) -> ExprId {
1220    let factors = mul_factors(arena, f);
1221    let mut out = Vec::with_capacity(factors.len());
1222    let mut changed = false;
1223    for g in factors {
1224        let replaced = match arena.node(g).clone() {
1225            ExprNode::Add(_) => polybridge::expr_to_poly(arena, g, var).and_then(|p| {
1226                let d = p.degree()?;
1227                let lc = p.leading_coeff()?.clone();
1228                let ce = rat_expr(arena, lc);
1229                let kd = pow_rat(arena, var, &rat_i(d as i64));
1230                Some(arena.mul(&[ce, kd]))
1231            }),
1232            ExprNode::Pow(base, exp)
1233                if matches!(arena.node(base), ExprNode::Add(_)) && !depends_on(arena, exp, var) =>
1234            {
1235                polybridge::expr_to_poly(arena, base, var).and_then(|p| {
1236                    let d = p.degree()?;
1237                    let lc = p.leading_coeff()?.clone();
1238                    let ce = rat_expr(arena, lc);
1239                    let kd = pow_rat(arena, var, &rat_i(d as i64));
1240                    let mono = arena.mul(&[ce, kd]);
1241                    Some(arena.pow(mono, exp))
1242                })
1243            }
1244            _ => None,
1245        };
1246        match replaced {
1247            Some(r) => {
1248                changed = true;
1249                out.push(r);
1250            }
1251            None => out.push(g),
1252        }
1253    }
1254    if changed {
1255        let p = mul_all(arena, &out);
1256        eval::eval(arena, p)
1257    } else {
1258        f
1259    }
1260}
1261
1262/// `lim_{k→∞}` of a rational function with rational coefficients.
1263/// `Some(None)` means the limit is infinite.
1264fn rational_limit_at_infinity(arena: &mut Arena, f: ExprId, var: ExprId) -> Option<Option<Rat>> {
1265    let (n, d) = polybridge::as_numer_denom(arena, f);
1266    let np = polybridge::expr_to_poly(arena, n, var)?;
1267    let dp = polybridge::expr_to_poly(arena, d, var)?;
1268    if dp.is_zero() {
1269        return None;
1270    }
1271    if np.is_zero() {
1272        return Some(Some(Rat::zero()));
1273    }
1274    let dn = np.degree()?;
1275    let dd = dp.degree()?;
1276    if dn < dd {
1277        Some(Some(Rat::zero()))
1278    } else if dn == dd {
1279        Some(Some(np.coeff(dn) / dp.coeff(dd)))
1280    } else {
1281        Some(None)
1282    }
1283}
1284
1285// ═══════════════════════════════════════════════════════════════════════════
1286// Term shape analysis (hypergeometric monomials)
1287// ═══════════════════════════════════════════════════════════════════════════
1288
1289/// Normalised multiplicative structure of a summand `t(k)`:
1290///
1291/// ```text
1292/// t(k) = constant · (−1)^k[if alternating] · numeric_base^k · Π baseᵢ^(aᵢ·k)
1293///        · Π (k + βⱼ)^pⱼ · Π ((αₗ k + βₗ)!)^eₗ · Π C(nᵣ, k)^eᵣ
1294/// ```
1295///
1296/// Every factor is either `k`-free (folded into `constant`) or one of the
1297/// recognised shapes; otherwise [`term_shape`] returns `None`.
1298#[derive(Debug, Clone)]
1299pub(crate) struct TermShape {
1300    /// Product of all `k`-free factors (including `base^b` leftovers).
1301    pub(crate) constant: ExprId,
1302    /// A `(−1)^k` alternation is present.
1303    pub(crate) alternating: bool,
1304    /// Exact rational `Π rᵢ^{aᵢ}` over numeric bases with integer `aᵢ`.
1305    pub(crate) numeric_base: Rat,
1306    /// Symbolic geometric bases `(base, a)` meaning `base^(a·k)`.
1307    pub(crate) bases: Vec<(ExprId, Rat)>,
1308    /// Monic linear powers `(β, p)` meaning `(k + β)^p`, sorted by `β`.
1309    pub(crate) lin_pows: Vec<(Rat, Rat)>,
1310    /// Factorial factors `(α, β, e)` meaning `((α·k + β)!)^e`, sorted.
1311    pub(crate) facts: Vec<(Rat, Rat, i64)>,
1312    /// Binomial factors `(n, e)` meaning `C(n, k)^e` with `n` free of `k`.
1313    pub(crate) binomials: Vec<(ExprId, i64)>,
1314}
1315
1316impl TermShape {
1317    fn is_pure_lin_pows(&self) -> bool {
1318        self.bases.is_empty()
1319            && self.numeric_base.is_one()
1320            && self.facts.is_empty()
1321            && self.binomials.is_empty()
1322    }
1323
1324    /// Combined geometric base `Y = numeric_base · Π baseᵢ^{aᵢ}` (with the
1325    /// sign alternation folded in), or `None` when there is none.
1326    pub(crate) fn geometric_base(&self, arena: &mut Arena) -> Option<ExprId> {
1327        if self.bases.is_empty() && self.numeric_base.is_one() && !self.alternating {
1328            return None;
1329        }
1330        let mut factors = Vec::new();
1331        let mut num = self.numeric_base.clone();
1332        if self.alternating {
1333            num = -num;
1334        }
1335        if !num.is_one() {
1336            factors.push(rat_expr(arena, num));
1337        }
1338        for (b, a) in &self.bases {
1339            factors.push(pow_rat(arena, *b, a));
1340        }
1341        let y = mul_all(arena, &factors);
1342        Some(eval::eval(arena, y))
1343    }
1344}
1345
1346/// Analyse the multiplicative structure of `body` with respect to `var`.
1347pub(crate) fn term_shape(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<TermShape> {
1348    let mut shape = TermShape {
1349        constant: arena.one,
1350        alternating: false,
1351        numeric_base: Rat::one(),
1352        bases: Vec::new(),
1353        lin_pows: Vec::new(),
1354        facts: Vec::new(),
1355        binomials: Vec::new(),
1356    };
1357    let mut consts: Vec<ExprId> = Vec::new();
1358    let factors = mul_factors(arena, body);
1359    for f in factors {
1360        shape_factor(arena, f, var, &Rat::one(), &mut shape, &mut consts)?;
1361    }
1362    shape.constant = mul_all(arena, &consts);
1363    shape.constant = eval::eval(arena, shape.constant);
1364    // Merge & sort.
1365    shape.lin_pows = merge_pairs(shape.lin_pows);
1366    shape.facts.sort_by_key(|a| (a.0.clone(), a.1.clone()));
1367    let mut merged_facts: Vec<(Rat, Rat, i64)> = Vec::new();
1368    for (a, b, e) in shape.facts {
1369        if let Some(last) = merged_facts.last_mut()
1370            && last.0 == a
1371            && last.1 == b
1372        {
1373            last.2 += e;
1374        } else {
1375            merged_facts.push((a, b, e));
1376        }
1377    }
1378    merged_facts.retain(|f| f.2 != 0);
1379    shape.facts = merged_facts;
1380    let mut merged_bases: Vec<(ExprId, Rat)> = Vec::new();
1381    for (b, a) in shape.bases {
1382        if let Some(slot) = merged_bases.iter_mut().find(|(bb, _)| *bb == b) {
1383            slot.1 += a;
1384        } else {
1385            merged_bases.push((b, a));
1386        }
1387    }
1388    merged_bases.retain(|(_, a)| !a.is_zero());
1389    merged_bases.sort_by_key(|(b, _)| b.0);
1390    shape.bases = merged_bases;
1391    let mut merged_bin: Vec<(ExprId, i64)> = Vec::new();
1392    for (n, e) in shape.binomials {
1393        if let Some(slot) = merged_bin.iter_mut().find(|(nn, _)| *nn == n) {
1394            slot.1 += e;
1395        } else {
1396            merged_bin.push((n, e));
1397        }
1398    }
1399    merged_bin.retain(|(_, e)| *e != 0);
1400    shape.binomials = merged_bin;
1401    Some(shape)
1402}
1403
1404fn merge_pairs(mut v: Vec<(Rat, Rat)>) -> Vec<(Rat, Rat)> {
1405    v.sort_by(|a, b| a.0.cmp(&b.0));
1406    let mut out: Vec<(Rat, Rat)> = Vec::new();
1407    for (b, p) in v {
1408        if let Some(last) = out.last_mut()
1409            && last.0 == b
1410        {
1411            last.1 += p;
1412        } else {
1413            out.push((b, p));
1414        }
1415    }
1416    out.retain(|(_, p)| !p.is_zero());
1417    out
1418}
1419
1420/// Classify one multiplicative factor raised to the rational power `outer`.
1421fn shape_factor(
1422    arena: &mut Arena,
1423    f: ExprId,
1424    var: ExprId,
1425    outer: &Rat,
1426    shape: &mut TermShape,
1427    consts: &mut Vec<ExprId>,
1428) -> Option<()> {
1429    if !depends_on(arena, f, var) {
1430        let c = pow_rat(arena, f, outer);
1431        consts.push(c);
1432        return Some(());
1433    }
1434    if f == var {
1435        shape.lin_pows.push((Rat::zero(), outer.clone()));
1436        return Some(());
1437    }
1438    match arena.node(f).clone() {
1439        ExprNode::Pow(base, exp) => {
1440            if !depends_on(arena, exp, var) {
1441                let p = as_rat(arena, exp)?;
1442                let total = outer * p;
1443                shape_factor(arena, base, var, &total, shape, consts)
1444            } else {
1445                if depends_on(arena, base, var) {
1446                    return None;
1447                }
1448                let (a, b) = linear_in(arena, exp, var)?;
1449                let a = a * outer;
1450                let b = b * outer;
1451                shape_geometric(arena, base, &a, &b, shape, consts)
1452            }
1453        }
1454        ExprNode::Exp(arg) => {
1455            let (a, b) = linear_in(arena, arg, var)?;
1456            let e = arena.e_const;
1457            let a = a * outer;
1458            let b = b * outer;
1459            shape_geometric(arena, e, &a, &b, shape, consts)
1460        }
1461        ExprNode::Factorial(arg) => {
1462            let e = outer.to_integer();
1463            if !outer.is_integer() {
1464                return None;
1465            }
1466            let (alpha, beta) = linear_in(arena, arg, var)?;
1467            shape.facts.push((alpha, beta, e.to_i64()?));
1468            Some(())
1469        }
1470        ExprNode::Gamma(arg) => {
1471            if !outer.is_integer() {
1472                return None;
1473            }
1474            let (alpha, beta) = linear_in(arena, arg, var)?;
1475            shape
1476                .facts
1477                .push((alpha, beta - Rat::one(), outer.to_integer().to_i64()?));
1478            Some(())
1479        }
1480        ExprNode::Binomial(n, kk) => {
1481            if !outer.is_integer() {
1482                return None;
1483            }
1484            let e = outer.to_integer().to_i64()?;
1485            if kk == var && !depends_on(arena, n, var) {
1486                shape.binomials.push((n, e));
1487                return Some(());
1488            }
1489            // C(2k, k) = (2k)!/(k!)²
1490            if kk == var
1491                && let Some((a, b)) = linear_in(arena, n, var)
1492            {
1493                shape.facts.push((a, b, e));
1494                shape.facts.push((Rat::one(), Rat::zero(), -e));
1495                let nm = arena.sub(n, var);
1496                let (a2, b2) = linear_in(arena, nm, var)?;
1497                shape.facts.push((a2, b2, -e));
1498                return Some(());
1499            }
1500            None
1501        }
1502        ExprNode::Mul(ref inner) => {
1503            let inner: Vec<ExprId> = inner.to_vec();
1504            for g in inner {
1505                shape_factor(arena, g, var, outer, shape, consts)?;
1506            }
1507            Some(())
1508        }
1509        ExprNode::Add(_) => {
1510            // Polynomial in k with rational coefficients → linear factors.
1511            let p = polybridge::expr_to_poly(arena, f, var)?;
1512            shape_polynomial(arena, &p, outer, shape, consts)
1513        }
1514        ExprNode::Neg(inner) => {
1515            let m1 = arena.neg_one;
1516            let c = pow_rat(arena, m1, outer);
1517            consts.push(c);
1518            shape_factor(arena, inner, var, outer, shape, consts)
1519        }
1520        _ => None,
1521    }
1522}
1523
1524/// `base^(a·k + b)` with `base` free of `k`.
1525fn shape_geometric(
1526    arena: &mut Arena,
1527    base: ExprId,
1528    a: &Rat,
1529    b: &Rat,
1530    shape: &mut TermShape,
1531    consts: &mut Vec<ExprId>,
1532) -> Option<()> {
1533    if a.is_zero() {
1534        let c = pow_rat(arena, base, b);
1535        consts.push(c);
1536        return Some(());
1537    }
1538    if let Some(r) = as_rat(arena, base) {
1539        if r.is_zero() {
1540            return None;
1541        }
1542        if a.is_integer() {
1543            let ai = a.to_integer().to_i64()?;
1544            if r == -Rat::one() {
1545                if ai.rem_euclid(2) == 1 {
1546                    shape.alternating = !shape.alternating;
1547                }
1548            } else {
1549                shape.numeric_base *= rat_pow_i(&r, ai);
1550            }
1551            if !b.is_zero() {
1552                let c = pow_rat(arena, base, b);
1553                consts.push(c);
1554            }
1555            return Some(());
1556        }
1557        // Non-integer multiple of k (e.g. 2^(k/2)) — keep as symbolic base.
1558        shape.bases.push((base, a.clone()));
1559        if !b.is_zero() {
1560            let c = pow_rat(arena, base, b);
1561            consts.push(c);
1562        }
1563        return Some(());
1564    }
1565    shape.bases.push((base, a.clone()));
1566    if !b.is_zero() {
1567        let c = pow_rat(arena, base, b);
1568        consts.push(c);
1569    }
1570    Some(())
1571}
1572
1573/// `P(k)^outer` for a rational-coefficient polynomial: factor over ℤ and
1574/// require every irreducible factor to be linear.
1575fn shape_polynomial(
1576    arena: &mut Arena,
1577    p: &Poly,
1578    outer: &Rat,
1579    shape: &mut TermShape,
1580    consts: &mut Vec<ExprId>,
1581) -> Option<()> {
1582    let (content, factors) = p.factor_over_z();
1583    if content.is_zero() {
1584        return None;
1585    }
1586    if !content.is_one() {
1587        let c = rat_expr(arena, content);
1588        let c = pow_rat(arena, c, outer);
1589        consts.push(c);
1590    }
1591    for (fac, mult) in factors {
1592        if fac.degree() != Some(1) {
1593            return None;
1594        }
1595        let alpha = fac.coeff(1);
1596        let beta = fac.coeff(0);
1597        let e = outer * rat_i(mult as i64);
1598        if !alpha.is_one() {
1599            if alpha.is_negative() && !e.is_integer() {
1600                return None;
1601            }
1602            let ae = rat_expr(arena, alpha.clone());
1603            let c = pow_rat(arena, ae, &e);
1604            consts.push(c);
1605        }
1606        shape.lin_pows.push((beta / alpha, e));
1607    }
1608    Some(())
1609}
1610
1611// ═══════════════════════════════════════════════════════════════════════════
1612// Binomial sums
1613// ═══════════════════════════════════════════════════════════════════════════
1614
1615/// Stirling numbers of the second kind `S(m, j)` for `0 ≤ j ≤ m`.
1616fn stirling_second_row(m: usize) -> Vec<Rat> {
1617    // S(0,0) = 1; S(m, j) = j·S(m−1, j) + S(m−1, j−1).
1618    let mut row = vec![Rat::one()];
1619    for _ in 0..m {
1620        let mut next = vec![Rat::zero(); row.len() + 1];
1621        for (j, s) in row.iter().enumerate() {
1622            next[j] += rat_i(j as i64) * s;
1623            next[j + 1] += s;
1624        }
1625        row = next;
1626    }
1627    row
1628}
1629
1630/// `Σ_{k=0}^{n} P(k)·C(n,k)·x^k` for a polynomial `P` (symbolic `k`-free
1631/// coefficients allowed) via the falling-factorial basis:
1632/// `k^(j)·C(n,k) = n^(j)·C(n−j, k−j)`, so the sum is
1633/// `Σ_j s_j·n^(j)·x^j·(1+x)^{n−j}` where `P(k) = Σ_j s_j·k^(j)`.
1634fn binomial_poly_sum(
1635    arena: &mut Arena,
1636    body: ExprId,
1637    var: ExprId,
1638    lo: ExprId,
1639    hi: ExprId,
1640) -> Option<ExprId> {
1641    if as_i64(arena, lo) != Some(0) {
1642        return None;
1643    }
1644    let mut consts = Vec::new();
1645    let mut poly_factors = Vec::new();
1646    let mut binomial_n: Option<ExprId> = None;
1647    let mut x_parts = Vec::new();
1648    for f in mul_factors(arena, body) {
1649        if !depends_on(arena, f, var) {
1650            consts.push(f);
1651            continue;
1652        }
1653        match arena.node(f).clone() {
1654            ExprNode::Binomial(n, kk) if kk == var && !depends_on(arena, n, var) => {
1655                if binomial_n.is_some() {
1656                    return None;
1657                }
1658                binomial_n = Some(n);
1659            }
1660            ExprNode::Pow(base, exp)
1661                if !depends_on(arena, base, var) && depends_on(arena, exp, var) =>
1662            {
1663                let (a, b) = linear_in(arena, exp, var)?;
1664                x_parts.push(pow_rat(arena, base, &a));
1665                if !b.is_zero() {
1666                    consts.push(pow_rat(arena, base, &b));
1667                }
1668            }
1669            _ => {
1670                sym_poly_in(arena, f, var)?;
1671                poly_factors.push(f);
1672            }
1673        }
1674    }
1675    let n = binomial_n?;
1676    if poly_factors.is_empty() {
1677        return None; // the shape table handles the pure cases
1678    }
1679    let diff = arena.sub(hi, n);
1680    let diff = eval::eval(arena, diff);
1681    if !arena.is_zero_structural(diff) {
1682        return None;
1683    }
1684    let p_expr = mul_all(arena, &poly_factors);
1685    let monomials = sym_poly_in(arena, p_expr, var)?;
1686    let deg = monomials.iter().map(|(d, _)| *d).max()?;
1687    // x (the geometric base) and 1 + x.
1688    let x = if x_parts.is_empty() {
1689        arena.one
1690    } else {
1691        let xx = mul_all(arena, &x_parts);
1692        eval::eval(arena, xx)
1693    };
1694    if as_rat(arena, x) == Some(-Rat::one()) {
1695        return None; // (1+x)^{n−j} = 0^{n−j} needs a case split; leave to Gosper
1696    }
1697    let one = arena.one;
1698    let one_plus_x = arena.add(&[one, x]);
1699    let one_plus_x = eval::eval(arena, one_plus_x);
1700    // Falling-factorial coefficients s_j = Σ_m c_m S(m, j).
1701    let mut s: Vec<Vec<ExprId>> = vec![Vec::new(); deg + 1];
1702    for (m, c) in &monomials {
1703        let row = stirling_second_row(*m);
1704        for (j, st) in row.iter().enumerate() {
1705            if st.is_zero() {
1706                continue;
1707            }
1708            let se = rat_expr(arena, st.clone());
1709            s[j].push(arena.mul(&[se, *c]));
1710        }
1711    }
1712    let mut terms = Vec::new();
1713    let mut falling = arena.one; // n^(j)
1714    for (j, parts) in s.iter().enumerate() {
1715        if j > 0 {
1716            let shift = arena.int(-(j as i64 - 1));
1717            let factor = arena.add(&[n, shift]);
1718            falling = arena.mul(&[falling, factor]);
1719        }
1720        if parts.is_empty() {
1721            continue;
1722        }
1723        let sj = add_all(arena, parts);
1724        let xj = pow_rat(arena, x, &rat_i(j as i64));
1725        let nj = arena.int(-(j as i64));
1726        let n_minus_j = arena.add(&[n, nj]);
1727        let tail = arena.pow(one_plus_x, n_minus_j);
1728        terms.push(arena.mul(&[sj, falling, xj, tail]));
1729    }
1730    let total = add_all(arena, &terms);
1731    consts.push(total);
1732    let result = mul_all(arena, &consts);
1733    Some(eval::eval(arena, result))
1734}
1735
1736fn binomial_sum(
1737    arena: &mut Arena,
1738    body: ExprId,
1739    var: ExprId,
1740    lo: ExprId,
1741    hi: ExprId,
1742) -> Option<ExprId> {
1743    if let Some(r) = binomial_poly_sum(arena, body, var, lo, hi) {
1744        return Some(r);
1745    }
1746    let shape = term_shape(arena, body, var)?;
1747    if shape.binomials.len() != 1 || !shape.facts.is_empty() {
1748        return None;
1749    }
1750    let (n, e) = shape.binomials[0];
1751    // Require lo = 0 and hi = n.
1752    if as_i64(arena, lo) != Some(0) {
1753        return None;
1754    }
1755    let diff = arena.sub(hi, n);
1756    let diff = eval::eval(arena, diff);
1757    if !arena.is_zero_structural(diff) {
1758        return None;
1759    }
1760    let one = arena.one;
1761    let two = arena.int(2);
1762    let n_m1 = arena.sub(n, one);
1763    let n_p1 = arena.add(&[n, one]);
1764    let x = shape.geometric_base(arena); // includes sign alternation
1765    let result = match (e, shape.lin_pows.as_slice(), x) {
1766        // Σ C(n,k) = 2^n
1767        (1, [], None) => arena.pow(two, n),
1768        // Σ k C(n,k) = n 2^(n−1)
1769        (1, [(b, p)], None) if b.is_zero() && p.is_one() => {
1770            let t = arena.pow(two, n_m1);
1771            arena.mul(&[n, t])
1772        }
1773        // Σ k² C(n,k) = n(n+1) 2^(n−2)
1774        (1, [(b, p)], None) if b.is_zero() && *p == rat_i(2) => {
1775            let n_m2 = arena.int(-2);
1776            let n_m2 = arena.add(&[n, n_m2]);
1777            let t = arena.pow(two, n_m2);
1778            arena.mul(&[n, n_p1, t])
1779        }
1780        // Σ C(n,k)/(k+1) = (2^(n+1) − 1)/(n+1)
1781        (1, [(b, p)], None) if b.is_one() && *p == -Rat::one() => {
1782            let t = arena.pow(two, n_p1);
1783            let num = arena.sub(t, one);
1784            arena.div(num, n_p1)
1785        }
1786        // Σ C(n,k)² = C(2n, n)
1787        (2, [], None) => {
1788            let two_n = arena.mul(&[two, n]);
1789            arena.binomial(two_n, n)
1790        }
1791        // Σ C(n,k) x^k = (1+x)^n      (x = −1 gives 0 for n ≥ 1, 1 for n = 0)
1792        (1, [], Some(x)) => {
1793            if as_rat(arena, x) == Some(-Rat::one()) {
1794                let zero = arena.zero;
1795                let cond = arena.eq_(n, zero);
1796                let ncond = arena.ne_(n, zero);
1797                arena.piecewise(&[(one, cond), (zero, ncond)])
1798            } else {
1799                let base = arena.add(&[one, x]);
1800                arena.pow(base, n)
1801            }
1802        }
1803        // Σ k C(n,k) x^k = n x (1+x)^(n−1)
1804        (1, [(b, p)], Some(x)) if b.is_zero() && p.is_one() => {
1805            let base = arena.add(&[one, x]);
1806            let t = arena.pow(base, n_m1);
1807            arena.mul(&[n, x, t])
1808        }
1809        _ => return None,
1810    };
1811    let total = arena.mul(&[shape.constant, result]);
1812    Some(eval::eval(arena, total))
1813}
1814
1815// ═══════════════════════════════════════════════════════════════════════════
1816// Geometric / arithmetico-geometric sums
1817// ═══════════════════════════════════════════════════════════════════════════
1818
1819/// `(P as monomial list, Y, const_factor)` — see [`geometric_split`].
1820type GeometricSplit = (Vec<(usize, ExprId)>, ExprId, ExprId);
1821
1822/// Split `body = P(k)·Y^k·const` where `Y` collects every exponential factor.
1823/// Returns `(P as monomial list, Y, const_factor)`; `P` may have symbolic
1824/// coefficients.
1825fn geometric_split(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<GeometricSplit> {
1826    let factors = mul_factors(arena, body);
1827    let mut y_parts = Vec::new();
1828    let mut c_parts = Vec::new();
1829    let mut p_parts = Vec::new();
1830    for f in factors {
1831        if !depends_on(arena, f, var) {
1832            c_parts.push(f);
1833            continue;
1834        }
1835        match arena.node(f).clone() {
1836            ExprNode::Pow(base, exp)
1837                if depends_on(arena, exp, var) && !depends_on(arena, base, var) =>
1838            {
1839                let (a, b) = linear_in(arena, exp, var)?;
1840                y_parts.push(pow_rat(arena, base, &a));
1841                if !b.is_zero() {
1842                    c_parts.push(pow_rat(arena, base, &b));
1843                }
1844            }
1845            ExprNode::Exp(arg) => {
1846                let (a, b) = linear_in(arena, arg, var)?;
1847                let e = arena.e_const;
1848                y_parts.push(pow_rat(arena, e, &a));
1849                if !b.is_zero() {
1850                    c_parts.push(pow_rat(arena, e, &b));
1851                }
1852            }
1853            _ => p_parts.push(f),
1854        }
1855    }
1856    if y_parts.is_empty() {
1857        return None;
1858    }
1859    let y = mul_all(arena, &y_parts);
1860    let y = eval::eval(arena, y);
1861    let p_expr = mul_all(arena, &p_parts);
1862    let poly = sym_poly_in(arena, p_expr, var)?;
1863    if poly.is_empty() {
1864        return None;
1865    }
1866    let c = mul_all(arena, &c_parts);
1867    Some((poly, y, c))
1868}
1869
1870/// Apply `Σ_j p_j (r·d/dr)^j` to `s0(r)` and substitute `r = y`.
1871fn apply_euler_operator(
1872    arena: &mut Arena,
1873    poly: &[(usize, ExprId)],
1874    s0: ExprId,
1875    r: ExprId,
1876    y: ExprId,
1877) -> ExprId {
1878    let max_p = poly.iter().map(|(p, _)| *p).max().unwrap_or(0);
1879    let mut derivs = Vec::with_capacity(max_p + 1);
1880    derivs.push(s0);
1881    for _ in 0..max_p {
1882        let prev = *derivs.last().unwrap_or(&s0);
1883        let d = crate::transforms::diff::diff(arena, prev, r);
1884        let rd = arena.mul(&[r, d]);
1885        let rd = eval::eval(arena, rd);
1886        derivs.push(rd);
1887    }
1888    let mut terms = Vec::new();
1889    for &(p, c) in poly {
1890        terms.push(arena.mul(&[c, derivs[p]]));
1891    }
1892    let total = add_all(arena, &terms);
1893    let at_y = subs::subs(arena, total, r, y);
1894    let at_y = eval::eval(arena, at_y);
1895    let together = arena.together_expr(at_y);
1896    eval::eval(arena, together)
1897}
1898
1899fn geometric_poly_finite(
1900    arena: &mut Arena,
1901    body: ExprId,
1902    var: ExprId,
1903    lo: ExprId,
1904    hi: ExprId,
1905) -> Option<ExprId> {
1906    let (poly, y, c) = geometric_split(arena, body, var)?;
1907    if as_rat(arena, y) == Some(Rat::one()) {
1908        return None; // degenerate; polynomial path applies
1909    }
1910    // Numeric ratio with rational coefficients → Gosper gives the tidiest form.
1911    if as_rat(arena, y).is_some()
1912        && let Some(g) = gosper::gosper_sum(arena, body, var, lo, hi)
1913    {
1914        return Some(g);
1915    }
1916    let r = arena.symbol("_r");
1917    let one = arena.one;
1918    let r_lo = arena.pow(r, lo);
1919    let hi1 = arena.add(&[hi, one]);
1920    let r_hi1 = arena.pow(r, hi1);
1921    let num = arena.sub(r_lo, r_hi1);
1922    let den = arena.sub(one, r);
1923    let s0 = arena.div(num, den);
1924    let formula = apply_euler_operator(arena, &poly, s0, r, y);
1925    let formula = arena.mul(&[c, formula]);
1926    if as_rat(arena, y).is_some() || const_value(arena, y).is_some() {
1927        return Some(formula);
1928    }
1929    // Symbolic ratio: Piecewise on y = 1.  Both conditions are explicit so
1930    // that `eval` only selects a branch once `y` is known.
1931    let poly_sum = faulhaber_sym(arena, &poly, lo, hi);
1932    let poly_sum = arena.mul(&[c, poly_sum]);
1933    let cond = arena.eq_(y, one);
1934    let ncond = arena.ne_(y, one);
1935    Some(arena.piecewise(&[(poly_sum, cond), (formula, ncond)]))
1936}
1937
1938fn geometric_poly_infinite(
1939    arena: &mut Arena,
1940    body: ExprId,
1941    var: ExprId,
1942    lo: ExprId,
1943) -> Option<SumOutcome> {
1944    let (poly, y, c) = geometric_split(arena, body, var)?;
1945    match abs_less_than_one(arena, y) {
1946        Some(true) => {}
1947        Some(false) => {
1948            // Divergent: direction known only for positive ratio & rational sign.
1949            let y_pos = const_sign(arena, y) == Some(true);
1950            let lead = poly.last().map(|(_, c)| *c);
1951            let sign = if y_pos {
1952                lead.and_then(|l| {
1953                    let lc = arena.mul(&[c, l]);
1954                    const_sign(arena, lc)
1955                })
1956            } else {
1957                None
1958            };
1959            return Some(SumOutcome::Divergent(infinity_of_sign(arena, sign)));
1960        }
1961        None => return Some(SumOutcome::Unevaluated),
1962    }
1963    let r = arena.symbol("_r");
1964    let one = arena.one;
1965    let r_lo = arena.pow(r, lo);
1966    let den = arena.sub(one, r);
1967    let s0 = arena.div(r_lo, den);
1968    let formula = apply_euler_operator(arena, &poly, s0, r, y);
1969    let total = arena.mul(&[c, formula]);
1970    Some(SumOutcome::Closed(eval::eval(arena, total)))
1971}
1972
1973// ═══════════════════════════════════════════════════════════════════════════
1974// Infinite sums
1975// ═══════════════════════════════════════════════════════════════════════════
1976
1977fn infinite_sum(arena: &mut Arena, body: ExprId, var: ExprId, lo: ExprId) -> SumOutcome {
1978    // 0. Constant body.
1979    if !depends_on(arena, body, var) {
1980        let v = eval::eval(arena, body);
1981        if arena.is_zero_structural(v) {
1982            return SumOutcome::Closed(v);
1983        }
1984        let sign = const_sign(arena, v);
1985        return SumOutcome::Divergent(infinity_of_sign(arena, sign));
1986    }
1987    let node = arena.node(body).clone();
1988
1989    // 1. Rational functions (handles cancellation between divergent pieces).
1990    if let Some(r) = rational_sum_infinite(arena, body, var, lo) {
1991        return r;
1992    }
1993
1994    // 2. Sums.
1995    if let ExprNode::Add(ref terms) = node {
1996        let terms: Vec<ExprId> = terms.to_vec();
1997        if let Some(r) = telescoping_infinite(arena, &terms, var, lo) {
1998            return r;
1999        }
2000        let mut closed = Vec::new();
2001        let mut divergent: Vec<Option<ExprId>> = Vec::new();
2002        let mut unknown = 0usize;
2003        for &t in &terms {
2004            match infinite_sum(arena, t, var, lo) {
2005                SumOutcome::Closed(v) => closed.push(v),
2006                SumOutcome::Divergent(d) => divergent.push(d),
2007                SumOutcome::Unevaluated => unknown += 1,
2008            }
2009        }
2010        if unknown == 0 && divergent.is_empty() {
2011            let s = add_all(arena, &closed);
2012            return SumOutcome::Closed(eval::eval(arena, s));
2013        }
2014        if unknown == 0 && divergent.len() == 1 {
2015            return SumOutcome::Divergent(divergent[0]);
2016        }
2017        if unknown == 0 && !divergent.is_empty() {
2018            // Several divergent pieces: decisive only if they all point the same way.
2019            let first = divergent[0];
2020            if first.is_some() && divergent.iter().all(|d| *d == first) {
2021                return SumOutcome::Divergent(first);
2022            }
2023        }
2024        return SumOutcome::Unevaluated;
2025    }
2026
2027    // 3. Constant-factor extraction.
2028    if let ExprNode::Mul(ref factors) = node {
2029        let factors: Vec<ExprId> = factors.to_vec();
2030        let (consts, varf): (Vec<ExprId>, Vec<ExprId>) = factors
2031            .iter()
2032            .copied()
2033            .partition(|&f| !depends_on(arena, f, var));
2034        if !consts.is_empty() && !varf.is_empty() {
2035            let c = mul_all(arena, &consts);
2036            let c = eval::eval(arena, c);
2037            if arena.is_zero_structural(c) {
2038                return SumOutcome::Closed(c);
2039            }
2040            let inner = mul_all(arena, &varf);
2041            return match infinite_sum(arena, inner, var, lo) {
2042                SumOutcome::Closed(v) => {
2043                    let s = arena.mul(&[c, v]);
2044                    SumOutcome::Closed(eval::eval(arena, s))
2045                }
2046                SumOutcome::Divergent(Some(inf)) => match const_sign(arena, c) {
2047                    Some(true) => SumOutcome::Divergent(Some(inf)),
2048                    Some(false) => {
2049                        let flipped = if inf == arena.infinity {
2050                            arena.neg_infinity
2051                        } else {
2052                            arena.infinity
2053                        };
2054                        SumOutcome::Divergent(Some(flipped))
2055                    }
2056                    None => SumOutcome::Divergent(None),
2057                },
2058                other => other,
2059            };
2060        }
2061    }
2062
2063    // 4. Shape-based recognisers: p-series, then geometric / arithmetico-
2064    //    geometric (symbolic polynomial multipliers), then the power-series table.
2065    let shape = term_shape(arena, body, var);
2066    if let Some(shape) = &shape
2067        && let Some(r) = p_series_infinite(arena, shape, lo)
2068    {
2069        return r;
2070    }
2071    if let Some(r) = geometric_poly_infinite(arena, body, var, lo) {
2072        return r;
2073    }
2074    if let Some(shape) = &shape
2075        && let Some(r) = power_series_infinite(arena, shape, var, lo)
2076    {
2077        return r;
2078    }
2079
2080    // 6. Gosper antidifference with an exactly computable tail limit.
2081    if let Some(r) = gosper_infinite(arena, body, var, lo) {
2082        return r;
2083    }
2084
2085    // 7. Convergence tests can still prove divergence.
2086    if crate::calculus::convergence::is_convergent(arena, body, var) == Some(false) {
2087        let sign = eventual_sign(arena, body, var);
2088        return SumOutcome::Divergent(infinity_of_sign(arena, sign));
2089    }
2090
2091    SumOutcome::Unevaluated
2092}
2093
2094/// Eventual sign of `body(k)` for large `k`, when structurally obvious:
2095/// a hypergeometric monomial with a non-alternating sign and positive
2096/// geometric base has the sign of its constant.
2097fn eventual_sign(arena: &mut Arena, body: ExprId, var: ExprId) -> Option<bool> {
2098    let shape = term_shape(arena, body, var)?;
2099    if shape.alternating {
2100        return None;
2101    }
2102    for (b, a) in &shape.bases {
2103        if a.is_integer() && a.to_integer().to_i64()? % 2 == 0 {
2104            continue;
2105        }
2106        if const_sign(arena, *b) != Some(true) {
2107            return None;
2108        }
2109    }
2110    if shape.numeric_base.is_negative() {
2111        return None;
2112    }
2113    const_sign(arena, shape.constant)
2114}
2115
2116/// Gosper for infinite sums: `Σ_{k=lo}^{∞} t(k) = lim_{N→∞} g(N+1) − g(lo)`
2117/// when the tail limit is exactly computable (rational tail, or a
2118/// `P(N)·r^N` tail with `|r| < 1`).
2119fn gosper_infinite(arena: &mut Arena, body: ExprId, var: ExprId, lo: ExprId) -> Option<SumOutcome> {
2120    let n = arena.symbol("_N");
2121    let g = gosper::gosper_sum(arena, body, var, lo, n)?;
2122    let limit = limit_at_infinity(arena, g, n)?;
2123    Some(SumOutcome::Closed(limit))
2124}
2125
2126// ── p-series ────────────────────────────────────────────────────────────────
2127
2128/// `Σ_{k=lo}^{∞} c·(±1)^k·(k+β)^(−p)`.
2129fn p_series_infinite(arena: &mut Arena, shape: &TermShape, lo: ExprId) -> Option<SumOutcome> {
2130    if !shape.is_pure_lin_pows() || shape.lin_pows.len() != 1 {
2131        return None;
2132    }
2133    let (beta, exp) = shape.lin_pows[0].clone();
2134    let c = shape.constant;
2135    if exp >= -Rat::one() {
2136        // Terms do not decay fast enough (or grow).
2137        if shape.alternating {
2138            if exp >= Rat::zero() {
2139                return Some(SumOutcome::Divergent(None));
2140            }
2141            // Alternating with (k+β)^(−p), 0 < p ≤ 1: conditionally convergent.
2142            // Closed forms only for p = 1 (handled below); otherwise leave.
2143        } else {
2144            let sign = const_sign(arena, c);
2145            return Some(SumOutcome::Divergent(infinity_of_sign(arena, sign)));
2146        }
2147    }
2148    if !exp.is_integer() {
2149        return None;
2150    }
2151    let p = (-exp.to_integer().to_i64()?) as usize;
2152    let lo_r = const_value(arena, lo)?;
2153    let q = &lo_r + &beta; // first argument k+β
2154    let value = if !shape.alternating {
2155        // Integer or half-integer offsets have Hurwitz-zeta closed forms.
2156        if beta.is_integer() || (&beta * rat_i(2)).is_integer() {
2157            hurwitz_zeta_closed(arena, p, &q)?
2158        } else {
2159            return None;
2160        }
2161    } else if beta.is_integer() {
2162        // Σ_{k=lo}^{∞} (−1)^k (k+β)^(−p) = (−1)^β [−η(p) − Σ_{j=1}^{q−1} (−1)^j j^(−p)]
2163        if !q.is_positive() {
2164            return None;
2165        }
2166        let qi = q.to_integer().to_i64()?;
2167        if qi > MAX_TELESCOPE_SHIFT {
2168            return None;
2169        }
2170        let eta = eta_value(arena, p)?;
2171        let mut acc = Rat::zero();
2172        for j in 1..qi {
2173            let s = if j % 2 == 0 { Rat::one() } else { -Rat::one() };
2174            acc += s * rat_pow_i(&rat_i(j), -(p as i64));
2175        }
2176        let neg_eta = arena.neg(eta);
2177        let ce = rat_expr(arena, -acc);
2178        let inner = arena.add(&[neg_eta, ce]);
2179        let sign_beta = if beta.to_integer().to_i64()?.rem_euclid(2) == 0 {
2180            arena.one
2181        } else {
2182            arena.neg_one
2183        };
2184        let v = arena.mul(&[sign_beta, inner]);
2185        eval::eval(arena, v)
2186    } else if (&beta * rat_i(2)).is_integer() {
2187        // β = n + 1/2:  (k + β)^(−p) = 2^p (2(k+n) + 1)^(−p).  With j = k + n,
2188        // Σ_{k=lo}^{∞} (−1)^k (k+β)^(−p) = 2^p (−1)^n [β(p) − Σ_{j=0}^{s−1} (−1)^j (2j+1)^(−p)],
2189        // where s = lo + n is the first index of the tail.
2190        let half = Rat::new(BigInt::one(), BigInt::from(2));
2191        let n = &beta - &half;
2192        let s = &q - &half;
2193        if !s.is_integer() || s.is_negative() {
2194            return None;
2195        }
2196        let si = s.to_integer().to_i64()?;
2197        if si > MAX_TELESCOPE_SHIFT {
2198            return None;
2199        }
2200        let ni = n.to_integer().to_i64()?;
2201        let beta_p = dirichlet_beta_value(arena, p)?;
2202        let mut acc = Rat::zero();
2203        for j in 0..si {
2204            let sg = if j % 2 == 0 { Rat::one() } else { -Rat::one() };
2205            acc += sg * rat_pow_i(&rat_i(2 * j + 1), -(p as i64));
2206        }
2207        let ce = rat_expr(arena, -acc);
2208        let inner = arena.add(&[beta_p, ce]);
2209        let scale = rat_pow_i(&rat_i(2), p as i64)
2210            * if ni.rem_euclid(2) == 0 {
2211                Rat::one()
2212            } else {
2213                -Rat::one()
2214            };
2215        let se = rat_expr(arena, scale);
2216        let v = arena.mul(&[se, inner]);
2217        eval::eval(arena, v)
2218    } else {
2219        return None;
2220    };
2221    let total = arena.mul(&[c, value]);
2222    Some(SumOutcome::Closed(eval::eval(arena, total)))
2223}
2224
2225// ── power-series table ──────────────────────────────────────────────────────
2226
2227/// Convergence domain of a power series in `x`.
2228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2229enum Domain {
2230    /// Converges for every `x`.
2231    Everywhere,
2232    /// `|x| < 1`.
2233    OpenUnit,
2234    /// `|x| ≤ 1`.
2235    ClosedUnit,
2236    /// `|x| < 1` or `x = −1`.
2237    OpenUnitOrMinusOne,
2238}
2239
2240/// One entry of the classical power-series table: the canonical term
2241///
2242/// ```text
2243/// u(k) = table_const · (−1)^k[alt] · base_const^k · x^(A·k + B) · Π (k+β)^p · Π ((αk+β)!)^e
2244/// ```
2245///
2246/// with `Σ_{k=k0}^{∞} u(k) = F(x)`.
2247struct SeriesEntry {
2248    name: &'static str,
2249    alternating: bool,
2250    base_const: Rat,
2251    lin_pows: &'static [(i64, i64, i64)], // (β numer, β denom, p)
2252    facts: &'static [(i64, i64, i64)],    // (α, β, e)
2253    a: i64,
2254    b: i64,
2255    k0: i64,
2256    table_const: Rat,
2257    domain: Domain,
2258    build: fn(&mut Arena, ExprId) -> ExprId,
2259}
2260
2261fn series_table() -> Vec<SeriesEntry> {
2262    let one = Rat::one;
2263    let half = || Rat::new(BigInt::one(), BigInt::from(2));
2264    vec![
2265        SeriesEntry {
2266            name: "exp",
2267            alternating: false,
2268            base_const: one(),
2269            lin_pows: &[],
2270            facts: &[(1, 0, -1)],
2271            a: 1,
2272            b: 0,
2273            k0: 0,
2274            table_const: one(),
2275            domain: Domain::Everywhere,
2276            build: |a, x| a.exp(x),
2277        },
2278        SeriesEntry {
2279            name: "sin",
2280            alternating: true,
2281            base_const: one(),
2282            lin_pows: &[],
2283            facts: &[(2, 1, -1)],
2284            a: 2,
2285            b: 1,
2286            k0: 0,
2287            table_const: one(),
2288            domain: Domain::Everywhere,
2289            build: |a, x| a.sin(x),
2290        },
2291        SeriesEntry {
2292            name: "cos",
2293            alternating: true,
2294            base_const: one(),
2295            lin_pows: &[],
2296            facts: &[(2, 0, -1)],
2297            a: 2,
2298            b: 0,
2299            k0: 0,
2300            table_const: one(),
2301            domain: Domain::Everywhere,
2302            build: |a, x| a.cos(x),
2303        },
2304        SeriesEntry {
2305            name: "sinh",
2306            alternating: false,
2307            base_const: one(),
2308            lin_pows: &[],
2309            facts: &[(2, 1, -1)],
2310            a: 2,
2311            b: 1,
2312            k0: 0,
2313            table_const: one(),
2314            domain: Domain::Everywhere,
2315            build: |a, x| a.sinh(x),
2316        },
2317        SeriesEntry {
2318            name: "cosh",
2319            alternating: false,
2320            base_const: one(),
2321            lin_pows: &[],
2322            facts: &[(2, 0, -1)],
2323            a: 2,
2324            b: 0,
2325            k0: 0,
2326            table_const: one(),
2327            domain: Domain::Everywhere,
2328            build: |a, x| a.cosh(x),
2329        },
2330        SeriesEntry {
2331            name: "-ln(1-x)",
2332            alternating: false,
2333            base_const: one(),
2334            lin_pows: &[(0, 1, -1)],
2335            facts: &[],
2336            a: 1,
2337            b: 0,
2338            k0: 1,
2339            table_const: one(),
2340            domain: Domain::OpenUnitOrMinusOne,
2341            build: |a, x| {
2342                let one = a.one;
2343                let omx = a.sub(one, x);
2344                let omx = eval::eval(a, omx);
2345                // −ln(p/q) = ln(q/p) for rational arguments.
2346                if let Some(r) = as_rat(a, omx)
2347                    && r.is_positive()
2348                {
2349                    let inv = rat_expr(a, Rat::one() / r);
2350                    return a.ln(inv);
2351                }
2352                let l = a.ln(omx);
2353                a.neg(l)
2354            },
2355        },
2356        SeriesEntry {
2357            name: "atan",
2358            alternating: true,
2359            base_const: one(),
2360            lin_pows: &[(1, 2, -1)],
2361            facts: &[],
2362            a: 2,
2363            b: 1,
2364            k0: 0,
2365            table_const: half(),
2366            domain: Domain::ClosedUnit,
2367            build: |a, x| a.atan(x),
2368        },
2369        SeriesEntry {
2370            name: "atanh",
2371            alternating: false,
2372            base_const: one(),
2373            lin_pows: &[(1, 2, -1)],
2374            facts: &[],
2375            a: 2,
2376            b: 1,
2377            k0: 0,
2378            table_const: half(),
2379            domain: Domain::OpenUnit,
2380            build: |a, x| a.atanh(x),
2381        },
2382        SeriesEntry {
2383            name: "1/(1-x)",
2384            alternating: false,
2385            base_const: one(),
2386            lin_pows: &[],
2387            facts: &[],
2388            a: 1,
2389            b: 0,
2390            k0: 0,
2391            table_const: one(),
2392            domain: Domain::OpenUnit,
2393            build: |a, x| {
2394                let one = a.one;
2395                let omx = a.sub(one, x);
2396                a.div(one, omx)
2397            },
2398        },
2399        // asin x = Σ (2k)!/(4^k (k!)² (2k+1)) x^(2k+1)
2400        SeriesEntry {
2401            name: "asin",
2402            alternating: false,
2403            base_const: Rat::new(BigInt::one(), BigInt::from(4)),
2404            lin_pows: &[(1, 2, -1)],
2405            facts: &[(1, 0, -2), (2, 0, 1)],
2406            a: 2,
2407            b: 1,
2408            k0: 0,
2409            table_const: half(),
2410            domain: Domain::ClosedUnit,
2411            build: |a, x| a.asin(x),
2412        },
2413        SeriesEntry {
2414            name: "asinh",
2415            alternating: true,
2416            base_const: Rat::new(BigInt::one(), BigInt::from(4)),
2417            lin_pows: &[(1, 2, -1)],
2418            facts: &[(1, 0, -2), (2, 0, 1)],
2419            a: 2,
2420            b: 1,
2421            k0: 0,
2422            table_const: half(),
2423            domain: Domain::ClosedUnit,
2424            build: |a, x| a.asinh(x),
2425        },
2426        // erf x = (2/√π) Σ (−1)^k x^(2k+1)/(k! (2k+1))
2427        SeriesEntry {
2428            name: "erf",
2429            alternating: true,
2430            base_const: one(),
2431            lin_pows: &[(1, 2, -1)],
2432            facts: &[(1, 0, -1)],
2433            a: 2,
2434            b: 1,
2435            k0: 0,
2436            table_const: half(),
2437            domain: Domain::Everywhere,
2438            build: |a, x| {
2439                let e = a.erf(x);
2440                let pi = a.pi;
2441                let sp = a.sqrt(pi);
2442                let two = a.int(2);
2443                let half_sqrt_pi = a.div(sp, two);
2444                a.mul(&[half_sqrt_pi, e])
2445            },
2446        },
2447        // J₀(x) = Σ (−1)^k x^(2k)/(4^k (k!)²),  I₀(x) = Σ x^(2k)/(4^k (k!)²)
2448        SeriesEntry {
2449            name: "besselj0",
2450            alternating: true,
2451            base_const: Rat::new(BigInt::one(), BigInt::from(4)),
2452            lin_pows: &[],
2453            facts: &[(1, 0, -2)],
2454            a: 2,
2455            b: 0,
2456            k0: 0,
2457            table_const: one(),
2458            domain: Domain::Everywhere,
2459            build: |a, x| {
2460                let z = a.zero;
2461                a.besselj(z, x)
2462            },
2463        },
2464        SeriesEntry {
2465            name: "besseli0",
2466            alternating: false,
2467            base_const: Rat::new(BigInt::one(), BigInt::from(4)),
2468            lin_pows: &[],
2469            facts: &[(1, 0, -2)],
2470            a: 2,
2471            b: 0,
2472            k0: 0,
2473            table_const: one(),
2474            domain: Domain::Everywhere,
2475            build: |a, x| {
2476                let z = a.zero;
2477                a.besseli(z, x)
2478            },
2479        },
2480    ]
2481}
2482
2483fn entry_lin_pows(e: &SeriesEntry) -> Vec<(Rat, Rat)> {
2484    e.lin_pows
2485        .iter()
2486        .map(|&(bn, bd, p)| (Rat::new(BigInt::from(bn), BigInt::from(bd)), rat_i(p)))
2487        .collect()
2488}
2489
2490fn entry_facts(e: &SeriesEntry) -> Vec<(Rat, Rat, i64)> {
2491    let mut v: Vec<(Rat, Rat, i64)> = e
2492        .facts
2493        .iter()
2494        .map(|&(a, b, ee)| (rat_i(a), rat_i(b), ee))
2495        .collect();
2496    v.sort_by_key(|a| (a.0.clone(), a.1.clone()));
2497    v
2498}
2499
2500/// Does `x` lie in the entry's convergence domain?  `None` = undecidable.
2501fn in_domain(arena: &mut Arena, x: ExprId, domain: Domain) -> Option<bool> {
2502    if domain == Domain::Everywhere {
2503        return Some(true);
2504    }
2505    if let Some(r) = const_value(arena, x) {
2506        let a = r.abs();
2507        let one = Rat::one();
2508        return Some(match domain {
2509            Domain::Everywhere => true,
2510            Domain::OpenUnit => a < one,
2511            Domain::ClosedUnit => a <= one,
2512            Domain::OpenUnitOrMinusOne => a < one || r == -one,
2513        });
2514    }
2515    // Non-rational constant: decide strictly inside / outside the unit disc
2516    // (a transcendental constant cannot sit exactly on the unit circle in a
2517    // way we could certify, so the boundary cases stay undecided).
2518    abs_less_than_one(arena, x)
2519}
2520
2521/// Recognise `Σ_{k=lo}^{∞} t(k)` against the classical power-series table.
2522fn power_series_infinite(
2523    arena: &mut Arena,
2524    shape: &TermShape,
2525    var: ExprId,
2526    lo: ExprId,
2527) -> Option<SumOutcome> {
2528    if !shape.binomials.is_empty() {
2529        return None;
2530    }
2531    let lo_i = as_i64(arena, lo)?;
2532    // Extra k^p factor → Euler operator ((x d/dx − B)/A)^p on F.
2533    let mut extra_power: i64 = 0;
2534    let mut lin = shape.lin_pows.clone();
2535    if let Some(pos) = lin
2536        .iter()
2537        .position(|(b, p)| b.is_zero() && p.is_integer() && p.is_positive())
2538    {
2539        let (_, p) = lin.remove(pos);
2540        extra_power = p.to_integer().to_i64()?;
2541        if extra_power > 6 {
2542            return None;
2543        }
2544    }
2545    let _ = var;
2546    for entry in series_table() {
2547        // Structural match of algebraic / factorial parts.
2548        if entry_lin_pows(&entry) != lin || entry_facts(&entry) != shape.facts {
2549            continue;
2550        }
2551        // Alternation: identical, or absorbed into x → −x when A is odd.
2552        let mut flip_x = false;
2553        if entry.alternating != shape.alternating {
2554            if entry.a % 2 == 1 {
2555                flip_x = true;
2556            } else {
2557                continue;
2558            }
2559        }
2560        // Geometric part: numeric_base · Π base^a = base_const · x^A  ⇒  x = (numeric_base/base_const · Π base^a)^(1/A)
2561        let ratio = &shape.numeric_base / &entry.base_const;
2562        let a_rat = rat_i(entry.a);
2563        let mut x_factors = Vec::new();
2564        if !ratio.is_one() {
2565            if ratio.is_negative() && entry.a % 2 == 0 {
2566                continue;
2567            }
2568            let re = rat_expr(arena, ratio.clone());
2569            let inv_a = Rat::one() / &a_rat;
2570            x_factors.push(pow_rat(arena, re, &inv_a));
2571        }
2572        let mut ok = true;
2573        for (base, a) in &shape.bases {
2574            let q = a / &a_rat;
2575            if !q.is_integer() {
2576                ok = false;
2577                break;
2578            }
2579            x_factors.push(pow_rat(arena, *base, &q));
2580        }
2581        if !ok {
2582            continue;
2583        }
2584        let x = mul_all(arena, &x_factors);
2585        let x = eval::eval(arena, x);
2586        let x = if flip_x {
2587            let nx = arena.neg(x);
2588            eval::eval(arena, nx)
2589        } else {
2590            x
2591        };
2592        // Constant adjustment: t(k) = C_body · u(k) / (table_const · x^B · (−1)^B[if flipped])
2593        let xb = pow_rat(arena, x, &rat_i(entry.b));
2594        let tc = rat_expr(arena, entry.table_const.clone());
2595        let mut denom_factors = vec![tc, xb];
2596        if flip_x && entry.b % 2 == 1 {
2597            denom_factors.push(arena.neg_one);
2598        }
2599        let denom = arena.mul(&denom_factors);
2600        let coeff = arena.div(shape.constant, denom);
2601        let coeff = eval::eval(arena, coeff);
2602        // Domain.
2603        match in_domain(arena, x, entry.domain) {
2604            Some(true) => {}
2605            Some(false) => {
2606                let sign = if !shape.alternating && const_sign(arena, x) == Some(true) {
2607                    const_sign(arena, coeff)
2608                } else {
2609                    None
2610                };
2611                return Some(SumOutcome::Divergent(infinity_of_sign(arena, sign)));
2612            }
2613            None => return Some(SumOutcome::Unevaluated),
2614        }
2615        if lo_i < entry.k0 {
2616            return None;
2617        }
2618        tracing::debug!("summation: power-series table hit `{}`", entry.name);
2619        let f = if extra_power > 0 {
2620            // Apply ((x d/dx − B)/A)^extra_power on a symbolic placeholder,
2621            // then substitute the actual argument.
2622            let xs = arena.symbol("_x");
2623            let mut g = (entry.build)(arena, xs);
2624            for _ in 0..extra_power {
2625                let d = crate::transforms::diff::diff(arena, g, xs);
2626                let xd = arena.mul(&[xs, d]);
2627                let be = arena.int(entry.b);
2628                let bg = arena.mul(&[be, g]);
2629                let num = arena.sub(xd, bg);
2630                let ae = arena.int(entry.a);
2631                g = arena.div(num, ae);
2632                g = eval::eval(arena, g);
2633            }
2634            let at_x = subs::subs(arena, g, xs, x);
2635            eval::eval(arena, at_x)
2636        } else {
2637            (entry.build)(arena, x)
2638        };
2639        // Subtract the skipped leading terms u(k0..lo−1) — build them from
2640        // the canonical term via the entry's structure: u(k) = t(k)/coeff.
2641        let mut skipped = Vec::new();
2642        for k in entry.k0..lo_i {
2643            let ke = arena.int(k);
2644            // u(k) = table_const · (−1)^k · base_const^k · x^(Ak+B) · Π(k+β)^p · Π((αk+β)!)^e · k^extra
2645            let mut fs = vec![tc];
2646            if entry.alternating && k % 2 != 0 {
2647                fs.push(arena.neg_one);
2648            }
2649            if !entry.base_const.is_one() {
2650                fs.push(rat_expr(arena, rat_pow_i(&entry.base_const, k)));
2651            }
2652            fs.push(pow_rat(arena, x, &rat_i(entry.a * k + entry.b)));
2653            for (beta, p) in entry_lin_pows(&entry) {
2654                let kb = rat_i(k) + beta;
2655                if kb.is_zero() && p.is_negative() {
2656                    return None;
2657                }
2658                fs.push(rat_expr(arena, rat_pow_i(&kb, p.to_integer().to_i64()?)));
2659            }
2660            for (al, be, e) in entry_facts(&entry) {
2661                let arg = (al * rat_i(k) + be).to_integer().to_i64()?;
2662                if arg < 0 {
2663                    return None;
2664                }
2665                let fv = Rat::from_integer(factorial_big(arg as u64));
2666                fs.push(rat_expr(arena, rat_pow_i(&fv, e)));
2667            }
2668            if extra_power > 0 {
2669                fs.push(pow_rat(arena, ke, &rat_i(extra_power)));
2670            }
2671            skipped.push(arena.mul(&fs));
2672        }
2673        let mut total_terms = vec![f];
2674        for s in skipped {
2675            total_terms.push(arena.neg(s));
2676        }
2677        let inner = add_all(arena, &total_terms);
2678        let total = arena.mul(&[coeff, inner]);
2679        return Some(SumOutcome::Closed(eval::eval(arena, total)));
2680    }
2681    None
2682}
2683
2684// ═══════════════════════════════════════════════════════════════════════════
2685// Products
2686// ═══════════════════════════════════════════════════════════════════════════
2687
2688/// Evaluate `Π_{var=lower}^{upper} body` symbolically.
2689pub(crate) fn product(
2690    arena: &mut Arena,
2691    body: ExprId,
2692    var: ExprId,
2693    lower: ExprId,
2694    upper: ExprId,
2695) -> SumOutcome {
2696    if !matches!(arena.node(var), ExprNode::Symbol(_)) {
2697        return SumOutcome::Unevaluated;
2698    }
2699    match (classify_bound(arena, lower), classify_bound(arena, upper)) {
2700        (Bound::Finite(lo), Bound::Finite(hi)) => finite_product(arena, body, var, lo, hi),
2701        (Bound::Finite(lo), Bound::PosInf) => infinite_product(arena, body, var, lo),
2702        _ => SumOutcome::Unevaluated,
2703    }
2704}
2705
2706fn finite_product(
2707    arena: &mut Arena,
2708    body: ExprId,
2709    var: ExprId,
2710    lo: ExprId,
2711    hi: ExprId,
2712) -> SumOutcome {
2713    if let (Some(a), Some(b)) = (as_i64(arena, lo), as_i64(arena, hi)) {
2714        if b < a {
2715            return SumOutcome::Closed(arena.one);
2716        }
2717        if b - a < MAX_ENUMERATION_TERMS {
2718            return SumOutcome::Closed(enumerate_product(arena, body, var, a, b));
2719        }
2720    }
2721    match product_closed(arena, body, var, lo, hi) {
2722        Some(id) => {
2723            let n = gamma_ratio_normalize(arena, id);
2724            let n = gamma_to_factorial(arena, n);
2725            let n = flatten_nested_pows(arena, n);
2726            SumOutcome::Closed(eval::eval(arena, n))
2727        }
2728        None => SumOutcome::Unevaluated,
2729    }
2730}
2731
2732fn arena_neg_one(arena: &Arena) -> ExprId {
2733    arena.neg_one
2734}
2735
2736/// Rewrite `(b^p)^q` factors with rational `p, q` as `b^(pq)` so that
2737/// same-base powers (e.g. `√π · (√π)⁻¹`) cancel in the canonical `Mul`.
2738fn flatten_nested_pows(arena: &mut Arena, expr: ExprId) -> ExprId {
2739    let factors = mul_factors(arena, expr);
2740    let mut out: Vec<ExprId> = Vec::with_capacity(factors.len());
2741    // Numeric-base powers with symbolic exponents, grouped by base: (base, [exps]).
2742    let mut numeric_pows: Vec<(ExprId, Vec<ExprId>)> = Vec::new();
2743    let mut changed = false;
2744    for f in factors {
2745        if let ExprNode::Pow(base, exp) = arena.node(f).clone() {
2746            // (b^p)^q → b^(pq) when q is an integer (always valid) or both
2747            // exponents are rational.
2748            if let ExprNode::Pow(b2, e2) = arena.node(base).clone()
2749                && let Some(q) = as_rat(arena, exp)
2750                && (q.is_integer() || as_rat(arena, e2).is_some())
2751            {
2752                let qe = rat_expr(arena, q);
2753                let pq = arena.mul(&[e2, qe]);
2754                let pq = eval::eval(arena, pq);
2755                let flat = arena.pow(b2, pq);
2756                // Re-classify the flattened factor (it may now be a numeric-base power).
2757                if let ExprNode::Pow(nb, ne) = arena.node(flat).clone()
2758                    && arena.as_num(nb).is_some()
2759                    && arena.as_num(ne).is_none()
2760                {
2761                    if let Some(slot) = numeric_pows.iter_mut().find(|(b, _)| *b == nb) {
2762                        slot.1.push(ne);
2763                    } else {
2764                        numeric_pows.push((nb, vec![ne]));
2765                    }
2766                } else {
2767                    out.push(flat);
2768                }
2769                changed = true;
2770                continue;
2771            }
2772            if arena.as_num(base).is_some() && arena.as_num(exp).is_none() {
2773                if let Some(slot) = numeric_pows.iter_mut().find(|(b, _)| *b == base) {
2774                    slot.1.push(exp);
2775                    changed = true;
2776                } else {
2777                    numeric_pows.push((base, vec![exp]));
2778                }
2779                continue;
2780            }
2781        }
2782        out.push(f);
2783    }
2784    for (base, exps) in numeric_pows {
2785        let e = add_all(arena, &exps);
2786        let e = eval::eval(arena, e);
2787        out.push(arena.pow(base, e));
2788    }
2789    if changed {
2790        let r = mul_all(arena, &out);
2791        eval::eval(arena, r)
2792    } else {
2793        expr
2794    }
2795}
2796
2797fn product_closed(
2798    arena: &mut Arena,
2799    body: ExprId,
2800    var: ExprId,
2801    lo: ExprId,
2802    hi: ExprId,
2803) -> Option<ExprId> {
2804    if !depends_on(arena, body, var) {
2805        let n = range_count(arena, lo, hi);
2806        return Some(arena.pow(body, n));
2807    }
2808    match arena.node(body).clone() {
2809        ExprNode::Mul(ref factors) => {
2810            let factors: Vec<ExprId> = factors.to_vec();
2811            let mut parts = Vec::new();
2812            for f in factors {
2813                parts.push(product_closed(arena, f, var, lo, hi)?);
2814            }
2815            Some(arena.mul(&parts))
2816        }
2817        ExprNode::Neg(inner) => {
2818            let m1 = arena.neg_one;
2819            let n = range_count(arena, lo, hi);
2820            let s = arena.pow(m1, n);
2821            let p = product_closed(arena, inner, var, lo, hi)?;
2822            Some(arena.mul(&[s, p]))
2823        }
2824        ExprNode::Pow(base, exp) if !depends_on(arena, exp, var) => {
2825            let p = product_closed(arena, base, var, lo, hi)?;
2826            Some(arena.pow(p, exp))
2827        }
2828        ExprNode::Pow(base, exp) if !depends_on(arena, base, var) => {
2829            let s = match summation(arena, exp, var, lo, hi) {
2830                SumOutcome::Closed(s) if !walk::has_unevaluated(arena, s) => s,
2831                _ => return None,
2832            };
2833            Some(arena.pow(base, s))
2834        }
2835        ExprNode::Exp(arg) => {
2836            let s = match summation(arena, arg, var, lo, hi) {
2837                SumOutcome::Closed(s) if !walk::has_unevaluated(arena, s) => s,
2838                _ => return None,
2839            };
2840            Some(arena.exp(s))
2841        }
2842        _ => rational_product(arena, body, var, lo, hi)
2843            .or_else(|| linear_symbolic_product(arena, body, var, lo, hi)),
2844    }
2845}
2846
2847/// `Π_{k=lo}^{hi} (αk + β)` with rational `α ≠ 0` and a `k`-free (possibly
2848/// symbolic) `β`: `α^count · Γ(hi + 1 + β/α) / Γ(lo + β/α)`.
2849fn linear_symbolic_product(
2850    arena: &mut Arena,
2851    body: ExprId,
2852    var: ExprId,
2853    lo: ExprId,
2854    hi: ExprId,
2855) -> Option<ExprId> {
2856    let terms = sym_poly_in(arena, body, var)?;
2857    let alpha_e = terms.iter().find(|(p, _)| *p == 1).map(|(_, c)| *c)?;
2858    if terms.iter().any(|(p, _)| *p > 1) {
2859        return None;
2860    }
2861    let alpha = as_rat(arena, alpha_e)?;
2862    if alpha.is_zero() {
2863        return None;
2864    }
2865    let beta = terms
2866        .iter()
2867        .find(|(p, _)| *p == 0)
2868        .map(|(_, c)| *c)
2869        .unwrap_or(arena.zero);
2870    let count = range_count(arena, lo, hi);
2871    let mut factors = Vec::new();
2872    if !alpha.is_one() {
2873        factors.push(arena.pow(alpha_e, count));
2874    }
2875    let inv_alpha = rat_expr(arena, Rat::one() / &alpha);
2876    let shift = arena.mul(&[beta, inv_alpha]);
2877    let one = arena.one;
2878    let hi1 = arena.add(&[hi, one]);
2879    let top_arg = arena.add(&[hi1, shift]);
2880    let bot_arg = arena.add(&[lo, shift]);
2881    let top = arena.gamma(top_arg);
2882    let bot = arena.gamma(bot_arg);
2883    factors.push(top);
2884    factors.push(pow_rat(arena, bot, &(-Rat::one())));
2885    Some(arena.mul(&factors))
2886}
2887
2888/// `Π_{k=lo}^{hi} N(k)/D(k)` for rational functions whose numerator and
2889/// denominator factor into linear factors over ℚ.
2890fn rational_product(
2891    arena: &mut Arena,
2892    body: ExprId,
2893    var: ExprId,
2894    lo: ExprId,
2895    hi: ExprId,
2896) -> Option<ExprId> {
2897    let body = combine_fractions(arena, body);
2898    let (n, d) = polybridge::as_numer_denom(arena, body);
2899    let np = polybridge::expr_to_poly(arena, n, var)?;
2900    let dp = polybridge::expr_to_poly(arena, d, var)?;
2901    if np.is_zero() || dp.is_zero() {
2902        return None;
2903    }
2904    let count = range_count(arena, lo, hi);
2905    let mut factors = Vec::new();
2906    for (poly, sign) in [(np, 1i64), (dp, -1i64)] {
2907        let (content, irreducibles) = poly.factor_over_z();
2908        if !content.is_one() {
2909            let ce = rat_expr(arena, content);
2910            let p = pow_rat(arena, ce, &rat_i(sign));
2911            factors.push(arena.pow(p, count));
2912        }
2913        for (fac, mult) in irreducibles {
2914            if fac.degree() != Some(1) {
2915                return None;
2916            }
2917            let alpha = fac.coeff(1);
2918            let beta = fac.coeff(0);
2919            let e = rat_i(sign * mult as i64);
2920            // Π (αk+β) = α^count · Γ(hi + 1 + β/α) / Γ(lo + β/α)
2921            if !alpha.is_one() {
2922                let ae = rat_expr(arena, alpha.clone());
2923                let ap = pow_rat(arena, ae, &e);
2924                factors.push(arena.pow(ap, count));
2925            }
2926            let shift = &beta / &alpha;
2927            let one = arena.one;
2928            let hi1 = arena.add(&[hi, one]);
2929            let top_arg = add_rat(arena, hi1, &shift);
2930            let bot_arg = add_rat(arena, lo, &shift);
2931            let top = arena.gamma(top_arg);
2932            let bot = arena.gamma(bot_arg);
2933            factors.push(pow_rat(arena, top, &e));
2934            factors.push(pow_rat(arena, bot, &(-e)));
2935        }
2936    }
2937    Some(arena.mul(&factors))
2938}
2939
2940/// Cancel integer-shifted Gamma functions in a product:
2941/// `Γ(X + d) = Γ(X)·X(X+1)…(X+d−1)`, grouping arguments that differ by an
2942/// integer.  Anything that is not a Gamma factor is left untouched.
2943pub(crate) fn gamma_ratio_normalize(arena: &mut Arena, expr: ExprId) -> ExprId {
2944    let expr = eval::eval(arena, expr);
2945    let factors = mul_factors(arena, expr);
2946    // (arg, exponent) for Gamma factors; others pass through.
2947    let mut gammas: Vec<(ExprId, Rat)> = Vec::new();
2948    let mut others: Vec<ExprId> = Vec::new();
2949    for f in factors {
2950        match arena.node(f).clone() {
2951            ExprNode::Gamma(arg) => gammas.push((arg, Rat::one())),
2952            ExprNode::Pow(base, exp) => {
2953                if let (ExprNode::Gamma(arg), Some(e)) =
2954                    (arena.node(base).clone(), as_rat(arena, exp))
2955                {
2956                    gammas.push((arg, e));
2957                } else {
2958                    others.push(f);
2959                }
2960            }
2961            _ => others.push(f),
2962        }
2963    }
2964    if gammas.len() < 2 {
2965        return expr;
2966    }
2967    // Group by integer difference of arguments.
2968    let mut groups: Vec<Vec<(ExprId, Rat, i64)>> = Vec::new(); // (arg, exp, offset from group ref)
2969    'outer: for (arg, e) in gammas {
2970        for g in groups.iter_mut() {
2971            let (ref_arg, _, _) = g[0];
2972            let diff = arena.sub(arg, ref_arg);
2973            if let Some(d) = eval_rat(arena, diff)
2974                && d.is_integer()
2975                && let Some(di) = d.to_integer().to_i64()
2976                && di.abs() <= MAX_GAMMA_SHIFT
2977            {
2978                g.push((arg, e, di));
2979                continue 'outer;
2980            }
2981        }
2982        groups.push(vec![(arg, e, 0)]);
2983    }
2984    let mut out = others;
2985    for g in groups {
2986        if g.len() == 1 {
2987            let (arg, e, _) = g[0].clone();
2988            let ga = arena.gamma(arg);
2989            out.push(pow_rat(arena, ga, &e));
2990            continue;
2991        }
2992        let min_off = g.iter().map(|(_, _, d)| *d).min().unwrap_or(0);
2993        let (ref_arg, _, _) = g[0];
2994        // X = ref_arg + min_off
2995        let x = add_rat(arena, ref_arg, &rat_i(min_off));
2996        let x = eval::eval(arena, x);
2997        let mut total_e = Rat::zero();
2998        for (_, e, d) in &g {
2999            total_e += e;
3000            let steps = d - min_off;
3001            for j in 0..steps {
3002                let lin = add_rat(arena, x, &rat_i(j));
3003                out.push(pow_rat(arena, lin, e));
3004            }
3005        }
3006        if !total_e.is_zero() {
3007            let gx = arena.gamma(x);
3008            out.push(pow_rat(arena, gx, &total_e));
3009        }
3010    }
3011    let r = mul_all(arena, &out);
3012    eval::eval(arena, r)
3013}
3014
3015/// Rewrite `Γ(n + c)` with a positive integer constant part `c` as `(n + c − 1)!`.
3016fn gamma_to_factorial(arena: &mut Arena, expr: ExprId) -> ExprId {
3017    let factors = mul_factors(arena, expr);
3018    let mut out = Vec::with_capacity(factors.len());
3019    let mut changed = false;
3020    for f in factors {
3021        let (base, exp) = arena.as_base_exp(f);
3022        if let ExprNode::Gamma(arg) = arena.node(base).clone()
3023            && let Some(fa) = gamma_arg_to_factorial(arena, arg)
3024        {
3025            changed = true;
3026            let p = if exp == arena.one {
3027                fa
3028            } else {
3029                arena.pow(fa, exp)
3030            };
3031            out.push(p);
3032        } else {
3033            out.push(f);
3034        }
3035    }
3036    if changed {
3037        let r = mul_all(arena, &out);
3038        eval::eval(arena, r)
3039    } else {
3040        expr
3041    }
3042}
3043
3044fn gamma_arg_to_factorial(arena: &mut Arena, arg: ExprId) -> Option<ExprId> {
3045    if let Some(r) = as_rat(arena, arg) {
3046        if r.is_integer() && r.is_positive() {
3047            let m1 = rat_expr(arena, r - Rat::one());
3048            return Some(arena.factorial(m1));
3049        }
3050        return None;
3051    }
3052    let terms = add_terms(arena, arg);
3053    let mut const_part = Rat::zero();
3054    let mut rest = Vec::new();
3055    for t in terms {
3056        if let Some(r) = as_rat(arena, t) {
3057            const_part += r;
3058        } else {
3059            rest.push(t);
3060        }
3061    }
3062    if rest.is_empty() {
3063        return None;
3064    }
3065    let inner = add_all(arena, &rest);
3066    if const_part.is_integer() && const_part.is_positive() {
3067        let shifted = add_rat(arena, inner, &(const_part - Rat::one()));
3068        return Some(arena.factorial(shifted));
3069    }
3070    // Legendre duplication: Γ(m + 1/2) = (2m)! √π / (4^m m!)  for m = inner + (c − 1/2).
3071    let half = Rat::new(BigInt::one(), BigInt::from(2));
3072    let m_shift = &const_part - &half;
3073    if m_shift.is_integer() && !m_shift.is_negative() {
3074        let m = add_rat(arena, inner, &m_shift);
3075        let two = arena.int(2);
3076        let two_m = arena.mul(&[two, m]);
3077        let num = arena.factorial(two_m);
3078        let pi = arena.pi;
3079        let sp = arena.sqrt(pi);
3080        // Write 4^m as 2^(2m) so it merges with other powers of two.
3081        let two_m_e = arena.mul(&[two, m]);
3082        let four_m = arena.pow(two, two_m_e);
3083        let mf = arena.factorial(m);
3084        let inv_four_m = arena.pow(four_m, arena_neg_one(arena));
3085        let inv_mf = arena.pow(mf, arena_neg_one(arena));
3086        return Some(arena.mul(&[num, inv_four_m, inv_mf, sp]));
3087    }
3088    None
3089}
3090
3091fn infinite_product(arena: &mut Arena, body: ExprId, var: ExprId, lo: ExprId) -> SumOutcome {
3092    if !depends_on(arena, body, var) {
3093        let v = eval::eval(arena, body);
3094        if let Some(r) = as_rat(arena, v) {
3095            if r.is_one() {
3096                return SumOutcome::Closed(v);
3097            }
3098            if r.is_zero() || r.abs() < Rat::one() {
3099                return SumOutcome::Closed(arena.zero);
3100            }
3101            if r > Rat::one() {
3102                return SumOutcome::Divergent(Some(arena.infinity));
3103            }
3104            return SumOutcome::Divergent(None);
3105        }
3106        return SumOutcome::Unevaluated;
3107    }
3108    let n = arena.symbol("_N");
3109    let Some(pn) = product_closed(arena, body, var, lo, n) else {
3110        return SumOutcome::Unevaluated;
3111    };
3112    let pn = gamma_ratio_normalize(arena, pn);
3113    // Exact limits only: rational functions of N.
3114    match rational_limit_at_infinity(arena, pn, n) {
3115        Some(Some(r)) => SumOutcome::Closed(rat_expr(arena, r)),
3116        Some(None) => {
3117            let sign = eventual_sign(arena, pn, n);
3118            SumOutcome::Divergent(infinity_of_sign(arena, sign))
3119        }
3120        None => {
3121            // P(N)·r^N with |r|<1 → 0
3122            if let Some(l) = limit_at_infinity(arena, pn, n) {
3123                return SumOutcome::Closed(l);
3124            }
3125            SumOutcome::Unevaluated
3126        }
3127    }
3128}
3129
3130// ═══════════════════════════════════════════════════════════════════════════
3131// Tests
3132// ═══════════════════════════════════════════════════════════════════════════
3133
3134#[cfg(test)]
3135mod tests {
3136    use super::*;
3137
3138    fn closed(o: SumOutcome) -> ExprId {
3139        match o {
3140            SumOutcome::Closed(id) => id,
3141            other => panic!("expected closed form, got {other:?}"),
3142        }
3143    }
3144
3145    fn eval_at(arena: &mut Arena, e: ExprId, n: ExprId, v: i64) -> Rat {
3146        let ve = arena.int(v);
3147        let s = subs::subs(arena, e, n, ve);
3148        let s = eval::eval(arena, s);
3149        as_rat(arena, s).unwrap_or_else(|| panic!("not rational: {}", arena.display(s)))
3150    }
3151
3152    fn brute(arena: &mut Arena, body: ExprId, k: ExprId, lo: i64, hi: i64) -> Rat {
3153        let mut acc = Rat::zero();
3154        for i in lo..=hi {
3155            let ie = arena.int(i);
3156            let t = subs::subs(arena, body, k, ie);
3157            let t = eval::eval(arena, t);
3158            acc += as_rat(arena, t)
3159                .unwrap_or_else(|| panic!("term not rational: {}", arena.display(t)));
3160        }
3161        acc
3162    }
3163
3164    #[test]
3165    fn faulhaber_matches_enumeration_p_0_to_8() {
3166        let mut arena = Arena::new();
3167        let k = arena.symbol("k");
3168        let n = arena.symbol("n");
3169        let one = arena.one;
3170        for p in 0..=8usize {
3171            let body = if p == 0 {
3172                arena.one
3173            } else if p == 1 {
3174                k
3175            } else {
3176                let pe = arena.int(p as i64);
3177                arena.pow(k, pe)
3178            };
3179            let s = closed(summation(&mut arena, body, k, one, n));
3180            for nv in [0i64, 1, 2, 5, 10, 17] {
3181                let expected = brute(&mut arena, body, k, 1, nv);
3182                let got = eval_at(&mut arena, s, n, nv);
3183                assert_eq!(got, expected, "p={p}, n={nv}");
3184            }
3185        }
3186    }
3187
3188    #[test]
3189    fn faulhaber_general_lower_bound() {
3190        let mut arena = Arena::new();
3191        let k = arena.symbol("k");
3192        let n = arena.symbol("n");
3193        let three = arena.int(3);
3194        let e = arena.int(3);
3195        let body = arena.pow(k, e);
3196        let s = closed(summation(&mut arena, body, k, three, n));
3197        for nv in [3i64, 4, 9, 20] {
3198            let expected = brute(&mut arena, body, k, 3, nv);
3199            assert_eq!(eval_at(&mut arena, s, n, nv), expected);
3200        }
3201    }
3202
3203    #[test]
3204    fn faulhaber_b1_sign_convention() {
3205        // Σ_{k=1}^{n} k = n²/2 + n/2  ⇒ coefficient of n is +1/2.
3206        let p = faulhaber_coefficients(1);
3207        assert_eq!(p.coeff(1), Rat::new(BigInt::from(1), BigInt::from(2)));
3208        assert_eq!(p.coeff(2), Rat::new(BigInt::from(1), BigInt::from(2)));
3209    }
3210
3211    #[test]
3212    fn telescoping_rational() {
3213        let mut arena = Arena::new();
3214        let k = arena.symbol("k");
3215        let n = arena.symbol("n");
3216        let one = arena.one;
3217        let k1 = arena.add(&[k, one]);
3218        let den = arena.mul(&[k, k1]);
3219        let body = arena.div(one, den);
3220        let s = closed(summation(&mut arena, body, k, one, n));
3221        for nv in [1i64, 2, 7, 30] {
3222            let expected = brute(&mut arena, body, k, 1, nv);
3223            assert_eq!(eval_at(&mut arena, s, n, nv), expected, "n={nv}");
3224        }
3225        // Infinite: 1
3226        let inf = arena.infinity;
3227        let s = closed(summation(&mut arena, body, k, one, inf));
3228        assert_eq!(as_rat(&arena, s), Some(Rat::one()));
3229    }
3230
3231    #[test]
3232    fn telescoping_shift_two() {
3233        let mut arena = Arena::new();
3234        let k = arena.symbol("k");
3235        let n = arena.symbol("n");
3236        let one = arena.one;
3237        let two = arena.int(2);
3238        let k2 = arena.add(&[k, two]);
3239        let den = arena.mul(&[k, k2]);
3240        let body = arena.div(one, den);
3241        let s = closed(summation(&mut arena, body, k, one, n));
3242        for nv in [1i64, 2, 5, 12] {
3243            let expected = brute(&mut arena, body, k, 1, nv);
3244            assert_eq!(eval_at(&mut arena, s, n, nv), expected, "n={nv}");
3245        }
3246        let inf = arena.infinity;
3247        let s = closed(summation(&mut arena, body, k, one, inf));
3248        assert_eq!(
3249            as_rat(&arena, s),
3250            Some(Rat::new(BigInt::from(3), BigInt::from(4)))
3251        );
3252    }
3253
3254    #[test]
3255    fn odd_reciprocals_product() {
3256        // Σ 1/((2k−1)(2k+1)) from 1 to n = n/(2n+1)
3257        let mut arena = Arena::new();
3258        let k = arena.symbol("k");
3259        let n = arena.symbol("n");
3260        let one = arena.one;
3261        let two = arena.int(2);
3262        let m1 = arena.neg_one;
3263        let twok = arena.mul(&[two, k]);
3264        let a = arena.add(&[twok, m1]);
3265        let b = arena.add(&[twok, one]);
3266        let den = arena.mul(&[a, b]);
3267        let body = arena.div(one, den);
3268        let s = closed(summation(&mut arena, body, k, one, n));
3269        for nv in [1i64, 3, 8] {
3270            let expected = brute(&mut arena, body, k, 1, nv);
3271            assert_eq!(eval_at(&mut arena, s, n, nv), expected, "n={nv}");
3272        }
3273        let inf = arena.infinity;
3274        let s = closed(summation(&mut arena, body, k, one, inf));
3275        assert_eq!(
3276            as_rat(&arena, s),
3277            Some(Rat::new(BigInt::from(1), BigInt::from(2)))
3278        );
3279    }
3280
3281    #[test]
3282    fn harmonic_finite() {
3283        let mut arena = Arena::new();
3284        let k = arena.symbol("k");
3285        let n = arena.symbol("n");
3286        let one = arena.one;
3287        let body = arena.div(one, k);
3288        let s = closed(summation(&mut arena, body, k, one, n));
3289        assert_eq!(arena.display(s).to_string(), "harmonic(n)");
3290        assert_eq!(
3291            eval_at(&mut arena, s, n, 4),
3292            Rat::new(BigInt::from(25), BigInt::from(12))
3293        );
3294    }
3295
3296    #[test]
3297    fn harmonic_infinite_diverges() {
3298        let mut arena = Arena::new();
3299        let k = arena.symbol("k");
3300        let one = arena.one;
3301        let inf = arena.infinity;
3302        let body = arena.div(one, k);
3303        assert_eq!(
3304            summation(&mut arena, body, k, one, inf),
3305            SumOutcome::Divergent(Some(arena.infinity))
3306        );
3307    }
3308
3309    #[test]
3310    fn basel_and_friends() {
3311        let mut arena = Arena::new();
3312        let k = arena.symbol("k");
3313        let one = arena.one;
3314        let inf = arena.infinity;
3315        let pi = arena.pi;
3316        // ζ(2) = π²/6
3317        let m2 = arena.int(-2);
3318        let body = arena.pow(k, m2);
3319        let s = closed(summation(&mut arena, body, k, one, inf));
3320        let two = arena.int(2);
3321        let pi2 = arena.pow(pi, two);
3322        let six = arena.int(6);
3323        let expected = arena.div(pi2, six);
3324        assert_eq!(s, expected, "got {}", arena.display(s));
3325        // ζ(4) = π⁴/90
3326        let m4 = arena.int(-4);
3327        let body = arena.pow(k, m4);
3328        let s = closed(summation(&mut arena, body, k, one, inf));
3329        let four = arena.int(4);
3330        let pi4 = arena.pow(pi, four);
3331        let ninety = arena.int(90);
3332        let expected = arena.div(pi4, ninety);
3333        assert_eq!(s, expected, "got {}", arena.display(s));
3334        // ζ(3) → Zeta(3) node (Apéry's constant has no elementary form)
3335        let m3 = arena.int(-3);
3336        let body = arena.pow(k, m3);
3337        let s = closed(summation(&mut arena, body, k, one, inf));
3338        assert_eq!(arena.display(s).to_string(), "zeta(3)");
3339        // Σ (−1)^k/(2k+1)² = Catalan
3340        let two = arena.int(2);
3341        let two_k = arena.mul(&[two, k]);
3342        let odd = arena.add(&[two_k, one]);
3343        let m2 = arena.int(-2);
3344        let inv_sq = arena.pow(odd, m2);
3345        let neg_one = arena.neg_one;
3346        let alt = arena.pow(neg_one, k);
3347        let body = arena.mul(&[alt, inv_sq]);
3348        let zero = arena.zero;
3349        let s = closed(summation(&mut arena, body, k, zero, inf));
3350        assert_eq!(s, arena.catalan);
3351    }
3352
3353    #[test]
3354    fn zeta_even_rationals() {
3355        assert_eq!(
3356            zeta_even_rational(1),
3357            Rat::new(BigInt::from(1), BigInt::from(6))
3358        );
3359        assert_eq!(
3360            zeta_even_rational(2),
3361            Rat::new(BigInt::from(1), BigInt::from(90))
3362        );
3363        assert_eq!(
3364            zeta_even_rational(3),
3365            Rat::new(BigInt::from(1), BigInt::from(945))
3366        );
3367        assert_eq!(
3368            zeta_even_rational(4),
3369            Rat::new(BigInt::from(1), BigInt::from(9450))
3370        );
3371    }
3372
3373    #[test]
3374    fn euler_numbers() {
3375        assert_eq!(euler_number(0), BigInt::from(1));
3376        assert_eq!(euler_number(1), BigInt::from(-1));
3377        assert_eq!(euler_number(2), BigInt::from(5));
3378        assert_eq!(euler_number(3), BigInt::from(-61));
3379        assert_eq!(euler_number(4), BigInt::from(1385));
3380    }
3381
3382    #[test]
3383    fn alternating_constants() {
3384        let mut arena = Arena::new();
3385        let k = arena.symbol("k");
3386        let one = arena.one;
3387        let zero = arena.zero;
3388        let inf = arena.infinity;
3389        let m1 = arena.neg_one;
3390        // Σ (−1)^(k+1)/k = ln 2
3391        let k1 = arena.add(&[k, one]);
3392        let sgn = arena.pow(m1, k1);
3393        let body = arena.div(sgn, k);
3394        let s = closed(summation(&mut arena, body, k, one, inf));
3395        assert_eq!(arena.display(s).to_string(), "ln(2)");
3396        // Σ (−1)^k/(2k+1) = π/4
3397        let two = arena.int(2);
3398        let twok1 = arena.mul(&[two, k]);
3399        let twok1 = arena.add(&[twok1, one]);
3400        let sgn = arena.pow(m1, k);
3401        let body = arena.div(sgn, twok1);
3402        let s = closed(summation(&mut arena, body, k, zero, inf));
3403        let pi = arena.pi;
3404        let four = arena.int(4);
3405        let expected = arena.div(pi, four);
3406        assert_eq!(s, expected, "got {}", arena.display(s));
3407        // Σ (−1)^(k+1)/k² = π²/12
3408        let k1 = arena.add(&[k, one]);
3409        let sgn = arena.pow(m1, k1);
3410        let m2 = arena.int(-2);
3411        let k2 = arena.pow(k, m2);
3412        let body = arena.mul(&[sgn, k2]);
3413        let s = closed(summation(&mut arena, body, k, one, inf));
3414        let pi2 = arena.pow(pi, two);
3415        let twelve = arena.int(12);
3416        let expected = arena.div(pi2, twelve);
3417        assert_eq!(s, expected, "got {}", arena.display(s));
3418        // Σ_{k≥0} 1/(2k+1)² = π²/8
3419        let m2 = arena.int(-2);
3420        let body = arena.pow(twok1, m2);
3421        let s = closed(summation(&mut arena, body, k, zero, inf));
3422        let eight = arena.int(8);
3423        let expected = arena.div(pi2, eight);
3424        assert_eq!(s, expected, "got {}", arena.display(s));
3425    }
3426
3427    #[test]
3428    fn geometric_finite_and_infinite() {
3429        let mut arena = Arena::new();
3430        let k = arena.symbol("k");
3431        let n = arena.symbol("n");
3432        let zero = arena.zero;
3433        let inf = arena.infinity;
3434        let half = arena.rational(1, 2);
3435        let body = arena.pow(half, k);
3436        let s = closed(summation(&mut arena, body, k, zero, n));
3437        for nv in [0i64, 1, 5, 9] {
3438            let expected = brute(&mut arena, body, k, 0, nv);
3439            assert_eq!(eval_at(&mut arena, s, n, nv), expected);
3440        }
3441        let s = closed(summation(&mut arena, body, k, zero, inf));
3442        assert_eq!(as_rat(&arena, s), Some(rat_i(2)));
3443        // Σ k/2^k = 2
3444        let body2 = arena.mul(&[k, body]);
3445        let s = closed(summation(&mut arena, body2, k, zero, inf));
3446        assert_eq!(
3447            as_rat(&arena, s),
3448            Some(rat_i(2)),
3449            "got {}",
3450            arena.display(s)
3451        );
3452        // Σ k²/2^k = 6
3453        let two = arena.int(2);
3454        let k2 = arena.pow(k, two);
3455        let body3 = arena.mul(&[k2, body]);
3456        let s = closed(summation(&mut arena, body3, k, zero, inf));
3457        assert_eq!(
3458            as_rat(&arena, s),
3459            Some(rat_i(6)),
3460            "got {}",
3461            arena.display(s)
3462        );
3463        // Σ 2^k diverges
3464        let body4 = arena.pow(two, k);
3465        assert_eq!(
3466            summation(&mut arena, body4, k, zero, inf),
3467            SumOutcome::Divergent(Some(arena.infinity))
3468        );
3469    }
3470
3471    #[test]
3472    fn arithmetico_geometric_symbolic_ratio() {
3473        let mut arena = Arena::new();
3474        let k = arena.symbol("k");
3475        let n = arena.symbol("n");
3476        let r = arena.symbol("r");
3477        let zero = arena.zero;
3478        let rk = arena.pow(r, k);
3479        let body = arena.mul(&[k, rk]);
3480        let s = closed(summation(&mut arena, body, k, zero, n));
3481        assert!(
3482            matches!(arena.node(s), ExprNode::Piecewise(_)),
3483            "{}",
3484            arena.display(s)
3485        );
3486        // Evaluate at r = 3, n = 4: Σ k 3^k = 3 + 18 + 81 + 324 = 426
3487        let three = arena.int(3);
3488        let four = arena.int(4);
3489        let v = subs::subs(&mut arena, s, r, three);
3490        let v = subs::subs(&mut arena, v, n, four);
3491        let v = eval::eval(&mut arena, v);
3492        assert_eq!(
3493            as_rat(&arena, v),
3494            Some(rat_i(426)),
3495            "got {}",
3496            arena.display(v)
3497        );
3498        // r = 1 branch: Σ k = 10
3499        let one = arena.one;
3500        let v = subs::subs(&mut arena, s, r, one);
3501        let v = subs::subs(&mut arena, v, n, four);
3502        let v = eval::eval(&mut arena, v);
3503        assert_eq!(
3504            as_rat(&arena, v),
3505            Some(rat_i(10)),
3506            "got {}",
3507            arena.display(v)
3508        );
3509    }
3510
3511    #[test]
3512    fn binomial_identities() {
3513        let mut arena = Arena::new();
3514        let k = arena.symbol("k");
3515        let n = arena.symbol("n");
3516        let zero = arena.zero;
3517        let bin = arena.binomial(n, k);
3518        let s = closed(summation(&mut arena, bin, k, zero, n));
3519        let two = arena.int(2);
3520        assert_eq!(s, arena.pow(two, n), "got {}", arena.display(s));
3521        let body = arena.mul(&[k, bin]);
3522        let s = closed(summation(&mut arena, body, k, zero, n));
3523        for nv in [1i64, 2, 5, 8] {
3524            let nve = arena.int(nv);
3525            let body_n = subs::subs(&mut arena, body, n, nve);
3526            let expected = brute(&mut arena, body_n, k, 0, nv);
3527            assert_eq!(eval_at(&mut arena, s, n, nv), expected);
3528        }
3529        let body = arena.pow(bin, two);
3530        let s = closed(summation(&mut arena, body, k, zero, n));
3531        for nv in [0i64, 1, 3, 6] {
3532            let nve = arena.int(nv);
3533            let body_n = subs::subs(&mut arena, body, n, nve);
3534            let expected = brute(&mut arena, body_n, k, 0, nv);
3535            assert_eq!(eval_at(&mut arena, s, n, nv), expected);
3536        }
3537        let x = arena.symbol("x");
3538        let xk = arena.pow(x, k);
3539        let body = arena.mul(&[bin, xk]);
3540        let s = closed(summation(&mut arena, body, k, zero, n));
3541        let one = arena.one;
3542        let opx = arena.add(&[one, x]);
3543        assert_eq!(s, arena.pow(opx, n), "got {}", arena.display(s));
3544        let m1 = arena.neg_one;
3545        let sgn = arena.pow(m1, k);
3546        let body = arena.mul(&[bin, sgn]);
3547        let s = closed(summation(&mut arena, body, k, zero, n));
3548        assert_eq!(eval_at(&mut arena, s, n, 0), Rat::one());
3549        assert_eq!(eval_at(&mut arena, s, n, 5), Rat::zero());
3550    }
3551
3552    #[test]
3553    fn power_series_table() {
3554        let mut arena = Arena::new();
3555        let k = arena.symbol("k");
3556        let x = arena.symbol("x");
3557        let zero = arena.zero;
3558        let one = arena.one;
3559        let inf = arena.infinity;
3560        let kf = arena.factorial(k);
3561        let xk = arena.pow(x, k);
3562        let body = arena.div(xk, kf);
3563        let s = closed(summation(&mut arena, body, k, zero, inf));
3564        assert_eq!(s, arena.exp(x), "got {}", arena.display(s));
3565        // sin
3566        let two = arena.int(2);
3567        let m1 = arena.neg_one;
3568        let twok1 = arena.mul(&[two, k]);
3569        let twok1 = arena.add(&[twok1, one]);
3570        let sgn = arena.pow(m1, k);
3571        let xp = arena.pow(x, twok1);
3572        let f = arena.factorial(twok1);
3573        let num = arena.mul(&[sgn, xp]);
3574        let body = arena.div(num, f);
3575        let s = closed(summation(&mut arena, body, k, zero, inf));
3576        assert_eq!(s, arena.sin(x), "got {}", arena.display(s));
3577        // cosh
3578        let twok = arena.mul(&[two, k]);
3579        let xp = arena.pow(x, twok);
3580        let f = arena.factorial(twok);
3581        let body = arena.div(xp, f);
3582        let s = closed(summation(&mut arena, body, k, zero, inf));
3583        assert_eq!(s, arena.cosh(x), "got {}", arena.display(s));
3584        // Σ (1/2)^k / k = ln 2
3585        let half = arena.rational(1, 2);
3586        let hk = arena.pow(half, k);
3587        let body = arena.div(hk, k);
3588        let s = closed(summation(&mut arena, body, k, one, inf));
3589        assert_eq!(arena.display(s).to_string(), "ln(2)");
3590        // Σ x^k/k with symbolic x → unevaluated (convergence unknown)
3591        let body = arena.div(xk, k);
3592        assert_eq!(
3593            summation(&mut arena, body, k, one, inf),
3594            SumOutcome::Unevaluated
3595        );
3596        // Σ 1/k! = e
3597        let body = arena.div(one, kf);
3598        let s = closed(summation(&mut arena, body, k, zero, inf));
3599        assert_eq!(s, arena.e_const, "got {}", arena.display(s));
3600        // Σ k x^k/k! = x e^x
3601        let body = arena.mul(&[k, xk]);
3602        let body = arena.div(body, kf);
3603        let s = closed(summation(&mut arena, body, k, zero, inf));
3604        let ex = arena.exp(x);
3605        let expected = arena.mul(&[x, ex]);
3606        assert_eq!(s, expected, "got {}", arena.display(s));
3607        // Σ_{k≥1} x^k/k! = e^x − 1
3608        let body = arena.div(xk, kf);
3609        let s = closed(summation(&mut arena, body, k, one, inf));
3610        let expected = arena.sub(ex, one);
3611        assert_eq!(s, expected, "got {}", arena.display(s));
3612    }
3613
3614    #[test]
3615    fn gosper_fallback_k_factorial() {
3616        let mut arena = Arena::new();
3617        let k = arena.symbol("k");
3618        let n = arena.symbol("n");
3619        let zero = arena.zero;
3620        let kf = arena.factorial(k);
3621        let body = arena.mul(&[k, kf]);
3622        let s = closed(summation(&mut arena, body, k, zero, n));
3623        for nv in [0i64, 1, 3, 5] {
3624            let expected = brute(&mut arena, body, k, 0, nv);
3625            assert_eq!(eval_at(&mut arena, s, n, nv), expected);
3626        }
3627    }
3628
3629    #[test]
3630    fn linearity_partial_keeps_unevaluated_sum() {
3631        let mut arena = Arena::new();
3632        let k = arena.symbol("k");
3633        let n = arena.symbol("n");
3634        let one = arena.one;
3635        let sk = arena.sin(k);
3636        let body = arena.add(&[k, sk]);
3637        let s = closed(summation(&mut arena, body, k, one, n));
3638        assert!(walk::has_unevaluated(&arena, s));
3639        assert!(arena.display(s).to_string().contains("Sum"));
3640    }
3641
3642    #[test]
3643    fn products_basic() {
3644        let mut arena = Arena::new();
3645        let k = arena.symbol("k");
3646        let n = arena.symbol("n");
3647        let one = arena.one;
3648        let two = arena.int(2);
3649        // Π k = n!
3650        let p = closed(product(&mut arena, k, k, one, n));
3651        assert_eq!(p, arena.factorial(n), "got {}", arena.display(p));
3652        // Π 2k = 2^n n!
3653        let body = arena.mul(&[two, k]);
3654        let p = closed(product(&mut arena, body, k, one, n));
3655        for nv in [1i64, 2, 4, 6] {
3656            let expected: Rat = (1..=nv).map(|i| rat_i(2 * i)).product();
3657            assert_eq!(eval_at(&mut arena, p, n, nv), expected);
3658        }
3659        // Π (1 + 1/k) = n + 1
3660        let inv = arena.div(one, k);
3661        let body = arena.add(&[one, inv]);
3662        let p = closed(product(&mut arena, body, k, one, n));
3663        let expected = arena.add(&[n, one]);
3664        assert_eq!(p, expected, "got {}", arena.display(p));
3665        // Π_{k=2}^{n} (1 − 1/k²) = (n+1)/(2n)
3666        let m2 = arena.int(-2);
3667        let k2 = arena.pow(k, m2);
3668        let body = arena.sub(one, k2);
3669        let p = closed(product(&mut arena, body, k, two, n));
3670        for nv in [2i64, 3, 5, 9] {
3671            let expected = Rat::new(BigInt::from(nv + 1), BigInt::from(2 * nv));
3672            assert_eq!(
3673                eval_at(&mut arena, p, n, nv),
3674                expected,
3675                "got {}",
3676                arena.display(p)
3677            );
3678        }
3679        let inf = arena.infinity;
3680        let p = closed(product(&mut arena, body, k, two, inf));
3681        assert_eq!(
3682            as_rat(&arena, p),
3683            Some(Rat::new(BigInt::from(1), BigInt::from(2)))
3684        );
3685        // Π (2k−1) = (2n)!/(2^n n!)
3686        let m1 = arena.neg_one;
3687        let twok = arena.mul(&[two, k]);
3688        let body = arena.add(&[twok, m1]);
3689        let p = closed(product(&mut arena, body, k, one, n));
3690        for nv in [1i64, 2, 3, 5] {
3691            let expected: Rat = (1..=nv).map(|i| rat_i(2 * i - 1)).product();
3692            assert_eq!(
3693                eval_at(&mut arena, p, n, nv),
3694                expected,
3695                "got {}",
3696                arena.display(p)
3697            );
3698        }
3699        // Π a^k = a^(n(n+1)/2)
3700        let a = arena.symbol("a");
3701        let body = arena.pow(a, k);
3702        let p = closed(product(&mut arena, body, k, one, n));
3703        let three = arena.int(3);
3704        let v = subs::subs(&mut arena, p, n, three);
3705        let v = eval::eval(&mut arena, v);
3706        let six = arena.int(6);
3707        assert_eq!(v, arena.pow(a, six), "got {}", arena.display(v));
3708    }
3709}