1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum LimitDirection {
19 Bidirectional,
21 Plus,
23 Minus,
25}
26
27#[derive(Debug)]
28pub enum LimitError {
29 Series(SeriesError),
31 Diff(DiffError),
33 NeedsOneSided,
35 DepthExceeded,
37 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
110pub 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
130fn 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
172fn 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 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 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 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 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
354fn 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 if !depends_on(base, var, pool) {
381 return Ok(None);
382 }
383
384 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 let indeterminate = (base_is_one && exp_is_inf) || (base_is_inf && exp_is_zero) || (base_is_zero && exp_is_zero); if !indeterminate {
406 return Ok(None);
407 }
408
409 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 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 let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
438 Ok(Some(result))
439}
440
441fn 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
593fn 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
604fn 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
632fn 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
729fn 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 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 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
882fn 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 #[test]
988 fn limit_compound_interest_is_e() {
989 let p = ExprPool::new();
990 let x = p.symbol("x", Domain::Real);
991 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 #[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 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 #[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 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 #[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}