Skip to main content

problemreductions/
growth.rs

1//! Symbolic growth domain: a dedicated asymptotic normal form for reduction
2//! parameter expressions.
3//!
4//! Where full monomial canonicalization answers Big-O questions by expanding an
5//! [`Expr`] to monomial normal form, with exponential cost in nesting depth, the
6//! growth domain computes a Big-O normal form bottom-up without rewriting
7//! the source AST into a fully distributed polynomial. Work is output-sensitive:
8//! antichains are retained up to 32 terms; larger fronts are reported as
9//! unsupported instead of silently approximated.
10//!
11//! # Representation
12//!
13//! One internal growth term is a monomial
14//!
15//! ```text
16//! ∏_v ∏_f base[f]^(coefficient[f] · v)
17//!     · ∏_v v^(poly[v]) · ∏_v (log v)^(logs[v])
18//! ```
19//!
20//! and a [`Growth`] is either a known antichain of pairwise-incomparable dominant
21//! terms (each summand of an asymptotic sum), or an unknown result with explicit
22//! reasons for content we cannot represent symbolically.
23//!
24//! # Semantic foundation (the trust contract)
25//!
26//! Every expression admitted to the domain is assumed **nonnegative** and
27//! **weakly monotone** (nondecreasing in each variable) on `vars ≥ 2`. Under
28//! these axioms Howell's multivariate-O inconsistencies vanish and
29//! `f + g ≍ max(f, g)` up to a constant factor, which licenses
30//! `add = antichain union + prune`. All bounds produced are **upper** bounds.
31//!
32//! Widening (always toward a valid upper bound):
33//! - Subtraction is normalized to addition of a negative term, and
34//!   [`Growth::from_expr`] widens it to the union of both operands.
35//!   This also covers the
36//!   `sqrt((a − b)^2)` absolute-value idiom (`|a − b| ≤ a + b`).
37//! - Constants and constant multipliers/divisors are dropped on entry.
38//! - Exponentials with a **linear** exponent (`c^x`, `c^(r·x)`, `exp(x)`) are
39//!   first-class via symbolic base/coefficient factors. The original base is
40//!   authoritative: it is never normalized through a floating-point logarithm
41//!   and never reconstructed by rounding. Nonlinear exponents (`2^(n·k)`,
42//!   `2^sqrt(n)`), `factorial(·)`, and negative polynomial exponents widen to
43//!   an unknown result, which preserves its reasons through every operation.
44//! - The explicit approximation boundary treats [`Expr::log`] as the natural
45//!   logarithm, but all fixed
46//!   logarithm bases greater than one have the same asymptotic class and are
47//!   intentionally represented by the single `log(v)` factor.
48//!
49//! # `Pow` note
50//!
51//! `Pow(base, k)` for a nonnegative constant `k` raises **each** antichain term
52//! of `base` to the power `k` (scaling its exponents). This is the tight
53//! asymptotic answer — `(n + m)^2 ≍ max(n, m)^2 = max(n^2, m^2)` by AM-GM, so no
54//! binomial cross term is introduced — and it is what makes the widening chain
55//! `sqrt((n − m)^2) ≍ n + m` hold exactly.
56
57use crate::expr::{AlgebraicAnalysis, BigInt, Expr, ExprNode, ExprNodeId};
58use num_rational::BigRational;
59use num_traits::{One, Signed, ToPrimitive, Zero};
60use std::cmp::Ordering;
61use std::collections::{BTreeMap, HashMap};
62
63/// Maximum number of incomparable terms retained in one Big-O normal form.
64const ANTICHAIN_CAP: usize = 32;
65
66/// An exact fixed exponential base.
67#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
68enum ExpBase {
69    /// A positive rational constant used as the base of `Pow`.
70    Rational(BigRational),
71    /// The distinguished base of the `exp(...)` AST constructor.
72    Natural,
73}
74
75impl ExpBase {
76    fn directly_comparable_value(&self) -> Option<&BigRational> {
77        match self {
78            ExpBase::Rational(base) => Some(base),
79            ExpBase::Natural => None,
80        }
81    }
82
83    fn direction(&self) -> Ordering {
84        match self {
85            ExpBase::Rational(base) => base.cmp(&BigRational::one()),
86            ExpBase::Natural => Ordering::Greater,
87        }
88    }
89
90    fn coefficient_cmp(&self, a: &BigRational, b: &BigRational) -> Ordering {
91        if self.direction() == Ordering::Greater {
92            a.cmp(b)
93        } else {
94            a.cmp(b).reverse()
95        }
96    }
97}
98
99/// Exponential, polynomial, and logarithmic growth associated with one size
100/// variable. Missing components have exponent zero.
101#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
102struct VariableGrowth {
103    exp: BTreeMap<ExpBase, BigRational>,
104    poly: BigRational,
105    log: u32,
106}
107
108impl VariableGrowth {
109    fn empty() -> Self {
110        Self {
111            exp: BTreeMap::new(),
112            poly: BigRational::zero(),
113            log: 0,
114        }
115    }
116
117    fn exponential(base: ExpBase, coefficient: BigRational) -> Self {
118        Self {
119            exp: BTreeMap::from([(base, coefficient)]),
120            poly: BigRational::zero(),
121            log: 0,
122        }
123    }
124
125    fn polynomial(degree: BigRational) -> Self {
126        Self {
127            exp: BTreeMap::new(),
128            poly: degree,
129            log: 0,
130        }
131    }
132
133    fn logarithmic(power: u32) -> Self {
134        Self {
135            exp: BTreeMap::new(),
136            poly: BigRational::zero(),
137            log: power,
138        }
139    }
140
141    fn is_empty(&self) -> bool {
142        self.exp.is_empty() && self.poly.is_zero() && self.log == 0
143    }
144
145    fn mul(&self, other: &Self) -> Result<Self, GrowthFailure> {
146        let mut result = self.clone();
147        for (base, coefficient) in &other.exp {
148            *result
149                .exp
150                .entry(base.clone())
151                .or_insert_with(BigRational::zero) += coefficient;
152        }
153        result.exp.retain(|_, coefficient| !coefficient.is_zero());
154        result.poly += &other.poly;
155        result.log = result.log.checked_add(other.log).ok_or_else(|| {
156            GrowthFailure::RepresentedExponentOutOfRange(format!("{} + {}", self.log, other.log))
157        })?;
158        Ok(result)
159    }
160
161    fn pow(&self, power: &BigRational) -> Result<Self, GrowthFailure> {
162        let exp = self
163            .exp
164            .iter()
165            .filter_map(|(base, coefficient)| {
166                let coefficient = coefficient * power;
167                (!coefficient.is_zero()).then(|| (base.clone(), coefficient))
168            })
169            .collect();
170        let poly = &self.poly * power;
171        let scaled_log = BigRational::from_integer(BigInt::from(self.log)) * power;
172        let rounded =
173            (scaled_log.numer() + scaled_log.denom() - BigInt::one()) / scaled_log.denom();
174        let Some(log) = rounded.to_u32() else {
175            return Err(GrowthFailure::RepresentedExponentOutOfRange(
176                scaled_log.to_string(),
177            ));
178        };
179        Ok(Self { exp, poly, log })
180    }
181
182    fn cmp_exp(&self, other: &Self) -> Option<Ordering> {
183        let mut left_count = 0;
184        let mut right_count = 0;
185        let mut left_single: Option<(&ExpBase, BigRational)> = None;
186        let mut right_single: Option<(&ExpBase, BigRational)> = None;
187
188        for (base, a) in &self.exp {
189            if let Some(b) = other.exp.get(base) {
190                match base.coefficient_cmp(a, b) {
191                    Ordering::Equal => {}
192                    Ordering::Greater => {
193                        left_count += 1;
194                        left_single = Some((base, a - b));
195                    }
196                    Ordering::Less => {
197                        right_count += 1;
198                        right_single = Some((base, b - a));
199                    }
200                }
201            } else {
202                left_count += 1;
203                left_single = Some((base, a.clone()));
204            }
205        }
206
207        for (base, coefficient) in &other.exp {
208            if !self.exp.contains_key(base) {
209                right_count += 1;
210                right_single = Some((base, coefficient.clone()));
211            }
212        }
213
214        match (left_count, right_count) {
215            (0, 0) => Some(Ordering::Equal),
216            (0, _) => Some(Ordering::Less),
217            (_, 0) => Some(Ordering::Greater),
218            (1, 1) => {
219                let (a_base, a_coefficient) = left_single?;
220                let (b_base, b_coefficient) = right_single?;
221                Self::cmp_single_factor(a_base, &a_coefficient, b_base, &b_coefficient)
222            }
223            _ => None,
224        }
225    }
226
227    fn cmp_growth(&self, other: &Self) -> Option<Ordering> {
228        let exponential = self.cmp_exp(other)?;
229        Some(if exponential == Ordering::Equal {
230            self.poly.cmp(&other.poly).then(self.log.cmp(&other.log))
231        } else {
232            exponential
233        })
234    }
235
236    fn cmp_single_factor(
237        a_base: &ExpBase,
238        a_coefficient: &BigRational,
239        b_base: &ExpBase,
240        b_coefficient: &BigRational,
241    ) -> Option<Ordering> {
242        if a_base == b_base {
243            return Some(a_base.coefficient_cmp(a_coefficient, b_coefficient));
244        }
245
246        if a_coefficient == b_coefficient {
247            match (a_base, b_base) {
248                (ExpBase::Natural, ExpBase::Rational(_)) => {
249                    let base = b_base.directly_comparable_value()?;
250                    if base <= &BigRational::from_integer(2.into()) {
251                        return Some(Ordering::Greater);
252                    }
253                    if base >= &BigRational::from_integer(3.into()) {
254                        return Some(Ordering::Less);
255                    }
256                    return None;
257                }
258                (ExpBase::Rational(_), ExpBase::Natural) => {
259                    return Self::cmp_single_factor(b_base, b_coefficient, a_base, a_coefficient)
260                        .map(Ordering::reverse);
261                }
262                _ => {}
263            }
264        }
265
266        let (a_base, b_base) = (
267            a_base.directly_comparable_value()?,
268            b_base.directly_comparable_value()?,
269        );
270        if a_coefficient == b_coefficient {
271            let base_order = a_base.cmp(b_base);
272            return if a_coefficient.is_positive() {
273                Some(base_order)
274            } else {
275                Some(base_order.reverse())
276            };
277        }
278
279        if a_base > &BigRational::one() && b_base > &BigRational::one() {
280            match (a_base.cmp(b_base), a_coefficient.cmp(b_coefficient)) {
281                (Ordering::Greater | Ordering::Equal, Ordering::Greater | Ordering::Equal) => {
282                    Some(Ordering::Greater)
283                }
284                (Ordering::Less | Ordering::Equal, Ordering::Less | Ordering::Equal) => {
285                    Some(Ordering::Less)
286                }
287                _ => None,
288            }
289        } else if a_base < &BigRational::one() && b_base < &BigRational::one() {
290            match (a_base.cmp(b_base), a_coefficient.cmp(b_coefficient)) {
291                (Ordering::Less | Ordering::Equal, Ordering::Less | Ordering::Equal) => {
292                    Some(Ordering::Greater)
293                }
294                (Ordering::Greater | Ordering::Equal, Ordering::Greater | Ordering::Equal) => {
295                    Some(Ordering::Less)
296                }
297                _ => None,
298            }
299        } else {
300            None
301        }
302    }
303}
304
305/// One growth monomial, e.g. `2^(3k) · n^2 · m · log(n)`.
306///
307/// Empty maps represent `O(1)`.
308#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
309struct GrowthTerm {
310    variables: BTreeMap<Box<str>, VariableGrowth>,
311}
312
313/// The asymptotic growth class of an [`Expr`].
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct Growth(GrowthState);
316
317#[derive(Clone, Debug, PartialEq, Eq)]
318enum GrowthState {
319    Known(Vec<GrowthTerm>),
320    /// Content outside the represented growth domain, with every reason that
321    /// contributed to the result.
322    Unknown(Vec<GrowthFailure>),
323}
324
325/// A precise reason why an expression has no represented [`Growth`] value.
326#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)]
327pub enum GrowthFailure {
328    #[error("invalid or unproved constant domain for {expression}")]
329    InvalidConstantDomain { expression: String },
330    #[error("negative exponent is unsupported: {0}")]
331    NegativeExponent(String),
332    #[error("nonlinear exponent is unsupported: {0}")]
333    NonlinearExponent(String),
334    #[error("variable base and exponent are unsupported: {0}")]
335    VariableBaseAndExponent(String),
336    #[error("factorial of a nonconstant expression is unsupported: {0}")]
337    FactorialOfNonconstant(String),
338    #[error("invalid exponential base: {0}")]
339    InvalidExponentialBase(String),
340    #[error("represented exponent is outside the growth domain: {0}")]
341    RepresentedExponentOutOfRange(String),
342    #[error(
343        "exponential factor {base}^({coefficient} * {variable}) decreases as {variable} grows"
344    )]
345    DecayingExponential {
346        base: String,
347        variable: String,
348        coefficient: String,
349    },
350    #[error("missing substitution for {0}")]
351    MissingSubstitution(String),
352    #[error("Big-O antichain has {terms} terms, exceeding the limit of {limit}")]
353    AntichainLimitExceeded { limit: usize, terms: usize },
354}
355
356impl GrowthTerm {
357    /// The `O(1)` term (all maps empty).
358    fn one() -> Self {
359        GrowthTerm {
360            variables: BTreeMap::new(),
361        }
362    }
363
364    fn insert(&mut self, variable: Box<str>, growth: VariableGrowth) {
365        if !growth.is_empty() {
366            self.variables.insert(variable, growth);
367        }
368    }
369
370    /// Raise this term to a nonnegative real power `k` (scale every exponent).
371    /// Log powers are `u32`; a fractional result is rounded **up** (a valid
372    /// upper bound, since `(log v)^p ≤ (log v)^⌈p⌉` for `v ≥ 2`).
373    fn pow(&self, k: &BigRational) -> Result<GrowthTerm, GrowthFailure> {
374        let mut r = GrowthTerm::one();
375        for (variable, growth) in &self.variables {
376            r.insert(variable.clone(), growth.pow(k)?);
377        }
378        Ok(r)
379    }
380
381    /// Multiply two monomials (add matching exponents).
382    fn mul(&self, other: &GrowthTerm) -> Result<GrowthTerm, GrowthFailure> {
383        let mut t = self.clone();
384        for (variable, growth) in &other.variables {
385            let combined = match t.variables.get(variable) {
386                Some(current) => current.mul(growth)?,
387                None => growth.clone(),
388            };
389            if combined.is_empty() {
390                t.variables.remove(variable);
391            } else {
392                t.variables.insert(variable.clone(), combined);
393            }
394        }
395        Ok(t)
396    }
397
398    /// Partial order on terms: `Some(Greater)` iff `self` dominates `other`
399    /// (`≥` on every variable and `>` on at least one). Per variable,
400    /// exponential products are compared only when a symbolic proof succeeds;
401    /// polynomial degree and log power then break proven exponential ties.
402    /// Returns `None` for incomparable or unproved terms.
403    fn cmp(&self, other: &GrowthTerm) -> Option<Ordering> {
404        let mut saw_gt = false;
405        let mut saw_lt = false;
406        let empty = VariableGrowth::empty();
407        let mut left = self.variables.iter().peekable();
408        let mut right = other.variables.iter().peekable();
409        loop {
410            let (a, b) = match (left.peek(), right.peek()) {
411                (None, None) => break,
412                (Some((left_variable, _)), Some((right_variable, _))) => {
413                    match left_variable.cmp(right_variable) {
414                        Ordering::Less => (left.next().unwrap().1, &empty),
415                        Ordering::Greater => (&empty, right.next().unwrap().1),
416                        Ordering::Equal => (left.next().unwrap().1, right.next().unwrap().1),
417                    }
418                }
419                (Some(_), None) => (left.next().unwrap().1, &empty),
420                (None, Some(_)) => (&empty, right.next().unwrap().1),
421            };
422            let order = a.cmp_growth(b)?;
423            match order {
424                Ordering::Greater => saw_gt = true,
425                Ordering::Less => saw_lt = true,
426                Ordering::Equal => {}
427            }
428        }
429        match (saw_gt, saw_lt) {
430            (true, true) => None,
431            (true, false) => Some(Ordering::Greater),
432            (false, true) => Some(Ordering::Less),
433            (false, false) => Some(Ordering::Equal),
434        }
435    }
436
437    /// `true` iff `self` dominates `other` (grows at least as fast, and strictly
438    /// faster on at least one variable).
439    fn dominates(&self, other: &GrowthTerm) -> bool {
440        matches!(self.cmp(other), Some(Ordering::Greater))
441    }
442
443    /// `true` iff `self` dominates `other` or is asymptotically equal to it.
444    fn dominates_or_eq(&self, other: &GrowthTerm) -> bool {
445        matches!(
446            self.cmp(other),
447            Some(Ordering::Greater) | Some(Ordering::Equal)
448        )
449    }
450}
451
452impl Growth {
453    pub(crate) fn unknown(failure: GrowthFailure) -> Self {
454        Self(GrowthState::Unknown(vec![failure]))
455    }
456
457    pub fn failures(&self) -> Option<&[GrowthFailure]> {
458        match &self.0 {
459            GrowthState::Known(_) => None,
460            GrowthState::Unknown(failures) => Some(failures),
461        }
462    }
463
464    /// Compute the growth class of an expression in a single bottom-up pass.
465    pub fn from_expr(expr: &Expr) -> Growth {
466        let analysis = AlgebraicAnalysis::new(&[expr]);
467        Self::from_analysis(expr, &analysis)
468    }
469
470    pub(crate) fn from_analysis(expr: &Expr, analysis: &AlgebraicAnalysis) -> Growth {
471        growth_from_analysis(expr, analysis, &mut HashMap::new())
472    }
473
474    /// Partial order on represented Big-O normal forms.
475    ///
476    /// Unknown values are incomparable. For two known term antichains, `self`
477    /// dominates `other` iff every term of `other` is dominated-or-equal by
478    /// some term of `self` — the standard antichain (Pareto) comparison.
479    pub fn dominates(&self, other: &Growth) -> bool {
480        match (&self.0, &other.0) {
481            (GrowthState::Known(a), GrowthState::Known(b)) => {
482                b.iter().all(|tb| a.iter().any(|ta| ta.dominates_or_eq(tb)))
483            }
484            _ => false,
485        }
486    }
487
488    /// Render this growth class back to a display [`Expr`] (a sum of monomials),
489    /// or `None` for unknown growth. Terms are already in the deterministic
490    /// sort order, so the rendered expression is platform-stable.
491    ///
492    /// Exponential factors are rendered directly from their authoritative
493    /// symbolic bases and coefficients; no base reconstruction is performed.
494    pub fn to_expr(&self) -> Option<Expr> {
495        match &self.0 {
496            GrowthState::Unknown(_) => None,
497            GrowthState::Known(terms) => {
498                if terms.is_empty() {
499                    return Some(Expr::integer(1));
500                }
501                let mut it = terms.iter().map(term_to_expr);
502                let mut acc = it.next().unwrap();
503                for e in it {
504                    acc = acc + e;
505                }
506                Some(acc)
507            }
508        }
509    }
510
511    /// Canonical Big-O string for this growth class: `O(<expr>)` for a bounded
512    /// class, or `O(?)` for unknown growth (no honest asymptotic bound —
513    /// nonlinear exponent or factorial). This is the single source of truth for
514    /// how a growth is displayed as Big-O; presentation layers must call it rather
515    /// than re-deriving the mapping (and the `Unknown` spelling) themselves.
516    pub fn to_big_o(&self) -> String {
517        match self.to_expr() {
518            Some(e) => format!("O({e})"),
519            None => "O(?)".to_string(),
520        }
521    }
522}
523
524fn growth_from_analysis(
525    expression: &Expr,
526    analysis: &AlgebraicAnalysis,
527    memo: &mut HashMap<ExprNodeId, Growth>,
528) -> Growth {
529    if let Some(growth) = memo.get(&expression.node_identity()) {
530        return growth.clone();
531    }
532
533    let facts = analysis.facts(expression);
534    if facts.is_constant {
535        let growth = if facts.constant_domain == Some(true) {
536            constant_growth()
537        } else {
538            unknown(GrowthFailure::InvalidConstantDomain {
539                expression: expression.to_string(),
540            })
541        };
542        memo.insert(expression.node_identity(), growth.clone());
543        return growth;
544    }
545
546    let growth = match expression.node() {
547        ExprNode::Const(_) => unreachable!("constants are handled before node projection"),
548        ExprNode::Var(variable) => {
549            let mut term = GrowthTerm::one();
550            term.insert(
551                variable.as_str().into(),
552                VariableGrowth::polynomial(BigRational::one()),
553            );
554            exact_growth(vec![term])
555        }
556        ExprNode::Add(values) => values
557            .iter()
558            .map(|value| growth_from_analysis(value, analysis, memo))
559            .reduce(add)
560            .expect("normalized sum has at least two terms"),
561        ExprNode::Mul(values) => values
562            .iter()
563            .map(|value| growth_from_analysis(value, analysis, memo))
564            .reduce(mul)
565            .expect("normalized product has at least two factors"),
566        ExprNode::Pow(base, exponent) => {
567            let base_facts = analysis.facts(base);
568            let exponent_facts = analysis.facts(exponent);
569            if base_facts.is_constant && base_facts.constant_domain != Some(true) {
570                unknown(GrowthFailure::InvalidConstantDomain {
571                    expression: base.to_string(),
572                })
573            } else if exponent_facts.is_constant && exponent_facts.constant_domain != Some(true) {
574                unknown(GrowthFailure::InvalidConstantDomain {
575                    expression: exponent.to_string(),
576                })
577            } else if let Some(power) = exponent_facts.exact_rational.as_ref() {
578                if power.is_negative() {
579                    unknown(GrowthFailure::NegativeExponent(exponent.to_string()))
580                } else {
581                    pow_const(growth_from_analysis(base, analysis, memo), power)
582                }
583            } else if let ExprNode::Exp(argument) = base.node() {
584                match analysis.facts(argument).exact_rational.as_ref() {
585                    Some(coefficient) => exponential(
586                        ExpBase::Natural,
587                        scale_growth_linear(exponent_facts.linear.clone(), coefficient),
588                        exponent,
589                    ),
590                    None => unknown(GrowthFailure::InvalidExponentialBase(base.to_string())),
591                }
592            } else if let Some(base_value) = base_facts.exact_rational.as_ref() {
593                if base_value.is_positive() {
594                    exponential(
595                        ExpBase::Rational(base_value.clone()),
596                        growth_linear(exponent_facts.linear.clone()),
597                        exponent,
598                    )
599                } else {
600                    unknown(GrowthFailure::InvalidExponentialBase(base.to_string()))
601                }
602            } else if base_facts.is_constant {
603                unknown(GrowthFailure::InvalidExponentialBase(base.to_string()))
604            } else {
605                unknown(GrowthFailure::VariableBaseAndExponent(
606                    expression.to_string(),
607                ))
608            }
609        }
610        ExprNode::Exp(value) => {
611            let value_growth = growth_from_analysis(value, analysis, memo);
612            if value_growth.failures().is_some() {
613                value_growth
614            } else {
615                exponential(
616                    ExpBase::Natural,
617                    growth_linear(analysis.facts(value).linear.clone()),
618                    expression,
619                )
620            }
621        }
622        ExprNode::Log(value) => log_growth(growth_from_analysis(value, analysis, memo)),
623        ExprNode::Factorial(_) => unknown(GrowthFailure::FactorialOfNonconstant(
624            expression.to_string(),
625        )),
626    };
627    memo.insert(expression.node_identity(), growth.clone());
628    growth
629}
630
631fn growth_linear(
632    linear: Option<BTreeMap<crate::expr::Symbol, BigRational>>,
633) -> Option<BTreeMap<Box<str>, BigRational>> {
634    Some(
635        linear?
636            .into_iter()
637            .map(|(symbol, coefficient)| (symbol.as_str().into(), coefficient))
638            .collect(),
639    )
640}
641
642fn scale_growth_linear(
643    linear: Option<BTreeMap<crate::expr::Symbol, BigRational>>,
644    coefficient: &BigRational,
645) -> Option<BTreeMap<Box<str>, BigRational>> {
646    Some(
647        linear?
648            .into_iter()
649            .map(|(symbol, value)| (symbol.as_str().into(), coefficient * value))
650            .collect(),
651    )
652}
653fn constant_growth() -> Growth {
654    exact_growth(vec![GrowthTerm::one()])
655}
656
657fn unknown(failure: GrowthFailure) -> Growth {
658    Growth::unknown(failure)
659}
660
661fn merge_unknown(left: Growth, right: Growth) -> Growth {
662    let mut failures = Vec::new();
663    if let GrowthState::Unknown(left) = left.0 {
664        failures.extend(left);
665    }
666    if let GrowthState::Unknown(right) = right.0 {
667        failures.extend(right);
668    }
669    failures.sort();
670    failures.dedup();
671    Growth(GrowthState::Unknown(failures))
672}
673
674/// Render one monomial as a product of its factors (or `Const(1)` when empty).
675fn term_to_expr(t: &GrowthTerm) -> Expr {
676    let mut factors: Vec<Expr> = Vec::new();
677    for (variable, growth) in &t.variables {
678        factors.extend(
679            growth
680                .exp
681                .iter()
682                .map(|(base, coefficient)| exp_factor(variable, base, coefficient)),
683        );
684        if !growth.poly.is_zero() {
685            factors.push(poly_factor(variable, &growth.poly));
686        }
687        if growth.log != 0 {
688            factors.push(log_factor(variable, growth.log));
689        }
690    }
691    let mut it = factors.into_iter();
692    match it.next() {
693        None => Expr::integer(1),
694        Some(first) => it.fold(first, |acc, f| acc * f),
695    }
696}
697
698/// Render a stored exponential factor without changing its base or coefficient.
699fn exp_factor(v: &str, base: &ExpBase, coefficient: &BigRational) -> Expr {
700    let exponent = if coefficient.is_one() {
701        Expr::variable(v)
702    } else {
703        Expr::constant(coefficient.clone()) * Expr::variable(v)
704    };
705    match base {
706        ExpBase::Rational(base) => Expr::pow(Expr::constant(base.clone()), exponent),
707        ExpBase::Natural => Expr::exp(exponent),
708    }
709}
710
711/// Render `v^degree` (`Display` turns degree `0.5` into `sqrt(v)`).
712fn poly_factor(v: &str, degree: &BigRational) -> Expr {
713    if degree.is_one() {
714        Expr::variable(v)
715    } else {
716        Expr::pow(Expr::variable(v), Expr::constant(degree.clone()))
717    }
718}
719
720/// Render `(log v)^power`.
721fn log_factor(v: &str, power: u32) -> Expr {
722    let log = Expr::log(Expr::variable(v));
723    if power == 1 {
724        log
725    } else {
726        Expr::pow(log, Expr::integer(power))
727    }
728}
729
730/// Prune a bag of terms to its maximal antichain: drop any term dominated by
731/// another and collapse exact duplicates. The resulting *set* is independent of
732/// input order.
733fn prune(mut terms: Vec<GrowthTerm>) -> Vec<GrowthTerm> {
734    // Proven-equal terms can retain different symbolic spellings (for example,
735    // `exp(n)` and a literal-e base). Sort first so the representative does not
736    // depend on operand order.
737    terms.sort();
738    let mut result: Vec<GrowthTerm> = Vec::new();
739    for t in terms {
740        if result.iter().any(|r| r.dominates_or_eq(&t)) {
741            continue;
742        }
743        result.retain(|r| !t.dominates(r));
744        result.push(t);
745    }
746    result
747}
748
749fn exact_growth(terms: Vec<GrowthTerm>) -> Growth {
750    finish_growth(terms)
751}
752
753fn finish_growth(terms: Vec<GrowthTerm>) -> Growth {
754    let terms = prune(terms);
755    if terms.len() > ANTICHAIN_CAP {
756        return unknown(GrowthFailure::AntichainLimitExceeded {
757            limit: ANTICHAIN_CAP,
758            terms: terms.len(),
759        });
760    }
761    Growth(GrowthState::Known(terms))
762}
763
764/// Antichain union (asymptotic `+ ≍ max`).
765fn add(a: Growth, b: Growth) -> Growth {
766    if a.failures().is_some() || b.failures().is_some() {
767        return merge_unknown(a, b);
768    }
769    let mut terms = into_terms(a);
770    terms.extend(into_terms(b));
771    finish_growth(terms)
772}
773
774/// Pairwise product of two antichains.
775fn mul(a: Growth, b: Growth) -> Growth {
776    if a.failures().is_some() || b.failures().is_some() {
777        return merge_unknown(a, b);
778    }
779    let x = into_terms(a);
780    let y = into_terms(b);
781    let mut product = Vec::with_capacity(x.len() * y.len());
782    for tx in &x {
783        for ty in &y {
784            match tx.mul(ty) {
785                Ok(term) => product.push(term),
786                Err(failure) => return unknown(failure),
787            }
788        }
789    }
790    finish_growth(product)
791}
792
793/// Raise a whole antichain to a nonnegative real power `k` (raise each term).
794fn pow_const(g: Growth, k: &BigRational) -> Growth {
795    match g.0 {
796        GrowthState::Unknown(failures) => Growth(GrowthState::Unknown(failures)),
797        GrowthState::Known(terms) => match terms.iter().map(|term| term.pow(k)).collect() {
798            Ok(terms) => finish_growth(terms),
799            Err(failure) => unknown(failure),
800        },
801    }
802}
803
804fn into_terms(growth: Growth) -> Vec<GrowthTerm> {
805    match growth.0 {
806        GrowthState::Known(terms) => terms,
807        GrowthState::Unknown(_) => unreachable!("unknown growth is handled before term access"),
808    }
809}
810
811/// Transfer function for a symbolic fixed-base exponential.
812fn exponential(
813    base: ExpBase,
814    linear: Option<BTreeMap<Box<str>, BigRational>>,
815    exponent: &Expr,
816) -> Growth {
817    let direction = base.direction();
818    if direction == Ordering::Equal {
819        // 1^x = 1 for every x: bounded by O(1).
820        return exact_growth(vec![GrowthTerm::one()]);
821    }
822    match linear {
823        None => unknown(GrowthFailure::NonlinearExponent(exponent.to_string())),
824        Some(coeffs) => {
825            let mut term = GrowthTerm::one();
826            for (v, coeff) in coeffs {
827                if (direction == Ordering::Greater && coeff.is_positive())
828                    || (direction == Ordering::Less && coeff.is_negative())
829                {
830                    term.insert(v, VariableGrowth::exponential(base.clone(), coeff));
831                } else if !coeff.is_zero() {
832                    return unknown(GrowthFailure::DecayingExponential {
833                        base: match &base {
834                            ExpBase::Rational(value) => value.to_string(),
835                            ExpBase::Natural => "e".to_string(),
836                        },
837                        variable: v.to_string(),
838                        coefficient: coeff.to_string(),
839                    });
840                }
841            }
842            exact_growth(vec![term])
843        }
844    }
845}
846
847/// Transfer function for `Log(a)`: `log` of an antichain is `log` of its
848/// dominant term(s), unioned. Uses `log(n^a · m^b) ≍ log n + log m` and
849/// `log(2^(r·n)) ≍ n`.
850fn log_growth(g: Growth) -> Growth {
851    match g.0 {
852        GrowthState::Unknown(failures) => Growth(GrowthState::Unknown(failures)),
853        GrowthState::Known(terms) => {
854            let mut out = Vec::new();
855            for t in &terms {
856                out.extend(log_term(t));
857            }
858            if out.is_empty() {
859                out.push(GrowthTerm::one()); // log(O(1)) = O(1)
860            }
861            finish_growth(out)
862        }
863    }
864}
865
866/// `log` of a single monomial, returned as its own (small) antichain of
867/// summands. `log(∏ baseᵢ^(rᵢ·vᵢ) · ∏vⱼ^aⱼ · ∏(log vₖ)^sₖ)` distributes over the
868/// product into a *sum* of the log of each factor, so every factor class of the
869/// monomial contributes its own summand — none may be dropped (e.g. `log(2^n·m)`
870/// is `n + log m`, not `n`). `finish_growth`/`prune` then collapse any dominated
871/// summands (so `log(2^n·n^2)` reduces back to `n`).
872fn log_term(t: &GrowthTerm) -> Vec<GrowthTerm> {
873    let mut out = Vec::new();
874    for (variable, growth) in &t.variables {
875        if !growth.exp.is_empty() {
876            let mut term = GrowthTerm::one();
877            term.insert(
878                variable.clone(),
879                VariableGrowth::polynomial(BigRational::one()),
880            );
881            out.push(term);
882        }
883        if growth.poly.is_positive() || growth.log != 0 {
884            let mut term = GrowthTerm::one();
885            term.insert(variable.clone(), VariableGrowth::logarithmic(1));
886            out.push(term);
887        }
888    }
889    // Empty term: log(O(1)) = O(1).
890    if out.is_empty() {
891        out.push(GrowthTerm::one());
892    }
893    out
894}
895
896#[cfg(test)]
897#[path = "unit_tests/growth.rs"]
898mod tests;