Skip to main content

alkahest_cas/calculus/
limits.rs

1//! Symbolic limits towards finite points or ±∞ via local expansions (`Series`),
2//! L'Hôpital iterations, algebraic transforms, and the Gruntz comparability-graph
3//! algorithm for exp-log combinations (V2-16/V2-17).
4
5use crate::calculus::gruntz::try_gruntz;
6use crate::calculus::series::{local_expansion, LocalExpansion};
7use crate::diff::{diff, DiffError};
8use crate::kernel::pool::POS_INFINITY_SYMBOL;
9use crate::kernel::{subs, ExprData, ExprId, ExprPool};
10use crate::poly::{poly_normal, RationalFunction};
11use crate::simplify::{simplify, simplify_expanded};
12use crate::SeriesError;
13use std::collections::HashMap;
14use std::fmt;
15
16/// Approach direction toward `point` (real-axis ordering).
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum LimitDirection {
19    /// Ordinary two-sided limit.
20    Bidirectional,
21    /// Limits with `var > point` (approach from the right on the usual number line picture).
22    Plus,
23    /// Limits with `var < point`.
24    Minus,
25}
26
27#[derive(Debug)]
28pub enum LimitError {
29    /// Sub-problem rejected by [`mod@crate::calculus::series`].
30    Series(SeriesError),
31    /// Derivative unavailable for L'Hôpital.
32    Diff(DiffError),
33    /// Odd-order pole requires a one-sided direction.
34    NeedsOneSided,
35    /// Maximal L'Hôpital / recursion depth exceeded.
36    DepthExceeded,
37    /// No implemented rule applies (non-comparable growth, oscillation, …).
38    Unsupported,
39}
40
41impl fmt::Display for LimitError {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            LimitError::Series(e) => write!(f, "{e}"),
45            LimitError::Diff(e) => write!(f, "{e}"),
46            LimitError::NeedsOneSided => {
47                write!(
48                    f,
49                    "two-sided limit undefined at this pole; pass direction Plus or Minus"
50                )
51            }
52            LimitError::DepthExceeded => write!(f, "limit refinement depth exceeded"),
53            LimitError::Unsupported => write!(f, "limit could not be computed with current rules"),
54        }
55    }
56}
57
58impl std::error::Error for LimitError {
59    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        match self {
61            LimitError::Series(e) => Some(e),
62            LimitError::Diff(e) => Some(e),
63            _ => None,
64        }
65    }
66}
67
68impl crate::errors::AlkahestError for LimitError {
69    fn code(&self) -> &'static str {
70        match self {
71            LimitError::Series(_) => "E-LIMIT-001",
72            LimitError::Diff(_) => "E-LIMIT-002",
73            LimitError::NeedsOneSided => "E-LIMIT-003",
74            LimitError::DepthExceeded => "E-LIMIT-004",
75            LimitError::Unsupported => "E-LIMIT-005",
76        }
77    }
78
79    fn remediation(&self) -> Option<&'static str> {
80        Some(match self {
81            LimitError::Series(_) => {
82                "increase truncation order indirectly by simplifying the expression, or rewrite using standard limits"
83            }
84            LimitError::Diff(_) => {
85                "ensure primitives have differentiation rules, or simplify before taking the limit"
86            }
87            LimitError::NeedsOneSided => "use LimitDirection::Plus or Minus matching the desired one-sided approach",
88            LimitError::DepthExceeded => {
89                "try manual algebra (quotient form, cancellations) or split into simpler sub-expressions"
90            }
91            LimitError::Unsupported => {
92                "limit could not be computed — try manual algebra, or the expression may involve oscillation or non-comparable growth not yet handled"
93            }
94        })
95    }
96}
97
98impl From<SeriesError> for LimitError {
99    fn from(e: SeriesError) -> Self {
100        LimitError::Series(e)
101    }
102}
103
104impl From<DiffError> for LimitError {
105    fn from(e: DiffError) -> Self {
106        LimitError::Diff(e)
107    }
108}
109
110// ---------------------------------------------------------------------------
111
112/// `limit(expr, var, point, dir)` — see [`LimitDirection`].
113///
114/// `point` may be finite or [`ExprPool::pos_infinity`]. Limits at `-∞` use
115/// `pool.mul(pool.integer(-1), pool.pos_infinity())`.
116pub fn limit(
117    expr: ExprId,
118    var: ExprId,
119    point: ExprId,
120    direction: LimitDirection,
121    pool: &ExprPool,
122) -> Result<ExprId, LimitError> {
123    let r = limit_inner(expr, var, point, direction, pool, 0)?;
124    let r_simp = simplify(r, pool).value;
125    let r_fold = fold_known_reals(r_simp, pool);
126    let result = simplify(r_fold, pool).value;
127    Ok(result)
128}
129
130/// `(g^m)^n ↦ g^{m n}` when `m,n ∈ ℤ`, so substitutions like `(1/t)^k` become `t^{-k}` Laurent heads.
131fn flatten_nested_integer_pow(expr: ExprId, pool: &ExprPool) -> ExprId {
132    match pool.get(expr) {
133        ExprData::Pow { base, exp } => {
134            let base = flatten_nested_integer_pow(base, pool);
135            let exp_fl = flatten_nested_integer_pow(exp, pool);
136            if let (
137                ExprData::Pow {
138                    base: b2,
139                    exp: inner_exp,
140                },
141                ExprData::Integer(outer_e),
142            ) = (pool.get(base), pool.get(exp_fl))
143            {
144                if let ExprData::Integer(inner_e) = pool.get(inner_exp) {
145                    let prod = inner_e.0.clone() * outer_e.0.clone();
146                    return pool.pow(flatten_nested_integer_pow(b2, pool), pool.integer(prod));
147                }
148            }
149            pool.pow(base, exp_fl)
150        }
151        ExprData::Mul(xs) => pool.mul(
152            xs.iter()
153                .map(|x| flatten_nested_integer_pow(*x, pool))
154                .collect(),
155        ),
156        ExprData::Add(xs) => pool.add(
157            xs.iter()
158                .map(|x| flatten_nested_integer_pow(*x, pool))
159                .collect(),
160        ),
161        ExprData::Func { name, args } => {
162            let na: Vec<ExprId> = args
163                .iter()
164                .map(|a| flatten_nested_integer_pow(*a, pool))
165                .collect();
166            pool.func(name.clone(), na)
167        }
168        _ => expr,
169    }
170}
171
172/// After ``x ↦ 1/t``, common forms are ``Mul(numer, denom^{-1})`` with ``Pow(t,-1)``
173/// sprinkled through both.  Clear those poles by multiplying by ``t^k`` until
174/// numerator and denominator describe an honest polynomial quotient in ``t``.
175fn canonical_polynomial_quotient_in_var(expr: ExprId, t: ExprId, pool: &ExprPool) -> ExprId {
176    let (n_raw, d_raw) = numerator_denominator(expr, pool);
177    let has_trivial_denom = d_raw == pool.integer(1_i32);
178    // When d_raw == 1 the expr might still have negative powers of t in a sum (e.g. 1 + t^{-1}).
179    // Skip k=0 in that case to avoid an infinite loop, but still try higher k values.
180    for k in 0_i64..=40 {
181        if has_trivial_denom && k == 0 {
182            continue;
183        }
184        let tk = pool.pow(t, pool.integer(k));
185        let n = simplify_expanded(pool.mul(vec![tk, n_raw]), pool).value;
186        let d = simplify_expanded(pool.mul(vec![tk, d_raw]), pool).value;
187        let (n, d) = match (poly_normal(n, vec![t], pool), poly_normal(d, vec![t], pool)) {
188            (Ok(nn), Ok(dd)) => (nn, dd),
189            _ => continue,
190        };
191        if let Ok(rf) = RationalFunction::from_symbolic(n, d, vec![t], pool) {
192            let nx = rf.numer.to_expr(pool);
193            let dx = rf.denom.to_expr(pool);
194            return simplify(pool.mul(vec![nx, pool.pow(dx, pool.integer(-1_i32))]), pool).value;
195        }
196    }
197    expr
198}
199
200fn limit_inner(
201    expr: ExprId,
202    var: ExprId,
203    point: ExprId,
204    direction: LimitDirection,
205    pool: &ExprPool,
206    depth: u32,
207) -> Result<ExprId, LimitError> {
208    const MAX_DEPTH: u32 = 48;
209    const SERIES_ORDER: u32 = 32;
210    if depth > MAX_DEPTH {
211        return Err(LimitError::DepthExceeded);
212    }
213
214    if !depends_on(expr, var, pool) {
215        if substitution_is_singular(expr, pool) {
216            return Err(LimitError::Unsupported);
217        }
218        return Ok(fold_known_reals(simplify(expr, pool).value, pool));
219    }
220
221    if let Some(r) = try_special_function_limits(expr, var, point, direction, pool)? {
222        return Ok(r);
223    }
224
225    // Indeterminate power f^g (1^∞, 0^0, ∞^0) at ±∞: rewrite to exp(g·log f) so the
226    // exp-aware Gruntz/series machinery can handle it.  Plain `(1+1/x)^x → e` lives here.
227    if is_pos_infinity(point, pool) || is_neg_infinity(point, pool) {
228        if let Some(r) = try_indeterminate_power(expr, var, point, direction, pool, depth)? {
229            return Ok(r);
230        }
231    }
232
233    // Gruntz algorithm — best for exp/log expressions at +∞ (runs before the 1/t substitution
234    // so the exp structure is still visible in the original variable).
235    if is_pos_infinity(point, pool) {
236        if let Some(r) = try_gruntz(expr, var, pool)? {
237            return Ok(r);
238        }
239    }
240
241    if is_pos_infinity(point, pool) {
242        let t = pool.symbol("__lt_inf", crate::kernel::Domain::Real);
243        let inv_t = pool.pow(t, pool.integer(-1_i32));
244        let mut m = HashMap::new();
245        m.insert(var, inv_t);
246        let after_subs = subs(expr, &m, pool);
247        let after_flatten = flatten_nested_integer_pow(after_subs, pool);
248        let after_canon = canonical_polynomial_quotient_in_var(after_flatten, t, pool);
249        let e2 = simplify(after_canon, pool).value;
250        return limit_inner(
251            e2,
252            t,
253            pool.integer(0_i32),
254            LimitDirection::Plus,
255            pool,
256            depth + 1,
257        );
258    }
259
260    if is_neg_infinity(point, pool) {
261        let t = pool.symbol("__lt_ninf", crate::kernel::Domain::Real);
262        let rep = pool.mul(vec![
263            pool.integer(-1_i32),
264            pool.pow(t, pool.integer(-1_i32)),
265        ]);
266        let mut m = HashMap::new();
267        m.insert(var, rep);
268        let e2 = simplify(
269            canonical_polynomial_quotient_in_var(
270                flatten_nested_integer_pow(subs(expr, &m, pool), pool),
271                t,
272                pool,
273            ),
274            pool,
275        )
276        .value;
277        return limit_inner(
278            e2,
279            t,
280            pool.integer(0_i32),
281            LimitDirection::Plus,
282            pool,
283            depth + 1,
284        );
285    }
286
287    if let Some(r) = try_direct_substitution(expr, var, point, pool) {
288        return Ok(r);
289    }
290
291    if let Some(r) = try_x_log_x_at_zero(expr, var, point, direction, pool, depth)? {
292        return Ok(r);
293    }
294
295    if let Some(r) = try_lhopital(expr, var, point, direction, pool, depth)? {
296        return Ok(r);
297    }
298
299    if let Some(r) = try_expansion_limit(expr, var, point, direction, pool, SERIES_ORDER)? {
300        return Ok(r);
301    }
302
303    Err(LimitError::Unsupported)
304}
305
306fn try_x_log_x_at_zero(
307    expr: ExprId,
308    var: ExprId,
309    point: ExprId,
310    direction: LimitDirection,
311    pool: &ExprPool,
312    depth: u32,
313) -> Result<Option<ExprId>, LimitError> {
314    if direction == LimitDirection::Minus {
315        return Ok(None);
316    }
317    if !matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
318        return Ok(None);
319    }
320    let ExprData::Mul(args) = pool.get(expr) else {
321        return Ok(None);
322    };
323    if args.len() != 2 {
324        return Ok(None);
325    }
326    let (a, b) = (args[0], args[1]);
327    let log_of_var = |u: ExprId| {
328        matches!(
329            pool.get(u),
330            ExprData::Func { name, args: av } if name == "log" && av.len() == 1 && av[0] == var
331        )
332    };
333    let is_var = |u: ExprId| u == var;
334    let ok = (is_var(a) && log_of_var(b)) || (is_var(b) && log_of_var(a));
335    if !ok {
336        return Ok(None);
337    }
338    // L'Hôpital on log(x) / x^{-1}: (1/x) / (-1/x^2) = -x  → 0 as x→0+.
339    let f = pool.func("log", vec![var]);
340    let g = pool.pow(var, pool.integer(-1_i32));
341    let fp = diff(f, var, pool)?.value;
342    let gp = diff(g, var, pool)?.value;
343    let ratio = rational_quotient(fp, gp, pool);
344    Ok(Some(limit_inner(
345        ratio,
346        var,
347        point,
348        LimitDirection::Plus,
349        pool,
350        depth + 1,
351    )?))
352}
353
354/// Detect an indeterminate power `base^exp` as `var → ±∞` and rewrite it to
355/// `exp(exp · log(base))`, feeding that through the recursive limit machinery
356/// (Gruntz collects the resulting `exp(h)` and gets the right answer).
357///
358/// Only the genuinely indeterminate exponential forms are rewritten:
359///   * `1^∞`  (base → 1, exp → ±∞)
360///   * `∞^0`  (base → ±∞, exp → 0)
361///   * `0^0`  (base → 0, exp → 0)  — only when `base` is structurally positive
362///
363/// Non-indeterminate powers (e.g. `2^x → ∞`, `x^2 → ∞`, or `base → c ≠ 1` with
364/// `exp → ∞`) are left untouched so the existing fast paths still apply and no
365/// new silent-wrong answers are introduced.  `log(base)` is only formed when we
366/// can establish `base > 0` near the limit.
367fn try_indeterminate_power(
368    expr: ExprId,
369    var: ExprId,
370    point: ExprId,
371    direction: LimitDirection,
372    pool: &ExprPool,
373    depth: u32,
374) -> Result<Option<ExprId>, LimitError> {
375    let ExprData::Pow { base, exp } = pool.get(expr) else {
376        return Ok(None);
377    };
378    // A constant base (e.g. exp(...) form is already handled, and 2^x is not
379    // indeterminate) — only proceed when the base genuinely varies with `var`.
380    if !depends_on(base, var, pool) {
381        return Ok(None);
382    }
383
384    // Limits of the base and exponent (independently).  If either sub-limit is
385    // not computable, do not attempt the rewrite.
386    let base_lim = match limit_inner(base, var, point, direction, pool, depth + 1) {
387        Ok(b) => b,
388        Err(_) => return Ok(None),
389    };
390    let exp_lim = match limit_inner(exp, var, point, direction, pool, depth + 1) {
391        Ok(e) => e,
392        Err(_) => return Ok(None),
393    };
394
395    let base_is_one = is_one_like(base_lim, pool);
396    let base_is_zero = is_zero_like(base_lim, pool);
397    let base_is_inf = is_pos_infinity(base_lim, pool) || is_neg_infinity(base_lim, pool);
398    let exp_is_zero = is_zero_like(exp_lim, pool);
399    let exp_is_inf = is_pos_infinity(exp_lim, pool) || is_neg_infinity(exp_lim, pool);
400
401    // Classify the indeterminate exponential forms.
402    let indeterminate = (base_is_one && exp_is_inf)            // 1^∞
403        || (base_is_inf && exp_is_zero)                       // ∞^0
404        || (base_is_zero && exp_is_zero); // 0^0
405    if !indeterminate {
406        return Ok(None);
407    }
408
409    // `log(base)` must be valid: require base > 0 near the limit.  base → 1 or
410    // base → +∞ are positive; base → 0 only qualifies if structurally positive.
411    let base_positive = base_is_one
412        || is_pos_infinity(base_lim, pool)
413        || (base_is_zero && structurally_positive(base, pool));
414    if !base_positive {
415        return Ok(None);
416    }
417
418    // Rewrite f^g → exp(g · log f).  Compute the inner limit `L = lim(g · log f)`
419    // via the existing (correct) machinery, then map it through exp:
420    //   L finite → exp(L),   L = +∞ → +∞,   L = -∞ → 0.
421    // Computing L directly (rather than recursing on `exp(g·log f)`) avoids the
422    // Gruntz `exp(finite)` path, which only retains the leading order and would
423    // give e.g. exp(1) for (1+2/x)^x instead of exp(2).
424    let log_base = pool.func("log", vec![base]);
425    let inner = simplify(pool.mul(vec![exp, log_base]), pool).value;
426    let inner_lim = match limit_inner(inner, var, point, direction, pool, depth + 1) {
427        Ok(l) => l,
428        Err(_) => return Ok(None),
429    };
430    if is_pos_infinity(inner_lim, pool) {
431        return Ok(Some(pool.pos_infinity()));
432    }
433    if is_neg_infinity(inner_lim, pool) {
434        return Ok(Some(pool.integer(0_i32)));
435    }
436    // Finite inner limit ⇒ exp(L).
437    let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
438    Ok(Some(result))
439}
440
441/// Conservative structural test that `e > 0` everywhere it is defined — used to
442/// license `log(e)` for `0^0` rewrites.  `1 + h` with positive constant part,
443/// positive constants, even powers, and products/sums of positives qualify.
444fn structurally_positive(e: ExprId, pool: &ExprPool) -> bool {
445    match pool.get(e) {
446        ExprData::Integer(n) => n.0 > 0,
447        ExprData::Rational(r) => r.0 > 0,
448        ExprData::Func { name, .. } if name == "exp" || name == "cosh" => true,
449        ExprData::Pow { base, exp } => {
450            if let ExprData::Integer(n) = pool.get(exp) {
451                if n.0.clone() % 2 == 0 {
452                    return true;
453                }
454            }
455            structurally_positive(base, pool)
456        }
457        ExprData::Mul(xs) => xs.iter().all(|x| structurally_positive(*x, pool)),
458        _ => false,
459    }
460}
461
462fn try_special_function_limits(
463    expr: ExprId,
464    var: ExprId,
465    point: ExprId,
466    direction: LimitDirection,
467    pool: &ExprPool,
468) -> Result<Option<ExprId>, LimitError> {
469    let ExprData::Func { name, args } = pool.get(expr) else {
470        return Ok(None);
471    };
472    if args.len() != 1 || args[0] != var {
473        return Ok(None);
474    }
475    match name.as_str() {
476        "exp" => {
477            if is_pos_infinity(point, pool) {
478                return Ok(Some(pool.pos_infinity()));
479            }
480            if is_neg_infinity(point, pool) {
481                return Ok(Some(pool.integer(0_i32)));
482            }
483            if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
484                return Ok(Some(pool.integer(1_i32)));
485            }
486        }
487        "log" => {
488            if is_pos_infinity(point, pool) {
489                return Ok(Some(pool.pos_infinity()));
490            }
491            if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
492                if direction == LimitDirection::Plus {
493                    return Ok(Some(neg_infinity(pool)));
494                }
495                return Err(LimitError::NeedsOneSided);
496            }
497        }
498        _ => {}
499    }
500    Ok(None)
501}
502
503fn neg_infinity(pool: &ExprPool) -> ExprId {
504    pool.mul(vec![pool.integer(-1_i32), pool.pos_infinity()])
505}
506
507fn is_pos_infinity(e: ExprId, pool: &ExprPool) -> bool {
508    matches!(
509        pool.get(e),
510        ExprData::Symbol {
511            name,
512            domain: crate::kernel::Domain::Positive,
513            ..
514        } if name == POS_INFINITY_SYMBOL
515    ) || matches!(
516        pool.get(e),
517        ExprData::Symbol {
518            name,
519            domain: crate::kernel::Domain::Real,
520            ..
521        } if name == POS_INFINITY_SYMBOL
522    )
523}
524
525fn is_neg_infinity(e: ExprId, pool: &ExprPool) -> bool {
526    let ExprData::Mul(args) = pool.get(e) else {
527        return false;
528    };
529    if args.len() != 2 {
530        return false;
531    }
532    let (a, b) = (args[0], args[1]);
533    let m_one = pool.integer(-1_i32);
534    (a == m_one && is_pos_infinity(b, pool)) || (b == m_one && is_pos_infinity(a, pool))
535}
536
537fn depends_on(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
538    if expr == var {
539        return true;
540    }
541    match pool.get(expr) {
542        ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|a| depends_on(*a, var, pool)),
543        ExprData::Pow { base, exp } => depends_on(base, var, pool) || depends_on(exp, var, pool),
544        ExprData::Func { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
545        ExprData::Piecewise { branches, default } => {
546            branches
547                .iter()
548                .any(|(c, v)| depends_on(*c, var, pool) || depends_on(*v, var, pool))
549                || depends_on(default, var, pool)
550        }
551        ExprData::Predicate { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
552        ExprData::Forall { var: bv, body } | ExprData::Exists { var: bv, body } => {
553            bv != var && depends_on(body, var, pool)
554        }
555        ExprData::RootSum {
556            poly,
557            var: bv,
558            body,
559        } => depends_on(poly, var, pool) || (bv != var && depends_on(body, var, pool)),
560        ExprData::BigO(a) => depends_on(a, var, pool),
561        ExprData::Integer(_)
562        | ExprData::Rational(_)
563        | ExprData::Float(_)
564        | ExprData::Symbol { .. } => false,
565    }
566}
567
568fn try_direct_substitution(
569    expr: ExprId,
570    var: ExprId,
571    point: ExprId,
572    pool: &ExprPool,
573) -> Option<ExprId> {
574    if quotient_is_zero_over_zero(expr, var, point, pool) {
575        return None;
576    }
577    let mut m = HashMap::new();
578    m.insert(var, point);
579    let raw = subs(expr, &m, pool);
580    if is_zero_times_pole_indeterminate(raw, pool) {
581        return None;
582    }
583    let sub = fold_known_reals(simplify(raw, pool).value, pool);
584    let dep = depends_on(sub, var, pool);
585    let sing = substitution_is_singular(sub, pool);
586    if dep || sing {
587        None
588    } else {
589        Some(sub)
590    }
591}
592
593/// True when ``expr`` is a product quotient `n/d` with `n,d → 0` at substitution (classic `0/0`).
594fn quotient_is_zero_over_zero(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> bool {
595    let (n, d) = numerator_denominator(expr, pool);
596    if d == pool.integer(1_i32) {
597        return false;
598    }
599    let n0 = substitute_fully(n, var, point, pool);
600    let d0 = substitute_fully(d, var, point, pool);
601    is_zero_like(n0, pool) && is_zero_like(d0, pool)
602}
603
604/// `0 · (pole at 0)` style indeterminate — must not simplify to misleading `0`.
605fn is_zero_times_pole_indeterminate(expr: ExprId, pool: &ExprPool) -> bool {
606    let factors: Vec<ExprId> = if matches!(pool.get(expr), ExprData::Mul(_)) {
607        flatten_mul(expr, pool)
608    } else {
609        vec![expr]
610    };
611    let mut any_zero_factor = false;
612    let mut any_pole = false;
613    for f in factors {
614        if substitution_is_singular(f, pool) {
615            any_pole = true;
616        }
617        if matches!(pool.get(f), ExprData::Integer(z) if z.0 == 0) {
618            any_zero_factor = true;
619        }
620        if let ExprData::Func { name, args } = pool.get(f) {
621            if args.len() == 1
622                && matches!(name.as_str(), "sin" | "sinh" | "tan")
623                && matches!(pool.get(args[0]), ExprData::Integer(z) if z.0 == 0)
624            {
625                any_zero_factor = true;
626            }
627        }
628    }
629    any_zero_factor && any_pole
630}
631
632/// `true` after substitution if some sub-expression is ``0^{-n}`` (possibly nested via ``(0^{-1})^e``).
633fn substitution_is_singular(expr: ExprId, pool: &ExprPool) -> bool {
634    match pool.get(expr) {
635        ExprData::Pow { base, exp } => {
636            if let ExprData::Integer(nn) = pool.get(exp) {
637                if nn.0 < 0 {
638                    let b = simplify(base, pool).value;
639                    if matches!(pool.get(b), ExprData::Integer(z) if z.0 == 0) {
640                        return true;
641                    }
642                }
643            }
644            substitution_is_singular(base, pool) || substitution_is_singular(exp, pool)
645        }
646        ExprData::Add(xs) | ExprData::Mul(xs) => {
647            xs.iter().any(|a| substitution_is_singular(*a, pool))
648        }
649        ExprData::Func { args, .. } => args.iter().any(|a| substitution_is_singular(*a, pool)),
650        _ => false,
651    }
652}
653
654fn try_lhopital(
655    expr: ExprId,
656    var: ExprId,
657    point: ExprId,
658    direction: LimitDirection,
659    pool: &ExprPool,
660    depth: u32,
661) -> Result<Option<ExprId>, LimitError> {
662    let (nume, deno) = numerator_denominator(expr, pool);
663    if simplify(nume, pool).value == simplify(deno, pool).value {
664        return Ok(None);
665    }
666    let n0 = substitute_fully(nume, var, point, pool);
667    let d0 = substitute_fully(deno, var, point, pool);
668
669    if !is_zero_like(n0, pool) || !is_zero_like(d0, pool) {
670        return Ok(None);
671    }
672
673    let dn = diff(nume, var, pool)?.value;
674    let dd = diff(deno, var, pool)?.value;
675    if dn == nume && dd == deno {
676        return Ok(None);
677    }
678    let quot = rational_quotient(dn, dd, pool);
679    Ok(Some(limit_inner(
680        quot,
681        var,
682        point,
683        direction,
684        pool,
685        depth + 1,
686    )?))
687}
688
689fn substitute_fully(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> ExprId {
690    let mut m = HashMap::new();
691    m.insert(var, point);
692    let s = simplify(subs(expr, &m, pool), pool).value;
693    fold_known_reals(s, pool)
694}
695
696fn rational_quotient(n: ExprId, d: ExprId, pool: &ExprPool) -> ExprId {
697    simplify(pool.mul(vec![n, pool.pow(d, pool.integer(-1_i32))]), pool).value
698}
699
700fn is_zero_like(e: ExprId, pool: &ExprPool) -> bool {
701    let e = simplify(e, pool).value;
702    if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
703        return true;
704    }
705    if let ExprData::Rational(r) = pool.get(e) {
706        if r.0 == 0 {
707            return true;
708        }
709    }
710    if let ExprData::Func { name, args } = pool.get(e) {
711        if args.len() == 1 && matches!(name.as_str(), "sin" | "tan" | "sinh") {
712            return is_zero_like(args[0], pool);
713        }
714    }
715    false
716}
717
718fn is_one_like(e: ExprId, pool: &ExprPool) -> bool {
719    let e = simplify(e, pool).value;
720    if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 1) {
721        return true;
722    }
723    if let ExprData::Rational(r) = pool.get(e) {
724        return r.0 == 1;
725    }
726    false
727}
728
729/// Constant-fold `sin`, `cos`, `exp`, … after limits (`sin(0) → 0`, `cos(0) → 1`).
730fn fold_known_reals(expr: ExprId, pool: &ExprPool) -> ExprId {
731    let e = simplify(expr, pool).value;
732    match pool.get(e) {
733        ExprData::Add(xs) => {
734            let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
735            simplify(pool.add(ys), pool).value
736        }
737        ExprData::Mul(xs) => {
738            let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
739            simplify(pool.mul(ys), pool).value
740        }
741        ExprData::Pow { base, exp } => {
742            let b = fold_known_reals(base, pool);
743            let xp = fold_known_reals(exp, pool);
744            if is_one_like(b, pool) {
745                return pool.integer(1_i32);
746            }
747            simplify(pool.pow(b, xp), pool).value
748        }
749        ExprData::Func { name, args } if args.len() == 1 => {
750            let inner = fold_known_reals(args[0], pool);
751            if is_zero_like(inner, pool) {
752                match name.as_str() {
753                    "sin" | "tan" | "sinh" => return pool.integer(0_i32),
754                    "cos" | "cosh" => return pool.integer(1_i32),
755                    "exp" => return pool.integer(1_i32),
756                    _ => {}
757                }
758            }
759            simplify(pool.func(name, vec![inner]), pool).value
760        }
761        ExprData::Func { name, args } => {
762            let ys: Vec<ExprId> = args.iter().map(|x| fold_known_reals(*x, pool)).collect();
763            simplify(pool.func(name, ys), pool).value
764        }
765        _ => e,
766    }
767}
768
769fn flatten_mul(expr: ExprId, pool: &ExprPool) -> Vec<ExprId> {
770    match pool.get(expr) {
771        ExprData::Mul(xs) => xs.iter().flat_map(|a| flatten_mul(*a, pool)).collect(),
772        _ => vec![expr],
773    }
774}
775
776fn numerator_denominator(expr: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
777    let fac = flatten_mul(expr, pool);
778    let mut nums = Vec::new();
779    let mut dens = Vec::new();
780    for f in fac {
781        match pool.get(f) {
782            ExprData::Pow { base, exp } => {
783                if let ExprData::Integer(n) = pool.get(exp) {
784                    let nn = &n.0;
785                    if *nn == 0 {
786                        nums.push(pool.integer(1_i32));
787                    } else if *nn > 0 {
788                        nums.push(f);
789                    } else {
790                        let m = nn
791                            .clone()
792                            .abs()
793                            .to_u64()
794                            .and_then(|u| u32::try_from(u).ok())
795                            .map(|mag| pool.pow(base, pool.integer(mag as i64)));
796                        if let Some(p) = m {
797                            dens.push(p);
798                        } else {
799                            nums.push(f);
800                        }
801                    }
802                } else {
803                    nums.push(f);
804                }
805            }
806            _ => nums.push(f),
807        }
808    }
809    let n = if nums.is_empty() {
810        pool.integer(1_i32)
811    } else if nums.len() == 1 {
812        nums[0]
813    } else {
814        pool.mul(nums)
815    };
816    let d = if dens.is_empty() {
817        pool.integer(1_i32)
818    } else if dens.len() == 1 {
819        dens[0]
820    } else {
821        pool.mul(dens)
822    };
823    (n, d)
824}
825
826fn try_expansion_limit(
827    expr: ExprId,
828    var: ExprId,
829    point: ExprId,
830    direction: LimitDirection,
831    pool: &ExprPool,
832    order: u32,
833) -> Result<Option<ExprId>, LimitError> {
834    let exp = match local_expansion(expr, var, point, order, pool) {
835        Ok(e) => e,
836        Err(_) => return Ok(None),
837    };
838    expansion_to_limit(exp, pool, direction)
839}
840
841fn expansion_to_limit(
842    exp: LocalExpansion,
843    pool: &ExprPool,
844    direction: LimitDirection,
845) -> Result<Option<ExprId>, LimitError> {
846    let LocalExpansion {
847        valuation,
848        coeffs,
849        h_expr: _,
850    } = exp;
851
852    let mut idx = 0usize;
853    while idx < coeffs.len() && is_zero_like(coeffs[idx], pool) {
854        idx += 1;
855    }
856    if idx >= coeffs.len() {
857        // Truncation hit all zeros — indeterminate within this order.
858        return Ok(None);
859    }
860    let power = valuation + idx as i32;
861    let coeff = coeffs[idx];
862
863    if power > 0 {
864        return Ok(Some(pool.integer(0_i32)));
865    }
866    if power == 0 {
867        return Ok(Some(coeff));
868    }
869
870    // Polar — power < 0
871    let pole_order = (-power) as u32;
872    let sgn_c = structural_sign(coeff, pool).unwrap_or(1);
873    if pole_order % 2 == 0 {
874        return Ok(Some(signed_infinity(pool, sgn_c)));
875    }
876    let Some(hdir) = sign_from_h(direction, power) else {
877        return Err(LimitError::NeedsOneSided);
878    };
879    Ok(Some(signed_infinity(pool, sgn_c * hdir)))
880}
881
882/// For odd pole: sign of `h^power` with `power < 0` as `h → 0` from one side.
883fn sign_from_h(direction: LimitDirection, power: i32) -> Option<i8> {
884    if power >= 0 {
885        return Some(1);
886    }
887    let odd = (-power) % 2 != 0;
888    if !odd {
889        return Some(1);
890    }
891    match direction {
892        LimitDirection::Plus => Some(1),
893        LimitDirection::Minus => Some(-1),
894        LimitDirection::Bidirectional => None,
895    }
896}
897
898fn signed_infinity(pool: &ExprPool, sign: i8) -> ExprId {
899    if sign < 0 {
900        neg_infinity(pool)
901    } else {
902        pool.pos_infinity()
903    }
904}
905
906fn structural_sign(e: ExprId, pool: &ExprPool) -> Option<i8> {
907    match pool.get(e) {
908        ExprData::Integer(n) => {
909            if n.0 > 0 {
910                Some(1)
911            } else if n.0 < 0 {
912                Some(-1)
913            } else {
914                None
915            }
916        }
917        ExprData::Rational(r) => {
918            if r.0 == 0 {
919                None
920            } else if r.0 > 0 {
921                Some(1)
922            } else {
923                Some(-1)
924            }
925        }
926        ExprData::Mul(xs) => {
927            let mut s = 1i8;
928            for a in xs {
929                let sa = structural_sign(a, pool)?;
930                s *= sa;
931            }
932            Some(s)
933        }
934        ExprData::Pow { base: _, exp } if matches!(pool.get(exp), ExprData::Integer(n) if n.0.clone() % 2 == 0) => {
935            Some(1)
936        }
937        _ => None,
938    }
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944    use crate::kernel::Domain;
945
946    #[test]
947    fn limit_sin_over_x_zero() {
948        let p = ExprPool::new();
949        let x = p.symbol("x", Domain::Real);
950        let ex = simplify(
951            p.mul(vec![p.func("sin", vec![x]), p.pow(x, p.integer(-1_i32))]),
952            &p,
953        )
954        .value;
955        let r = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap();
956        assert_eq!(r, p.integer(1_i32));
957    }
958
959    #[test]
960    fn limit_x_log_x_zero_plus() {
961        let p = ExprPool::new();
962        let x = p.symbol("x", Domain::Real);
963        let ex = simplify(p.mul(vec![x, p.func("log", vec![x])]), &p).value;
964        let r = limit(ex, x, p.integer(0_i32), LimitDirection::Plus, &p).unwrap();
965        assert_eq!(r, p.integer(0_i32));
966    }
967
968    #[test]
969    fn limit_exp_inf() {
970        let p = ExprPool::new();
971        let x = p.symbol("x", Domain::Real);
972        let ex = p.func("exp", vec![x]);
973        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
974        assert_eq!(r, p.pos_infinity());
975    }
976
977    #[test]
978    fn limit_x_squared_at_positive_infinity() {
979        let p = ExprPool::new();
980        let x = p.symbol("x", Domain::Real);
981        let ex = simplify(p.pow(x, p.integer(2_i32)), &p).value;
982        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
983        assert_eq!(r, p.pos_infinity(), "{}", p.display(r));
984    }
985
986    /// `lim_{x→∞} (1 + 1/x)^x = e = exp(1)`  (the silent-wrong-answer regression).
987    #[test]
988    fn limit_compound_interest_is_e() {
989        let p = ExprPool::new();
990        let x = p.symbol("x", Domain::Real);
991        // (1 + 1/x)^x
992        let base = p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]);
993        let ex = simplify(p.pow(base, x), &p).value;
994        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
995        let expected = simplify(p.func("exp", vec![p.integer(1)]), &p).value;
996        assert_eq!(r, expected, "got {}", p.display(r));
997    }
998
999    /// `lim_{x→∞} (1 + a/x)^x = exp(a)` for a concrete integer `a = 2`.
1000    #[test]
1001    fn limit_one_plus_a_over_x_pow_x_is_exp_a() {
1002        let p = ExprPool::new();
1003        let x = p.symbol("x", Domain::Real);
1004        // (1 + 2/x)^x
1005        let two_over_x = p.mul(vec![p.integer(2), p.pow(x, p.integer(-1))]);
1006        let base = p.add(vec![p.integer(1), two_over_x]);
1007        let ex = simplify(p.pow(base, x), &p).value;
1008        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1009        let expected = simplify(p.func("exp", vec![p.integer(2)]), &p).value;
1010        assert_eq!(r, expected, "got {}", p.display(r));
1011    }
1012
1013    /// Non-regression: `2^x` has a constant base (not the indeterminate `1^∞`
1014    /// form), so the new rewrite must NOT fire and must NOT fabricate a finite
1015    /// value.  The engine declines it (as it did before this fix); the key point
1016    /// is that it never returns a wrong finite limit.
1017    #[test]
1018    fn limit_two_pow_x_not_rewritten_to_finite() {
1019        let p = ExprPool::new();
1020        let x = p.symbol("x", Domain::Real);
1021        let ex = simplify(p.pow(p.integer(2), x), &p).value;
1022        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p);
1023        // Either it stays unsupported or returns +∞ — but never a finite number.
1024        if let Ok(v) = r {
1025            assert_eq!(
1026                v,
1027                p.pos_infinity(),
1028                "2^x must not be a finite value: {}",
1029                p.display(v)
1030            );
1031        }
1032    }
1033
1034    /// Non-regression: `lim_{x→∞} (1 + 1/x) = 1` (not a power; sanity that the
1035    /// helper does not perturb the simple base limit).
1036    #[test]
1037    fn limit_one_plus_one_over_x_is_one() {
1038        let p = ExprPool::new();
1039        let x = p.symbol("x", Domain::Real);
1040        let ex = simplify(p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]), &p).value;
1041        let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1042        assert_eq!(r, p.integer(1), "got {}", p.display(r));
1043    }
1044
1045    #[test]
1046    fn rational_x_over_x_plus_one_after_inf_subst() {
1047        let p = ExprPool::new();
1048        let t = p.symbol("__lt_inf", Domain::Real);
1049        let inv = p.pow(t, p.integer(-1));
1050        let ex = p.mul(vec![
1051            inv,
1052            p.pow(p.add(vec![p.integer(1), inv]), p.integer(-1)),
1053        ]);
1054        let folded = flatten_nested_integer_pow(ex, &p);
1055        let canon = canonical_polynomial_quotient_in_var(folded, t, &p);
1056        let r = simplify(canon, &p).value;
1057        let mut m = HashMap::new();
1058        m.insert(t, p.integer(0));
1059        let sub = fold_known_reals(simplify(subs(r, &m, &p), &p).value, &p);
1060        assert_eq!(sub, p.integer(1), "canonical={}", p.display(canon));
1061    }
1062}