1use crate::budget::BudgetError;
6use crate::calculus::asymptotic::regularize_at_zero;
7use crate::calculus::gruntz::try_gruntz;
8use crate::calculus::series::{enter_coeff_ceiling, local_expansion, LocalExpansion};
9use crate::diff::{diff, DiffError};
10use crate::kernel::pool::POS_INFINITY_SYMBOL;
11use crate::kernel::{subs, ExprData, ExprId, ExprPool};
12use crate::poly::{poly_normal, RationalFunction};
13use crate::simplify::{simplify, simplify_expanded};
14use crate::SeriesError;
15use std::cell::Cell;
16use std::collections::HashMap;
17use std::fmt;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub enum LimitDirection {
22 Bidirectional,
24 Plus,
26 Minus,
28}
29
30#[derive(Debug)]
31pub enum LimitError {
32 Series(SeriesError),
34 Diff(DiffError),
36 NeedsOneSided,
38 DepthExceeded,
43 Unsupported,
45}
46
47impl fmt::Display for LimitError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 LimitError::Series(e) => write!(f, "{e}"),
51 LimitError::Diff(e) => write!(f, "{e}"),
52 LimitError::NeedsOneSided => {
53 write!(
54 f,
55 "two-sided limit undefined at this pole; pass direction Plus or Minus"
56 )
57 }
58 LimitError::DepthExceeded => write!(f, "limit refinement depth exceeded"),
59 LimitError::Unsupported => write!(f, "limit could not be computed with current rules"),
60 }
61 }
62}
63
64impl std::error::Error for LimitError {
65 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
66 match self {
67 LimitError::Series(e) => Some(e),
68 LimitError::Diff(e) => Some(e),
69 _ => None,
70 }
71 }
72}
73
74impl crate::errors::AlkahestError for LimitError {
75 fn code(&self) -> &'static str {
76 match self {
77 LimitError::Series(_) => "E-LIMIT-001",
78 LimitError::Diff(_) => "E-LIMIT-002",
79 LimitError::NeedsOneSided => "E-LIMIT-003",
80 LimitError::DepthExceeded => "E-LIMIT-004",
81 LimitError::Unsupported => "E-LIMIT-005",
82 }
83 }
84
85 fn remediation(&self) -> Option<&'static str> {
86 Some(match self {
87 LimitError::Series(_) => {
88 "increase truncation order indirectly by simplifying the expression, or rewrite using standard limits"
89 }
90 LimitError::Diff(_) => {
91 "ensure primitives have differentiation rules, or simplify before taking the limit"
92 }
93 LimitError::NeedsOneSided => "use LimitDirection::Plus or Minus matching the desired one-sided approach",
94 LimitError::DepthExceeded => {
95 "try manual algebra (quotient form, cancellations) or split into simpler sub-expressions"
96 }
97 LimitError::Unsupported => {
98 "limit could not be computed — try manual algebra, or the expression may involve oscillation or non-comparable growth not yet handled"
99 }
100 })
101 }
102}
103
104impl From<SeriesError> for LimitError {
105 fn from(e: SeriesError) -> Self {
106 LimitError::Series(e)
107 }
108}
109
110impl From<DiffError> for LimitError {
111 fn from(e: DiffError) -> Self {
112 LimitError::Diff(e)
113 }
114}
115
116const MAX_LIMIT_POOL_GROWTH: usize = 100_000;
139
140thread_local! {
141 static WORK_BASELINE: Cell<Option<usize>> = const { Cell::new(None) };
144 static BUDGET_TRIP: Cell<Option<BudgetError>> = const { Cell::new(None) };
147}
148
149struct WorkFrame {
155 outermost: bool,
156}
157
158impl Drop for WorkFrame {
159 fn drop(&mut self) {
160 if self.outermost {
161 WORK_BASELINE.with(|c| c.set(None));
162 }
163 }
164}
165
166fn enter_work_frame(pool: &ExprPool) -> WorkFrame {
167 WORK_BASELINE.with(|c| {
168 if c.get().is_some() {
169 return WorkFrame { outermost: false };
170 }
171 c.set(Some(pool.len()));
172 WorkFrame { outermost: true }
173 })
174}
175
176fn work_exhausted(pool: &ExprPool) -> bool {
179 WORK_BASELINE.with(|c| match c.get() {
180 Some(base) => pool.len().saturating_sub(base) > MAX_LIMIT_POOL_GROWTH,
181 None => false,
182 })
183}
184
185fn coeff_ceiling(pool: &ExprPool) -> usize {
189 WORK_BASELINE.with(|c| {
190 c.get()
191 .unwrap_or_else(|| pool.len())
192 .saturating_add(MAX_LIMIT_POOL_GROWTH)
193 })
194}
195
196pub(crate) fn checkpoint(pool: &ExprPool) -> Result<(), LimitError> {
203 if let Err(e) = crate::budget::check() {
204 BUDGET_TRIP.with(|c| c.set(Some(e)));
205 return Err(LimitError::DepthExceeded);
206 }
207 if work_exhausted(pool) {
208 return Err(LimitError::DepthExceeded);
209 }
210 Ok(())
211}
212
213pub fn last_budget_trip() -> Option<BudgetError> {
227 BUDGET_TRIP.with(|c| c.get())
228}
229
230pub fn limit(
243 expr: ExprId,
244 var: ExprId,
245 point: ExprId,
246 direction: LimitDirection,
247 pool: &ExprPool,
248) -> Result<ExprId, LimitError> {
249 let frame = enter_work_frame(pool);
250 if frame.outermost {
251 BUDGET_TRIP.with(|c| c.set(None));
252 }
253 let _ceiling = enter_coeff_ceiling(coeff_ceiling(pool));
257
258 limit_body(expr, var, point, direction, pool).map_err(|e| attribute_failure(e, pool))
259}
260
261fn attribute_failure(e: LimitError, pool: &ExprPool) -> LimitError {
273 if BUDGET_TRIP.with(|c| c.get()).is_some() || work_exhausted(pool) {
274 return LimitError::DepthExceeded;
275 }
276 if let Err(b) = crate::budget::check() {
277 BUDGET_TRIP.with(|c| c.set(Some(b)));
278 return LimitError::DepthExceeded;
279 }
280 e
281}
282
283fn limit_body(
284 expr: ExprId,
285 var: ExprId,
286 point: ExprId,
287 direction: LimitDirection,
288 pool: &ExprPool,
289) -> Result<ExprId, LimitError> {
290 let r = limit_inner(expr, var, point, direction, pool, 0)?;
291 let r_simp = simplify(r, pool).value;
292 let r_fold = fold_known_reals(r_simp, pool);
293 let result = simplify(r_fold, pool).value;
294 if contains_zero_to_negative_power(result, pool) {
301 return Err(LimitError::Unsupported);
302 }
303 if approach_side_is_outside_the_domain(expr, var, point, direction, pool) {
304 return Err(LimitError::Unsupported);
305 }
306 if numeric_evidence_contradicts(expr, var, point, direction, result, pool) {
307 return Err(LimitError::Unsupported);
308 }
309 Ok(result)
310}
311
312fn approach_side_is_outside_the_domain(
339 expr: ExprId,
340 var: ExprId,
341 point: ExprId,
342 direction: LimitDirection,
343 pool: &ExprPool,
344) -> bool {
345 let sign = match direction {
346 LimitDirection::Plus => 1.0,
347 LimitDirection::Minus => -1.0,
348 LimitDirection::Bidirectional => return false,
349 };
350 if is_polynomial_in(expr, var, pool) || has_free_symbol_besides(expr, var, pool) {
353 return false;
354 }
355 let Some(at) = constant_f64(point, pool) else {
356 return false;
357 };
358
359 let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
360 let mut sample = |offset: f64| -> Option<f64> {
361 env.insert(var, at + offset);
362 crate::jit::eval_interp(expr, &env, pool)
363 };
364
365 for offset in APPROACH_OFFSETS {
366 match sample(sign * offset) {
367 Some(v) if v.is_nan() => {}
368 _ => return false,
370 }
371 }
372 APPROACH_OFFSETS
374 .iter()
375 .any(|&offset| sample(-sign * offset).is_some_and(|v| v.is_finite()))
376}
377
378const APPROACH_OFFSETS: [f64; 4] = [1e-1, 1e-2, 1e-3, 1e-4];
385
386struct SideEstimate {
388 value: f64,
390 movement: f64,
393}
394
395fn side_estimate(
404 expr: ExprId,
405 var: ExprId,
406 at: f64,
407 sign: f64,
408 pool: &ExprPool,
409) -> Option<SideEstimate> {
410 let mut samples = Vec::with_capacity(APPROACH_OFFSETS.len());
411 let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
412 for offset in APPROACH_OFFSETS {
413 env.insert(var, at + sign * offset);
414 match crate::jit::eval_interp(expr, &env, pool) {
415 Some(v) if v.is_finite() => samples.push(v),
416 _ => continue,
419 }
420 }
421 if samples.len() < 3 {
422 return None;
423 }
424 let last = samples[samples.len() - 1];
425 let prev = samples[samples.len() - 2];
426 let movement = (last - prev).abs();
427 let scale = 1.0 + last.abs();
428 if movement > 0.01 * scale {
430 return None;
431 }
432 Some(SideEstimate {
433 value: last,
434 movement,
435 })
436}
437
438fn numeric_evidence_contradicts(
452 expr: ExprId,
453 var: ExprId,
454 point: ExprId,
455 direction: LimitDirection,
456 result: ExprId,
457 pool: &ExprPool,
458) -> bool {
459 if is_polynomial_in(expr, var, pool) {
469 return false;
470 }
471 if has_free_symbol_besides(expr, var, pool) {
473 return false;
474 }
475 let Some(at) = constant_f64(point, pool) else {
477 return false;
478 };
479
480 let claimed = constant_f64(result, pool);
481
482 if !probe_looks_suspicious(expr, var, at, direction, claimed, pool) {
491 return false;
492 }
493
494 let left = side_estimate(expr, var, at, -1.0, pool);
495 let right = side_estimate(expr, var, at, 1.0, pool);
496
497 if direction == LimitDirection::Bidirectional {
500 if let (Some(l), Some(r)) = (&left, &right) {
501 let tol = 1e-6 + 20.0 * (l.movement + r.movement);
502 if (l.value - r.value).abs() > tol {
503 return true;
504 }
505 }
506 }
507
508 let Some(claimed) = claimed else {
512 return false;
513 };
514 let sides: [&Option<SideEstimate>; 2] = match direction {
515 LimitDirection::Plus => [&right, &None],
516 LimitDirection::Minus => [&left, &None],
517 LimitDirection::Bidirectional => [&left, &right],
518 };
519 for side in sides.into_iter().flatten() {
520 let tol = 1e-6 * (1.0 + claimed.abs()) + 20.0 * side.movement;
521 if (side.value - claimed).abs() > tol {
522 return true;
523 }
524 }
525 false
526}
527
528fn is_polynomial_in(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
535 if expr == var {
536 return true;
537 }
538 match pool.get(expr) {
539 ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => true,
540 ExprData::Symbol { .. } => false,
541 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().all(|&x| is_polynomial_in(x, var, pool)),
542 ExprData::Pow { base, exp } => {
543 matches!(pool.get(exp), ExprData::Integer(n) if n.0 >= 0)
544 && is_polynomial_in(base, var, pool)
545 }
546 _ => false,
547 }
548}
549
550fn probe_looks_suspicious(
559 expr: ExprId,
560 var: ExprId,
561 at: f64,
562 direction: LimitDirection,
563 claimed: Option<f64>,
564 pool: &ExprPool,
565) -> bool {
566 let far = APPROACH_OFFSETS[1];
575 let near = APPROACH_OFFSETS[APPROACH_OFFSETS.len() - 1];
576 let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
579 let mut sample = |sign: f64, offset: f64| -> Option<f64> {
580 env.insert(var, at + sign * offset);
581 crate::jit::eval_interp(expr, &env, pool).filter(|v| v.is_finite())
582 };
583
584 let mut side_is_suspicious = |sign: f64| -> bool {
585 let (Some(f_far), Some(f_near)) = (sample(sign, far), sample(sign, near)) else {
586 return false;
589 };
590 match claimed {
591 Some(c) => (f_near - c).abs() > 0.75 * (f_far - c).abs(),
597 None => false,
598 }
599 };
600
601 let left_bad = direction != LimitDirection::Plus && side_is_suspicious(-1.0);
602 let right_bad = direction != LimitDirection::Minus && side_is_suspicious(1.0);
603 if left_bad || right_bad {
604 return true;
605 }
606
607 if claimed.is_none() && direction == LimitDirection::Bidirectional {
610 if let (Some(l), Some(r)) = (sample(-1.0, near), sample(1.0, near)) {
611 return (l - r).abs() > 1e-6 * (1.0 + l.abs().max(r.abs()));
612 }
613 }
614 false
615}
616
617fn constant_f64(expr: ExprId, pool: &ExprPool) -> Option<f64> {
620 let env = HashMap::new();
621 crate::jit::eval_interp(expr, &env, pool).filter(|v| v.is_finite())
622}
623
624fn has_free_symbol_besides(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
626 if expr == var {
627 return false;
628 }
629 match pool.get(expr) {
630 ExprData::Symbol { .. } => true,
631 ExprData::Add(xs) | ExprData::Mul(xs) => {
632 xs.iter().any(|&x| has_free_symbol_besides(x, var, pool))
633 }
634 ExprData::Pow { base, exp } => {
635 has_free_symbol_besides(base, var, pool) || has_free_symbol_besides(exp, var, pool)
636 }
637 ExprData::Func { args, .. } => args.iter().any(|&a| has_free_symbol_besides(a, var, pool)),
638 _ => false,
639 }
640}
641
642fn contains_zero_to_negative_power(expr: ExprId, pool: &ExprPool) -> bool {
645 match pool.get(expr) {
646 ExprData::Pow { base, exp } => {
647 let zero_base = matches!(pool.get(base), ExprData::Integer(n) if n.0 == 0);
648 let negative_exp = match pool.get(exp) {
649 ExprData::Integer(n) => n.0 < 0,
650 ExprData::Rational(r) => r.0 < 0,
651 _ => false,
652 };
653 (zero_base && negative_exp)
654 || contains_zero_to_negative_power(base, pool)
655 || contains_zero_to_negative_power(exp, pool)
656 }
657 ExprData::Add(xs) | ExprData::Mul(xs) => {
658 xs.iter().any(|&x| contains_zero_to_negative_power(x, pool))
659 }
660 ExprData::Func { args, .. } => args
661 .iter()
662 .any(|&a| contains_zero_to_negative_power(a, pool)),
663 _ => false,
664 }
665}
666
667fn flatten_nested_integer_pow(expr: ExprId, pool: &ExprPool) -> ExprId {
669 match pool.get(expr) {
670 ExprData::Pow { base, exp } => {
671 let base = flatten_nested_integer_pow(base, pool);
672 let exp_fl = flatten_nested_integer_pow(exp, pool);
673 if let (
674 ExprData::Pow {
675 base: b2,
676 exp: inner_exp,
677 },
678 ExprData::Integer(outer_e),
679 ) = (pool.get(base), pool.get(exp_fl))
680 {
681 if let ExprData::Integer(inner_e) = pool.get(inner_exp) {
682 let prod = inner_e.0.clone() * outer_e.0.clone();
683 return pool.pow(flatten_nested_integer_pow(b2, pool), pool.integer(prod));
684 }
685 }
686 pool.pow(base, exp_fl)
687 }
688 ExprData::Mul(xs) => pool.mul(
689 xs.iter()
690 .map(|x| flatten_nested_integer_pow(*x, pool))
691 .collect(),
692 ),
693 ExprData::Add(xs) => pool.add(
694 xs.iter()
695 .map(|x| flatten_nested_integer_pow(*x, pool))
696 .collect(),
697 ),
698 ExprData::Func { name, args } => {
699 let na: Vec<ExprId> = args
700 .iter()
701 .map(|a| flatten_nested_integer_pow(*a, pool))
702 .collect();
703 pool.func(name.clone(), na)
704 }
705 _ => expr,
706 }
707}
708
709fn canonical_polynomial_quotient_in_var(
713 expr: ExprId,
714 t: ExprId,
715 pool: &ExprPool,
716) -> Result<ExprId, LimitError> {
717 let (n_raw, d_raw) = numerator_denominator(expr, pool);
718 let has_trivial_denom = d_raw == pool.integer(1_i32);
719 for k in 0_i64..=40 {
722 if has_trivial_denom && k == 0 {
723 continue;
724 }
725 checkpoint(pool)?;
729 let tk = pool.pow(t, pool.integer(k));
730 let n = simplify_expanded(pool.mul(vec![tk, n_raw]), pool).value;
731 let d = simplify_expanded(pool.mul(vec![tk, d_raw]), pool).value;
732 let (n, d) = match (poly_normal(n, vec![t], pool), poly_normal(d, vec![t], pool)) {
733 (Ok(nn), Ok(dd)) => (nn, dd),
734 _ => continue,
735 };
736 if let Ok(rf) = RationalFunction::from_symbolic(n, d, vec![t], pool) {
737 let nx = rf.numer.to_expr(pool);
738 let dx = rf.denom.to_expr(pool);
739 return Ok(
740 simplify(pool.mul(vec![nx, pool.pow(dx, pool.integer(-1_i32))]), pool).value,
741 );
742 }
743 }
744 Ok(expr)
745}
746
747fn limit_inner(
748 expr: ExprId,
749 var: ExprId,
750 point: ExprId,
751 direction: LimitDirection,
752 pool: &ExprPool,
753 depth: u32,
754) -> Result<ExprId, LimitError> {
755 const MAX_DEPTH: u32 = 48;
756 const SERIES_ORDER: u32 = 32;
757 if depth > MAX_DEPTH {
758 return Err(LimitError::DepthExceeded);
759 }
760 checkpoint(pool)?;
761
762 if !depends_on(expr, var, pool) {
763 if substitution_is_singular(expr, pool) {
764 return Err(LimitError::Unsupported);
765 }
766 return Ok(fold_known_reals(simplify(expr, pool).value, pool));
767 }
768
769 if let Some(r) = try_special_function_limits(expr, var, point, direction, pool)? {
770 return Ok(r);
771 }
772
773 if let Some(r) = try_indeterminate_power(expr, var, point, direction, pool, depth)? {
777 return Ok(r);
778 }
779
780 if is_pos_infinity(point, pool) {
783 if let Some(r) = try_gruntz(expr, var, pool)? {
784 return Ok(r);
785 }
786 }
787
788 if is_pos_infinity(point, pool) || is_neg_infinity(point, pool) {
793 let toward_pos = is_pos_infinity(point, pool);
794 if let Some(r) = try_regularized_infinity_limit(expr, var, toward_pos, pool)? {
795 return Ok(r);
796 }
797 }
798
799 if is_pos_infinity(point, pool) {
800 let t = pool.symbol("__lt_inf", crate::kernel::Domain::Real);
801 let inv_t = pool.pow(t, pool.integer(-1_i32));
802 let mut m = HashMap::new();
803 m.insert(var, inv_t);
804 let after_subs = subs(expr, &m, pool);
805 let after_flatten = flatten_nested_integer_pow(after_subs, pool);
806 let after_canon = canonical_polynomial_quotient_in_var(after_flatten, t, pool)?;
807 let e2 = simplify(after_canon, pool).value;
808 return limit_inner(
809 e2,
810 t,
811 pool.integer(0_i32),
812 LimitDirection::Plus,
813 pool,
814 depth + 1,
815 );
816 }
817
818 if is_neg_infinity(point, pool) {
819 let t = pool.symbol("__lt_ninf", crate::kernel::Domain::Real);
820 let rep = pool.mul(vec![
821 pool.integer(-1_i32),
822 pool.pow(t, pool.integer(-1_i32)),
823 ]);
824 let mut m = HashMap::new();
825 m.insert(var, rep);
826 let canon = canonical_polynomial_quotient_in_var(
827 flatten_nested_integer_pow(subs(expr, &m, pool), pool),
828 t,
829 pool,
830 )?;
831 let e2 = simplify(canon, pool).value;
832 return limit_inner(
833 e2,
834 t,
835 pool.integer(0_i32),
836 LimitDirection::Plus,
837 pool,
838 depth + 1,
839 );
840 }
841
842 if let Some(r) = try_direct_substitution(expr, var, point, pool) {
843 return Ok(r);
844 }
845
846 if let Some(r) = try_x_log_x_at_zero(expr, var, point, direction, pool, depth)? {
847 return Ok(r);
848 }
849
850 if let Some(r) = try_lhopital(expr, var, point, direction, pool, depth)? {
851 return Ok(r);
852 }
853
854 if let Some(r) = try_expansion_limit(expr, var, point, direction, pool, SERIES_ORDER)? {
855 return Ok(r);
856 }
857
858 Err(LimitError::Unsupported)
859}
860
861fn contains_radical(expr: ExprId, pool: &ExprPool) -> bool {
864 match pool.get(expr) {
865 ExprData::Func { name, args } => {
866 name == "sqrt" || name == "cbrt" || args.iter().any(|&a| contains_radical(a, pool))
867 }
868 ExprData::Pow { base, exp } => {
869 matches!(pool.get(exp), ExprData::Rational(_))
870 || contains_radical(base, pool)
871 || contains_radical(exp, pool)
872 }
873 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|&x| contains_radical(x, pool)),
874 _ => false,
875 }
876}
877
878fn try_regularized_infinity_limit(
899 expr: ExprId,
900 var: ExprId,
901 toward_pos: bool,
902 pool: &ExprPool,
903) -> Result<Option<ExprId>, LimitError> {
904 const ORDERS: [u32; 3] = [4, 10, 24];
909
910 if !contains_radical(expr, pool) {
911 return Ok(None);
912 }
913
914 let t = pool.symbol("__lt_reg", crate::kernel::Domain::Positive);
918 let inv_t = pool.pow(t, pool.integer(-1_i32));
919 let rep = if toward_pos {
920 inv_t
921 } else {
922 pool.mul(vec![pool.integer(-1_i32), inv_t])
923 };
924 let mut m = HashMap::new();
925 m.insert(var, rep);
926 let f_of_t = simplify(subs(expr, &m, pool), pool).value;
927
928 let Some((val, analytic)) = regularize_at_zero(f_of_t, t, pool) else {
929 return Ok(None);
930 };
931 let Ok(val) = i32::try_from(val) else {
932 return Ok(None);
933 };
934
935 let zero = pool.integer(0_i32);
936 for order in ORDERS {
937 checkpoint(pool)?;
938 let Ok(exp) = local_expansion(analytic, t, zero, order, pool) else {
939 return Ok(None);
940 };
941 let LocalExpansion {
942 valuation,
943 coeffs,
944 h_expr,
945 } = exp;
946 let Some(total) = val.checked_add(valuation) else {
947 return Ok(None);
948 };
949 let shifted = LocalExpansion {
950 valuation: total,
951 coeffs,
952 h_expr,
953 };
954 if let Some(r) = expansion_to_limit(shifted, pool, LimitDirection::Plus)? {
956 return Ok(Some(r));
957 }
958 }
959 Ok(None)
960}
961
962fn try_x_log_x_at_zero(
963 expr: ExprId,
964 var: ExprId,
965 point: ExprId,
966 direction: LimitDirection,
967 pool: &ExprPool,
968 depth: u32,
969) -> Result<Option<ExprId>, LimitError> {
970 if direction == LimitDirection::Minus {
971 return Ok(None);
972 }
973 if !matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
974 return Ok(None);
975 }
976 let ExprData::Mul(args) = pool.get(expr) else {
977 return Ok(None);
978 };
979 if args.len() != 2 {
980 return Ok(None);
981 }
982 let (a, b) = (args[0], args[1]);
983 let log_of_var = |u: ExprId| {
984 matches!(
985 pool.get(u),
986 ExprData::Func { name, args: av } if name == "log" && av.len() == 1 && av[0] == var
987 )
988 };
989 let is_var = |u: ExprId| u == var;
990 let ok = (is_var(a) && log_of_var(b)) || (is_var(b) && log_of_var(a));
991 if !ok {
992 return Ok(None);
993 }
994 let f = pool.func("log", vec![var]);
996 let g = pool.pow(var, pool.integer(-1_i32));
997 let fp = diff(f, var, pool)?.value;
998 let gp = diff(g, var, pool)?.value;
999 let ratio = rational_quotient(fp, gp, pool);
1000 Ok(Some(limit_inner(
1001 ratio,
1002 var,
1003 point,
1004 LimitDirection::Plus,
1005 pool,
1006 depth + 1,
1007 )?))
1008}
1009
1010fn try_indeterminate_power(
1024 expr: ExprId,
1025 var: ExprId,
1026 point: ExprId,
1027 direction: LimitDirection,
1028 pool: &ExprPool,
1029 depth: u32,
1030) -> Result<Option<ExprId>, LimitError> {
1031 let ExprData::Pow { base, exp } = pool.get(expr) else {
1032 return Ok(None);
1033 };
1034 if !depends_on(base, var, pool) {
1037 return Ok(None);
1038 }
1039
1040 let base_lim = match limit_inner(base, var, point, direction, pool, depth + 1) {
1042 Ok(b) => b,
1043 Err(_) => return Ok(None),
1044 };
1045
1046 if is_one_like(base_lim, pool) {
1050 let log_base = pool.func("log", vec![base]);
1051 let inner = simplify(pool.mul(vec![exp, log_base]), pool).value;
1052 if let Ok(inner_lim) = limit_inner(inner, var, point, direction, pool, depth + 1) {
1053 if is_pos_infinity(inner_lim, pool) {
1054 return Ok(Some(pool.pos_infinity()));
1055 }
1056 if is_neg_infinity(inner_lim, pool) {
1057 return Ok(Some(pool.integer(0_i32)));
1058 }
1059 let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
1060 return Ok(Some(result));
1061 }
1062 }
1063
1064 let exp_lim = match limit_inner(exp, var, point, direction, pool, depth + 1) {
1065 Ok(e) => e,
1066 Err(_) => return Ok(None),
1067 };
1068
1069 let base_is_one = is_one_like(base_lim, pool);
1070 let base_is_zero = is_zero_like(base_lim, pool);
1071 let base_is_inf = is_pos_infinity(base_lim, pool) || is_neg_infinity(base_lim, pool);
1072 let exp_is_zero = is_zero_like(exp_lim, pool);
1073 let exp_is_inf = is_pos_infinity(exp_lim, pool) || is_neg_infinity(exp_lim, pool);
1074
1075 let indeterminate = (base_is_one && exp_is_inf) || (base_is_inf && exp_is_zero) || (base_is_zero && exp_is_zero); if !indeterminate {
1080 return Ok(None);
1081 }
1082
1083 let base_positive = base_is_one
1086 || is_pos_infinity(base_lim, pool)
1087 || (base_is_zero && structurally_positive(base, pool));
1088 if !base_positive {
1089 return Ok(None);
1090 }
1091
1092 let log_base = pool.func("log", vec![base]);
1099 let inner = simplify(pool.mul(vec![exp, log_base]), pool).value;
1100 let inner_lim = match limit_inner(inner, var, point, direction, pool, depth + 1) {
1101 Ok(l) => l,
1102 Err(_) => return Ok(None),
1103 };
1104 if is_pos_infinity(inner_lim, pool) {
1105 return Ok(Some(pool.pos_infinity()));
1106 }
1107 if is_neg_infinity(inner_lim, pool) {
1108 return Ok(Some(pool.integer(0_i32)));
1109 }
1110 let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
1112 Ok(Some(result))
1113}
1114
1115fn structurally_positive(e: ExprId, pool: &ExprPool) -> bool {
1119 match pool.get(e) {
1120 ExprData::Integer(n) => n.0 > 0,
1121 ExprData::Rational(r) => r.0 > 0,
1122 ExprData::Func { name, .. } if name == "exp" || name == "cosh" => true,
1123 ExprData::Pow { base, exp } => {
1124 if let ExprData::Integer(n) = pool.get(exp) {
1125 if n.0.clone() % 2 == 0 {
1126 return true;
1127 }
1128 }
1129 structurally_positive(base, pool)
1130 }
1131 ExprData::Mul(xs) => xs.iter().all(|x| structurally_positive(*x, pool)),
1132 _ => false,
1133 }
1134}
1135
1136fn try_special_function_limits(
1137 expr: ExprId,
1138 var: ExprId,
1139 point: ExprId,
1140 direction: LimitDirection,
1141 pool: &ExprPool,
1142) -> Result<Option<ExprId>, LimitError> {
1143 let ExprData::Func { name, args } = pool.get(expr) else {
1144 return Ok(None);
1145 };
1146 if args.len() != 1 || args[0] != var {
1147 return Ok(None);
1148 }
1149 match name.as_str() {
1150 "exp" => {
1151 if is_pos_infinity(point, pool) {
1152 return Ok(Some(pool.pos_infinity()));
1153 }
1154 if is_neg_infinity(point, pool) {
1155 return Ok(Some(pool.integer(0_i32)));
1156 }
1157 if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
1158 return Ok(Some(pool.integer(1_i32)));
1159 }
1160 }
1161 "log" => {
1162 if is_pos_infinity(point, pool) {
1163 return Ok(Some(pool.pos_infinity()));
1164 }
1165 if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
1166 if direction == LimitDirection::Plus {
1167 return Ok(Some(neg_infinity(pool)));
1168 }
1169 return Err(LimitError::NeedsOneSided);
1170 }
1171 }
1172 _ => {}
1173 }
1174 Ok(None)
1175}
1176
1177fn neg_infinity(pool: &ExprPool) -> ExprId {
1178 pool.mul(vec![pool.integer(-1_i32), pool.pos_infinity()])
1179}
1180
1181fn is_pos_infinity(e: ExprId, pool: &ExprPool) -> bool {
1182 matches!(
1183 pool.get(e),
1184 ExprData::Symbol {
1185 name,
1186 domain: crate::kernel::Domain::Positive,
1187 ..
1188 } if name == POS_INFINITY_SYMBOL
1189 ) || matches!(
1190 pool.get(e),
1191 ExprData::Symbol {
1192 name,
1193 domain: crate::kernel::Domain::Real,
1194 ..
1195 } if name == POS_INFINITY_SYMBOL
1196 )
1197}
1198
1199fn is_neg_infinity(e: ExprId, pool: &ExprPool) -> bool {
1200 let ExprData::Mul(args) = pool.get(e) else {
1201 return false;
1202 };
1203 if args.len() != 2 {
1204 return false;
1205 }
1206 let (a, b) = (args[0], args[1]);
1207 let m_one = pool.integer(-1_i32);
1208 (a == m_one && is_pos_infinity(b, pool)) || (b == m_one && is_pos_infinity(a, pool))
1209}
1210
1211fn depends_on(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
1212 if expr == var {
1213 return true;
1214 }
1215 match pool.get(expr) {
1216 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|a| depends_on(*a, var, pool)),
1217 ExprData::Pow { base, exp } => depends_on(base, var, pool) || depends_on(exp, var, pool),
1218 ExprData::Func { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
1219 ExprData::Piecewise { branches, default } => {
1220 branches
1221 .iter()
1222 .any(|(c, v)| depends_on(*c, var, pool) || depends_on(*v, var, pool))
1223 || depends_on(default, var, pool)
1224 }
1225 ExprData::Predicate { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
1226 ExprData::Forall { var: bv, body } | ExprData::Exists { var: bv, body } => {
1227 bv != var && depends_on(body, var, pool)
1228 }
1229 ExprData::RootSum {
1230 poly,
1231 var: bv,
1232 body,
1233 } => depends_on(poly, var, pool) || (bv != var && depends_on(body, var, pool)),
1234 ExprData::BigO(a) => depends_on(a, var, pool),
1235 ExprData::Integer(_)
1236 | ExprData::Rational(_)
1237 | ExprData::Float(_)
1238 | ExprData::Symbol { .. } => false,
1239 }
1240}
1241
1242fn try_direct_substitution(
1243 expr: ExprId,
1244 var: ExprId,
1245 point: ExprId,
1246 pool: &ExprPool,
1247) -> Option<ExprId> {
1248 if quotient_is_zero_over_zero(expr, var, point, pool) {
1249 return None;
1250 }
1251 let mut m = HashMap::new();
1252 m.insert(var, point);
1253 let raw = subs(expr, &m, pool);
1254 if is_zero_times_pole_indeterminate(raw, pool) {
1255 return None;
1256 }
1257 let sub = fold_known_reals(simplify(raw, pool).value, pool);
1258 let dep = depends_on(sub, var, pool);
1259 let sing = substitution_is_singular(sub, pool);
1260 if dep || sing {
1261 None
1262 } else {
1263 Some(sub)
1264 }
1265}
1266
1267fn quotient_is_zero_over_zero(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> bool {
1269 let (n, d) = numerator_denominator(expr, pool);
1270 if d == pool.integer(1_i32) {
1271 return false;
1272 }
1273 let n0 = substitute_fully(n, var, point, pool);
1274 let d0 = substitute_fully(d, var, point, pool);
1275 is_zero_like(n0, pool) && is_zero_like(d0, pool)
1276}
1277
1278fn is_zero_times_pole_indeterminate(expr: ExprId, pool: &ExprPool) -> bool {
1280 let factors: Vec<ExprId> = if matches!(pool.get(expr), ExprData::Mul(_)) {
1281 flatten_mul(expr, pool)
1282 } else {
1283 vec![expr]
1284 };
1285 let mut any_zero_factor = false;
1286 let mut any_pole = false;
1287 for f in factors {
1288 if substitution_is_singular(f, pool) {
1289 any_pole = true;
1290 }
1291 if matches!(pool.get(f), ExprData::Integer(z) if z.0 == 0) {
1292 any_zero_factor = true;
1293 }
1294 if let ExprData::Func { name, args } = pool.get(f) {
1295 if args.len() == 1
1296 && matches!(name.as_str(), "sin" | "sinh" | "tan")
1297 && matches!(pool.get(args[0]), ExprData::Integer(z) if z.0 == 0)
1298 {
1299 any_zero_factor = true;
1300 }
1301 }
1302 }
1303 any_zero_factor && any_pole
1304}
1305
1306fn substitution_is_singular(expr: ExprId, pool: &ExprPool) -> bool {
1308 match pool.get(expr) {
1309 ExprData::Pow { base, exp } => {
1310 if let ExprData::Integer(nn) = pool.get(exp) {
1311 if nn.0 < 0 {
1312 let b = simplify(base, pool).value;
1313 if matches!(pool.get(b), ExprData::Integer(z) if z.0 == 0) {
1314 return true;
1315 }
1316 }
1317 }
1318 substitution_is_singular(base, pool) || substitution_is_singular(exp, pool)
1319 }
1320 ExprData::Add(xs) | ExprData::Mul(xs) => {
1321 xs.iter().any(|a| substitution_is_singular(*a, pool))
1322 }
1323 ExprData::Func { args, .. } => args.iter().any(|a| substitution_is_singular(*a, pool)),
1324 _ => false,
1325 }
1326}
1327
1328fn try_lhopital(
1329 expr: ExprId,
1330 var: ExprId,
1331 point: ExprId,
1332 direction: LimitDirection,
1333 pool: &ExprPool,
1334 depth: u32,
1335) -> Result<Option<ExprId>, LimitError> {
1336 let (nume, deno) = numerator_denominator(expr, pool);
1337 if simplify(nume, pool).value == simplify(deno, pool).value {
1338 return Ok(None);
1339 }
1340 let n0 = substitute_fully(nume, var, point, pool);
1341 let d0 = substitute_fully(deno, var, point, pool);
1342
1343 if !is_zero_like(n0, pool) || !is_zero_like(d0, pool) {
1344 return Ok(None);
1345 }
1346
1347 let dn = diff(nume, var, pool)?.value;
1348 let dd = diff(deno, var, pool)?.value;
1349 if dn == nume && dd == deno {
1350 return Ok(None);
1351 }
1352 let quot = rational_quotient(dn, dd, pool);
1353 Ok(Some(limit_inner(
1354 quot,
1355 var,
1356 point,
1357 direction,
1358 pool,
1359 depth + 1,
1360 )?))
1361}
1362
1363fn substitute_fully(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> ExprId {
1364 let mut m = HashMap::new();
1365 m.insert(var, point);
1366 let s = simplify(subs(expr, &m, pool), pool).value;
1367 fold_known_reals(s, pool)
1368}
1369
1370fn rational_quotient(n: ExprId, d: ExprId, pool: &ExprPool) -> ExprId {
1371 simplify(pool.mul(vec![n, pool.pow(d, pool.integer(-1_i32))]), pool).value
1372}
1373
1374fn is_zero_like(e: ExprId, pool: &ExprPool) -> bool {
1375 let e = simplify(e, pool).value;
1376 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
1377 return true;
1378 }
1379 if let ExprData::Rational(r) = pool.get(e) {
1380 if r.0 == 0 {
1381 return true;
1382 }
1383 }
1384 if let ExprData::Func { name, args } = pool.get(e) {
1385 if args.len() == 1 && matches!(name.as_str(), "sin" | "tan" | "sinh") {
1386 return is_zero_like(args[0], pool);
1387 }
1388 }
1389 false
1390}
1391
1392fn is_one_like(e: ExprId, pool: &ExprPool) -> bool {
1393 let e = simplify(e, pool).value;
1394 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 1) {
1395 return true;
1396 }
1397 if let ExprData::Rational(r) = pool.get(e) {
1398 return r.0 == 1;
1399 }
1400 false
1401}
1402
1403fn fold_known_reals(expr: ExprId, pool: &ExprPool) -> ExprId {
1405 let e = simplify(expr, pool).value;
1406 match pool.get(e) {
1407 ExprData::Add(xs) => {
1408 let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
1409 simplify(pool.add(ys), pool).value
1410 }
1411 ExprData::Mul(xs) => {
1412 let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
1413 simplify(pool.mul(ys), pool).value
1414 }
1415 ExprData::Pow { base, exp } => {
1416 let b = fold_known_reals(base, pool);
1417 let xp = fold_known_reals(exp, pool);
1418 if is_one_like(b, pool) {
1422 if substitution_is_singular(xp, pool)
1423 || is_pos_infinity(xp, pool)
1424 || is_neg_infinity(xp, pool)
1425 {
1426 return simplify(pool.pow(b, xp), pool).value;
1427 }
1428 return pool.integer(1_i32);
1429 }
1430 simplify(pool.pow(b, xp), pool).value
1431 }
1432 ExprData::Func { name, args } if args.len() == 1 => {
1433 let inner = fold_known_reals(args[0], pool);
1434 if is_zero_like(inner, pool) {
1435 match name.as_str() {
1436 "sin" | "tan" | "sinh" => return pool.integer(0_i32),
1437 "cos" | "cosh" => return pool.integer(1_i32),
1438 "exp" => return pool.integer(1_i32),
1439 _ => {}
1440 }
1441 }
1442 simplify(pool.func(name, vec![inner]), pool).value
1443 }
1444 ExprData::Func { name, args } => {
1445 let ys: Vec<ExprId> = args.iter().map(|x| fold_known_reals(*x, pool)).collect();
1446 simplify(pool.func(name, ys), pool).value
1447 }
1448 _ => e,
1449 }
1450}
1451
1452fn flatten_mul(expr: ExprId, pool: &ExprPool) -> Vec<ExprId> {
1453 match pool.get(expr) {
1454 ExprData::Mul(xs) => xs.iter().flat_map(|a| flatten_mul(*a, pool)).collect(),
1455 _ => vec![expr],
1456 }
1457}
1458
1459fn numerator_denominator(expr: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
1460 let fac = flatten_mul(expr, pool);
1461 let mut nums = Vec::new();
1462 let mut dens = Vec::new();
1463 for f in fac {
1464 match pool.get(f) {
1465 ExprData::Pow { base, exp } => {
1466 if let ExprData::Integer(n) = pool.get(exp) {
1467 let nn = &n.0;
1468 if *nn == 0 {
1469 nums.push(pool.integer(1_i32));
1470 } else if *nn > 0 {
1471 nums.push(f);
1472 } else {
1473 let m = nn
1474 .clone()
1475 .abs()
1476 .to_u64()
1477 .and_then(|u| u32::try_from(u).ok())
1478 .map(|mag| pool.pow(base, pool.integer(mag as i64)));
1479 if let Some(p) = m {
1480 dens.push(p);
1481 } else {
1482 nums.push(f);
1483 }
1484 }
1485 } else {
1486 nums.push(f);
1487 }
1488 }
1489 _ => nums.push(f),
1490 }
1491 }
1492 let n = if nums.is_empty() {
1493 pool.integer(1_i32)
1494 } else if nums.len() == 1 {
1495 nums[0]
1496 } else {
1497 pool.mul(nums)
1498 };
1499 let d = if dens.is_empty() {
1500 pool.integer(1_i32)
1501 } else if dens.len() == 1 {
1502 dens[0]
1503 } else {
1504 pool.mul(dens)
1505 };
1506 (n, d)
1507}
1508
1509fn try_expansion_limit(
1510 expr: ExprId,
1511 var: ExprId,
1512 point: ExprId,
1513 direction: LimitDirection,
1514 pool: &ExprPool,
1515 order: u32,
1516) -> Result<Option<ExprId>, LimitError> {
1517 let exp = match local_expansion(expr, var, point, order, pool) {
1518 Ok(e) => e,
1519 Err(_) => {
1520 checkpoint(pool)?;
1521 return Ok(None);
1522 }
1523 };
1524 let r = expansion_to_limit(exp, pool, direction)?;
1525 if r.is_none() {
1526 checkpoint(pool)?;
1530 }
1531 Ok(r)
1532}
1533
1534fn expansion_to_limit(
1535 exp: LocalExpansion,
1536 pool: &ExprPool,
1537 direction: LimitDirection,
1538) -> Result<Option<ExprId>, LimitError> {
1539 let LocalExpansion {
1540 valuation,
1541 coeffs,
1542 h_expr: _,
1543 } = exp;
1544
1545 let mut idx = 0usize;
1546 while idx < coeffs.len() && is_zero_like(coeffs[idx], pool) {
1547 idx += 1;
1548 }
1549 if idx >= coeffs.len() {
1550 return Ok(None);
1552 }
1553 let power = valuation + idx as i32;
1554 let coeff = coeffs[idx];
1555
1556 if power > 0 {
1557 return Ok(Some(pool.integer(0_i32)));
1558 }
1559 if power == 0 {
1560 return Ok(Some(coeff));
1561 }
1562
1563 let pole_order = (-power) as u32;
1565 let sgn_c = structural_sign(coeff, pool).unwrap_or(1);
1566 if pole_order % 2 == 0 {
1567 return Ok(Some(signed_infinity(pool, sgn_c)));
1568 }
1569 let Some(hdir) = sign_from_h(direction, power) else {
1570 return Err(LimitError::NeedsOneSided);
1571 };
1572 Ok(Some(signed_infinity(pool, sgn_c * hdir)))
1573}
1574
1575fn sign_from_h(direction: LimitDirection, power: i32) -> Option<i8> {
1577 if power >= 0 {
1578 return Some(1);
1579 }
1580 let odd = (-power) % 2 != 0;
1581 if !odd {
1582 return Some(1);
1583 }
1584 match direction {
1585 LimitDirection::Plus => Some(1),
1586 LimitDirection::Minus => Some(-1),
1587 LimitDirection::Bidirectional => None,
1588 }
1589}
1590
1591fn signed_infinity(pool: &ExprPool, sign: i8) -> ExprId {
1592 if sign < 0 {
1593 neg_infinity(pool)
1594 } else {
1595 pool.pos_infinity()
1596 }
1597}
1598
1599fn structural_sign(e: ExprId, pool: &ExprPool) -> Option<i8> {
1600 match pool.get(e) {
1601 ExprData::Integer(n) => {
1602 if n.0 > 0 {
1603 Some(1)
1604 } else if n.0 < 0 {
1605 Some(-1)
1606 } else {
1607 None
1608 }
1609 }
1610 ExprData::Rational(r) => {
1611 if r.0 == 0 {
1612 None
1613 } else if r.0 > 0 {
1614 Some(1)
1615 } else {
1616 Some(-1)
1617 }
1618 }
1619 ExprData::Mul(xs) => {
1620 let mut s = 1i8;
1621 for a in xs {
1622 let sa = structural_sign(a, pool)?;
1623 s *= sa;
1624 }
1625 Some(s)
1626 }
1627 ExprData::Pow { base: _, exp } if matches!(pool.get(exp), ExprData::Integer(n) if n.0.clone() % 2 == 0) => {
1628 Some(1)
1629 }
1630 _ => None,
1631 }
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636 use super::*;
1637 use crate::kernel::Domain;
1638
1639 #[test]
1640 fn limit_sin_over_x_zero() {
1641 let p = ExprPool::new();
1642 let x = p.symbol("x", Domain::Real);
1643 let ex = simplify(
1644 p.mul(vec![p.func("sin", vec![x]), p.pow(x, p.integer(-1_i32))]),
1645 &p,
1646 )
1647 .value;
1648 let r = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap();
1649 assert_eq!(r, p.integer(1_i32));
1650 }
1651
1652 #[test]
1653 fn one_sided_limit_off_the_domain_is_refused() {
1654 let p = ExprPool::new();
1659 let x = p.symbol("x", Domain::Real);
1660 let ex = simplify(p.func("sqrt", vec![x]), &p).value;
1661 assert!(
1662 limit(ex, x, p.integer(0_i32), LimitDirection::Minus, &p).is_err(),
1663 "√x has no left-hand limit at 0 over ℝ"
1664 );
1665 let r = limit(ex, x, p.integer(0_i32), LimitDirection::Plus, &p).unwrap();
1667 assert_eq!(constant_f64(r, &p), Some(0.0), "got {}", p.display(r));
1668
1669 let ac = simplify(p.func("acos", vec![x]), &p).value;
1671 assert!(
1672 limit(ac, x, p.integer(1_i32), LimitDirection::Plus, &p).is_err(),
1673 "arccos has no right-hand limit at 1 over ℝ"
1674 );
1675
1676 let inv = simplify(p.pow(x, p.integer(-1_i32)), &p).value;
1679 assert!(
1680 limit(inv, x, p.integer(0_i32), LimitDirection::Minus, &p).is_ok(),
1681 "lim_{{x→0⁻}} 1/x = −∞ must survive"
1682 );
1683 let sq = simplify(p.func("sqrt", vec![p.pow(x, p.integer(2_i32))]), &p).value;
1685 let r = limit(sq, x, p.integer(0_i32), LimitDirection::Minus, &p).unwrap();
1686 assert_eq!(constant_f64(r, &p), Some(0.0), "got {}", p.display(r));
1687 }
1688
1689 #[test]
1690 fn limit_x_log_x_zero_plus() {
1691 let p = ExprPool::new();
1692 let x = p.symbol("x", Domain::Real);
1693 let ex = simplify(p.mul(vec![x, p.func("log", vec![x])]), &p).value;
1694 let r = limit(ex, x, p.integer(0_i32), LimitDirection::Plus, &p).unwrap();
1695 assert_eq!(r, p.integer(0_i32));
1696 }
1697
1698 #[test]
1699 fn limit_exp_inf() {
1700 let p = ExprPool::new();
1701 let x = p.symbol("x", Domain::Real);
1702 let ex = p.func("exp", vec![x]);
1703 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1704 assert_eq!(r, p.pos_infinity());
1705 }
1706
1707 #[test]
1708 fn limit_x_squared_at_positive_infinity() {
1709 let p = ExprPool::new();
1710 let x = p.symbol("x", Domain::Real);
1711 let ex = simplify(p.pow(x, p.integer(2_i32)), &p).value;
1712 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1713 assert_eq!(r, p.pos_infinity(), "{}", p.display(r));
1714 }
1715
1716 #[test]
1718 fn limit_compound_interest_is_e() {
1719 let p = ExprPool::new();
1720 let x = p.symbol("x", Domain::Real);
1721 let base = p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]);
1723 let ex = simplify(p.pow(base, x), &p).value;
1724 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1725 let expected = simplify(p.func("exp", vec![p.integer(1)]), &p).value;
1726 assert_eq!(r, expected, "got {}", p.display(r));
1727 }
1728
1729 #[test]
1731 fn limit_one_plus_a_over_x_pow_x_is_exp_a() {
1732 let p = ExprPool::new();
1733 let x = p.symbol("x", Domain::Real);
1734 let two_over_x = p.mul(vec![p.integer(2), p.pow(x, p.integer(-1))]);
1736 let base = p.add(vec![p.integer(1), two_over_x]);
1737 let ex = simplify(p.pow(base, x), &p).value;
1738 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1739 let expected = simplify(p.func("exp", vec![p.integer(2)]), &p).value;
1740 assert_eq!(r, expected, "got {}", p.display(r));
1741 }
1742
1743 #[test]
1748 fn limit_two_pow_x_not_rewritten_to_finite() {
1749 let p = ExprPool::new();
1750 let x = p.symbol("x", Domain::Real);
1751 let ex = simplify(p.pow(p.integer(2), x), &p).value;
1752 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p);
1753 if let Ok(v) = r {
1755 assert_eq!(
1756 v,
1757 p.pos_infinity(),
1758 "2^x must not be a finite value: {}",
1759 p.display(v)
1760 );
1761 }
1762 }
1763
1764 #[test]
1766 fn limit_one_plus_x_to_one_over_x_is_e() {
1767 let p = ExprPool::new();
1768 let x = p.symbol("x", Domain::Real);
1769 let base = p.add(vec![p.integer(1), x]);
1770 let ex = simplify(p.pow(base, p.pow(x, p.integer(-1))), &p).value;
1771 let r = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap();
1772 let expected = simplify(p.func("exp", vec![p.integer(1)]), &p).value;
1773 assert_eq!(r, expected, "got {}", p.display(r));
1774 }
1775
1776 #[test]
1779 fn limit_one_plus_one_over_x_is_one() {
1780 let p = ExprPool::new();
1781 let x = p.symbol("x", Domain::Real);
1782 let ex = simplify(p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]), &p).value;
1783 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1784 assert_eq!(r, p.integer(1), "got {}", p.display(r));
1785 }
1786
1787 #[test]
1788 fn rational_x_over_x_plus_one_after_inf_subst() {
1789 let p = ExprPool::new();
1790 let t = p.symbol("__lt_inf", Domain::Real);
1791 let inv = p.pow(t, p.integer(-1));
1792 let ex = p.mul(vec![
1793 inv,
1794 p.pow(p.add(vec![p.integer(1), inv]), p.integer(-1)),
1795 ]);
1796 let folded = flatten_nested_integer_pow(ex, &p);
1797 let canon = canonical_polynomial_quotient_in_var(folded, t, &p).unwrap();
1798 let r = simplify(canon, &p).value;
1799 let mut m = HashMap::new();
1800 m.insert(t, p.integer(0));
1801 let sub = fold_known_reals(simplify(subs(r, &m, &p), &p).value, &p);
1802 assert_eq!(sub, p.integer(1), "canonical={}", p.display(canon));
1803 }
1804}
1805
1806#[cfg(test)]
1809mod termination_tests {
1810 use super::*;
1811 use crate::budget::{self, Budget, BudgetError};
1812 use crate::errors::AlkahestError;
1813 use crate::kernel::Domain;
1814
1815 #[test]
1822 fn sqrt_x_squared_plus_x_minus_x_at_infinity_is_one_half() {
1823 let p = ExprPool::new();
1824 let x = p.symbol("x", Domain::Real);
1825 let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1826 let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
1827 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1828 assert_eq!(r, p.rational(1, 2), "got {}", p.display(r));
1829 }
1830
1831 #[test]
1833 fn algebraic_cancellations_at_infinity() {
1834 let p = ExprPool::new();
1835 let x = p.symbol("x", Domain::Real);
1836 let neg_inf = p.mul(vec![p.integer(-1), p.pos_infinity()]);
1837
1838 let root = p.func(
1840 "sqrt",
1841 vec![p.add(vec![p.pow(x, p.integer(2)), p.mul(vec![p.integer(3), x])])],
1842 );
1843 let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
1844 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1845 assert_eq!(r, p.rational(3, 2), "√(x²+3x)−x: {}", p.display(r));
1846
1847 let root = p.func(
1849 "sqrt",
1850 vec![p.add(vec![p.pow(x, p.integer(2)), p.integer(1)])],
1851 );
1852 let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
1853 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1854 assert_eq!(r, p.integer(0), "√(x²+1)−x: {}", p.display(r));
1855
1856 let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1859 let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
1860 let r = limit(ex, x, neg_inf, LimitDirection::Bidirectional, &p).unwrap();
1861 assert_eq!(r, p.pos_infinity(), "√(x²+x)−x at −∞: {}", p.display(r));
1862 }
1863
1864 #[test]
1867 fn unsolvable_radical_limit_refuses_within_the_work_ceiling() {
1868 let p = ExprPool::new();
1869 let x = p.symbol("x", Domain::Real);
1870 let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1873 let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
1874 let err = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap_err();
1883 assert!(
1884 matches!(err, LimitError::DepthExceeded),
1885 "expected a bounded refusal, got {err:?}"
1886 );
1887 assert_eq!(err.code(), "E-LIMIT-004");
1888 assert_eq!(last_budget_trip(), None);
1890 }
1891
1892 #[test]
1899 fn step_budget_stops_a_hard_limit_and_is_attributed() {
1900 let p = ExprPool::new();
1901 let x = p.symbol("x", Domain::Real);
1902 let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1903 let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
1904
1905 let _guard = budget::enter(Budget::new().with_max_steps(3));
1906 let err = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap_err();
1907 assert!(matches!(err, LimitError::DepthExceeded), "{err:?}");
1908 assert!(
1909 matches!(last_budget_trip(), Some(BudgetError::Steps { .. })),
1910 "budget trip not recorded: {:?}",
1911 last_budget_trip()
1912 );
1913 }
1914
1915 #[test]
1918 fn a_solved_limit_leaves_no_budget_trip() {
1919 let p = ExprPool::new();
1920 let x = p.symbol("x", Domain::Real);
1921 {
1922 let _guard = budget::enter(Budget::new().with_max_steps(3));
1923 let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1924 let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
1925 assert!(limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).is_err());
1926 assert!(last_budget_trip().is_some());
1927 }
1928 let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
1929 let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
1930 let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
1931 assert_eq!(r, p.rational(1, 2));
1932 assert_eq!(last_budget_trip(), None, "stale trip left behind");
1933 }
1934
1935 #[test]
1939 fn work_baseline_is_installed_once_and_cleared_on_exit() {
1940 let p = ExprPool::new();
1941 let x = p.symbol("x", Domain::Real);
1942 assert!(WORK_BASELINE.with(|c| c.get()).is_none());
1943 let ex = simplify(p.pow(x, p.integer(2)), &p).value;
1944 let _ = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p);
1945 assert!(
1946 WORK_BASELINE.with(|c| c.get()).is_none(),
1947 "baseline leaked past the outermost call"
1948 );
1949 }
1950}
1951
1952#[cfg(test)]
1953mod numeric_refutation_tests {
1954 use super::*;
1955 use crate::kernel::Domain;
1956
1957 #[test]
1964 fn sign_function_limit_is_refused_in_every_direction() {
1965 for direction in [
1966 LimitDirection::Bidirectional,
1967 LimitDirection::Plus,
1968 LimitDirection::Minus,
1969 ] {
1970 let p = ExprPool::new();
1971 let x = p.symbol("x", Domain::Real);
1972 let ex = p.mul(vec![x, p.pow(p.func("abs", vec![x]), p.integer(-1_i32))]);
1973 let got = limit(ex, x, p.integer(0_i32), direction, &p);
1974 assert!(
1975 got.is_err(),
1976 "x/|x| at 0 ({direction:?}) should refuse, got {}",
1977 p.display(got.unwrap())
1978 );
1979 }
1980 }
1981
1982 #[test]
1985 fn ordinary_limits_survive_the_guard() {
1986 let p = ExprPool::new();
1987 let x = p.symbol("x", Domain::Real);
1988 let one = p.integer(1_i32);
1989
1990 let sinc = simplify(
1991 p.mul(vec![p.func("sin", vec![x]), p.pow(x, p.integer(-1_i32))]),
1992 &p,
1993 )
1994 .value;
1995 assert_eq!(
1996 limit(sinc, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap(),
1997 one
1998 );
1999
2000 let half = p.mul(vec![
2003 p.add(vec![
2004 one,
2005 p.mul(vec![p.integer(-1_i32), p.func("cos", vec![x])]),
2006 ]),
2007 p.pow(x, p.integer(-2_i32)),
2008 ]);
2009 let got = limit(
2010 simplify(half, &p).value,
2011 x,
2012 p.integer(0_i32),
2013 LimitDirection::Bidirectional,
2014 &p,
2015 )
2016 .unwrap();
2017 assert_eq!(got, p.rational(1, 2));
2018 }
2019
2020 #[test]
2026 fn oscillation_does_not_trigger_a_false_refusal() {
2027 let p = ExprPool::new();
2028 let x = p.symbol("x", Domain::Real);
2029 let ex = p.mul(vec![x, p.func("sin", vec![p.pow(x, p.integer(-1_i32))])]);
2030 let got = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p);
2031 assert_eq!(got.unwrap(), p.integer(0_i32));
2032 }
2033
2034 #[test]
2036 fn symbolic_parameter_abstains() {
2037 let p = ExprPool::new();
2038 let x = p.symbol("x", Domain::Real);
2039 let a = p.symbol("a", Domain::Real);
2040 assert!(has_free_symbol_besides(p.mul(vec![a, x]), x, &p));
2041 assert!(!has_free_symbol_besides(
2042 p.mul(vec![x, p.func("sin", vec![x])]),
2043 x,
2044 &p
2045 ));
2046 }
2047}