use crate::budget::BudgetError;
use crate::calculus::asymptotic::regularize_at_zero;
use crate::calculus::gruntz::try_gruntz;
use crate::calculus::series::{enter_coeff_ceiling, local_expansion, LocalExpansion};
use crate::diff::{diff, DiffError};
use crate::kernel::pool::POS_INFINITY_SYMBOL;
use crate::kernel::{subs, ExprData, ExprId, ExprPool};
use crate::poly::{poly_normal, RationalFunction};
use crate::simplify::{
ambient_assumptions_active, ambient_equalities, assumed_sign, simplify, simplify_expanded, Sign,
};
use crate::SeriesError;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LimitDirection {
Bidirectional,
Plus,
Minus,
}
#[derive(Debug)]
pub enum LimitError {
Series(SeriesError),
Diff(DiffError),
NeedsOneSided,
DepthExceeded,
Unsupported,
}
impl fmt::Display for LimitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LimitError::Series(e) => write!(f, "{e}"),
LimitError::Diff(e) => write!(f, "{e}"),
LimitError::NeedsOneSided => {
write!(
f,
"two-sided limit undefined at this pole; pass direction Plus or Minus"
)
}
LimitError::DepthExceeded => write!(f, "limit refinement depth exceeded"),
LimitError::Unsupported => write!(f, "limit could not be computed with current rules"),
}
}
}
impl std::error::Error for LimitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
LimitError::Series(e) => Some(e),
LimitError::Diff(e) => Some(e),
_ => None,
}
}
}
impl crate::errors::AlkahestError for LimitError {
fn code(&self) -> &'static str {
match self {
LimitError::Series(_) => "E-LIMIT-001",
LimitError::Diff(_) => "E-LIMIT-002",
LimitError::NeedsOneSided => "E-LIMIT-003",
LimitError::DepthExceeded => "E-LIMIT-004",
LimitError::Unsupported => "E-LIMIT-005",
}
}
fn remediation(&self) -> Option<&'static str> {
Some(match self {
LimitError::Series(_) => {
"increase truncation order indirectly by simplifying the expression, or rewrite using standard limits"
}
LimitError::Diff(_) => {
"ensure primitives have differentiation rules, or simplify before taking the limit"
}
LimitError::NeedsOneSided => "use LimitDirection::Plus or Minus matching the desired one-sided approach",
LimitError::DepthExceeded => {
"try manual algebra (quotient form, cancellations) or split into simpler sub-expressions"
}
LimitError::Unsupported => {
"limit could not be computed — try manual algebra, or the expression may involve oscillation or non-comparable growth not yet handled"
}
})
}
}
impl From<SeriesError> for LimitError {
fn from(e: SeriesError) -> Self {
LimitError::Series(e)
}
}
impl From<DiffError> for LimitError {
fn from(e: DiffError) -> Self {
LimitError::Diff(e)
}
}
const MAX_LIMIT_POOL_GROWTH: usize = 100_000;
thread_local! {
static WORK_BASELINE: Cell<Option<usize>> = const { Cell::new(None) };
static BUDGET_TRIP: Cell<Option<BudgetError>> = const { Cell::new(None) };
}
struct WorkFrame {
outermost: bool,
}
impl Drop for WorkFrame {
fn drop(&mut self) {
if self.outermost {
WORK_BASELINE.with(|c| c.set(None));
}
}
}
fn enter_work_frame(pool: &ExprPool) -> WorkFrame {
WORK_BASELINE.with(|c| {
if c.get().is_some() {
return WorkFrame { outermost: false };
}
c.set(Some(pool.len()));
WorkFrame { outermost: true }
})
}
fn work_exhausted(pool: &ExprPool) -> bool {
WORK_BASELINE.with(|c| match c.get() {
Some(base) => pool.len().saturating_sub(base) > MAX_LIMIT_POOL_GROWTH,
None => false,
})
}
fn coeff_ceiling(pool: &ExprPool) -> usize {
WORK_BASELINE.with(|c| {
c.get()
.unwrap_or_else(|| pool.len())
.saturating_add(MAX_LIMIT_POOL_GROWTH)
})
}
pub(crate) fn checkpoint(pool: &ExprPool) -> Result<(), LimitError> {
if let Err(e) = crate::budget::check() {
BUDGET_TRIP.with(|c| c.set(Some(e)));
return Err(LimitError::DepthExceeded);
}
if work_exhausted(pool) {
return Err(LimitError::DepthExceeded);
}
Ok(())
}
pub fn last_budget_trip() -> Option<BudgetError> {
BUDGET_TRIP.with(|c| c.get())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingAssumption {
pub parameter: String,
pub quantity: String,
}
impl fmt::Display for MissingAssumption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"the sign of `{}` is unknown, and the limit depends on it (the sign of `{}` decides \
it); assume `{} > 0` or `{} < 0`",
self.parameter, self.quantity, self.parameter, self.parameter
)
}
}
impl std::error::Error for MissingAssumption {}
impl crate::errors::AlkahestError for MissingAssumption {
fn code(&self) -> &'static str {
"E-LIMIT-006"
}
fn remediation(&self) -> Option<&'static str> {
Some(
"state the sign of the parameter — `Assumptions.refine(pool.gt(k, pool.integer(0)))`, \
or declare it with `Domain.Positive` — and retry inside \
`alkahest.context(assumptions=…)`",
)
}
}
thread_local! {
static MISSING_ASSUMPTION: RefCell<Option<MissingAssumption>> =
const { RefCell::new(None) };
}
pub fn last_missing_assumption() -> Option<MissingAssumption> {
MISSING_ASSUMPTION.with(|c| c.borrow().clone())
}
pub(crate) fn note_missing_assumption(quantity: ExprId, var: ExprId, pool: &ExprPool) {
MISSING_ASSUMPTION.with(|c| {
if c.borrow().is_some() {
return;
}
let Some(parameter) = first_free_parameter(quantity, var, pool) else {
return;
};
*c.borrow_mut() = Some(MissingAssumption {
parameter,
quantity: format!("{}", pool.display(quantity)),
});
});
}
fn first_free_parameter(expr: ExprId, var: ExprId, pool: &ExprPool) -> Option<String> {
if expr == var {
return None;
}
match pool.get(expr) {
ExprData::Symbol { name, .. } => {
if name == POS_INFINITY_SYMBOL {
None
} else {
Some(name.to_string())
}
}
ExprData::Add(xs) | ExprData::Mul(xs) | ExprData::Func { args: xs, .. } => {
xs.iter().find_map(|&x| first_free_parameter(x, var, pool))
}
ExprData::Pow { base, exp } => {
first_free_parameter(base, var, pool).or_else(|| first_free_parameter(exp, var, pool))
}
_ => None,
}
}
pub(crate) fn coefficient_sign(coeff: ExprId, pool: &ExprPool) -> Option<Sign> {
if let Some(s) = factor_sign(coeff, pool) {
return Some(s);
}
if let ExprData::Pow { exp, .. } = pool.get(coeff) {
if matches!(pool.get(exp), ExprData::Integer(n) if n.0.is_even()) {
return Some(Sign::Positive);
}
}
None
}
fn factor_sign(value: ExprId, pool: &ExprPool) -> Option<Sign> {
if let Some(s) = assumed_sign(value, pool) {
return Some(s);
}
let numeric = constant_f64(value, pool)?;
if numeric > 0.0 {
Some(Sign::Positive)
} else if numeric < 0.0 {
Some(Sign::Negative)
} else {
Some(Sign::Zero)
}
}
pub fn limit(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
) -> Result<ExprId, LimitError> {
let frame = enter_work_frame(pool);
if frame.outermost {
BUDGET_TRIP.with(|c| c.set(None));
MISSING_ASSUMPTION.with(|c| *c.borrow_mut() = None);
}
let _ceiling = enter_coeff_ceiling(coeff_ceiling(pool));
let expr = substitute_assumed_equalities(expr, var, pool);
limit_body(expr, var, point, direction, pool).map_err(|e| attribute_failure(e, pool))
}
fn substitute_assumed_equalities(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
if !ambient_assumptions_active(pool) {
return expr;
}
let map: HashMap<ExprId, ExprId> = ambient_equalities(pool)
.into_iter()
.filter(|&(symbol, _)| symbol != var)
.collect();
if map.is_empty() {
return expr;
}
simplify(subs(expr, &map, pool), pool).value
}
fn attribute_failure(e: LimitError, pool: &ExprPool) -> LimitError {
if BUDGET_TRIP.with(|c| c.get()).is_some() || work_exhausted(pool) {
return LimitError::DepthExceeded;
}
if let Err(b) = crate::budget::check() {
BUDGET_TRIP.with(|c| c.set(Some(b)));
return LimitError::DepthExceeded;
}
e
}
fn limit_body(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
) -> Result<ExprId, LimitError> {
let r = limit_inner(expr, var, point, direction, pool, 0)?;
let r_simp = simplify(r, pool).value;
let r_fold = fold_known_reals(r_simp, pool);
let result = simplify(r_fold, pool).value;
if !is_usable_limit_value(result, pool) {
return Err(LimitError::Unsupported);
}
if approach_side_is_outside_the_domain(expr, var, point, direction, pool) {
return Err(LimitError::Unsupported);
}
if numeric_evidence_contradicts(expr, var, point, direction, result, pool) {
return Err(LimitError::Unsupported);
}
Ok(result)
}
fn approach_side_is_outside_the_domain(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
) -> bool {
let sign = match direction {
LimitDirection::Plus => 1.0,
LimitDirection::Minus => -1.0,
LimitDirection::Bidirectional => return false,
};
if is_polynomial_in(expr, var, pool) || has_free_symbol_besides(expr, var, pool) {
return false;
}
let Some(at) = constant_f64(point, pool) else {
return false;
};
let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
let mut sample = |offset: f64| -> Option<f64> {
env.insert(var, at + offset);
crate::jit::eval_interp(expr, &env, pool)
};
for offset in APPROACH_OFFSETS {
match sample(sign * offset) {
Some(v) if v.is_nan() => {}
_ => return false,
}
}
APPROACH_OFFSETS
.iter()
.any(|&offset| sample(-sign * offset).is_some_and(|v| v.is_finite()))
}
const APPROACH_OFFSETS: [f64; 4] = [1e-1, 1e-2, 1e-3, 1e-4];
struct SideEstimate {
value: f64,
movement: f64,
}
fn side_estimate(
expr: ExprId,
var: ExprId,
at: f64,
sign: f64,
pool: &ExprPool,
) -> Option<SideEstimate> {
let mut samples = Vec::with_capacity(APPROACH_OFFSETS.len());
let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
for offset in APPROACH_OFFSETS {
env.insert(var, at + sign * offset);
match crate::jit::eval_interp(expr, &env, pool) {
Some(v) if v.is_finite() => samples.push(v),
_ => continue,
}
}
if samples.len() < 3 {
return None;
}
let last = samples[samples.len() - 1];
let prev = samples[samples.len() - 2];
let movement = (last - prev).abs();
let scale = 1.0 + last.abs();
if movement > 0.01 * scale {
return None;
}
Some(SideEstimate {
value: last,
movement,
})
}
fn numeric_evidence_contradicts(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
result: ExprId,
pool: &ExprPool,
) -> bool {
if is_polynomial_in(expr, var, pool) {
return false;
}
if has_free_symbol_besides(expr, var, pool) {
return false;
}
let Some(at) = constant_f64(point, pool) else {
return false;
};
let claimed = constant_f64(result, pool);
if !probe_looks_suspicious(expr, var, at, direction, claimed, pool) {
return false;
}
let left = side_estimate(expr, var, at, -1.0, pool);
let right = side_estimate(expr, var, at, 1.0, pool);
if direction == LimitDirection::Bidirectional {
if let (Some(l), Some(r)) = (&left, &right) {
let tol = 1e-6 + 20.0 * (l.movement + r.movement);
if (l.value - r.value).abs() > tol {
return true;
}
}
}
let Some(claimed) = claimed else {
return false;
};
let sides: [&Option<SideEstimate>; 2] = match direction {
LimitDirection::Plus => [&right, &None],
LimitDirection::Minus => [&left, &None],
LimitDirection::Bidirectional => [&left, &right],
};
for side in sides.into_iter().flatten() {
let tol = 1e-6 * (1.0 + claimed.abs()) + 20.0 * side.movement;
if (side.value - claimed).abs() > tol {
return true;
}
}
false
}
fn is_polynomial_in(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if expr == var {
return true;
}
match pool.get(expr) {
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => true,
ExprData::Symbol { .. } => false,
ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().all(|&x| is_polynomial_in(x, var, pool)),
ExprData::Pow { base, exp } => {
matches!(pool.get(exp), ExprData::Integer(n) if n.0 >= 0)
&& is_polynomial_in(base, var, pool)
}
_ => false,
}
}
fn probe_looks_suspicious(
expr: ExprId,
var: ExprId,
at: f64,
direction: LimitDirection,
claimed: Option<f64>,
pool: &ExprPool,
) -> bool {
let far = APPROACH_OFFSETS[1];
let near = APPROACH_OFFSETS[APPROACH_OFFSETS.len() - 1];
let mut env: HashMap<ExprId, f64> = HashMap::with_capacity(1);
let mut sample = |sign: f64, offset: f64| -> Option<f64> {
env.insert(var, at + sign * offset);
crate::jit::eval_interp(expr, &env, pool).filter(|v| v.is_finite())
};
let mut side_is_suspicious = |sign: f64| -> bool {
let (Some(f_far), Some(f_near)) = (sample(sign, far), sample(sign, near)) else {
return false;
};
match claimed {
Some(c) => (f_near - c).abs() > 0.75 * (f_far - c).abs(),
None => false,
}
};
let left_bad = direction != LimitDirection::Plus && side_is_suspicious(-1.0);
let right_bad = direction != LimitDirection::Minus && side_is_suspicious(1.0);
if left_bad || right_bad {
return true;
}
if claimed.is_none() && direction == LimitDirection::Bidirectional {
if let (Some(l), Some(r)) = (sample(-1.0, near), sample(1.0, near)) {
return (l - r).abs() > 1e-6 * (1.0 + l.abs().max(r.abs()));
}
}
false
}
fn constant_f64(expr: ExprId, pool: &ExprPool) -> Option<f64> {
let env = HashMap::new();
crate::jit::eval_interp(expr, &env, pool).filter(|v| v.is_finite())
}
fn has_free_symbol_besides(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if expr == var {
return false;
}
match pool.get(expr) {
ExprData::Symbol { .. } => true,
ExprData::Add(xs) | ExprData::Mul(xs) => {
xs.iter().any(|&x| has_free_symbol_besides(x, var, pool))
}
ExprData::Pow { base, exp } => {
has_free_symbol_besides(base, var, pool) || has_free_symbol_besides(exp, var, pool)
}
ExprData::Func { args, .. } => args.iter().any(|&a| has_free_symbol_besides(a, var, pool)),
_ => false,
}
}
fn contains_zero_to_negative_power(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Pow { base, exp } => {
let zero_base = matches!(pool.get(base), ExprData::Integer(n) if n.0 == 0);
let negative_exp = match pool.get(exp) {
ExprData::Integer(n) => n.0 < 0,
ExprData::Rational(r) => r.0 < 0,
_ => false,
};
(zero_base && negative_exp)
|| contains_zero_to_negative_power(base, pool)
|| contains_zero_to_negative_power(exp, pool)
}
ExprData::Add(xs) | ExprData::Mul(xs) => {
xs.iter().any(|&x| contains_zero_to_negative_power(x, pool))
}
ExprData::Func { args, .. } => args
.iter()
.any(|&a| contains_zero_to_negative_power(a, pool)),
_ => false,
}
}
fn flatten_nested_integer_pow(expr: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Pow { base, exp } => {
let base = flatten_nested_integer_pow(base, pool);
let exp_fl = flatten_nested_integer_pow(exp, pool);
if let (
ExprData::Pow {
base: b2,
exp: inner_exp,
},
ExprData::Integer(outer_e),
) = (pool.get(base), pool.get(exp_fl))
{
if let ExprData::Integer(inner_e) = pool.get(inner_exp) {
let prod = inner_e.0.clone() * outer_e.0.clone();
return pool.pow(flatten_nested_integer_pow(b2, pool), pool.integer(prod));
}
}
pool.pow(base, exp_fl)
}
ExprData::Mul(xs) => pool.mul(
xs.iter()
.map(|x| flatten_nested_integer_pow(*x, pool))
.collect(),
),
ExprData::Add(xs) => pool.add(
xs.iter()
.map(|x| flatten_nested_integer_pow(*x, pool))
.collect(),
),
ExprData::Func { name, args } => {
let na: Vec<ExprId> = args
.iter()
.map(|a| flatten_nested_integer_pow(*a, pool))
.collect();
pool.func(name.clone(), na)
}
_ => expr,
}
}
fn canonical_polynomial_quotient_in_var(
expr: ExprId,
t: ExprId,
pool: &ExprPool,
) -> Result<ExprId, LimitError> {
let (n_raw, d_raw) = numerator_denominator(expr, pool);
let has_trivial_denom = d_raw == pool.integer(1_i32);
for k in 0_i64..=40 {
if has_trivial_denom && k == 0 {
continue;
}
checkpoint(pool)?;
let tk = pool.pow(t, pool.integer(k));
let n = simplify_expanded(pool.mul(vec![tk, n_raw]), pool).value;
let d = simplify_expanded(pool.mul(vec![tk, d_raw]), pool).value;
let (n, d) = match (poly_normal(n, vec![t], pool), poly_normal(d, vec![t], pool)) {
(Ok(nn), Ok(dd)) => (nn, dd),
_ => continue,
};
if let Ok(rf) = RationalFunction::from_symbolic(n, d, vec![t], pool) {
let nx = rf.numer.to_expr(pool);
let dx = rf.denom.to_expr(pool);
return Ok(
simplify(pool.mul(vec![nx, pool.pow(dx, pool.integer(-1_i32))]), pool).value,
);
}
}
Ok(expr)
}
fn limit_inner(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<ExprId, LimitError> {
const MAX_DEPTH: u32 = 48;
const SERIES_ORDER: u32 = 32;
if depth > MAX_DEPTH {
return Err(LimitError::DepthExceeded);
}
checkpoint(pool)?;
if !depends_on(expr, var, pool) {
if substitution_is_singular(expr, pool) {
return Err(LimitError::Unsupported);
}
return Ok(fold_known_reals(simplify(expr, pool).value, pool));
}
if let Some(r) = try_special_function_limits(expr, var, point, direction, pool)? {
return Ok(r);
}
if let Some(r) = try_special_basis_limit(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_special_basis_algebra(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_continuous_composition(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_indeterminate_power(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if is_pos_infinity(point, pool) {
if let Some(r) = try_gruntz(expr, var, pool)? {
return Ok(r);
}
}
if is_pos_infinity(point, pool) || is_neg_infinity(point, pool) {
let toward_pos = is_pos_infinity(point, pool);
if let Some(r) = try_regularized_infinity_limit(expr, var, toward_pos, pool)? {
return Ok(r);
}
}
if is_pos_infinity(point, pool) {
let t = pool.symbol("__lt_inf", crate::kernel::Domain::Real);
let inv_t = pool.pow(t, pool.integer(-1_i32));
let mut m = HashMap::new();
m.insert(var, inv_t);
let after_subs = subs(expr, &m, pool);
let after_flatten = flatten_nested_integer_pow(after_subs, pool);
let after_canon = canonical_polynomial_quotient_in_var(after_flatten, t, pool)?;
let e2 = simplify(after_canon, pool).value;
return limit_inner(
e2,
t,
pool.integer(0_i32),
LimitDirection::Plus,
pool,
depth + 1,
);
}
if is_neg_infinity(point, pool) {
let t = pool.symbol("__lt_ninf", crate::kernel::Domain::Real);
let rep = pool.mul(vec![
pool.integer(-1_i32),
pool.pow(t, pool.integer(-1_i32)),
]);
let mut m = HashMap::new();
m.insert(var, rep);
let canon = canonical_polynomial_quotient_in_var(
flatten_nested_integer_pow(subs(expr, &m, pool), pool),
t,
pool,
)?;
let e2 = simplify(canon, pool).value;
let substituted = limit_inner(
e2,
t,
pool.integer(0_i32),
LimitDirection::Plus,
pool,
depth + 1,
);
let unresolved = match &substituted {
Err(_) => true,
Ok(v) => contains_zero_to_negative_power(*v, pool) || depends_on(*v, t, pool),
};
if unresolved {
if let Ok(v) = reflected_limit(expr, var, direction, pool, depth) {
return Ok(v);
}
}
return substituted;
}
if let Some(r) = try_direct_substitution(expr, var, point, pool) {
return Ok(r);
}
if let Some(r) = try_x_log_x_at_zero(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_lhopital(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_expansion_limit(expr, var, point, direction, pool, SERIES_ORDER)? {
return Ok(r);
}
if let Some(r) = try_puiseux_substitution(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
if let Some(r) = try_constant_factor(expr, var, point, direction, pool, depth)? {
return Ok(r);
}
Err(LimitError::Unsupported)
}
fn try_constant_factor(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let ExprData::Mul(args) = pool.get(expr) else {
return Ok(None);
};
let (constant, varying): (Vec<ExprId>, Vec<ExprId>) =
args.iter().partition(|&&a| !depends_on(a, var, pool));
if constant.is_empty() || varying.is_empty() {
return Ok(None);
}
let regroup = |factors: Vec<ExprId>| match factors.len() {
1 => factors[0],
_ => simplify(pool.mul(factors), pool).value,
};
let coefficient = regroup(constant);
if !is_usable_limit_value(coefficient, pool) {
return Ok(None);
}
let rest = regroup(varying);
let rest_limit = match limit_inner(rest, var, point, direction, pool, depth + 1) {
Ok(l) => l,
Err(LimitError::Unsupported) => return Ok(None),
Err(e) => return Err(e),
};
let infinite = if is_pos_infinity(rest_limit, pool) {
Some(1_i8)
} else if is_neg_infinity(rest_limit, pool) {
Some(-1_i8)
} else {
None
};
let Some(rest_sign) = infinite else {
return Ok(Some(
simplify(pool.mul(vec![coefficient, rest_limit]), pool).value,
));
};
match factor_sign(coefficient, pool) {
Some(Sign::Positive) => Ok(Some(signed_infinity(pool, rest_sign))),
Some(Sign::Negative) => Ok(Some(signed_infinity(pool, -rest_sign))),
Some(Sign::Zero) => Ok(Some(pool.integer(0_i32))),
None => {
note_missing_assumption(coefficient, var, pool);
Ok(None)
}
}
}
fn try_puiseux_substitution(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
if is_pos_infinity(point, pool) || is_neg_infinity(point, pool) {
return Ok(None);
}
let Some(q) = fractional_power_denominator(expr, var, pool) else {
return Ok(None);
};
checkpoint(pool)?;
let one_side = |toward_plus: bool| -> Result<Option<ExprId>, LimitError> {
let h = pool.symbol(
if toward_plus {
"__lt_puiseux_p"
} else {
"__lt_puiseux_m"
},
crate::kernel::Domain::Positive,
);
let h_q = pool.pow(h, pool.integer(i64::from(q)));
let offset = if toward_plus {
h_q
} else {
pool.mul(vec![pool.integer(-1_i32), h_q])
};
let mut m = HashMap::new();
m.insert(var, pool.add(vec![point, offset]));
let substituted = simplify(subs(expr, &m, pool), pool).value;
if fractional_power_denominator(substituted, h, pool).is_some() {
return Ok(None);
}
match limit_inner(
substituted,
h,
pool.integer(0_i32),
LimitDirection::Plus,
pool,
depth + 1,
) {
Ok(r) => Ok(Some(r)),
Err(LimitError::Unsupported) | Err(LimitError::Series(_)) => Ok(None),
Err(e) => Err(e),
}
};
match direction {
LimitDirection::Plus => one_side(true),
LimitDirection::Minus => one_side(false),
LimitDirection::Bidirectional => {
let right = one_side(true)?;
let left = one_side(false)?;
match (left, right) {
(Some(l), Some(r)) if l == r => Ok(Some(l)),
(Some(_), Some(_)) => Err(LimitError::NeedsOneSided),
(None, Some(r))
if approach_side_is_outside_the_domain(
expr,
var,
point,
LimitDirection::Minus,
pool,
) =>
{
Ok(Some(r))
}
(Some(l), None)
if approach_side_is_outside_the_domain(
expr,
var,
point,
LimitDirection::Plus,
pool,
) =>
{
Ok(Some(l))
}
_ => Ok(None),
}
}
}
}
fn fractional_power_denominator(expr: ExprId, var: ExprId, pool: &ExprPool) -> Option<u32> {
fn walk(expr: ExprId, var: ExprId, pool: &ExprPool, acc: &mut u32) -> bool {
if !depends_on(expr, var, pool) {
return true;
}
let absorb = |index: u32, acc: &mut u32| match lcm_small(*acc, index) {
Some(l) => {
*acc = l;
true
}
None => false,
};
match pool.get(expr) {
ExprData::Func { name, args } => {
let index = match name.as_str() {
"sqrt" => Some(2_u32),
"cbrt" => Some(3_u32),
_ => None,
};
if let Some(index) = index {
if !absorb(index, acc) {
return false;
}
}
args.iter().all(|&a| walk(a, var, pool, acc))
}
ExprData::Pow { base, exp } => {
if let ExprData::Rational(r) = pool.get(exp) {
match r.0.denom().to_u32() {
Some(denom) if absorb(denom, acc) => {}
_ => return false,
}
}
walk(base, var, pool, acc) && walk(exp, var, pool, acc)
}
ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().all(|&x| walk(x, var, pool, acc)),
_ => true,
}
}
let mut acc = 1_u32;
if !walk(expr, var, pool, &mut acc) {
return None;
}
(acc > 1).then_some(acc)
}
const MAX_PUISEUX_INDEX: u32 = 12;
fn lcm_small(a: u32, b: u32) -> Option<u32> {
fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
if b == 0 {
return None;
}
let l = a / gcd(a, b) * b;
(l <= MAX_PUISEUX_INDEX).then_some(l)
}
fn contains_radical(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Func { name, args } => {
name == "sqrt" || name == "cbrt" || args.iter().any(|&a| contains_radical(a, pool))
}
ExprData::Pow { base, exp } => {
matches!(pool.get(exp), ExprData::Rational(_))
|| contains_radical(base, pool)
|| contains_radical(exp, pool)
}
ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|&x| contains_radical(x, pool)),
_ => false,
}
}
fn try_regularized_infinity_limit(
expr: ExprId,
var: ExprId,
toward_pos: bool,
pool: &ExprPool,
) -> Result<Option<ExprId>, LimitError> {
const ORDERS: [u32; 3] = [4, 10, 24];
if !contains_radical(expr, pool) {
return Ok(None);
}
let t = pool.symbol("__lt_reg", crate::kernel::Domain::Positive);
let inv_t = pool.pow(t, pool.integer(-1_i32));
let rep = if toward_pos {
inv_t
} else {
pool.mul(vec![pool.integer(-1_i32), inv_t])
};
let mut m = HashMap::new();
m.insert(var, rep);
let f_of_t = simplify(subs(expr, &m, pool), pool).value;
let Some((val, analytic)) = regularize_at_zero(f_of_t, t, pool) else {
return Ok(None);
};
let Ok(val) = i32::try_from(val) else {
return Ok(None);
};
let zero = pool.integer(0_i32);
for order in ORDERS {
checkpoint(pool)?;
let Ok(exp) = local_expansion(analytic, t, zero, order, pool) else {
return Ok(None);
};
let LocalExpansion {
valuation,
coeffs,
h_expr,
} = exp;
let Some(total) = val.checked_add(valuation) else {
return Ok(None);
};
let shifted = LocalExpansion {
valuation: total,
coeffs,
h_expr,
};
match expansion_to_limit(shifted, t, pool, LimitDirection::Plus) {
Ok(Some(r)) => return Ok(Some(r)),
Ok(None) => {}
Err(LimitError::Unsupported) => return Ok(None),
Err(e) => return Err(e),
}
}
Ok(None)
}
fn try_x_log_x_at_zero(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
if direction == LimitDirection::Minus {
return Ok(None);
}
if !matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
return Ok(None);
}
let ExprData::Mul(args) = pool.get(expr) else {
return Ok(None);
};
if args.len() != 2 {
return Ok(None);
}
let (a, b) = (args[0], args[1]);
let log_of_var = |u: ExprId| {
matches!(
pool.get(u),
ExprData::Func { name, args: av } if name == "log" && av.len() == 1 && av[0] == var
)
};
let is_var = |u: ExprId| u == var;
let ok = (is_var(a) && log_of_var(b)) || (is_var(b) && log_of_var(a));
if !ok {
return Ok(None);
}
let f = pool.func("log", vec![var]);
let g = pool.pow(var, pool.integer(-1_i32));
let fp = diff(f, var, pool)?.value;
let gp = diff(g, var, pool)?.value;
let ratio = rational_quotient(fp, gp, pool);
Ok(Some(limit_inner(
ratio,
var,
point,
LimitDirection::Plus,
pool,
depth + 1,
)?))
}
fn try_indeterminate_power(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let ExprData::Pow { base, exp } = pool.get(expr) else {
return Ok(None);
};
if !depends_on(base, var, pool) {
return Ok(None);
}
let base_lim = match limit_inner(base, var, point, direction, pool, depth + 1) {
Ok(b) => b,
Err(_) => return Ok(None),
};
if is_one_like(base_lim, pool) {
let log_base = pool.func("log", vec![base]);
let inner = simplify(pool.mul(vec![exp, log_base]), pool).value;
if let Ok(inner_lim) = limit_inner(inner, var, point, direction, pool, depth + 1) {
if is_pos_infinity(inner_lim, pool) {
return Ok(Some(pool.pos_infinity()));
}
if is_neg_infinity(inner_lim, pool) {
return Ok(Some(pool.integer(0_i32)));
}
let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
return Ok(Some(result));
}
}
let exp_lim = match limit_inner(exp, var, point, direction, pool, depth + 1) {
Ok(e) => e,
Err(_) => return Ok(None),
};
let base_is_one = is_one_like(base_lim, pool);
let base_is_zero = is_zero_like(base_lim, pool);
let base_is_inf = is_pos_infinity(base_lim, pool) || is_neg_infinity(base_lim, pool);
let exp_is_zero = is_zero_like(exp_lim, pool);
let exp_is_inf = is_pos_infinity(exp_lim, pool) || is_neg_infinity(exp_lim, pool);
let indeterminate = (base_is_one && exp_is_inf) || (base_is_inf && exp_is_zero) || (base_is_zero && exp_is_zero); if !indeterminate {
return Ok(None);
}
let base_positive = base_is_one
|| is_pos_infinity(base_lim, pool)
|| (base_is_zero && structurally_positive(base, pool));
if !base_positive {
return Ok(None);
}
let log_base = pool.func("log", vec![base]);
let inner = simplify(pool.mul(vec![exp, log_base]), pool).value;
let inner_lim = match limit_inner(inner, var, point, direction, pool, depth + 1) {
Ok(l) => l,
Err(_) => return Ok(None),
};
if is_pos_infinity(inner_lim, pool) {
return Ok(Some(pool.pos_infinity()));
}
if is_neg_infinity(inner_lim, pool) {
return Ok(Some(pool.integer(0_i32)));
}
let result = simplify(pool.func("exp", vec![inner_lim]), pool).value;
Ok(Some(result))
}
fn structurally_positive(e: ExprId, pool: &ExprPool) -> bool {
match pool.get(e) {
ExprData::Integer(n) => n.0 > 0,
ExprData::Rational(r) => r.0 > 0,
ExprData::Func { name, .. } if name == "exp" || name == "cosh" => true,
ExprData::Pow { base, exp } => {
if let ExprData::Integer(n) = pool.get(exp) {
if n.0.clone() % 2 == 0 {
return true;
}
}
structurally_positive(base, pool)
}
ExprData::Mul(xs) => xs.iter().all(|x| structurally_positive(*x, pool)),
_ => false,
}
}
fn try_special_function_limits(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
) -> Result<Option<ExprId>, LimitError> {
let ExprData::Func { name, args } = pool.get(expr) else {
return Ok(None);
};
if args.len() != 1 || args[0] != var {
return Ok(None);
}
match name.as_str() {
"exp" => {
if is_pos_infinity(point, pool) {
return Ok(Some(pool.pos_infinity()));
}
if is_neg_infinity(point, pool) {
return Ok(Some(pool.integer(0_i32)));
}
if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
return Ok(Some(pool.integer(1_i32)));
}
}
"log" => {
if is_pos_infinity(point, pool) {
return Ok(Some(pool.pos_infinity()));
}
if matches!(pool.get(point), ExprData::Integer(n) if n.0 == 0) {
if direction == LimitDirection::Plus {
return Ok(Some(neg_infinity(pool)));
}
return Err(LimitError::NeedsOneSided);
}
}
_ => {}
}
Ok(None)
}
const CONTINUOUS_ENDPOINTS: [(&str, Option<i32>, Option<i32>); 3] = [
("exp", Some(0), None),
("erf", Some(-1), Some(1)),
("tanh", Some(-1), Some(1)),
];
fn try_continuous_composition(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let ExprData::Func { name, args } = pool.get(expr) else {
return Ok(None);
};
if args.len() != 1 {
return Ok(None);
}
let Some(&(_, at_neg_inf, at_pos_inf)) = CONTINUOUS_ENDPOINTS
.iter()
.find(|(known, _, _)| *known == name)
else {
return Ok(None);
};
let inner = args[0];
if !depends_on(inner, var, pool) {
return Ok(None);
}
let Ok(inner_limit) = limit_inner(inner, var, point, direction, pool, depth + 1) else {
return Ok(None);
};
for (matches_end, value) in [
(is_pos_infinity(inner_limit, pool), at_pos_inf),
(is_neg_infinity(inner_limit, pool), at_neg_inf),
] {
if !matches_end {
continue;
}
return Ok(Some(match value {
Some(v) => pool.integer(v),
None => pool.pos_infinity(),
}));
}
if !is_usable_limit_value(inner_limit, pool) {
return Ok(None);
}
Ok(Some(
simplify(pool.func(name, vec![inner_limit]), pool).value,
))
}
fn try_special_basis_limit(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let ExprData::Func { name, args } = pool.get(expr) else {
return Ok(None);
};
if args.len() != 1 {
return Ok(None);
}
if !matches!(
name.as_str(),
"erf" | "erfc" | "Si" | "Ci" | "Shi" | "Chi" | "Ei" | "li" | "fresnels" | "fresnelc"
) {
return Ok(None);
}
let Some(toward_pos) = argument_runs_to_infinity(args[0], var, point, direction, pool, depth)
else {
return Ok(None);
};
let half_pi = || {
pool.mul(vec![
pool.symbol("pi", crate::kernel::Domain::Real),
pool.rational(1, 2),
])
};
let value = match (name.as_str(), toward_pos) {
("erf", true) => pool.integer(1_i32),
("erf", false) => pool.integer(-1_i32),
("erfc", true) => pool.integer(0_i32),
("erfc", false) => pool.integer(2_i32),
("Si", true) => half_pi(),
("Si", false) => pool.mul(vec![pool.integer(-1_i32), half_pi()]),
("Ci", true) => pool.integer(0_i32),
("Shi" | "Chi" | "Ei" | "li", true) => pool.pos_infinity(),
("Shi", false) => neg_infinity(pool),
("Ei", false) => pool.integer(0_i32),
("fresnels" | "fresnelc", true) => pool.rational(1, 2),
("fresnels" | "fresnelc", false) => pool.rational(-1, 2),
_ => return Ok(None),
};
Ok(Some(value))
}
fn argument_runs_to_infinity(
arg: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Option<bool> {
if let Ok(inner) = limit_inner(arg, var, point, direction, pool, depth + 1) {
if is_pos_infinity(inner, pool) {
return Some(true);
}
if is_neg_infinity(inner, pool) {
return Some(false);
}
}
let point_pos = if is_pos_infinity(point, pool) {
true
} else if is_neg_infinity(point, pool) {
false
} else {
return None;
};
let c = linear_coefficient_f64(arg, var, pool)?;
Some((c > 0.0) == point_pos)
}
fn linear_coefficient_f64(arg: ExprId, var: ExprId, pool: &ExprPool) -> Option<f64> {
if arg == var {
return Some(1.0);
}
if !depends_on(arg, var, pool) {
return None;
}
match pool.get(arg) {
ExprData::Mul(xs) => {
let mut coeff = 1.0_f64;
let mut seen_var = false;
for x in xs {
if x == var {
if seen_var {
return None;
}
seen_var = true;
continue;
}
if depends_on(x, var, pool) {
return None;
}
coeff *= constant_f64(x, pool).filter(|v| v.is_finite())?;
}
(seen_var && coeff != 0.0 && coeff.is_finite()).then_some(coeff)
}
ExprData::Add(xs) => {
let mut coeff: Option<f64> = None;
for x in xs {
if depends_on(x, var, pool) {
if coeff.is_some() {
return None;
}
coeff = Some(linear_coefficient_f64(x, var, pool)?);
} else {
constant_f64(x, pool).filter(|v| v.is_finite())?;
}
}
coeff
}
_ => None,
}
}
const LIMIT_SUBSTITUTION_PREFIX: &str = "__lt_";
const SPECIAL_BASIS_HEADS: [&str; 10] = [
"erf", "erfc", "Si", "Ci", "Shi", "Chi", "Ei", "li", "fresnels", "fresnelc",
];
fn mentions_special_basis(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Func { name, args } => {
SPECIAL_BASIS_HEADS.contains(&name.as_str())
|| args.iter().any(|&a| mentions_special_basis(a, pool))
}
ExprData::Add(args) | ExprData::Mul(args) => {
args.iter().any(|&a| mentions_special_basis(a, pool))
}
ExprData::Pow { base, exp } => {
mentions_special_basis(base, pool) || mentions_special_basis(exp, pool)
}
_ => false,
}
}
fn is_finite_limit_value(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Symbol { name, .. } => {
name != POS_INFINITY_SYMBOL && !name.starts_with(LIMIT_SUBSTITUTION_PREFIX)
}
ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => {
args.iter().all(|&a| is_finite_limit_value(a, pool))
}
ExprData::Pow { base, exp } => {
is_finite_limit_value(base, pool) && is_finite_limit_value(exp, pool)
}
_ => true,
}
}
fn try_special_basis_algebra(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let args = match pool.get(expr) {
ExprData::Add(args) | ExprData::Mul(args) => args,
_ => return Ok(None),
};
if !mentions_special_basis(expr, pool) {
return Ok(None);
}
let is_sum = matches!(pool.get(expr), ExprData::Add(_));
let mut parts = Vec::with_capacity(args.len());
for a in args {
let Ok(v) = limit_inner(a, var, point, direction, pool, depth + 1) else {
return Ok(None);
};
if !is_finite_limit_value(v, pool) || depends_on(v, var, pool) {
return Ok(None);
}
parts.push(v);
}
let combined = if is_sum {
pool.add(parts)
} else {
pool.mul(parts)
};
Ok(Some(fold_known_reals(simplify(combined, pool).value, pool)))
}
fn reflected_limit(
expr: ExprId,
var: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<ExprId, LimitError> {
let y = pool.symbol("__lt_refl", crate::kernel::Domain::Real);
let mut m = HashMap::new();
m.insert(var, pool.mul(vec![pool.integer(-1_i32), y]));
let reflected = simplify(subs(expr, &m, pool), pool).value;
let flipped = match direction {
LimitDirection::Plus => LimitDirection::Minus,
LimitDirection::Minus => LimitDirection::Plus,
LimitDirection::Bidirectional => LimitDirection::Bidirectional,
};
limit_inner(reflected, y, pool.pos_infinity(), flipped, pool, depth + 1)
}
fn neg_infinity(pool: &ExprPool) -> ExprId {
pool.mul(vec![pool.integer(-1_i32), pool.pos_infinity()])
}
fn is_pos_infinity(e: ExprId, pool: &ExprPool) -> bool {
matches!(
pool.get(e),
ExprData::Symbol {
name,
domain: crate::kernel::Domain::Positive,
..
} if name == POS_INFINITY_SYMBOL
) || matches!(
pool.get(e),
ExprData::Symbol {
name,
domain: crate::kernel::Domain::Real,
..
} if name == POS_INFINITY_SYMBOL
)
}
fn is_neg_infinity(e: ExprId, pool: &ExprPool) -> bool {
let ExprData::Mul(args) = pool.get(e) else {
return false;
};
if args.len() != 2 {
return false;
}
let (a, b) = (args[0], args[1]);
let m_one = pool.integer(-1_i32);
(a == m_one && is_pos_infinity(b, pool)) || (b == m_one && is_pos_infinity(a, pool))
}
fn depends_on(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
if expr == var {
return true;
}
match pool.get(expr) {
ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|a| depends_on(*a, var, pool)),
ExprData::Pow { base, exp } => depends_on(base, var, pool) || depends_on(exp, var, pool),
ExprData::Func { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
ExprData::Piecewise { branches, default } => {
branches
.iter()
.any(|(c, v)| depends_on(*c, var, pool) || depends_on(*v, var, pool))
|| depends_on(default, var, pool)
}
ExprData::Predicate { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
ExprData::Forall { var: bv, body } | ExprData::Exists { var: bv, body } => {
bv != var && depends_on(body, var, pool)
}
ExprData::RootSum {
poly,
var: bv,
body,
} => depends_on(poly, var, pool) || (bv != var && depends_on(body, var, pool)),
ExprData::BigO(a) => depends_on(a, var, pool),
ExprData::Integer(_)
| ExprData::Rational(_)
| ExprData::Float(_)
| ExprData::Symbol { .. } => false,
}
}
fn try_direct_substitution(
expr: ExprId,
var: ExprId,
point: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
if quotient_is_zero_over_zero(expr, var, point, pool) {
return None;
}
let mut m = HashMap::new();
m.insert(var, point);
let raw = subs(expr, &m, pool);
if is_zero_times_pole_indeterminate(raw, pool) {
return None;
}
let sub = fold_known_reals(simplify(raw, pool).value, pool);
let dep = depends_on(sub, var, pool);
let sing = substitution_is_singular(sub, pool);
if dep || sing || !is_usable_limit_value(sub, pool) {
None
} else {
Some(sub)
}
}
fn is_usable_limit_value(value: ExprId, pool: &ExprPool) -> bool {
if contains_zero_to_negative_power(value, pool) {
return false;
}
if is_pos_infinity(value, pool) || is_neg_infinity(value, pool) {
return true;
}
if !is_closed_arithmetic(value, pool) {
return true;
}
constant_f64(value, pool).is_some()
}
fn is_closed_arithmetic(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => true,
ExprData::Symbol { .. } => false,
ExprData::Add(xs) | ExprData::Mul(xs) | ExprData::Func { args: xs, .. } => {
xs.iter().all(|&x| is_closed_arithmetic(x, pool))
}
ExprData::Pow { base, exp } => {
is_closed_arithmetic(base, pool) && is_closed_arithmetic(exp, pool)
}
_ => false,
}
}
fn quotient_is_zero_over_zero(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> bool {
let (n, d) = numerator_denominator(expr, pool);
if d == pool.integer(1_i32) {
return false;
}
let n0 = substitute_fully(n, var, point, pool);
let d0 = substitute_fully(d, var, point, pool);
is_zero_like(n0, pool) && is_zero_like(d0, pool)
}
fn is_zero_times_pole_indeterminate(expr: ExprId, pool: &ExprPool) -> bool {
let factors: Vec<ExprId> = if matches!(pool.get(expr), ExprData::Mul(_)) {
flatten_mul(expr, pool)
} else {
vec![expr]
};
let mut any_zero_factor = false;
let mut any_pole = false;
for f in factors {
if substitution_is_singular(f, pool) {
any_pole = true;
}
if matches!(pool.get(f), ExprData::Integer(z) if z.0 == 0) {
any_zero_factor = true;
}
if let ExprData::Func { name, args } = pool.get(f) {
if args.len() == 1
&& matches!(name.as_str(), "sin" | "sinh" | "tan")
&& matches!(pool.get(args[0]), ExprData::Integer(z) if z.0 == 0)
{
any_zero_factor = true;
}
}
}
any_zero_factor && any_pole
}
fn substitution_is_singular(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Pow { base, exp } => {
if let ExprData::Integer(nn) = pool.get(exp) {
if nn.0 < 0 {
let b = simplify(base, pool).value;
if matches!(pool.get(b), ExprData::Integer(z) if z.0 == 0) {
return true;
}
}
}
substitution_is_singular(base, pool) || substitution_is_singular(exp, pool)
}
ExprData::Add(xs) | ExprData::Mul(xs) => {
xs.iter().any(|a| substitution_is_singular(*a, pool))
}
ExprData::Func { args, .. } => args.iter().any(|a| substitution_is_singular(*a, pool)),
_ => false,
}
}
fn try_lhopital(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
depth: u32,
) -> Result<Option<ExprId>, LimitError> {
let (nume, deno) = numerator_denominator(expr, pool);
if simplify(nume, pool).value == simplify(deno, pool).value {
return Ok(None);
}
let n0 = substitute_fully(nume, var, point, pool);
let d0 = substitute_fully(deno, var, point, pool);
if !is_zero_like(n0, pool) || !is_zero_like(d0, pool) {
return Ok(None);
}
let dn = diff(nume, var, pool)?.value;
let dd = diff(deno, var, pool)?.value;
if dn == nume && dd == deno {
return Ok(None);
}
let quot = rational_quotient(dn, dd, pool);
Ok(Some(limit_inner(
quot,
var,
point,
direction,
pool,
depth + 1,
)?))
}
fn substitute_fully(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> ExprId {
let mut m = HashMap::new();
m.insert(var, point);
let s = simplify(subs(expr, &m, pool), pool).value;
fold_known_reals(s, pool)
}
fn rational_quotient(n: ExprId, d: ExprId, pool: &ExprPool) -> ExprId {
simplify(pool.mul(vec![n, pool.pow(d, pool.integer(-1_i32))]), pool).value
}
fn is_zero_like(e: ExprId, pool: &ExprPool) -> bool {
let e = simplify(e, pool).value;
if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
return true;
}
if let ExprData::Rational(r) = pool.get(e) {
if r.0 == 0 {
return true;
}
}
if let ExprData::Func { name, args } = pool.get(e) {
if args.len() == 1 && matches!(name.as_str(), "sin" | "tan" | "sinh") {
return is_zero_like(args[0], pool);
}
}
false
}
fn is_one_like(e: ExprId, pool: &ExprPool) -> bool {
let e = simplify(e, pool).value;
if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 1) {
return true;
}
if let ExprData::Rational(r) = pool.get(e) {
return r.0 == 1;
}
false
}
fn fold_known_reals(expr: ExprId, pool: &ExprPool) -> ExprId {
let e = simplify(expr, pool).value;
match pool.get(e) {
ExprData::Add(xs) => {
let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
simplify(pool.add(ys), pool).value
}
ExprData::Mul(xs) => {
let ys: Vec<ExprId> = xs.iter().map(|x| fold_known_reals(*x, pool)).collect();
simplify(pool.mul(ys), pool).value
}
ExprData::Pow { base, exp } => {
let b = fold_known_reals(base, pool);
let xp = fold_known_reals(exp, pool);
if is_one_like(b, pool) {
if substitution_is_singular(xp, pool)
|| is_pos_infinity(xp, pool)
|| is_neg_infinity(xp, pool)
{
return simplify(pool.pow(b, xp), pool).value;
}
return pool.integer(1_i32);
}
simplify(pool.pow(b, xp), pool).value
}
ExprData::Func { name, args } if args.len() == 1 => {
let inner = fold_known_reals(args[0], pool);
if is_zero_like(inner, pool) {
match name.as_str() {
"sin" | "tan" | "sinh" => return pool.integer(0_i32),
"cos" | "cosh" => return pool.integer(1_i32),
"exp" => return pool.integer(1_i32),
_ => {}
}
}
simplify(pool.func(name, vec![inner]), pool).value
}
ExprData::Func { name, args } => {
let ys: Vec<ExprId> = args.iter().map(|x| fold_known_reals(*x, pool)).collect();
simplify(pool.func(name, ys), pool).value
}
_ => e,
}
}
fn flatten_mul(expr: ExprId, pool: &ExprPool) -> Vec<ExprId> {
match pool.get(expr) {
ExprData::Mul(xs) => xs.iter().flat_map(|a| flatten_mul(*a, pool)).collect(),
_ => vec![expr],
}
}
fn numerator_denominator(expr: ExprId, pool: &ExprPool) -> (ExprId, ExprId) {
let fac = flatten_mul(expr, pool);
let mut nums = Vec::new();
let mut dens = Vec::new();
for f in fac {
match pool.get(f) {
ExprData::Pow { base, exp } => {
if let ExprData::Integer(n) = pool.get(exp) {
let nn = &n.0;
if *nn == 0 {
nums.push(pool.integer(1_i32));
} else if *nn > 0 {
nums.push(f);
} else {
let m = nn
.clone()
.abs()
.to_u64()
.and_then(|u| u32::try_from(u).ok())
.map(|mag| pool.pow(base, pool.integer(mag as i64)));
if let Some(p) = m {
dens.push(p);
} else {
nums.push(f);
}
}
} else {
nums.push(f);
}
}
_ => nums.push(f),
}
}
let n = if nums.is_empty() {
pool.integer(1_i32)
} else if nums.len() == 1 {
nums[0]
} else {
pool.mul(nums)
};
let d = if dens.is_empty() {
pool.integer(1_i32)
} else if dens.len() == 1 {
dens[0]
} else {
pool.mul(dens)
};
(n, d)
}
fn try_expansion_limit(
expr: ExprId,
var: ExprId,
point: ExprId,
direction: LimitDirection,
pool: &ExprPool,
order: u32,
) -> Result<Option<ExprId>, LimitError> {
if has_essential_singularity(expr, var, point, pool) {
return Ok(None);
}
let exp = match local_expansion(expr, var, point, order, pool) {
Ok(e) => e,
Err(_) => {
checkpoint(pool)?;
return Ok(None);
}
};
let r = expansion_to_limit(exp, var, pool, direction)?;
if r.is_none() {
checkpoint(pool)?;
}
Ok(r)
}
fn has_essential_singularity(expr: ExprId, var: ExprId, point: ExprId, pool: &ExprPool) -> bool {
const OUTGROWS_EVERY_POWER: [&str; 3] = ["exp", "gamma", "Ei"];
match pool.get(expr) {
ExprData::Func { name, args } => {
if OUTGROWS_EVERY_POWER.contains(&name.as_str())
&& args.iter().any(|&a| {
let mut m = HashMap::with_capacity(1);
m.insert(var, point);
substitution_is_singular(subs(a, &m, pool), pool)
})
{
return true;
}
args.iter()
.any(|&a| has_essential_singularity(a, var, point, pool))
}
ExprData::Add(xs) | ExprData::Mul(xs) => xs
.iter()
.any(|&x| has_essential_singularity(x, var, point, pool)),
ExprData::Pow { base, exp } => {
has_essential_singularity(base, var, point, pool)
|| has_essential_singularity(exp, var, point, pool)
}
_ => false,
}
}
fn expansion_to_limit(
exp: LocalExpansion,
var: ExprId,
pool: &ExprPool,
direction: LimitDirection,
) -> Result<Option<ExprId>, LimitError> {
let LocalExpansion {
valuation,
coeffs,
h_expr: _,
} = exp;
let mut idx = 0usize;
while idx < coeffs.len() && is_zero_like(coeffs[idx], pool) {
idx += 1;
}
if idx >= coeffs.len() {
return Ok(None);
}
let power = valuation + idx as i32;
let coeff = coeffs[idx];
if power > 0 {
return Ok(Some(pool.integer(0_i32)));
}
if power == 0 {
if !is_usable_limit_value(coeff, pool) {
return Ok(None);
}
return Ok(Some(coeff));
}
let pole_order = (-power) as u32;
let sgn_c = match coefficient_sign(coeff, pool) {
Some(Sign::Positive) => 1_i8,
Some(Sign::Negative) => -1_i8,
Some(Sign::Zero) | None => {
note_missing_assumption(coeff, var, pool);
return Err(LimitError::Unsupported);
}
};
if pole_order % 2 == 0 {
return Ok(Some(signed_infinity(pool, sgn_c)));
}
let Some(hdir) = sign_from_h(direction, power) else {
return Err(LimitError::NeedsOneSided);
};
Ok(Some(signed_infinity(pool, sgn_c * hdir)))
}
fn sign_from_h(direction: LimitDirection, power: i32) -> Option<i8> {
if power >= 0 {
return Some(1);
}
let odd = (-power) % 2 != 0;
if !odd {
return Some(1);
}
match direction {
LimitDirection::Plus => Some(1),
LimitDirection::Minus => Some(-1),
LimitDirection::Bidirectional => None,
}
}
fn signed_infinity(pool: &ExprPool, sign: i8) -> ExprId {
if sign < 0 {
neg_infinity(pool)
} else {
pool.pos_infinity()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
#[test]
fn limit_sin_over_x_zero() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(
p.mul(vec![p.func("sin", vec![x]), p.pow(x, p.integer(-1_i32))]),
&p,
)
.value;
let r = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.integer(1_i32));
}
#[test]
fn one_sided_limit_off_the_domain_is_refused() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(p.func("sqrt", vec![x]), &p).value;
assert!(
limit(ex, x, p.integer(0_i32), LimitDirection::Minus, &p).is_err(),
"√x has no left-hand limit at 0 over ℝ"
);
let r = limit(ex, x, p.integer(0_i32), LimitDirection::Plus, &p).unwrap();
assert_eq!(constant_f64(r, &p), Some(0.0), "got {}", p.display(r));
let ac = simplify(p.func("acos", vec![x]), &p).value;
assert!(
limit(ac, x, p.integer(1_i32), LimitDirection::Plus, &p).is_err(),
"arccos has no right-hand limit at 1 over ℝ"
);
let inv = simplify(p.pow(x, p.integer(-1_i32)), &p).value;
assert!(
limit(inv, x, p.integer(0_i32), LimitDirection::Minus, &p).is_ok(),
"lim_{{x→0⁻}} 1/x = −∞ must survive"
);
let sq = simplify(p.func("sqrt", vec![p.pow(x, p.integer(2_i32))]), &p).value;
let r = limit(sq, x, p.integer(0_i32), LimitDirection::Minus, &p).unwrap();
assert_eq!(constant_f64(r, &p), Some(0.0), "got {}", p.display(r));
}
#[test]
fn limit_x_log_x_zero_plus() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(p.mul(vec![x, p.func("log", vec![x])]), &p).value;
let r = limit(ex, x, p.integer(0_i32), LimitDirection::Plus, &p).unwrap();
assert_eq!(r, p.integer(0_i32));
}
#[test]
fn limit_exp_inf() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = p.func("exp", vec![x]);
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.pos_infinity());
}
#[test]
fn limit_x_squared_at_positive_infinity() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(p.pow(x, p.integer(2_i32)), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.pos_infinity(), "{}", p.display(r));
}
#[test]
fn limit_compound_interest_is_e() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let base = p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]);
let ex = simplify(p.pow(base, x), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
let expected = simplify(p.func("exp", vec![p.integer(1)]), &p).value;
assert_eq!(r, expected, "got {}", p.display(r));
}
#[test]
fn limit_one_plus_a_over_x_pow_x_is_exp_a() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let two_over_x = p.mul(vec![p.integer(2), p.pow(x, p.integer(-1))]);
let base = p.add(vec![p.integer(1), two_over_x]);
let ex = simplify(p.pow(base, x), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
let expected = simplify(p.func("exp", vec![p.integer(2)]), &p).value;
assert_eq!(r, expected, "got {}", p.display(r));
}
#[test]
fn limit_two_pow_x_not_rewritten_to_finite() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(p.pow(p.integer(2), x), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p);
if let Ok(v) = r {
assert_eq!(
v,
p.pos_infinity(),
"2^x must not be a finite value: {}",
p.display(v)
);
}
}
#[test]
fn limit_one_plus_x_to_one_over_x_is_e() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let base = p.add(vec![p.integer(1), x]);
let ex = simplify(p.pow(base, p.pow(x, p.integer(-1))), &p).value;
let r = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap();
let expected = simplify(p.func("exp", vec![p.integer(1)]), &p).value;
assert_eq!(r, expected, "got {}", p.display(r));
}
#[test]
fn limit_one_plus_one_over_x_is_one() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = simplify(p.add(vec![p.integer(1), p.pow(x, p.integer(-1))]), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.integer(1), "got {}", p.display(r));
}
#[test]
fn rational_x_over_x_plus_one_after_inf_subst() {
let p = ExprPool::new();
let t = p.symbol("__lt_inf", Domain::Real);
let inv = p.pow(t, p.integer(-1));
let ex = p.mul(vec![
inv,
p.pow(p.add(vec![p.integer(1), inv]), p.integer(-1)),
]);
let folded = flatten_nested_integer_pow(ex, &p);
let canon = canonical_polynomial_quotient_in_var(folded, t, &p).unwrap();
let r = simplify(canon, &p).value;
let mut m = HashMap::new();
m.insert(t, p.integer(0));
let sub = fold_known_reals(simplify(subs(r, &m, &p), &p).value, &p);
assert_eq!(sub, p.integer(1), "canonical={}", p.display(canon));
}
}
#[cfg(test)]
mod termination_tests {
use super::*;
use crate::budget::{self, Budget, BudgetError};
use crate::errors::AlkahestError;
use crate::kernel::Domain;
#[test]
fn sqrt_x_squared_plus_x_minus_x_at_infinity_is_one_half() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.rational(1, 2), "got {}", p.display(r));
}
#[test]
fn algebraic_cancellations_at_infinity() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let neg_inf = p.mul(vec![p.integer(-1), p.pos_infinity()]);
let root = p.func(
"sqrt",
vec![p.add(vec![p.pow(x, p.integer(2)), p.mul(vec![p.integer(3), x])])],
);
let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.rational(3, 2), "√(x²+3x)−x: {}", p.display(r));
let root = p.func(
"sqrt",
vec![p.add(vec![p.pow(x, p.integer(2)), p.integer(1)])],
);
let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.integer(0), "√(x²+1)−x: {}", p.display(r));
let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
let r = limit(ex, x, neg_inf, LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.pos_infinity(), "√(x²+x)−x at −∞: {}", p.display(r));
}
#[test]
fn unsolvable_radical_limit_refuses_within_the_work_ceiling() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
let err = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap_err();
assert!(
matches!(err, LimitError::DepthExceeded),
"expected a bounded refusal, got {err:?}"
);
assert_eq!(err.code(), "E-LIMIT-004");
assert_eq!(last_budget_trip(), None);
}
#[test]
fn step_budget_stops_a_hard_limit_and_is_attributed() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
let _guard = budget::enter(Budget::new().with_max_steps(3));
let err = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap_err();
assert!(matches!(err, LimitError::DepthExceeded), "{err:?}");
assert!(
matches!(last_budget_trip(), Some(BudgetError::Steps { .. })),
"budget trip not recorded: {:?}",
last_budget_trip()
);
}
#[test]
fn a_solved_limit_leaves_no_budget_trip() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
{
let _guard = budget::enter(Budget::new().with_max_steps(3));
let inner = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = p.func("sqrt", vec![p.add(vec![inner, x])]);
assert!(limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).is_err());
assert!(last_budget_trip().is_some());
}
let root = p.func("sqrt", vec![p.add(vec![p.pow(x, p.integer(2)), x])]);
let ex = simplify(p.add(vec![root, p.mul(vec![p.integer(-1), x])]), &p).value;
let r = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap();
assert_eq!(r, p.rational(1, 2));
assert_eq!(last_budget_trip(), None, "stale trip left behind");
}
#[test]
fn work_baseline_is_installed_once_and_cleared_on_exit() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
assert!(WORK_BASELINE.with(|c| c.get()).is_none());
let ex = simplify(p.pow(x, p.integer(2)), &p).value;
let _ = limit(ex, x, p.pos_infinity(), LimitDirection::Bidirectional, &p);
assert!(
WORK_BASELINE.with(|c| c.get()).is_none(),
"baseline leaked past the outermost call"
);
}
}
#[cfg(test)]
mod numeric_refutation_tests {
use super::*;
use crate::kernel::Domain;
use crate::simplify::AssumptionContext;
#[test]
fn sign_function_limit_is_refused_in_every_direction() {
for direction in [
LimitDirection::Bidirectional,
LimitDirection::Plus,
LimitDirection::Minus,
] {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = p.mul(vec![x, p.pow(p.func("abs", vec![x]), p.integer(-1_i32))]);
let got = limit(ex, x, p.integer(0_i32), direction, &p);
assert!(
got.is_err(),
"x/|x| at 0 ({direction:?}) should refuse, got {}",
p.display(got.unwrap())
);
}
}
#[test]
fn ordinary_limits_survive_the_guard() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let one = p.integer(1_i32);
let sinc = simplify(
p.mul(vec![p.func("sin", vec![x]), p.pow(x, p.integer(-1_i32))]),
&p,
)
.value;
assert_eq!(
limit(sinc, x, p.integer(0_i32), LimitDirection::Bidirectional, &p).unwrap(),
one
);
let half = p.mul(vec![
p.add(vec![
one,
p.mul(vec![p.integer(-1_i32), p.func("cos", vec![x])]),
]),
p.pow(x, p.integer(-2_i32)),
]);
let got = limit(
simplify(half, &p).value,
x,
p.integer(0_i32),
LimitDirection::Bidirectional,
&p,
)
.unwrap();
assert_eq!(got, p.rational(1, 2));
}
#[test]
fn oscillation_does_not_trigger_a_false_refusal() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let ex = p.mul(vec![x, p.func("sin", vec![p.pow(x, p.integer(-1_i32))])]);
let got = limit(ex, x, p.integer(0_i32), LimitDirection::Bidirectional, &p);
assert_eq!(got.unwrap(), p.integer(0_i32));
}
#[test]
fn symbolic_parameter_abstains() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let a = p.symbol("a", Domain::Real);
assert!(has_free_symbol_besides(p.mul(vec![a, x]), x, &p));
assert!(!has_free_symbol_besides(
p.mul(vec![x, p.func("sin", vec![x])]),
x,
&p
));
}
fn decay(p: &ExprPool) -> (ExprId, ExprId, ExprId) {
let t = p.symbol("t", Domain::Real);
let k = p.symbol("ke", Domain::Real);
let inner = simplify(p.mul(vec![p.integer(-1_i32), k, t]), p).value;
let e = simplify(p.func("exp", vec![inner]), p).value;
(e, t, k)
}
fn assume(
p: &ExprPool,
kind: crate::kernel::expr::PredicateKind,
k: ExprId,
) -> AssumptionContext {
let mut a = AssumptionContext::new();
a.refine(p.predicate(kind, vec![k, p.integer(0_i32)]), p)
.expect("the assumption is consistent");
a
}
#[test]
fn a_stated_rate_sign_decides_the_exponential_limit() {
use crate::kernel::expr::PredicateKind;
use crate::simplify::enter_assumptions;
let p = ExprPool::new();
let (e, t, k) = decay(&p);
let oo = p.pos_infinity();
let positive = assume(&p, PredicateKind::Gt, k);
{
let _scope = enter_assumptions(&positive, &p);
assert_eq!(
limit(e, t, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.integer(0_i32)
);
}
let negative = assume(&p, PredicateKind::Lt, k);
{
let _scope = enter_assumptions(&negative, &p);
assert_eq!(
limit(e, t, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.pos_infinity()
);
}
let zero = assume(&p, PredicateKind::Eq, k);
{
let _scope = enter_assumptions(&zero, &p);
assert_eq!(
limit(e, t, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.integer(1_i32)
);
}
assert!(limit(e, t, oo, LimitDirection::Bidirectional, &p).is_err());
}
#[test]
fn an_unstated_sign_is_refused_and_the_refusal_names_the_parameter() {
let p = ExprPool::new();
let (e, t, _k) = decay(&p);
let oo = p.pos_infinity();
let err = limit(e, t, oo, LimitDirection::Bidirectional, &p)
.expect_err("nothing establishes the sign of ke, so there is no answer");
assert!(matches!(err, LimitError::Unsupported), "got {err:?}");
let missing = last_missing_assumption().expect("the refusal must name the missing fact");
assert_eq!(missing.parameter, "ke");
let rendered = missing.to_string();
assert!(rendered.contains("ke > 0"), "unhelpful message: {rendered}");
assert!(rendered.contains("ke < 0"), "unhelpful message: {rendered}");
assert_eq!(crate::errors::AlkahestError::code(&missing), "E-LIMIT-006");
}
#[test]
fn a_parameter_sign_is_never_guessed_for_a_pole() {
use crate::kernel::expr::PredicateKind;
use crate::simplify::enter_assumptions;
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let k = p.symbol("k", Domain::Real);
let oo = p.pos_infinity();
let kx = simplify(p.mul(vec![k, x]), &p).value;
let neg_kx = simplify(p.mul(vec![p.integer(-1_i32), k, x]), &p).value;
assert!(limit(kx, x, oo, LimitDirection::Bidirectional, &p).is_err());
assert!(limit(neg_kx, x, oo, LimitDirection::Bidirectional, &p).is_err());
let positive = assume(&p, PredicateKind::Gt, k);
let _scope = enter_assumptions(&positive, &p);
assert_eq!(
limit(kx, x, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.pos_infinity()
);
assert_eq!(
limit(neg_kx, x, oo, LimitDirection::Bidirectional, &p).unwrap(),
neg_infinity(&p)
);
}
#[test]
fn a_positive_domain_symbol_carries_its_own_assumption() {
let p = ExprPool::new();
let t = p.symbol("t", Domain::Real);
let k = p.symbol("k", Domain::Positive);
let e = simplify(
p.func("exp", vec![p.mul(vec![p.integer(-1_i32), k, t])]),
&p,
)
.value;
assert_eq!(
limit(e, t, p.pos_infinity(), LimitDirection::Bidirectional, &p).unwrap(),
p.integer(0_i32)
);
}
#[test]
fn assumptions_do_not_leak_out_of_their_scope_or_pool() {
use crate::kernel::expr::PredicateKind;
use crate::simplify::enter_assumptions;
let p = ExprPool::new();
let (e, t, k) = decay(&p);
let oo = p.pos_infinity();
let positive = assume(&p, PredicateKind::Gt, k);
let other = ExprPool::new();
let (other_e, other_t, _) = decay(&other);
let _scope = enter_assumptions(&positive, &p);
assert!(limit(
other_e,
other_t,
other.pos_infinity(),
LimitDirection::Bidirectional,
&other
)
.is_err());
assert!(limit(e, t, oo, LimitDirection::Bidirectional, &p).is_ok());
}
#[test]
fn the_sqrt_pole_takes_the_same_path_as_the_reciprocal_pole() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let zero = p.integer(0_i32);
let inv_sqrt = simplify(p.pow(p.func("sqrt", vec![x]), p.integer(-1_i32)), &p).value;
assert_eq!(
limit(inv_sqrt, x, zero, LimitDirection::Plus, &p).unwrap(),
p.pos_infinity()
);
assert_eq!(
limit(inv_sqrt, x, zero, LimitDirection::Bidirectional, &p).unwrap(),
p.pos_infinity()
);
assert!(limit(inv_sqrt, x, zero, LimitDirection::Minus, &p).is_err());
let inv = simplify(p.pow(x, p.integer(-1_i32)), &p).value;
assert!(matches!(
limit(inv, x, zero, LimitDirection::Bidirectional, &p),
Err(LimitError::NeedsOneSided)
));
}
#[test]
fn registered_primitives_have_their_endpoint_values_at_infinity() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let oo = p.pos_infinity();
let noo = neg_infinity(&p);
for name in ["erf", "tanh"] {
let e = simplify(p.func(name, vec![x]), &p).value;
assert_eq!(
limit(e, x, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.integer(1_i32),
"lim_{{x→∞}} {name}(x)"
);
assert_eq!(
limit(e, x, noo, LimitDirection::Bidirectional, &p).unwrap(),
p.integer(-1_i32),
"lim_{{x→−∞}} {name}(x)"
);
}
let scaled = simplify(
p.func(
"erf",
vec![p.mul(vec![x, p.pow(p.integer(2_i32), p.integer(-1_i32))])],
),
&p,
)
.value;
assert_eq!(
limit(scaled, x, oo, LimitDirection::Bidirectional, &p).unwrap(),
p.integer(1_i32)
);
}
#[test]
fn no_successful_limit_returns_a_value_that_is_not_a_value() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let i = |n: i32| p.integer(n);
let f = |n: &str, a: Vec<ExprId>| p.func(n, a);
let inv = |e: ExprId| p.pow(e, i(-1));
let neg = |e: ExprId| p.mul(vec![i(-1), e]);
let sqrt = |e: ExprId| f("sqrt", vec![e]);
let log = |e: ExprId| f("log", vec![e]);
let cases: Vec<(&str, ExprId, ExprId)> = vec![
("1/sqrt(x) @0", inv(sqrt(x)), i(0)),
("1/cbrt(x) @0", inv(f("cbrt", vec![x])), i(0)),
("1/abs(x) @0", inv(f("abs", vec![x])), i(0)),
("sqrt(x)*log(x) @0", p.mul(vec![sqrt(x), log(x)]), i(0)),
("log(x)/sqrt(x) @0", p.mul(vec![log(x), inv(sqrt(x))]), i(0)),
("log(sqrt(x)) @0", log(sqrt(x)), i(0)),
("gamma(x) @0", f("gamma", vec![x]), i(0)),
("Ei(x) @0", f("Ei", vec![x]), i(0)),
("1/acos(x) @1", inv(f("acos", vec![x])), i(1)),
("x^(-1/2) @0", p.pow(x, p.rational(-1, 2)), i(0)),
("1/gamma(x) @0", inv(f("gamma", vec![x])), i(0)),
("sqrt(x) @0", sqrt(x), i(0)),
("1/x @0", inv(x), i(0)),
("1/x^2 @0", p.pow(x, i(-2)), i(0)),
("exp(-x) @oo", f("exp", vec![neg(x)]), p.pos_infinity()),
];
let dirs = [
LimitDirection::Bidirectional,
LimitDirection::Plus,
LimitDirection::Minus,
];
for (name, e, point) in cases {
let e = simplify(e, &p).value;
for dir in dirs {
let Ok(r) = limit(e, x, point, dir, &p) else {
continue;
};
if is_pos_infinity(r, &p) || is_neg_infinity(r, &p) || !is_closed_arithmetic(r, &p)
{
continue;
}
assert!(
constant_f64(r, &p).is_some(),
"{name} ({dir:?}) reported success with {}, which is not a finite real",
p.display(r)
);
}
}
}
fn at_infinity(src_fn: &str, sign: i32) -> (ExprPool, Result<ExprId, LimitError>) {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e = p.func(src_fn, vec![x]);
let point = if sign > 0 {
p.pos_infinity()
} else {
neg_infinity(&p)
};
let r = limit(e, x, point, LimitDirection::Bidirectional, &p);
(p, r)
}
#[test]
fn special_basis_limits_match_dlmf() {
for (name, sign, want) in [
("erf", 1, 1.0),
("erf", -1, -1.0),
("erfc", 1, 0.0),
("erfc", -1, 2.0),
("Si", 1, std::f64::consts::FRAC_PI_2),
("Si", -1, -std::f64::consts::FRAC_PI_2),
("Ci", 1, 0.0),
("Ei", -1, 0.0),
("fresnels", 1, 0.5),
("fresnels", -1, -0.5),
("fresnelc", 1, 0.5),
("fresnelc", -1, -0.5),
] {
let (p, r) = at_infinity(name, sign);
let v = r.unwrap_or_else(|e| panic!("lim {name} at {sign}∞: {e}"));
let mut binds = HashMap::new();
binds.insert(p.symbol("pi", Domain::Real), std::f64::consts::PI);
let got = crate::eval::eval_f64(v, &p, &binds)
.unwrap_or_else(|e| panic!("{} did not evaluate: {e}", p.display(v)));
assert!(
(got - want).abs() < 1e-12,
"lim_{{x→{sign}∞}} {name}(x) = {want}, got {got}"
);
}
}
#[test]
fn the_cut_functions_have_no_limit_at_minus_infinity() {
for name in ["Ci", "Chi", "li"] {
let (_, r) = at_infinity(name, -1);
assert!(
r.is_err(),
"{name} has no real value on the negative reals; a limit at −∞ must not be invented"
);
}
}
#[test]
fn a_scaled_erf_product_reaches_its_limit() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let inner = p.mul(vec![p.float(std::f64::consts::FRAC_1_SQRT_2, 53), x]);
let outer = (std::f64::consts::PI / 2.0).sqrt();
let e = p.mul(vec![p.float(outer, 53), p.func("erf", vec![inner])]);
let v = limit(e, x, p.pos_infinity(), LimitDirection::Bidirectional, &p).expect("limit");
let got = crate::eval::eval_f64(v, &p, &HashMap::new()).expect("value");
assert!((got - outer).abs() < 1e-12, "got {got}");
}
#[test]
fn the_reflection_fallback_reaches_a_gruntz_only_limit_at_minus_infinity() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e = p.mul(vec![
x,
p.func(
"exp",
vec![p.mul(vec![p.integer(-1_i32), p.pow(x, p.integer(2_i32))])],
),
]);
let v = limit(e, x, neg_infinity(&p), LimitDirection::Bidirectional, &p).expect("0");
assert_eq!(v, p.integer(0_i32));
}
#[test]
fn the_reflection_fallback_does_not_invent_a_missing_limit() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e = p.func("sin", vec![x]);
assert!(limit(e, x, neg_infinity(&p), LimitDirection::Bidirectional, &p).is_err());
}
#[test]
fn the_algebra_rule_declines_an_indeterminate_difference() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e = p.add(vec![
p.func("Ei", vec![x]),
p.mul(vec![p.integer(-1_i32), p.func("Shi", vec![x])]),
]);
assert!(try_special_basis_algebra(
e,
x,
p.pos_infinity(),
LimitDirection::Bidirectional,
&p,
0
)
.unwrap()
.is_none());
}
}