Skip to main content

tract_data/dim/
tree.rs

1use crate::dim::Assertion;
2use crate::internal::*;
3
4use super::{DimLike, sym::*};
5use itertools::Itertools;
6use num_integer::Integer;
7use num_traits::{AsPrimitive, PrimInt, Zero};
8use std::cmp::Ordering;
9use std::collections::{HashMap, HashSet};
10use std::fmt::Debug;
11use std::ops::Neg;
12use std::{fmt, ops};
13
14#[derive(Debug)]
15pub enum TooEarly {
16    UndeterminedSymbol(String),
17    Other(String),
18}
19
20impl std::fmt::Display for TooEarly {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            TooEarly::UndeterminedSymbol(s) => write!(f, "Undetermined symbol in expression: {s}"),
24            TooEarly::Other(s) => write!(f, "{s}"),
25        }
26    }
27}
28
29impl std::error::Error for TooEarly {}
30
31macro_rules! b( ($e:expr) => { Box::new($e) } );
32
33// `Hash` stays structural while `PartialEq` accepts an algebraic second chance:
34// see the `PartialEq` impl below for the rationale (the simplifier's internal
35// `HashMap<TDim, _>` only ever compares within same-canonical-form buckets, so
36// the standard `a == b => hash(a) == hash(b)` contract being violated outside
37// that path is acceptable here).
38#[allow(clippy::derived_hash_with_manual_eq)]
39#[derive(Clone, Eq, Hash, Debug)]
40pub enum TDim {
41    Val(i64),
42    Sym(Symbol),
43    Add(Vec<TDim>),
44    Mul(Vec<TDim>),
45    MulInt(i64, Box<TDim>),
46    Div(Box<TDim>, u64),
47    Broadcast(Vec<TDim>),
48    Min(Vec<TDim>),
49    Max(Vec<TDim>),
50    /// Comparison: evaluates to 1 (true) or 0 (false). lhs >= rhs
51    Ge(Box<TDim>, Box<TDim>),
52    /// Comparison: evaluates to 1 (true) or 0 (false). lhs == rhs
53    Eq(Box<TDim>, Box<TDim>),
54}
55
56use TDim::*;
57
58/// Structural equality on the TDim tree — what `#[derive(PartialEq)]` would
59/// produce.  Used as the fast-path inside `PartialEq` (and by the simplifier's
60/// internal `HashMap<TDim, _>`, which compares within same-hash buckets where
61/// structural equality is the only thing that matters).
62fn eq_structural(a: &TDim, b: &TDim) -> bool {
63    match (a, b) {
64        (Val(x), Val(y)) => x == y,
65        (Sym(x), Sym(y)) => x == y,
66        (Add(x), Add(y))
67        | (Mul(x), Mul(y))
68        | (Broadcast(x), Broadcast(y))
69        | (Min(x), Min(y))
70        | (Max(x), Max(y)) => {
71            x.len() == y.len() && x.iter().zip(y).all(|(a, b)| eq_structural(a, b))
72        }
73        (MulInt(p, x), MulInt(q, y)) => p == q && eq_structural(x, y),
74        (Div(x, p), Div(y, q)) => p == q && eq_structural(x, y),
75        (Ge(a, b), Ge(c, d)) | (Eq(a, b), Eq(c, d)) => eq_structural(a, c) && eq_structural(b, d),
76        _ => false,
77    }
78}
79
80// Thread-local guard: while simplifying the difference inside `eq`, fall back
81// to the structural-only path for any nested `==` calls.  Without this guard
82// the simplifier's internal `HashMap<TDim, i64>` would re-enter `eq` from
83// inside `(self - other).simplify()`, recursing without bound on
84// non-structurally-equal inputs.
85std::thread_local! {
86    static EQ_GUARD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
87}
88
89impl PartialEq for TDim {
90    fn eq(&self, other: &Self) -> bool {
91        // Fast path: structural tree equality.
92        if eq_structural(self, other) {
93            return true;
94        }
95        // Inside an enclosing simplification triggered by a previous
96        // second-chance call, fall back to structural equality only.
97        if EQ_GUARD.with(|g| g.get()) {
98            return false;
99        }
100        // Skip second-chance when either side is a leaf (`Val` or `Sym`).
101        // For `Val(c)` vs anything non-`Val`: if they were semantically
102        // equal, the simplifier should already have folded the other side
103        // to `Val(c)`; running a diff-and-simplify here just risks
104        // arithmetic overflow on extreme constants (e.g. the simplifier
105        // filters against `Val(i64::MAX)`/`Val(i64::MIN)` sentinels).
106        // For `Sym(x)` leaves, assertion-driven equality belongs in
107        // `simplify`, not in `eq`.
108        if matches!(self, Val(_) | Sym(_)) || matches!(other, Val(_) | Sym(_)) {
109            return false;
110        }
111        // Second chance: prove the difference simplifies to zero.  Two
112        // algebraically equal TDims often arrive at different canonical
113        // forms via different construction paths (e.g. `1 + (7S+3)/4` and
114        // `((S+1)*7)/4` after blockify substitutes T → k·S in encoder
115        // shapes).  Subtracting and simplifying lets the existing
116        // simplifier rules cancel them out.
117        EQ_GUARD.with(|g| g.set(true));
118        let diff = (self.clone() - other.clone()).simplify();
119        EQ_GUARD.with(|g| g.set(false));
120        matches!(diff, Val(0))
121    }
122}
123
124fn tdim_lexi_order(a: &TDim, b: &TDim) -> Ordering {
125    match (a, b) {
126        (Sym(a), Sym(b)) => a.cmp(b),
127        (Val(a), Val(b)) => a.cmp(b),
128        (Add(a), Add(b))
129        | (Mul(a), Mul(b))
130        | (Broadcast(a), Broadcast(b))
131        | (Min(a), Min(b))
132        | (Max(a), Max(b)) => a.len().cmp(&b.len()).then(
133            a.iter()
134                .zip(b.iter())
135                .fold(Ordering::Equal, |acc, (a, b)| acc.then_with(|| tdim_lexi_order(a, b))),
136        ),
137        (MulInt(p, d), MulInt(q, e)) => p.cmp(q).then_with(|| tdim_lexi_order(d, e)),
138        (Div(d, p), Div(e, q)) => p.cmp(q).then_with(|| tdim_lexi_order(d, e)),
139        (Sym(_), _) => Ordering::Less,
140        (_, Sym(_)) => Ordering::Greater,
141        (Val(_), _) => Ordering::Less,
142        (_, Val(_)) => Ordering::Greater,
143        (Add(_), _) => Ordering::Less,
144        (_, Add(_)) => Ordering::Greater,
145        (Mul(_), _) => Ordering::Less,
146        (_, Mul(_)) => Ordering::Greater,
147        (MulInt(_, _), _) => Ordering::Less,
148        (_, MulInt(_, _)) => Ordering::Greater,
149        (Broadcast(_), _) => Ordering::Less,
150        (_, Broadcast(_)) => Ordering::Greater,
151        (Min(_), _) => Ordering::Less,
152        (_, Min(_)) => Ordering::Greater,
153        (Max(_), _) => Ordering::Less,
154        (_, Max(_)) => Ordering::Greater,
155        (Ge(a1, b1), Ge(a2, b2)) | (Eq(a1, b1), Eq(a2, b2)) => {
156            tdim_lexi_order(a1, a2).then_with(|| tdim_lexi_order(b1, b2))
157        }
158        (Ge(_, _) | Eq(_, _), _) => Ordering::Less,
159        (_, Ge(_, _) | Eq(_, _)) => Ordering::Greater,
160    }
161}
162
163/// `Div(Add(terms), q)` — try to extract every `MulInt(c, X)` where `c % q == 0`
164/// out of the Div, leaving only a constant remainder in `[0, q)`.
165///
166/// Returns `Some(simplified)` when the residual constant is in `[0, q)` and
167/// every extracted symbolic factor `X` is provably non-negative — both
168/// conditions are required for soundness under tract's truncating
169/// division (`Rust i64 /`):
170///
171/// * the constant being in `[0, q)` makes `c/q_trunc = 0`;
172/// * `X ≥ 0` makes the identity `(k·X + c)/k_trunc = X` hold (it fails
173///   at e.g. `X = -1, k = 2, c = 0` because truncation rounds toward zero).
174///
175/// The `Val` arm above already handles constants outside `[0, q)`, so by
176/// the time we get here `terms` contains at most one `Val` and any number
177/// of `MulInt(c, X)` / other shapes.
178fn try_divide_multiple_plus_remainder(
179    terms: &[TDim],
180    q: u64,
181    scope: &SymbolScopeData,
182    extra: &[Assertion],
183) -> Option<TDim> {
184    let mut quotients: Vec<TDim> = vec![];
185    let mut const_rem: i64 = 0;
186    let mut any_extracted = false;
187    for term in terms {
188        match term {
189            MulInt(c, x) if *c != 0 && c.rem_euclid(q as i64) == 0 => {
190                if !scope.prove_positive_or_zero_with_extra(x, extra) {
191                    return None;
192                }
193                let new_coeff = c / (q as i64);
194                quotients.push(if new_coeff == 1 {
195                    (**x).clone()
196                } else if new_coeff == -1 {
197                    MulInt(-1, x.clone())
198                } else {
199                    MulInt(new_coeff, x.clone())
200                });
201                any_extracted = true;
202            }
203            Val(v) => const_rem += v,
204            _ => return None,
205        }
206    }
207    if !any_extracted {
208        return None;
209    }
210    if !(0..q as i64).contains(&const_rem) {
211        return None;
212    }
213    Some(if quotients.len() == 1 { quotients.remove(0) } else { Add(quotients) })
214}
215
216impl fmt::Display for TDim {
217    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
218        match &self {
219            Sym(sym) => write!(fmt, "{sym}"),
220            Val(it) => write!(fmt, "{it}"),
221            Add(it) => write!(fmt, "{}", it.iter().map(|x| format!("{x}")).join("+")),
222            Mul(it) => write!(fmt, "{}", it.iter().map(|x| format!("({x})")).join("*")),
223            Broadcast(it) => {
224                write!(fmt, "broadcast({})", it.iter().map(|x| format!("({x})")).join(", "))
225            }
226            Min(it) => write!(fmt, "min({})", it.iter().map(|x| format!("{x}")).join(",")),
227            Max(it) => write!(fmt, "max({})", it.iter().map(|x| format!("{x}")).join(",")),
228            MulInt(a, b) => write!(fmt, "{a}*{b}"),
229            Div(a, b) => write!(fmt, "({a})/{b}"),
230            Ge(a, b) => write!(fmt, "({a}>={b})"),
231            Eq(a, b) => write!(fmt, "({a}=={b})"),
232        }
233    }
234}
235
236impl TDim {
237    #[inline]
238    pub fn is_one(&self) -> bool {
239        matches!(self, Val(1))
240    }
241
242    #[inline]
243    /// Concrete value of the dim, or a lightweight `TooEarly` for a symbolic
244    /// one. The error is a plain enum — no anyhow conversion, no message
245    /// formatting, and (crucially) no backtrace — so probing concreteness by
246    /// discarding the error (`.ok()`, `if let Ok`) is cheap. Use `?` in a
247    /// `TractResult` context to promote a genuine failure to a full error.
248    pub fn to_i64(&self) -> Result<i64, TooEarly> {
249        if let Val(v) = self { Ok(*v) } else { Err(TooEarly::UndeterminedSymbol(self.to_string())) }
250    }
251
252    #[inline]
253    pub fn as_i64(&self) -> Option<i64> {
254        if let Val(v) = self { Some(*v) } else { None }
255    }
256
257    /// Non-erroring counterpart of `eval_to_i64`: returns `None` instead of
258    /// building an error when a symbol is undetermined or arithmetic overflows.
259    /// Use this on hot paths that only want the constant value and discard the
260    /// reason for failure, so no message string is formatted and (crucially,
261    /// when `RUST_BACKTRACE` is set) no backtrace is captured.
262    pub fn maybe_eval_to_i64(&self, values: &SymbolValues) -> Option<i64> {
263        match self {
264            Sym(sym) => values.get(sym),
265            Val(v) => Some(*v),
266            Add(terms) => terms
267                .iter()
268                .try_fold(0i64, |acc, it| acc.checked_add(it.maybe_eval_to_i64(values)?)),
269            Mul(terms) => terms
270                .iter()
271                .try_fold(1i64, |acc, it| acc.checked_mul(it.maybe_eval_to_i64(values)?)),
272            Min(terms) => terms
273                .iter()
274                .try_fold(i64::MAX, |acc, it| Some(acc.min(it.maybe_eval_to_i64(values)?))),
275            Max(terms) => terms
276                .iter()
277                .try_fold(i64::MIN, |acc, it| Some(acc.max(it.maybe_eval_to_i64(values)?))),
278            Broadcast(terms) => terms.iter().try_fold(1i64, |acc, it| {
279                (acc as usize)
280                    .broadcast(it.maybe_eval_to_i64(values)? as usize)
281                    .ok()
282                    .map(|x| x as i64)
283            }),
284            Div(a, q) => Some(a.maybe_eval_to_i64(values)? / *q as i64),
285            MulInt(p, a) => a.maybe_eval_to_i64(values)?.checked_mul(*p),
286            Ge(a, b) => Some((a.maybe_eval_to_i64(values)? >= b.maybe_eval_to_i64(values)?) as i64),
287            Eq(a, b) => Some((a.maybe_eval_to_i64(values)? == b.maybe_eval_to_i64(values)?) as i64),
288        }
289    }
290
291    pub fn eval_to_i64(&self, values: &SymbolValues) -> TractResult<i64> {
292        match self {
293            Sym(sym) => {
294                let Some(v) = values.get(sym) else {
295                    Err(TooEarly::UndeterminedSymbol(self.to_string()))?
296                };
297                Ok(v)
298            }
299            Val(v) => Ok(*v),
300            Add(terms) => terms.iter().try_fold(0i64, |acc, it| {
301                let x = it.eval_to_i64(values)?;
302                acc.checked_add(x)
303                    .with_context(|| format!("Overflow in TDim addition ({acc} + {x})"))
304            }),
305            Mul(terms) => terms.iter().try_fold(1i64, |acc, it| {
306                let x = it.eval_to_i64(values)?;
307                acc.checked_mul(x)
308                    .with_context(|| format!("Overflow in TDim multiplication ({acc} * {x})"))
309            }),
310            Min(terms) => terms
311                .iter()
312                .try_fold(i64::MAX, |acc, it| it.eval_to_i64(values).map(|x| acc.min(x))),
313            Max(terms) => terms
314                .iter()
315                .try_fold(i64::MIN, |acc, it| it.eval_to_i64(values).map(|x| acc.max(x))),
316            Broadcast(terms) => terms.iter().try_fold(1i64, |acc, it| {
317                it.eval_to_i64(values)
318                    .and_then(|x| ((acc as usize).broadcast(x as usize)).map(|x| x as i64))
319            }),
320            Div(a, q) => Ok(a.eval_to_i64(values)? / *q as i64),
321            MulInt(p, a) => {
322                let x = a.eval_to_i64(values)?;
323                x.checked_mul(*p)
324                    .with_context(|| format!("Overflow in TDim multiplication ({x} * {p})"))
325            }
326            Ge(a, b) => Ok(if a.eval_to_i64(values)? >= b.eval_to_i64(values)? { 1 } else { 0 }),
327            Eq(a, b) => Ok(if a.eval_to_i64(values)? == b.eval_to_i64(values)? { 1 } else { 0 }),
328        }
329    }
330
331    pub fn eval(&self, values: &SymbolValues) -> TDim {
332        match self {
333            Sym(sym) => values.get(sym).map(Val).unwrap_or_else(|| Sym(sym.clone())),
334            Val(v) => Val(*v),
335            Add(terms) => terms.iter().fold(Val(0), |acc, it| -> TDim { acc + it.eval(values) }),
336            Mul(terms) => terms.iter().fold(Val(1), |acc, it| -> TDim { acc * it.eval(values) }),
337            Min(terms) => {
338                terms.iter().fold(Val(i64::MAX), |acc, it| -> TDim { acc.mini(it.eval(values)) })
339            }
340            Max(terms) => {
341                terms.iter().fold(Val(i64::MIN), |acc, it| -> TDim { acc.maxi(it.eval(values)) })
342            }
343            Broadcast(terms) => terms.iter().fold(Val(1), |acc, it| -> TDim {
344                acc.broadcast(it.eval(values)).unwrap_or_else(|_| self.clone())
345            }),
346            Div(a, q) => a.eval(values) / *q as i64,
347            MulInt(p, a) => a.eval(values) * *p,
348            Ge(a, b) => {
349                let a2 = a.eval(values);
350                let b2 = b.eval(values);
351                if let (Val(av), Val(bv)) = (&a2, &b2) {
352                    Val(if av >= bv { 1 } else { 0 })
353                } else {
354                    Ge(b!(a2), b!(b2))
355                }
356            }
357            Eq(a, b) => {
358                let a2 = a.eval(values);
359                let b2 = b.eval(values);
360                if let (Val(av), Val(bv)) = (&a2, &b2) {
361                    Val(if av == bv { 1 } else { 0 })
362                } else {
363                    Eq(b!(a2), b!(b2))
364                }
365            }
366        }
367    }
368
369    pub fn eval_with_scenario(&self, scenario: &str) -> TDim {
370        if let Val(v) = self {
371            return Val(*v);
372        }
373        let scope = self.find_scope().unwrap();
374        let scope = scope.0;
375        let locked = scope.lock();
376        let scope = locked.borrow();
377        self.clone().simplify_rec(&scope, Some(scenario), &[])
378    }
379
380    pub fn substitute(&self, from: &Symbol, to: &Self) -> TractResult<Self> {
381        self.substitute_all(&std::collections::HashMap::from([(from.clone(), to.clone())]))
382    }
383
384    pub fn substitute_all(
385        &self,
386        map: &std::collections::HashMap<Symbol, Self>,
387    ) -> TractResult<Self> {
388        match self {
389            Sym(sym) => Ok(map.get(sym).cloned().unwrap_or_else(|| self.clone())),
390            Val(v) => Ok(Val(*v)),
391            Add(terms) => terms.iter().try_fold(Val(0), |acc, it| -> TractResult<TDim> {
392                Ok(acc + it.substitute_all(map)?)
393            }),
394            Mul(terms) => terms.iter().try_fold(Val(1), |acc, it| -> TractResult<TDim> {
395                Ok(acc * it.substitute_all(map)?)
396            }),
397            Broadcast(terms) => terms.iter().try_fold(Val(1), |acc, it| -> TractResult<TDim> {
398                acc.broadcast(it.substitute_all(map)?)
399            }),
400            Min(terms) => terms.iter().try_fold(Val(i64::MAX), |acc, it| -> TractResult<TDim> {
401                Ok(acc.mini(it.substitute_all(map)?))
402            }),
403            Max(terms) => terms.iter().try_fold(Val(i64::MIN), |acc, it| -> TractResult<TDim> {
404                Ok(acc.maxi(it.substitute_all(map)?))
405            }),
406            Div(a, q) => Ok(a.substitute_all(map)? / *q as i64),
407            MulInt(p, a) => Ok(a.substitute_all(map)? * *p),
408            Ge(a, b) => Ok(Ge(b!(a.substitute_all(map)?), b!(b.substitute_all(map)?))),
409            Eq(a, b) => Ok(Eq(b!(a.substitute_all(map)?), b!(b.substitute_all(map)?))),
410        }
411    }
412
413    pub fn reduce(self) -> TDim {
414        self.simplify()
415            .wiggle()
416            .into_iter()
417            .sorted_by(tdim_lexi_order)
418            .unique()
419            .map(|e| e.simplify())
420            .min_by_key(|e| e.cost())
421            .unwrap()
422    }
423
424    fn cost(&self) -> usize {
425        use self::TDim::*;
426        match self {
427            Sym(_) | Val(_) => 1,
428            Add(terms) => 2 * terms.iter().map(TDim::cost).sum::<usize>(),
429            Mul(terms) => 3 * terms.iter().map(TDim::cost).sum::<usize>(),
430            Broadcast(terms) => 4 * terms.iter().map(TDim::cost).sum::<usize>(),
431            Min(terms) | Max(terms) => 5 * terms.iter().map(TDim::cost).sum::<usize>(),
432            Div(a, _) => 3 * a.cost(),
433            MulInt(_, a) => 2 * a.cost(),
434            Ge(a, b) | Eq(a, b) => 5 * (a.cost() + b.cost()),
435        }
436    }
437
438    fn wiggle(&self) -> Vec<TDim> {
439        use self::TDim::*;
440        match self {
441            Sym(_) | Val(_) | Mul(_) | Broadcast(_) | Min(_) | Max(_) | Ge(_, _) | Eq(_, _) => {
442                vec![self.clone()]
443            }
444            Add(terms) => {
445                let mut forms = vec![];
446                let sub_exprs = terms.iter().map(|e| e.wiggle()).multi_cartesian_product();
447
448                fn first_div_term(terms: &[TDim]) -> Option<(usize, &TDim, u64)> {
449                    terms.iter().enumerate().find_map(|(index, t)| match t {
450                        Div(numerator, quotient) => Some((index, &**numerator, *quotient)),
451                        _ => None,
452                    })
453                }
454
455                fn generate_new_numerator(
456                    div_index: usize,
457                    numerator: &TDim,
458                    quotient: u64,
459                    expr: &[TDim],
460                ) -> Vec<TDim> {
461                    expr.iter()
462                        .enumerate()
463                        .map(|(index, term)| {
464                            if index == div_index {
465                                numerator.clone()
466                            } else {
467                                MulInt(quotient as i64, Box::new(term.clone()))
468                            }
469                        })
470                        .collect()
471                }
472
473                for expr in sub_exprs {
474                    if let Some((div_index, numerator, quotient)) = first_div_term(&expr) {
475                        let new_numerator =
476                            generate_new_numerator(div_index, numerator, quotient, &expr);
477                        forms.push(Div(Box::new(Add(new_numerator)), quotient))
478                    }
479
480                    forms.push(Add(expr));
481                }
482                forms
483            }
484            MulInt(p, a) => a.wiggle().into_iter().map(|a| MulInt(*p, b!(a))).collect(),
485            Div(a, q) => {
486                let mut forms = vec![];
487                for num in a.wiggle() {
488                    if let Add(terms) = &num {
489                        let (integer, non_integer): (Vec<_>, Vec<_>) =
490                            terms.iter().cloned().partition(|a| a.gcd() % q == 0);
491                        // Skip when the non-integer bucket holds a constant:
492                        // under tract's truncating `/`, splitting (k·X+c)/k →
493                        // X + c/k is unsound for negative X (X=-1, k=2, c=1:
494                        // (-1)/2 = 0 ≠ X). The sound version, gated on
495                        // prove_positive_or_zero, lives in simplify_rec::Div
496                        // via try_divide_multiple_plus_remainder. Cases where
497                        // the remainder is purely symbolic (e.g. A%2 → /2
498                        // lowers to (A − 2·(A/2))/2, non_integer=[A]) stay
499                        // here: the emitted Div(non_integer, q) cancels with
500                        // the extracted quotient and reduces to zero.
501                        if !non_integer.iter().any(|t| matches!(t, Val(_))) {
502                            let mut new_terms =
503                                integer.iter().map(|i| i.div(*q)).collect::<Vec<_>>();
504                            if non_integer.len() > 0 {
505                                new_terms.push(Div(b!(Add(non_integer)), *q));
506                            }
507                            forms.push(Add(new_terms))
508                        }
509                    }
510                    forms.push(Div(b!(num), *q))
511                }
512                forms
513            }
514        }
515    }
516
517    fn find_any_sym(tdim: &TDim) -> Option<&Symbol> {
518        match tdim {
519            Val(_) => None,
520            Sym(s) => Some(s),
521            Add(terms) | Mul(terms) | Min(terms) | Max(terms) | Broadcast(terms) => {
522                terms.iter().find_map(Self::find_any_sym)
523            }
524            MulInt(_, t) | Div(t, _) => Self::find_any_sym(t),
525            Ge(a, b) | Eq(a, b) => Self::find_any_sym(a).or_else(|| Self::find_any_sym(b)),
526        }
527    }
528
529    pub fn find_scope(&self) -> Option<SymbolScope> {
530        Self::find_any_sym(self).and_then(|s| s.scope().clone())
531    }
532
533    /// Fully distribute every `Mul` of `Add`s in `self` into a flat sum of
534    /// products, then `simplify`.  Used to compare two algebraically equal
535    /// but differently-factored TDims for equality (e.g. Reshape volume
536    /// checks on graphs where the same dimension is built two ways).
537    ///
538    /// Cost can blow up combinatorially on very deeply factored expressions
539    /// — call this only at boundaries where structural equality is needed,
540    /// not as a general-purpose simplifier.
541    pub fn expand_polynomial(self) -> TDim {
542        use self::TDim::*;
543        match self {
544            Mul(terms) => {
545                let terms: Vec<TDim> = terms.into_iter().map(Self::expand_polynomial).collect();
546                if let Some(add_idx) = terms.iter().position(|t| matches!(t, Add(_))) {
547                    let Add(add_terms) = terms[add_idx].clone() else { unreachable!() };
548                    let others: Vec<TDim> = terms
549                        .iter()
550                        .enumerate()
551                        .filter(|(i, _)| *i != add_idx)
552                        .map(|(_, t)| t.clone())
553                        .collect();
554                    Add(add_terms
555                        .into_iter()
556                        .map(|t| {
557                            let mut product = others.clone();
558                            product.push(t);
559                            Mul(product).expand_polynomial()
560                        })
561                        .collect())
562                    .simplify()
563                } else {
564                    Mul(terms).simplify()
565                }
566            }
567            MulInt(c, inner) => MulInt(c, Box::new(inner.expand_polynomial())).simplify(),
568            Add(terms) => Add(terms.into_iter().map(Self::expand_polynomial).collect()).simplify(),
569            Div(a, q) => Div(Box::new(a.expand_polynomial()), q).simplify(),
570            Min(terms) => Min(terms.into_iter().map(Self::expand_polynomial).collect()).simplify(),
571            Max(terms) => Max(terms.into_iter().map(Self::expand_polynomial).collect()).simplify(),
572            Broadcast(terms) => {
573                Broadcast(terms.into_iter().map(Self::expand_polynomial).collect()).simplify()
574            }
575            Ge(a, b) => {
576                Ge(Box::new(a.expand_polynomial()), Box::new(b.expand_polynomial())).simplify()
577            }
578            Eq(a, b) => {
579                Eq(Box::new(a.expand_polynomial()), Box::new(b.expand_polynomial())).simplify()
580            }
581            it @ (Sym(_) | Val(_)) => it,
582        }
583    }
584
585    pub fn simplify(self) -> TDim {
586        use self::TDim::*;
587        if let Some(v) = self.maybe_eval_to_i64(&SymbolValues::default()) {
588            return Val(v);
589        }
590        let Some(scope) = self.find_scope() else {
591            return self;
592        };
593        let scope = scope.0;
594        let locked = scope.lock();
595        let scope = locked.borrow();
596        let it = self.simplify_rec(&scope, None, &[]);
597        let mut current: Option<TDim> = None;
598        for scenario in scope.scenarios() {
599            let v = it.clone().simplify_rec(&scope, Some(scenario), &[]);
600            if current.is_some_and(|c| c != v) {
601                return it;
602            } else {
603                current = Some(v);
604            }
605        }
606        current.unwrap_or(it)
607    }
608
609    pub fn simplify_with_extra_assertions(self, extra: &[Assertion]) -> TDim {
610        use self::TDim::*;
611        if extra.is_empty() {
612            return self.simplify();
613        }
614        if let Some(v) = self.maybe_eval_to_i64(&SymbolValues::default()) {
615            return Val(v);
616        }
617        let Some(scope) = self.find_scope() else {
618            return self;
619        };
620        let scope = scope.0;
621        let locked = scope.lock();
622        let scope = locked.borrow();
623        let it = self.simplify_rec(&scope, None, extra);
624        let mut current: Option<TDim> = None;
625        for scenario in scope.scenarios() {
626            let v = it.clone().simplify_rec(&scope, Some(scenario), extra);
627            if current.is_some_and(|c| c != v) {
628                return it;
629            } else {
630                current = Some(v);
631            }
632        }
633        current.unwrap_or(it)
634    }
635
636    fn simplify_rec(
637        self,
638        scope: &SymbolScopeData,
639        scenario: Option<&str>,
640        extra: &[Assertion],
641    ) -> TDim {
642        match self {
643            Add(mut terms) => {
644                #[allow(clippy::mutable_key_type)]
645                let mut simplified_terms: HashMap<TDim, i64> = HashMap::new();
646                // factorize common sub-expr
647                while let Some(term) = terms.pop() {
648                    let simplified = term.simplify_rec(scope, scenario, extra);
649                    match simplified {
650                        Val(0) => {} // ignore
651                        Add(members) => {
652                            terms.extend(members);
653                            continue;
654                        }
655                        Val(value) => *simplified_terms.entry(Val(1)).or_insert(0) += value,
656                        MulInt(value, factor) => {
657                            *simplified_terms.entry((*factor).clone()).or_insert(0) += value;
658                        }
659                        n => *simplified_terms.entry(n).or_insert(0) += 1,
660                    };
661                }
662
663                pub fn evaluate_count(term: TDim, count: i64) -> Option<TDim> {
664                    match count {
665                        0 => None,
666                        _ if term == TDim::Val(1) => Some(TDim::Val(count)),
667                        1 => Some(term),
668                        _ => Some(TDim::MulInt(count, Box::new(term))),
669                    }
670                }
671
672                // Pull the integer GCD of all term coefficients out as a
673                // common factor: e.g. Add([Val(6), MulInt(14, S)]) becomes
674                // MulInt(2, Add([Val(3), MulInt(7, S)])).  The downstream
675                // Div(MulInt(p, a), q) arm then cancels (p, q) gcd, so
676                // (6 + 14·S) / 8 reduces to (3 + 7·S) / 4 with no special
677                // Div-over-Add rule needed.
678                //
679                // Only consider entries with non-zero counts — zero-count
680                // entries (canceled-out factors) get filtered later, but
681                // would otherwise drag the gcd to spurious values.  Only
682                // factor when at least one surviving entry has a
683                // non-constant key, otherwise the Add reduces to a single
684                // `Val` and wrapping it in `MulInt(g, Val(c/g))` is a
685                // strict regression in canonical form.
686                let has_non_const =
687                    simplified_terms.iter().any(|(k, &c)| c != 0 && !matches!(k, Val(_)));
688                let coef_gcd = if has_non_const {
689                    simplified_terms
690                        .values()
691                        .filter(|&&c| c != 0)
692                        .map(|c| c.unsigned_abs() as i64)
693                        .reduce(|a, b| a.gcd(&b))
694                        .unwrap_or(0)
695                } else {
696                    0
697                };
698                let outer_factor = if coef_gcd > 1 {
699                    for v in simplified_terms.values_mut() {
700                        *v /= coef_gcd;
701                    }
702                    Some(coef_gcd)
703                } else {
704                    None
705                };
706
707                let mut members: Vec<TDim> = simplified_terms
708                    .into_iter()
709                    .filter_map(|(term, count)| evaluate_count(term, count))
710                    .collect();
711                members.sort_by(tdim_lexi_order);
712
713                let inner = match members.len() {
714                    0 => TDim::Val(0),
715                    1 => members.into_iter().next().unwrap(),
716                    _ => TDim::Add(members),
717                };
718                match outer_factor {
719                    None => inner,
720                    Some(_) if matches!(inner, TDim::Val(0)) => TDim::Val(0),
721                    Some(g) => TDim::MulInt(g, Box::new(inner)),
722                }
723            }
724            Mul(terms) => {
725                // Distribute over Add: if exactly one factor is an Add,
726                // expand Mul([a, Add([b, c])]) => Add([Mul([a, b]), Mul([a, c])]).
727                // This lets (T+1)*P simplify to T*P + P, which is needed for
728                // cancellation in expressions like (T+1)*P - T*P.
729                //
730                // Multi-Add Muls are *not* eagerly expanded here — keeping them
731                // factored matters for `maybe_div`'s bag-of-factors path, which
732                // cancels common Add factors symbolically (e.g. dividing
733                // 16B·(1+Y)² by 8B·(1+Y) needs the (1+Y)s to be visible as
734                // factors, not melted into a polynomial sum).  Use
735                // `expand_polynomial` if you need a fully distributed canonical
736                // form for equality comparisons (see Reshape volume check).
737                {
738                    let add_indices: Vec<usize> = terms
739                        .iter()
740                        .enumerate()
741                        .filter(|(_, t)| matches!(t, Add(_)))
742                        .map(|(i, _)| i)
743                        .collect();
744                    if add_indices.len() == 1 {
745                        let add_idx = add_indices[0];
746                        let Add(add_terms) = &terms[add_idx] else { unreachable!() };
747                        let other_factors: Vec<TDim> = terms
748                            .iter()
749                            .enumerate()
750                            .filter(|(i, _)| *i != add_idx)
751                            .map(|(_, t)| t.clone())
752                            .collect();
753                        let distributed: Vec<TDim> = add_terms
754                            .iter()
755                            .map(|at| {
756                                let mut product = other_factors.clone();
757                                product.push(at.clone());
758                                Mul(product)
759                            })
760                            .collect();
761                        return Add(distributed).simplify_rec(scope, scenario, extra);
762                    }
763                }
764
765                // in case a term is a multiplication itself, flatten it
766                // e.g., (a*b)*c => a*b*c, and MulInt(k, x) => Val(k)*x
767                let mut flattened_terms = vec![];
768                for t in terms {
769                    match t.clone().reduce() {
770                        Mul(inner_terms) => flattened_terms.extend(inner_terms),
771                        MulInt(k, inner) => {
772                            flattened_terms.push(Val(k));
773                            flattened_terms.push(*inner);
774                        }
775                        other => flattened_terms.push(other),
776                    }
777                }
778                let mut terms = flattened_terms;
779
780                let mut gcd = Mul(terms.clone()).gcd() as i64;
781                if gcd == 0 {
782                    return Val(0);
783                }
784                terms = if gcd != 1 {
785                    terms
786                        .into_iter()
787                        .map(|t| {
788                            let gcd = t.gcd();
789                            (t / gcd).simplify_rec(scope, scenario, extra)
790                        })
791                        .collect()
792                } else {
793                    terms
794                };
795                if terms.iter().filter(|t| t == &&Val(-1)).count() % 2 == 1 {
796                    gcd = -gcd;
797                }
798                terms.retain(|t| !t.is_one() && t != &Val(-1));
799                terms.sort_by(tdim_lexi_order);
800
801                match (gcd, terms.len()) {
802                    (_, 0) => Val(gcd), // Case #1: If 0 variables, return product
803                    (0, _) => Val(0),   // Case #2: Result is 0 if coef is 0 (actually
804                    // unreachable as we check at the beginning)
805                    (1, 1) => terms.remove(0), // Case #3: Product is 1, so return the only term
806                    (1, _) => Mul(terms), // Case #4: Product is 1, so return the non-integer terms
807                    (_, 1) => MulInt(gcd, Box::new(terms.remove(0))), // Case #5: Single variable, convert to 1 MulInt
808                    _ => MulInt(gcd, Box::new(Mul(terms))), // Case #6: Multiple variables, convert to MulInt
809                }
810            }
811            MulInt(coef, expr) => {
812                match *expr {
813                    MulInt(c2, inner) => {
814                        if let Some(c) = coef.checked_mul(c2) {
815                            return MulInt(c, inner).simplify_rec(scope, scenario, extra);
816                        } else {
817                            return MulInt(coef, Box::new(MulInt(c2, inner)));
818                        }
819                    }
820                    Val(v) => {
821                        return coef
822                            .checked_mul(v)
823                            .map(Val)
824                            .unwrap_or_else(|| MulInt(coef, Box::new(Val(v))));
825                    }
826                    _ => {}
827                }
828
829                let simplified = expr.simplify_rec(scope, scenario, extra);
830                match (coef, simplified) {
831                    (0, _) => Val(0), // Case #1: If coef is 0, return 0
832                    (1, s) => s,      // Case #2: If coef is 1, return the simplified expression
833                    (_, Add(terms)) => Add(terms
834                        .into_iter()
835                        .map(|term| {
836                            MulInt(coef, Box::new(term)).simplify_rec(scope, scenario, extra)
837                        })
838                        .collect()), // Case #3: If expression is an addition, distribute the coef
839                    (c, Val(v)) => {
840                        c.checked_mul(v).map(Val).unwrap_or_else(|| MulInt(c, Box::new(Val(v))))
841                    } // Case #4: If expression is a value, combine coefs
842                    (c, MulInt(v, inner)) => {
843                        if let Some(cv) = c.checked_mul(v) {
844                            MulInt(cv, inner) // Case #5: If expression is a MulInt, combine coefs
845                        } else {
846                            MulInt(c, Box::new(MulInt(v, inner)))
847                        }
848                    }
849                    (_, s) => MulInt(coef, Box::new(s)), // Case #6: Otherwise, return the original
850                }
851            }
852            Div(a, q) => {
853                if q == 1 {
854                    return a.simplify_rec(scope, scenario, extra);
855                } else if let Div(a, q2) = *a {
856                    return Div(a, q * q2).simplify_rec(scope, scenario, extra);
857                }
858                let a = a.simplify_rec(scope, scenario, extra);
859                if let Val(a) = a {
860                    Val(a / q as i64)
861                } else if let MulInt(-1, a) = a {
862                    MulInt(-1, b!(Div(a, q)))
863                } else if let Add(mut terms) = a {
864                    if terms
865                        .iter()
866                        .any(|t| if let MulInt(-1, s) = t { matches!(&**s, Sym(_)) } else { false })
867                    {
868                        MulInt(
869                            -1,
870                            b!(Div(
871                                b!(Add(terms.into_iter().map(|t| MulInt(-1, b!(t))).collect())
872                                    .simplify_rec(scope, scenario, extra)),
873                                q
874                            )),
875                        )
876                    } else if let Some(val) = terms
877                        .iter()
878                        .find_map(|t| if let Val(v) = t { Some(*v) } else { None })
879                        .and_then(|v| {
880                            if v >= q as i64 {
881                                Some(v / q as i64)
882                            } else if v < 0 {
883                                Some(-Integer::div_ceil(&-v, &(q as i64)))
884                            } else {
885                                None
886                            }
887                        })
888                    {
889                        terms.push(Val(-val * q as i64));
890                        // simplify_rec the inner Div too so that follow-up rules
891                        // (e.g. divide-multiple-plus-remainder below) can collapse
892                        // it once the Val extraction has tidied up the residual.
893                        let inner = Div(b!(Add(terms).simplify_rec(scope, scenario, extra)), q)
894                            .simplify_rec(scope, scenario, extra);
895                        Add(vec![Val(val), inner])
896                    } else if let Some(simplified) =
897                        try_divide_multiple_plus_remainder(&terms, q, scope, extra)
898                    {
899                        // Match `Div(Add([k·X, …, c]), k)` where:
900                        //   - one or more terms have a coefficient that is a multiple of q,
901                        //   - the rest sum to a constant in [0, q),
902                        //   - every extracted X is provably non-negative.
903                        // Then `(k·X + c)/k = X + 0 = X` under tract's truncating
904                        // division.  This is sound for X ≥ 0 only — at X = -1
905                        // the truncation rounds toward zero, not floor, breaking
906                        // the identity.  We use prove_positive_or_zero to gate.
907                        simplified.simplify_rec(scope, scenario, extra)
908                    } else if let Some(found_idx) = terms.iter().position(|term| {
909                        // Rule: (Y − q·(Y/q)) / q = 0  [i.e. (Y mod q) / q = 0]
910                        // Always sound: |Y mod q| < q so (Y mod q)/q = 0 under
911                        // truncating division regardless of sign of Y.
912                        matches!(term, MulInt(p, inner)
913                            if *p == -(q as i64)
914                            && matches!(inner.as_ref(), Div(_, q2) if *q2 == q))
915                    }) {
916                        let MulInt(_, inner) = &terms[found_idx] else { unreachable!() };
917                        let Div(y, _) = inner.as_ref() else { unreachable!() };
918                        let remaining: Vec<TDim> = terms
919                            .iter()
920                            .enumerate()
921                            .filter(|&(i, _)| i != found_idx)
922                            .map(|(_, t)| t.clone())
923                            .collect();
924                        let remaining_sum = match remaining.len() {
925                            0 => Val(0),
926                            1 => remaining.into_iter().next().unwrap(),
927                            _ => Add(remaining),
928                        };
929                        if eq_structural(&remaining_sum, y) {
930                            Val(0)
931                        } else {
932                            Div(b!(Add(terms)), q)
933                        }
934                    } else {
935                        Div(b!(Add(terms)), q)
936                    }
937                } else if let MulInt(p, a) = a {
938                    if p == q as i64 {
939                        a.simplify()
940                    } else {
941                        let gcd = p.abs().gcd(&(q as i64));
942                        if gcd == p {
943                            Div(a, q / gcd as u64)
944                        } else if gcd == q as i64 {
945                            MulInt(p / gcd, a)
946                        } else if gcd > 1 {
947                            Div(b!(MulInt(p / gcd, a)), q / gcd as u64)
948                                .simplify_rec(scope, scenario, extra)
949                        } else {
950                            Div(b!(MulInt(p, a)), q)
951                        }
952                    }
953                } else {
954                    Div(b!(a), q)
955                }
956            }
957            Broadcast(terms) => {
958                let mut terms: Vec<TDim> = terms
959                    .iter()
960                    .map(|s| s.clone().simplify_rec(scope, scenario, extra))
961                    .flat_map(|t| if let Broadcast(t) = t { t } else { vec![t] })
962                    .filter(|t| !t.is_one())
963                    .sorted_by(tdim_lexi_order)
964                    .dedup()
965                    .collect_vec();
966                // a#min(a,b) if a>0 && b>0 => a
967                match &*terms {
968                    [] => Val(1),
969                    [_] => terms.remove(0),
970                    [a, Min(m)] | [Min(m), a]
971                        if m.contains(a)
972                            && m.iter()
973                                .all(|t| scope.prove_strict_positive_with_extra(t, extra)) =>
974                    {
975                        a.clone()
976                    }
977                    _ => Broadcast(terms),
978                }
979            }
980
981            Min(terms) => {
982                let mut flatten: Vec<TDim> = terms
983                    .into_iter()
984                    .map(|t| t.simplify_rec(scope, scenario, extra))
985                    .flat_map(|t| if let Min(t) = t { t } else { vec![t] })
986                    .filter(|t| t != &Val(i64::MAX))
987                    .sorted_by(tdim_lexi_order)
988                    .dedup()
989                    .collect();
990                #[allow(clippy::mutable_key_type)]
991                let mut redundant = HashSet::<TDim>::default();
992                for pair in flatten.iter().permutations(2) {
993                    let (a, b) = (pair[0], pair[1]);
994                    if redundant.contains(a) || redundant.contains(b) {
995                        continue;
996                    }
997                    let diff = a.clone() - b;
998                    if diff.as_i64().is_some_and(|i| i >= 0)
999                        || scope.prove_positive_or_zero_with_extra(&diff, extra)
1000                    {
1001                        redundant.insert(a.clone());
1002                    }
1003                }
1004                flatten.retain(|t| !redundant.contains(t));
1005                if flatten.len() == 0 {
1006                    i64::MAX.to_dim()
1007                } else if flatten.len() == 1 {
1008                    flatten.into_iter().next().unwrap()
1009                } else {
1010                    Min(flatten)
1011                }
1012            }
1013            Max(terms) => {
1014                let mut flatten: Vec<TDim> = terms
1015                    .into_iter()
1016                    .map(|t| t.simplify_rec(scope, scenario, extra))
1017                    .flat_map(|t| if let Max(t) = t { t } else { vec![t] })
1018                    .filter(|t| t != &Val(i64::MIN))
1019                    .sorted_by(tdim_lexi_order)
1020                    .dedup()
1021                    .collect();
1022                #[allow(clippy::mutable_key_type)]
1023                let mut redundant = HashSet::<TDim>::default();
1024                for pair in flatten.iter().permutations(2) {
1025                    let (a, b) = (pair[0], pair[1]);
1026                    if redundant.contains(a) || redundant.contains(b) {
1027                        continue;
1028                    }
1029                    let diff = a.clone() - b;
1030                    if diff.as_i64().is_some_and(|i| i >= 0)
1031                        || scope.prove_positive_or_zero_with_extra(&diff, extra)
1032                    {
1033                        redundant.insert(b.clone());
1034                    }
1035                }
1036                flatten.retain(|t| !redundant.contains(t));
1037                if flatten.len() == 0 {
1038                    i64::MIN.to_dim()
1039                } else if flatten.len() == 1 {
1040                    flatten.into_iter().next().unwrap()
1041                } else {
1042                    Max(flatten)
1043                }
1044            }
1045            Sym(s) => scope
1046                .assertions(scenario)
1047                .find_map(|a| match a {
1048                    Assertion::Eq(Sym(sym), v) if sym == &s => Some(v.clone()),
1049                    _ => None,
1050                })
1051                .unwrap_or(Sym(s)),
1052            Val(_) => self,
1053            Ge(a, b) => {
1054                let a = a.simplify_rec(scope, scenario, extra);
1055                let b = b.simplify_rec(scope, scenario, extra);
1056                match (&a, &b) {
1057                    (Val(av), Val(bv)) => Val(if av >= bv { 1 } else { 0 }),
1058                    _ => {
1059                        let diff = a.clone() - b.clone();
1060                        if scope.prove_positive_or_zero_with_extra(&diff, extra) {
1061                            Val(1)
1062                        } else if scope
1063                            .prove_strict_positive_with_extra(&(b.clone() - a.clone()), extra)
1064                        {
1065                            Val(0)
1066                        } else {
1067                            Ge(b!(a), b!(b))
1068                        }
1069                    }
1070                }
1071            }
1072            Eq(a, b) => {
1073                let a = a.simplify_rec(scope, scenario, extra);
1074                let b = b.simplify_rec(scope, scenario, extra);
1075                match (&a, &b) {
1076                    (Val(av), Val(bv)) => Val(if av == bv { 1 } else { 0 }),
1077                    _ => {
1078                        let diff = a.clone() - b.clone();
1079                        if scope.prove_strict_positive_with_extra(&diff, extra)
1080                            || scope
1081                                .prove_strict_positive_with_extra(&(b.clone() - a.clone()), extra)
1082                        {
1083                            Val(0)
1084                        } else {
1085                            // When one side is 0 or 1 and the other is
1086                            // provably in [0,1], reduce to boolean algebra:
1087                            //   Eq(expr, 0) → 1 - expr
1088                            //   Eq(expr, 1) → expr
1089                            let boolean_case = match (&a, &b) {
1090                                (Val(0), e) | (e, Val(0)) => Some((e, false)),
1091                                (Val(1), e) | (e, Val(1)) => Some((e, true)),
1092                                _ => None,
1093                            };
1094                            if let Some((expr, equals_one)) = boolean_case
1095                                && scope.prove_positive_or_zero_with_extra(expr, extra)
1096                                && scope.prove_positive_or_zero_with_extra(
1097                                    &(Val(1) - expr.clone()),
1098                                    extra,
1099                                )
1100                            {
1101                                return if equals_one {
1102                                    expr.clone()
1103                                } else {
1104                                    (Val(1) - expr.clone()).simplify_rec(scope, scenario, extra)
1105                                };
1106                            }
1107                            Eq(b!(a), b!(b))
1108                        }
1109                    }
1110                }
1111            }
1112        }
1113    }
1114
1115    pub(super) fn inclusive_bound(&self, scope: &SymbolScopeData, upper: bool) -> Option<i64> {
1116        use self::TDim::*;
1117        match self {
1118            Val(n) => Some(*n),
1119            Sym(_) => {
1120                if upper {
1121                    scope
1122                        .all_assertions()
1123                        .iter()
1124                        .filter_map(|assert| match &assert {
1125                            Assertion::LT(left, right)
1126                                if left == self && right.as_i64().is_some() =>
1127                            {
1128                                Some(right.as_i64().unwrap() - 1)
1129                            }
1130                            Assertion::LTE(left, right)
1131                                if left == self && right.as_i64().is_some() =>
1132                            {
1133                                Some(right.as_i64().unwrap())
1134                            }
1135                            _ => None,
1136                        })
1137                        .min()
1138                } else {
1139                    scope
1140                        .all_assertions()
1141                        .iter()
1142                        .filter_map(|assert| match &assert {
1143                            Assertion::GT(left, right)
1144                                if left == self && right.as_i64().is_some() =>
1145                            {
1146                                Some(right.as_i64().unwrap() + 1)
1147                            }
1148                            Assertion::GTE(left, right)
1149                                if left == self && right.as_i64().is_some() =>
1150                            {
1151                                Some(right.as_i64().unwrap())
1152                            }
1153                            _ => None,
1154                        })
1155                        .max()
1156                }
1157            }
1158            Add(terms) => {
1159                let mut bound: i64 = 0;
1160                for t in terms {
1161                    {
1162                        let b = t.inclusive_bound(scope, upper)?;
1163                        bound = bound.checked_add(b)?;
1164                    }
1165                }
1166                Some(bound)
1167            }
1168            MulInt(p, a) => match p.cmp(&0) {
1169                Ordering::Equal => Some(0),
1170                Ordering::Greater => {
1171                    a.inclusive_bound(scope, upper).and_then(|x| x.checked_mul(*p))
1172                }
1173                Ordering::Less => a.inclusive_bound(scope, !upper).and_then(|x| x.checked_mul(*p)),
1174            },
1175            Mul(terms) => {
1176                // If all factors have known non-negative bounds, we can bound the product.
1177                let mut lo: i64 = 1;
1178                let mut hi: i64 = 1;
1179                for t in terms {
1180                    let t_lo = t.inclusive_bound(scope, false)?;
1181                    let t_hi = t.inclusive_bound(scope, true)?;
1182                    if t_lo < 0 {
1183                        return None;
1184                    }
1185                    lo = lo.checked_mul(t_lo)?;
1186                    hi = hi.checked_mul(t_hi)?;
1187                }
1188                Some(if upper { hi } else { lo })
1189            }
1190            Min(terms) if !upper => {
1191                // All terms must have known lower bounds; if any is unknown,
1192                // the Min lower bound is unknown.
1193                let bounds: Option<Vec<i64>> =
1194                    terms.iter().map(|t| t.inclusive_bound(scope, false)).collect();
1195                bounds.map(|b| b.into_iter().min().unwrap_or(i64::MAX))
1196            }
1197            Max(terms) if upper => {
1198                // All terms must have known upper bounds; if any is unknown,
1199                // the Max upper bound is unknown.
1200                let bounds: Option<Vec<i64>> =
1201                    terms.iter().map(|t| t.inclusive_bound(scope, true)).collect();
1202                bounds.map(|b| b.into_iter().max().unwrap_or(i64::MIN))
1203            }
1204            Div(a, q) => a.inclusive_bound(scope, upper).map(|x| x / (*q as i64)),
1205            Broadcast(terms) => {
1206                if upper {
1207                    Max(terms.clone()).inclusive_bound(scope, true)
1208                } else {
1209                    Min(terms.clone()).inclusive_bound(scope, false)
1210                }
1211            }
1212            Ge(_, _) | Eq(_, _) => {
1213                if upper {
1214                    Some(1)
1215                } else {
1216                    Some(0)
1217                }
1218            }
1219            _ => None,
1220        }
1221    }
1222
1223    pub fn low_inclusive_bound(&self) -> Option<i64> {
1224        if let TDim::Val(v) = self {
1225            return Some(*v);
1226        }
1227        let scope = self.find_scope()?;
1228        let data = scope.0.lock();
1229        let data = data.borrow();
1230        self.inclusive_bound(&data, false)
1231    }
1232
1233    pub fn high_inclusive_bound(&self) -> Option<i64> {
1234        if let TDim::Val(v) = self {
1235            return Some(*v);
1236        }
1237        let scope = self.find_scope()?;
1238        let data = scope.0.lock();
1239        let data = data.borrow();
1240        self.inclusive_bound(&data, true)
1241    }
1242
1243    pub fn prove_positive_or_zero(&self) -> bool {
1244        if let TDim::Val(v) = self {
1245            return *v >= 0;
1246        }
1247        let Some(scope) = self.find_scope() else { return false };
1248        let data = scope.0.lock();
1249        let data = data.borrow();
1250        data.prove_positive_or_zero(self)
1251    }
1252
1253    pub fn prove_strict_positive(&self) -> bool {
1254        if let TDim::Val(v) = self {
1255            return *v > 0;
1256        }
1257        (self.clone() - 1).prove_positive_or_zero()
1258    }
1259
1260    pub fn prove_negative_or_zero(&self) -> bool {
1261        if let TDim::Val(v) = self {
1262            return *v <= 0;
1263        }
1264        self.clone().neg().prove_positive_or_zero()
1265    }
1266
1267    pub fn prove_strict_negative(&self) -> bool {
1268        if let TDim::Val(v) = self {
1269            return *v < 0;
1270        }
1271        self.clone().neg().prove_strict_positive()
1272    }
1273
1274    /// Least common multiple of two `TDim`s when both reduce to positive
1275    /// integers.
1276    ///
1277    /// Returns `Val(0)` if either operand is `0`, and `None` if either is
1278    /// symbolic, negative, or if the LCM would overflow `i64`. Callers
1279    /// that need a safe answer for symbolic operands should fall back at
1280    /// the call site.
1281    pub fn lcm(&self, other: &TDim) -> Option<TDim> {
1282        match (self.as_i64(), other.as_i64()) {
1283            (Some(a), Some(b)) if a > 0 && b > 0 => {
1284                let g = (a as u64).gcd(&(b as u64));
1285                let l = (a as u64 / g).saturating_mul(b as u64);
1286                if l > i64::MAX as u64 { None } else { Some(TDim::Val(l as i64)) }
1287            }
1288            (Some(0), _) | (_, Some(0)) => Some(TDim::Val(0)),
1289            _ => None,
1290        }
1291    }
1292
1293    pub fn gcd(&self) -> u64 {
1294        use self::TDim::*;
1295        match self {
1296            Val(v) => v.unsigned_abs(),
1297            Sym(_) => 1,
1298            Add(terms) => {
1299                let (head, tail) = terms.split_first().unwrap();
1300                tail.iter().fold(head.gcd(), |a, b| a.gcd(&b.gcd()))
1301            }
1302            MulInt(p, a) => a.gcd().saturating_mul(p.unsigned_abs()),
1303            Mul(terms) => terms.iter().map(|t| t.gcd()).fold(1u64, |a, b| a.saturating_mul(b)),
1304            Min(terms) => terms.iter().map(|t| t.gcd()).reduce(|a, b| a.gcd(&b)).unwrap(),
1305            Max(terms) => terms.iter().map(|t| t.gcd()).reduce(|a, b| a.gcd(&b)).unwrap(),
1306            Div(a, q) => {
1307                if a.gcd() % *q == 0 {
1308                    a.gcd() / *q
1309                } else {
1310                    1
1311                }
1312            }
1313            Broadcast(terms) => terms.iter().map(|t| t.gcd()).reduce(|a, b| a.gcd(&b)).unwrap_or(1),
1314            Ge(_, _) | Eq(_, _) => 1,
1315        }
1316    }
1317
1318    fn div(&self, d: u64) -> TDim {
1319        use self::TDim::*;
1320        if d == 1 {
1321            return self.clone();
1322        }
1323        match self {
1324            Val(v) => Val(v / d as i64),
1325            Sym(_) => panic!(),
1326            Add(terms) => Add(terms.iter().map(|t| t.div(d)).collect()),
1327            Min(terms) => Min(terms.iter().map(|t| t.div(d)).collect()),
1328            Max(terms) => Max(terms.iter().map(|t| t.div(d)).collect()),
1329            Broadcast(terms) => Broadcast(terms.iter().map(|t| t.div(d)).collect()),
1330            Mul(_) => Div(Box::new(self.clone()), d),
1331            MulInt(p, a) => {
1332                if *p == d as i64 {
1333                    (**a).clone()
1334                } else {
1335                    let gcd = p.unsigned_abs().gcd(&d);
1336                    MulInt(p / gcd as i64, b!(a.div(d / gcd)))
1337                }
1338            }
1339            Div(a, q) => Div(a.clone(), q * d),
1340            Ge(_, _) | Eq(_, _) => Div(Box::new(self.clone()), d),
1341        }
1342    }
1343
1344    pub fn div_ceil(self, rhs: u64) -> TDim {
1345        TDim::Div(Box::new(Add(vec![self, Val(rhs as i64 - 1)])), rhs).reduce()
1346    }
1347
1348    pub fn guess_slope(&self, sym: &Symbol) -> (i64, u64) {
1349        fn slope_rec(d: &TDim, sym: &Symbol) -> (i64, i64) {
1350            match d {
1351                Val(_) => (0, 1),
1352                Sym(s) => ((sym == s) as i64, 1),
1353                Add(terms) => terms
1354                    .iter()
1355                    .map(|d| slope_rec(d, sym))
1356                    .fold((0, 1), |a, b| ((a.0 * b.1 + a.1 * b.0), (b.1 * a.1))),
1357                Mul(terms) => terms
1358                    .iter()
1359                    .map(|d| slope_rec(d, sym))
1360                    .fold((1, 1), |a, b| ((a.0 * b.0), (b.1 * a.1))),
1361                MulInt(p, a) => {
1362                    let (n, d) = slope_rec(a, sym);
1363                    (p * n, d)
1364                }
1365                Div(a, q) => {
1366                    let (n, d) = slope_rec(a, sym);
1367                    (n, d * *q as i64)
1368                }
1369                Broadcast(terms) => slope_rec(&terms[0], sym),
1370                Min(terms) => slope_rec(&terms[0], sym),
1371                Max(terms) => slope_rec(&terms[0], sym),
1372                Ge(_, _) | Eq(_, _) => (0, 1),
1373            }
1374        }
1375        let (p, q) = slope_rec(self, sym);
1376        reduce_ratio(p, q)
1377    }
1378
1379    #[allow(clippy::mutable_key_type)]
1380    pub fn symbols(&self) -> std::collections::HashSet<Symbol> {
1381        match self {
1382            Val(_) => maplit::hashset!(),
1383            Sym(s) => maplit::hashset!(s.clone()),
1384            Add(terms) | Mul(terms) | Broadcast(terms) | Min(terms) | Max(terms) => {
1385                terms.iter().fold(maplit::hashset!(), |mut set, v| {
1386                    set.extend(v.symbols());
1387                    set
1388                })
1389            }
1390            MulInt(_, a) => a.symbols(),
1391            Div(a, _) => a.symbols(),
1392            Ge(a, b) | Eq(a, b) => {
1393                let mut set = a.symbols();
1394                set.extend(b.symbols());
1395                set
1396            }
1397        }
1398    }
1399
1400    pub fn compatible_with(&self, other: &TDim) -> bool {
1401        if let Some(x) = (self.clone() - other).as_i64() {
1402            return x == 0;
1403        }
1404        true // maybe ? :)
1405    }
1406}
1407
1408pub(super) fn reduce_ratio(mut p: i64, mut q: i64) -> (i64, u64) {
1409    let gcd = p.abs().gcd(&q.abs());
1410    if gcd > 1 {
1411        p /= gcd;
1412        q /= gcd;
1413    }
1414    if q < 0 { (-p, (-q) as u64) } else { (p, q as u64) }
1415}
1416
1417impl Zero for TDim {
1418    fn zero() -> Self {
1419        Val(0)
1420    }
1421    fn is_zero(&self) -> bool {
1422        matches!(self, Val(0))
1423    }
1424}
1425
1426impl Default for TDim {
1427    fn default() -> TDim {
1428        Val(0)
1429    }
1430}
1431
1432impl num_traits::Bounded for TDim {
1433    fn min_value() -> Self {
1434        TDim::Val(i64::MIN)
1435    }
1436
1437    fn max_value() -> Self {
1438        TDim::Val(i64::MAX)
1439    }
1440}
1441
1442impl num_traits::One for TDim {
1443    fn one() -> Self {
1444        TDim::Val(1)
1445    }
1446}
1447
1448impl ::std::iter::Sum for TDim {
1449    fn sum<I: Iterator<Item = TDim>>(iter: I) -> TDim {
1450        iter.fold(0.into(), |a, b| a + b)
1451    }
1452}
1453
1454impl<'a> ::std::iter::Sum<&'a TDim> for TDim {
1455    fn sum<I: Iterator<Item = &'a TDim>>(iter: I) -> TDim {
1456        iter.fold(0.into(), |a, b| a + b)
1457    }
1458}
1459
1460impl std::iter::Product for TDim {
1461    fn product<I: Iterator<Item = TDim>>(iter: I) -> Self {
1462        iter.fold(TDim::Val(1), |a, b| a * b)
1463    }
1464}
1465
1466impl<'a> ::std::iter::Product<&'a TDim> for TDim {
1467    fn product<I: Iterator<Item = &'a TDim>>(iter: I) -> TDim {
1468        iter.fold(1.into(), |a, b| a * b)
1469    }
1470}
1471
1472macro_rules! from_i {
1473    ($i: ty) => {
1474        impl From<$i> for TDim {
1475            fn from(v: $i) -> TDim {
1476                TDim::Val(v as _)
1477            }
1478        }
1479        impl<'a> From<&'a $i> for TDim {
1480            fn from(v: &'a $i) -> TDim {
1481                TDim::Val(*v as _)
1482            }
1483        }
1484    };
1485}
1486
1487from_i!(i32);
1488from_i!(i64);
1489from_i!(u64);
1490from_i!(isize);
1491from_i!(usize);
1492
1493impl From<Symbol> for TDim {
1494    fn from(it: Symbol) -> Self {
1495        TDim::Sym(it)
1496    }
1497}
1498
1499impl<'a> From<&'a Symbol> for TDim {
1500    fn from(it: &'a Symbol) -> Self {
1501        TDim::Sym(it.clone())
1502    }
1503}
1504
1505impl ops::Neg for TDim {
1506    type Output = Self;
1507    fn neg(self) -> Self {
1508        if let Val(v) = self { Val(-v) } else { TDim::MulInt(-1, Box::new(self)).reduce() }
1509    }
1510}
1511
1512impl<'a> ops::AddAssign<&'a TDim> for TDim {
1513    fn add_assign(&mut self, rhs: &'a TDim) {
1514        if rhs.is_zero() {
1515        } else if self.is_zero() {
1516            *self = rhs.clone();
1517        } else if let (Val(s), Val(o)) = (&mut *self, &rhs) {
1518            *s += o;
1519        } else {
1520            *self = TDim::Add(vec![std::mem::take(self), rhs.clone()]).reduce()
1521        }
1522    }
1523}
1524
1525impl<I> ops::AddAssign<I> for TDim
1526where
1527    I: Into<TDim>,
1528{
1529    fn add_assign(&mut self, rhs: I) {
1530        let rhs = rhs.into();
1531        if rhs.is_zero() {
1532        } else if self.is_zero() {
1533            *self = rhs;
1534        } else if let (Val(s), Val(o)) = (&mut *self, &rhs) {
1535            *s += o;
1536        } else {
1537            *self = TDim::Add(vec![std::mem::take(self), rhs]).reduce()
1538        }
1539    }
1540}
1541
1542impl<I> ops::Add<I> for TDim
1543where
1544    I: Into<TDim>,
1545{
1546    type Output = Self;
1547    fn add(mut self, rhs: I) -> Self {
1548        self += rhs;
1549        self
1550    }
1551}
1552
1553impl<'a> ops::Add<&'a TDim> for TDim {
1554    type Output = Self;
1555    fn add(mut self, rhs: &'a TDim) -> Self {
1556        self += rhs;
1557        self
1558    }
1559}
1560
1561#[allow(clippy::suspicious_op_assign_impl)]
1562impl<'a> ops::SubAssign<&'a TDim> for TDim {
1563    fn sub_assign(&mut self, rhs: &'a TDim) {
1564        if rhs.is_zero() {
1565        } else if self.is_zero() {
1566            *self = rhs.clone().neg();
1567        } else if let (Val(s), Val(o)) = (&mut *self, &rhs) {
1568            *s -= o;
1569        } else {
1570            *self = TDim::Add(vec![std::mem::take(self), rhs.clone().neg()]).reduce()
1571        }
1572    }
1573}
1574
1575impl<I> ops::SubAssign<I> for TDim
1576where
1577    I: Into<TDim>,
1578{
1579    fn sub_assign(&mut self, rhs: I) {
1580        let rhs = rhs.into();
1581        if rhs.is_zero() {
1582        } else if self.is_zero() {
1583            *self = rhs.neg();
1584        } else if let (Val(s), Val(o)) = (&mut *self, &rhs) {
1585            *s -= o;
1586        } else {
1587            *self = TDim::Add(vec![std::mem::take(self), rhs.neg()]).reduce()
1588        }
1589    }
1590}
1591
1592impl<I> ops::Sub<I> for TDim
1593where
1594    I: Into<TDim>,
1595{
1596    type Output = Self;
1597    fn sub(mut self, rhs: I) -> Self {
1598        self -= rhs;
1599        self
1600    }
1601}
1602
1603impl<'a> ops::Sub<&'a TDim> for TDim {
1604    type Output = Self;
1605    fn sub(mut self, rhs: &'a TDim) -> Self {
1606        self -= rhs;
1607        self
1608    }
1609}
1610
1611impl<I: Into<TDim>> ops::MulAssign<I> for TDim {
1612    fn mul_assign(&mut self, rhs: I) {
1613        let rhs = rhs.into();
1614        if self.is_one() {
1615            *self = rhs
1616        } else if rhs.is_one() {
1617        } else {
1618            *self = TDim::Mul(vec![rhs, std::mem::take(self)]).reduce()
1619        }
1620    }
1621}
1622
1623impl<'a> ops::MulAssign<&'a TDim> for TDim {
1624    fn mul_assign(&mut self, rhs: &'a TDim) {
1625        if self.is_one() {
1626            *self = rhs.clone()
1627        } else if rhs.is_one() {
1628        } else {
1629            *self = TDim::Mul(vec![std::mem::take(self), rhs.clone()]).reduce()
1630        }
1631    }
1632}
1633
1634impl<I: Into<TDim>> ops::Mul<I> for TDim {
1635    type Output = Self;
1636    fn mul(mut self, rhs: I) -> Self {
1637        self *= rhs.into();
1638        self
1639    }
1640}
1641
1642impl<'a> ops::Mul<&'a TDim> for TDim {
1643    type Output = Self;
1644    fn mul(mut self, rhs: &'a TDim) -> Self {
1645        self *= rhs;
1646        self
1647    }
1648}
1649
1650impl<I: AsPrimitive<u64> + PrimInt> ops::DivAssign<I> for TDim {
1651    fn div_assign(&mut self, rhs: I) {
1652        *self = TDim::Div(Box::new(std::mem::take(self)), rhs.as_()).reduce()
1653    }
1654}
1655
1656impl<I: AsPrimitive<u64> + PrimInt> ops::Div<I> for TDim {
1657    type Output = Self;
1658    fn div(mut self, rhs: I) -> Self {
1659        self /= rhs.as_();
1660        self
1661    }
1662}
1663
1664impl<I: AsPrimitive<u64> + PrimInt> ops::RemAssign<I> for TDim {
1665    fn rem_assign(&mut self, rhs: I) {
1666        *self += -(self.clone() / rhs.as_() * rhs.as_());
1667    }
1668}
1669
1670impl<I: AsPrimitive<u64> + PrimInt> ops::Rem<I> for TDim {
1671    type Output = Self;
1672    fn rem(mut self, rhs: I) -> Self {
1673        self %= rhs;
1674        self
1675    }
1676}
1677
1678#[cfg(test)]
1679mod tests {
1680    use super::*;
1681
1682    macro_rules! b( ($e:expr) => { Box::new($e) } );
1683
1684    lazy_static::lazy_static! {
1685        static ref table: SymbolScope = SymbolScope::default();
1686        static ref A: Symbol = table.sym("a");
1687        static ref B: Symbol = table.sym("b");
1688        static ref C: Symbol = table.sym("c");
1689        static ref D: Symbol = table.sym("d");
1690        static ref E: Symbol = table.sym("e");
1691    }
1692
1693    fn neg(a: &TDim) -> TDim {
1694        mul(-1, a)
1695    }
1696
1697    fn add(a: &TDim, b: &TDim) -> TDim {
1698        TDim::Add(vec![a.clone(), b.clone()])
1699    }
1700
1701    fn mul(a: i64, b: &TDim) -> TDim {
1702        TDim::MulInt(a, b![b.clone()])
1703    }
1704
1705    fn div(a: &TDim, b: u64) -> TDim {
1706        TDim::Div(b!(a.clone()), b)
1707    }
1708
1709    #[test]
1710    fn reduce_add() {
1711        assert_eq!(add(&A.to_dim(), &neg(&A.to_dim())).reduce(), Val(0))
1712    }
1713
1714    #[test]
1715    fn lcm_basic() {
1716        assert_eq!(Val(16).lcm(&Val(32)), Some(Val(32)));
1717        assert_eq!(Val(32).lcm(&Val(16)), Some(Val(32)));
1718        assert_eq!(Val(6).lcm(&Val(8)), Some(Val(24)));
1719        assert_eq!(Val(7).lcm(&Val(7)), Some(Val(7)));
1720        // Symbolic: not computable; callers fall back.
1721        assert_eq!(Val(16).lcm(&A.to_dim()), None);
1722    }
1723
1724    #[test]
1725    fn reduce_neg_mul() {
1726        assert_eq!(neg(&mul(2, &A.to_dim())).reduce(), mul(-2, &A.to_dim()))
1727    }
1728
1729    #[test]
1730    fn reduce_cplx_ex_2() {
1731        assert_eq!(
1732            add(
1733                &add(&Val(-4), &mul(-2, &div(&A.to_dim(), 4))),
1734                &mul(-2, &mul(-1, &div(&A.to_dim(), 4)))
1735            )
1736            .reduce(),
1737            Val(-4)
1738        )
1739    }
1740
1741    #[test]
1742    fn reduce_cplx_ex_3() {
1743        assert_eq!(div(&MulInt(1, b!(MulInt(4, b!(A.to_dim())))), 4).reduce(), A.to_dim())
1744    }
1745
1746    #[test]
1747    fn reduce_cplx_ex_4() {
1748        // (S+1)/2 + (1-S)/2 == 1
1749        assert_eq!(
1750            add(&div(&add(&A.to_dim(), &Val(1)), 2), &div(&add(&neg(&A.to_dim()), &Val(1)), 2))
1751                .reduce(),
1752            1.into()
1753        );
1754    }
1755
1756    #[test]
1757    fn reduce_mul_mul_1() {
1758        assert_eq!(mul(3, &mul(2, &A.to_dim())).reduce(), mul(6, &A.to_dim()))
1759    }
1760
1761    #[test]
1762    fn reduce_mul_mul_2() {
1763        assert_eq!(mul(-2, &mul(-1, &A.to_dim())).reduce(), mul(2, &A.to_dim()))
1764    }
1765
1766    #[test]
1767    fn reduce_mul_div_1() {
1768        assert_eq!(mul(2, &div(&mul(-1, &A.to_dim()), 3)).reduce(), mul(-2, &div(&A.to_dim(), 3)))
1769    }
1770
1771    #[test]
1772    fn const_and_add() {
1773        let e: TDim = 2i64.into();
1774        assert_eq!(e.eval(&SymbolValues::default()).to_i64().unwrap(), 2);
1775        let e: TDim = TDim::from(2) + 3;
1776        assert_eq!(e.eval(&SymbolValues::default()).to_i64().unwrap(), 5);
1777        let e: TDim = TDim::from(2) - 3;
1778        assert_eq!(e.eval(&SymbolValues::default()).to_i64().unwrap(), -1);
1779        let e: TDim = -TDim::from(2);
1780        assert_eq!(e.eval(&SymbolValues::default()).to_i64().unwrap(), -2);
1781    }
1782
1783    #[test]
1784    fn substitution() {
1785        let a: TDim = A.to_dim();
1786        assert_eq!(a.eval(&SymbolValues::default().with(&A, 2)).to_i64().unwrap(), 2);
1787        let e = a + 3;
1788        assert_eq!(e.eval(&SymbolValues::default().with(&A, 2)).to_i64().unwrap(), 5);
1789    }
1790
1791    #[test]
1792    fn reduce_adds() {
1793        let e: TDim = TDim::from(2) + 1;
1794        assert_eq!(e, TDim::from(3));
1795        let e: TDim = TDim::from(3) + 2;
1796        assert_eq!(e, TDim::from(5));
1797        let e: TDim = TDim::from(3) + 0;
1798        assert_eq!(e, TDim::from(3));
1799        let e: TDim = TDim::from(3) + 2 + 1;
1800        assert_eq!(e, TDim::from(6));
1801    }
1802
1803    #[test]
1804    fn reduce_muls() {
1805        let e: TDim = Val(1) * A.to_dim();
1806        assert_eq!(e, A.to_dim());
1807        let e: TDim = A.to_dim() * &B.to_dim() * 1;
1808        assert_eq!(e, A.to_dim() * &B.to_dim());
1809    }
1810
1811    #[test]
1812    fn reduce_divs() {
1813        let e: TDim = TDim::from(2) / 1;
1814        assert_eq!(e, TDim::from(2));
1815        let e: TDim = TDim::from(3) / 2;
1816        assert_eq!(e, TDim::from(1));
1817        let e: TDim = TDim::from(3) % 2;
1818        assert_eq!(e, TDim::from(1));
1819        let e: TDim = TDim::from(5) / 2;
1820        assert_eq!(e, TDim::from(2));
1821        let e: TDim = TDim::from(5) % 2;
1822        assert_eq!(e, TDim::from(1));
1823    }
1824
1825    #[test]
1826    fn reduce_div_bug_0() {
1827        let e1: TDim = (A.to_dim() + 23) / 2 - 1;
1828        let e2: TDim = (A.to_dim() + 21) / 2;
1829        assert_eq!(e1, e2);
1830    }
1831
1832    #[test]
1833    fn reduce_div_bug_1() {
1834        let e1: TDim = (A.to_dim() + -1) / 2;
1835        let e2: TDim = (A.to_dim() + 1) / 2 - 1;
1836        assert_eq!(e1, e2);
1837    }
1838
1839    #[test]
1840    fn reduce_div_bug_2() {
1841        let e1: TDim = ((A.to_dim() + 1) / 2 + 1) / 2;
1842        let e2: TDim = (A.to_dim() + 3) / 4;
1843        assert_eq!(e1, e2);
1844    }
1845
1846    #[test]
1847    fn divide_multiple_plus_remainder() {
1848        // (k·X + r)/k → X under truncating division when 0 ≤ r < k AND X ≥ 0.
1849        let scope = SymbolScope::default().with_assertion("S>=0").unwrap();
1850        let s = scope.sym("S");
1851
1852        // (2S+1)/2 → S
1853        let e: TDim = (s.to_dim() * 2 + 1) / 2;
1854        assert_eq!(e.simplify(), s.to_dim());
1855
1856        // -1 + (2S+1)/2 → S - 1
1857        let e: TDim = (s.to_dim() * 2 + 1) / 2 - 1;
1858        assert_eq!(e.simplify(), s.to_dim() - 1);
1859
1860        // (2S-1)/2 → S - 1   (Val rule extracts -1 first, then our rule)
1861        let e: TDim = (s.to_dim() * 2 - 1) / 2;
1862        assert_eq!(e.simplify(), s.to_dim() - 1);
1863
1864        // (4S+3)/2 → 2S + 1   (Val rule extracts 1 = 3/2, then our rule on (4S+1)/2 → 2S)
1865        let e: TDim = (s.to_dim() * 4 + 3) / 2;
1866        assert_eq!(e.simplify(), s.to_dim() * 2 + 1);
1867    }
1868
1869    #[test]
1870    fn divide_multiple_plus_remainder_no_assertion() {
1871        // Without an X≥0 assertion the (k·X+c)/k → X identity does NOT hold
1872        // (X=-1, k=2, c=1 gives -1/2=0 ≠ X under truncating division). The
1873        // wiggle Div arm used to emit that variant unconditionally; reduce()
1874        // would then pick it on cost. Now wiggle skips the variant when the
1875        // remainder bucket contains a Val, leaving only the sound rule
1876        // gated on prove_positive_or_zero in simplify_rec.
1877        let scope = SymbolScope::default();
1878        let s = scope.sym("S");
1879        let e: TDim = (s.to_dim() * 2 + 1) / 2;
1880        assert_ne!(e.simplify(), s.to_dim());
1881    }
1882
1883    #[test]
1884    fn modulo_div_is_zero() {
1885        // (Y − q·(Y/q)) / q = 0 for any Y and any q — the modulo remainder
1886        // divided by the modulus is always zero under truncating division.
1887        let scope = SymbolScope::default();
1888        let s = scope.sym("S");
1889        // Simple case: (S - 2*(S/2)) / 2 = (S mod 2) / 2 = 0
1890        let e: TDim = (s.to_dim() - s.to_dim() / 2 * 2) / 2;
1891        assert_eq!(e.simplify(), TDim::Val(0));
1892        // Composite case: ((S+1) - 2*((S+1)/2)) / 2 = 0
1893        // This is the exact pattern from SameUpper conv padding.
1894        let a = s.to_dim() + 1;
1895        let e2: TDim = (a.clone() - a.clone() / 2 * 2) / 2;
1896        assert_eq!(e2.simplify(), TDim::Val(0));
1897    }
1898
1899    #[test]
1900    fn reduce_div_bug_3() {
1901        let e1: TDim = (A.to_dim() / 2) * -4;
1902        let e2: TDim = (A.to_dim() / 2) * -4 / 1;
1903        assert_eq!(e1, e2);
1904    }
1905
1906    #[test]
1907    fn reduce_mul_div() {
1908        let e: TDim = A.to_dim() * 2 / 2;
1909        assert_eq!(e, A.to_dim());
1910    }
1911
1912    #[test]
1913    fn expand_polynomial_two_add_factors() {
1914        // (a + 2*a*b) * (1 + b)  ==poly==  a * (1 + b) * (1 + 2*b)
1915        // Both fully expand to a + 3*a*b + 2*a*b*b.  We don't auto-expand in
1916        // simplify (it would block maybe_div on factored-form denominators),
1917        // but expand_polynomial does, and Reshape uses it for volume checks.
1918        let a = A.to_dim();
1919        let b = B.to_dim();
1920        let lhs = (a.clone() + a.clone() * &b * 2) * (TDim::from(1) + &b);
1921        let rhs = a.clone() * (TDim::from(1) + &b) * (TDim::from(1) + b.clone() * 2);
1922        assert_eq!(lhs.expand_polynomial(), rhs.expand_polynomial());
1923    }
1924
1925    #[test]
1926    fn reduce_div_mul() {
1927        let e: TDim = A.to_dim() / 2 * 2;
1928        assert_ne!(e, A.to_dim());
1929    }
1930
1931    #[test]
1932    fn reduce_add_div() {
1933        let e: TDim = A.to_dim() / 2 + 1;
1934        assert_eq!(e, ((A.to_dim() + 2) / 2));
1935    }
1936
1937    #[test]
1938    fn reduce_neg_mul_() {
1939        let e: TDim = TDim::from(1) - A.to_dim() * 2;
1940        assert_eq!(e, TDim::from(1) + A.to_dim() * -2);
1941    }
1942
1943    #[test]
1944    fn reduce_add_rem_1() {
1945        assert_eq!(((A.to_dim() + 4) % 2), (A.to_dim() % 2));
1946    }
1947
1948    #[test]
1949    fn reduce_add_rem_2() {
1950        assert_eq!(((A.to_dim() - 4) % 2), (A.to_dim() % 2));
1951    }
1952
1953    #[test]
1954    fn reduce_rem_div() {
1955        let e: TDim = A.to_dim() % 2 / 2;
1956        assert_eq!(e, TDim::from(0));
1957    }
1958
1959    #[test]
1960    fn conv2d_ex_1() {
1961        let e = (TDim::from(1) - 1 + 1).div_ceil(1);
1962        assert_eq!(e, TDim::from(1));
1963    }
1964
1965    #[test]
1966    fn conv2d_ex_2() {
1967        let e = (A.to_dim() - 3 + 1).div_ceil(1);
1968        assert_eq!(e, A.to_dim() + -2);
1969    }
1970
1971    #[test]
1972    fn extract_int_gcd_from_muls() {
1973        let term = (A.to_dim() + 1) / 4;
1974        let mul = (term.clone() * 24 - 24) * (term.clone() * 2 - 2);
1975        let target = (term.clone() - 1) * (term.clone() - 1) * 48;
1976        assert_eq!(mul, target);
1977    }
1978
1979    #[test]
1980    fn equality_of_muls() {
1981        let term = (A.to_dim() + 1) / 4;
1982        let mul1 = (term.clone() * 2 - 3) * (term.clone() - 1);
1983        let mul2 = (term.clone() - 1) * (term.clone() * 2 - 3);
1984        assert_eq!(mul1, mul2);
1985    }
1986
1987    #[test]
1988    fn factorize_complex_expr_times_int() {
1989        let term = (A.to_dim() + 1) / 4;
1990        let e = term.clone() * 2 - &term - 1;
1991        assert_eq!(e, term - 1);
1992    }
1993
1994    #[test]
1995    fn broadcast_over_min() {
1996        // assuming a>0, b>0 then a#min(a,b) can be replaced by a
1997        // proof:
1998        //    if b == 1 => min(a,b)=1 => a#1=a => ok
1999        //    if a <= b => min(a,b)=a => ok
2000        //    if 1 < B < A => expression was invalid, we're generalizing over the non-domain and ignoring the constraint
2001        for a in 1..5 {
2002            for b in 1..5 {
2003                if b > 1 && a > b {
2004                    assert!(a.broadcast(a.min(b)).is_err());
2005                } else {
2006                    assert_eq!(a.broadcast(a.min(b)).unwrap(), a);
2007                }
2008            }
2009        }
2010    }
2011
2012    #[test]
2013    fn min_ints_1() {
2014        assert_eq!(2.to_dim().mini(1.to_dim()), 1.to_dim());
2015    }
2016
2017    #[test]
2018    fn min_ints_2() {
2019        assert_eq!(1.to_dim().mini(2.to_dim()), 1.to_dim());
2020    }
2021
2022    #[test]
2023    fn min_same() {
2024        assert_eq!(A.to_dim().mini(A.to_dim()), A.to_dim());
2025    }
2026
2027    #[test]
2028    fn min_noop() {
2029        assert_eq!(A.to_dim().mini(1.to_dim()), A.to_dim().mini(1.to_dim()));
2030    }
2031
2032    #[test]
2033    fn min_diff_1() {
2034        assert_eq!((A.to_dim() + 1).mini(A.to_dim() + 2), A.to_dim() + 1);
2035    }
2036
2037    #[test]
2038    fn slope_0() {
2039        assert_eq!(12.to_dim().guess_slope(&A), (0, 1));
2040    }
2041
2042    #[test]
2043    fn slope_1() {
2044        assert_eq!(A.to_dim().guess_slope(&A), (1, 1));
2045    }
2046
2047    #[test]
2048    fn slope_2() {
2049        assert_eq!((A.to_dim() * 2).guess_slope(&A), (2, 1));
2050    }
2051
2052    #[test]
2053    fn slope_3() {
2054        assert_eq!((A.to_dim() * 2 + A.to_dim() / 2).guess_slope(&A), (5, 2));
2055    }
2056
2057    #[test]
2058    fn slope_4() {
2059        assert_eq!((A.to_dim()).guess_slope(&B), (0, 1));
2060    }
2061
2062    #[test]
2063    fn slope_5() {
2064        assert_eq!((A.to_dim() + 1).guess_slope(&A), (1, 1));
2065        assert_eq!((A.to_dim() + 1).guess_slope(&B), (0, 1));
2066    }
2067
2068    #[test]
2069    fn slope_6() {
2070        assert_eq!((A.to_dim() + 1).guess_slope(&A), (1, 1));
2071        assert_eq!((A.to_dim() + B.to_dim()).guess_slope(&B), (1, 1));
2072    }
2073
2074    #[test]
2075    fn min_0() -> TractResult<()> {
2076        let symbols = SymbolScope::default();
2077        assert_eq!(
2078            symbols.parse_tdim("min(S+3, S+2)").unwrap().simplify(),
2079            symbols.parse_tdim("S+2").unwrap(),
2080        );
2081        Ok(())
2082    }
2083
2084    #[test]
2085    fn commutative_mul_parens() -> TractResult<()> {
2086        let symbols = SymbolScope::default();
2087        assert_eq!(
2088            symbols.parse_tdim("A*(B*C)").unwrap().simplify(),
2089            symbols.parse_tdim("(B*A)*C").unwrap().simplify(),
2090        );
2091        Ok(())
2092    }
2093
2094    #[test]
2095    fn commutative_in_nemo_parakeet_model() -> TractResult<()> {
2096        let symbols = SymbolScope::default();
2097        assert_eq!(
2098            symbols
2099                .parse_tdim("8*(1+-1*max(0,5000+-1*(S+7)/8)+max(0,4999+(S+7)/8))*((B)*((S+7)/8))")
2100                .unwrap()
2101                .simplify(),
2102            symbols
2103                .parse_tdim("8*((B)*(1+-1*max(0,5000+-1*(S+7)/8)+max(0,4999+(S+7)/8)))*((S+7)/8)")
2104                .unwrap()
2105                .simplify(),
2106        );
2107        Ok(())
2108    }
2109
2110    #[test]
2111    fn commutative_mul_parens_deep() -> TractResult<()> {
2112        let symbols = SymbolScope::default();
2113        let deep_tdim = Mul(vec![
2114            Mul(vec![Mul(vec![Mul(vec![A.to_dim(), B.to_dim()]), C.to_dim()]), D.to_dim()]),
2115            E.to_dim(),
2116        ])
2117        .simplify();
2118        assert_eq!(deep_tdim, symbols.parse_tdim("a*b*c*d*e").unwrap().simplify());
2119        Ok(())
2120    }
2121
2122    // ---- Tests for new comparison/not TDim variants ----
2123
2124    #[test]
2125    fn ge_concrete_true() {
2126        assert_eq!(Ge(b!(Val(5)), b!(Val(3))).reduce(), Val(1));
2127    }
2128
2129    #[test]
2130    fn ge_concrete_false() {
2131        assert_eq!(Ge(b!(Val(2)), b!(Val(3))).reduce(), Val(0));
2132    }
2133
2134    #[test]
2135    fn lt_concrete_true() {
2136        // Lt(2,3) normalizes to Ge(3, 2+1) = Ge(3, 3)
2137        assert_eq!(Ge(b!(Val(3)), b!(Val(3))).reduce(), Val(1));
2138    }
2139
2140    #[test]
2141    fn lt_concrete_false() {
2142        // Lt(5,3) normalizes to Ge(3, 5+1) = Ge(3, 6)
2143        assert_eq!(Ge(b!(Val(3)), b!(Val(6))).reduce(), Val(0));
2144    }
2145
2146    #[test]
2147    fn eq_concrete_true() {
2148        assert_eq!(Eq(b!(Val(3)), b!(Val(3))).reduce(), Val(1));
2149    }
2150
2151    #[test]
2152    fn eq_concrete_false() {
2153        assert_eq!(Eq(b!(Val(3)), b!(Val(4))).reduce(), Val(0));
2154    }
2155
2156    #[test]
2157    fn not_val_0() {
2158        // not(0) = 1 - 0 = 1
2159        assert_eq!((Val(1) - Val(0)).reduce(), Val(1));
2160    }
2161
2162    #[test]
2163    fn not_val_1() {
2164        // not(1) = 1 - 1 = 0
2165        assert_eq!((Val(1) - Val(1)).reduce(), Val(0));
2166    }
2167
2168    #[test]
2169    fn not_lt_becomes_ge() {
2170        // not(Lt(x1, T)) = 1 - Ge(T, x1+1); check it evaluates correctly at boundary
2171        let s = SymbolScope::default();
2172        let t = s.sym("T");
2173        let x1 = s.sym("x1");
2174        // at x1 = T (boundary), Ge(T, T+1) = 0, so 1 - 0 = 1 (not-lt is true when x1 >= T)
2175        let expr = Val(1) - Ge(b!(Sym(t.clone())), b!(Sym(x1.clone()) + Val(1)));
2176        let at_boundary = expr.substitute(&x1, &Sym(t.clone())).unwrap().simplify();
2177        assert_eq!(at_boundary, Val(1));
2178    }
2179
2180    #[test]
2181    fn eq_with_assertion_proves_false() {
2182        // Eq(T, 0) should reduce to Val(0) when T >= 1
2183        let s = SymbolScope::default();
2184        s.add_assertion("T >= 1").unwrap();
2185        let t = s.sym("T");
2186        let expr = Eq(b!(Sym(t)), b!(Val(0)));
2187        assert_eq!(expr.simplify(), Val(0));
2188    }
2189
2190    #[test]
2191    fn ge_coord_at_extremes() {
2192        // Ge(x1, T) should not simplify without coordinate substitution
2193        let s = SymbolScope::default();
2194        s.add_assertion("T >= 1").unwrap();
2195        let t = s.sym("T");
2196        let x1 = s.sym("x1");
2197        let expr = Ge(b!(Sym(x1.clone())), b!(Sym(t.clone())));
2198        // simplify() alone can't prove this false (x1 could be > T)
2199        // but with coordinate substitution (x1 = T-1), Ge(T-1, T) = 0
2200        let at_max = expr.substitute(&x1, &(Sym(t.clone()) - Val(1))).unwrap().simplify();
2201        assert_eq!(at_max, Val(0));
2202    }
2203
2204    #[test]
2205    fn eval_to_i64_new_variants() {
2206        use super::super::sym::SymbolValues;
2207        let sv = SymbolValues::default();
2208        assert_eq!(Ge(b!(Val(5)), b!(Val(3))).eval_to_i64(&sv).unwrap(), 1);
2209        assert_eq!(Ge(b!(Val(3)), b!(Val(5))).eval_to_i64(&sv).unwrap(), 0);
2210        assert_eq!(Eq(b!(Val(3)), b!(Val(3))).eval_to_i64(&sv).unwrap(), 1);
2211        assert_eq!(Eq(b!(Val(3)), b!(Val(4))).eval_to_i64(&sv).unwrap(), 0);
2212    }
2213
2214    #[test]
2215    fn eq_boolean_simplifies() {
2216        let s = SymbolScope::default();
2217        s.add_assertion("cw >= 0").unwrap();
2218        s.add_assertion("cw <= 1").unwrap();
2219        let cw = s.sym("cw");
2220        // Eq(1 - cw, 0) → cw
2221        assert_eq!(Eq(b!(Val(1) - Sym(cw.clone())), b!(Val(0))).simplify(), Sym(cw.clone()));
2222        // Eq(cw, 0) → 1 - cw
2223        assert_eq!(Eq(b!(Sym(cw.clone())), b!(Val(0))).simplify(), Val(1) - Sym(cw.clone()));
2224        // Eq(cw, 1) → cw
2225        assert_eq!(Eq(b!(Sym(cw.clone())), b!(Val(1))).simplify(), Sym(cw.clone()));
2226        // Eq(1 - cw, 1) → 1 - cw
2227        assert_eq!(Eq(b!(Val(1) - Sym(cw.clone())), b!(Val(1))).simplify(), Val(1) - Sym(cw));
2228    }
2229
2230    #[test]
2231    fn eq_boolean_mul_of_ge() {
2232        // Product of Ge terms: Ge(a,b) * Ge(c,d) is in [0,1]
2233        // so Eq(product, 0) should simplify to 1 - product
2234        let s = SymbolScope::default();
2235        let x = s.sym("x");
2236        let product =
2237            Mul(vec![Ge(b!(Val(2)), b!(Sym(x.clone()))), Ge(b!(Sym(x.clone())), b!(Val(0)))]);
2238        let eq = Eq(b!(product.clone()), b!(Val(0)));
2239        assert_eq!(eq.simplify(), Val(1) - product);
2240    }
2241
2242    #[test]
2243    fn min_1_max_0_sym() {
2244        // Min(1, Max(0, X)) must not simplify away the Min when X is unconstrained.
2245        let s = SymbolScope::default();
2246        let x = s.sym("X");
2247        let expr = Min(vec![Val(1), Max(vec![Val(0), Sym(x)])]);
2248        let simplified = expr.simplify();
2249        eprintln!("simplified: {simplified}");
2250        assert!(format!("{simplified}").contains("min"), "Min dropped: {simplified}");
2251    }
2252
2253    #[test]
2254    fn min_preserved_in_subtraction_parts() {
2255        // Test that Min([1, X]) simplifies correctly in isolation
2256        let s = SymbolScope::default();
2257        let t = s.sym("T");
2258        let p = s.sym("P");
2259        let ss = s.sym("S");
2260
2261        let cum_after =
2262            Max(vec![Val(0), (Sym(t.clone()) + Val(1)) * Sym(p.clone()) - Sym(ss.clone())]);
2263        let min_after = Min(vec![Val(1), cum_after.clone()]);
2264        let simplified = min_after.simplify();
2265        eprintln!("min_after simplified: {simplified}");
2266        // Must contain "min" — the Min must not be dropped
2267        assert!(format!("{simplified}").contains("min"), "Min wrapper was dropped: {simplified}");
2268    }
2269
2270    #[test]
2271    fn min_preserved_in_subtraction() {
2272        // min(1, X) - min(1, Y) must preserve the min() wrappers.
2273        // This is the pattern used by PulseV2Pad's output_facts for after-padding.
2274        let s = SymbolScope::default();
2275        let t = s.sym("T");
2276        let p = s.sym("P");
2277        let ss = s.sym("S");
2278
2279        let cum_after =
2280            Max(vec![Val(0), (Sym(t.clone()) + Val(1)) * Sym(p.clone()) - Sym(ss.clone())]);
2281        let cum_before = Max(vec![Val(0), Sym(t.clone()) * Sym(p.clone()) - Sym(ss.clone())]);
2282
2283        let ap = Min(vec![Val(1), cum_after.clone()]) - Min(vec![Val(1), cum_before.clone()]);
2284        let simplified = ap.simplify();
2285
2286        // At T=1, P=4, S=3: min(1, max(0, 8-3)) - min(1, max(0, 4-3)) = 1 - 1 = 0
2287        use super::super::sym::SymbolValues;
2288        let sv = SymbolValues::default().with(&t, 1).with(&p, 4).with(&ss, 3);
2289        assert_eq!(simplified.eval_to_i64(&sv).unwrap(), 0, "simplified: {simplified}");
2290
2291        // At T=0, P=4, S=3: min(1, max(0, 4-3)) - min(1, max(0, 0-3)) = 1 - 0 = 1
2292        let sv = SymbolValues::default().with(&t, 0).with(&p, 4).with(&ss, 3);
2293        assert_eq!(simplified.eval_to_i64(&sv).unwrap(), 1, "simplified: {simplified}");
2294
2295        // At T=0, P=1, S=1: min(1, max(0, 1-1)) - min(1, max(0, 0-1)) = 0 - 0 = 0
2296        let sv = SymbolValues::default().with(&t, 0).with(&p, 1).with(&ss, 1);
2297        assert_eq!(simplified.eval_to_i64(&sv).unwrap(), 0, "simplified: {simplified}");
2298    }
2299
2300    #[test]
2301    fn mul_neg_b_by_8() {
2302        let s = SymbolScope::default();
2303        let b = Sym(s.sym("B"));
2304        // 8*(-1*B) should equal -8*B
2305        let a = Mul(vec![Val(8), MulInt(-1, Box::new(b.clone()))]);
2306        let c = MulInt(-8, Box::new(b.clone()));
2307        let a_s = a.simplify();
2308        let c_s = c.simplify();
2309        assert_eq!(a_s, c_s, "8*(-1*B) should simplify the same as -8*B");
2310    }
2311
2312    /// Encoder-pulse case: (6 + 14·S) / 8 == (3 + 7·S) / 4.
2313    /// Both Add terms (Val(6) and MulInt(14, S)) share factor 2 with
2314    /// divisor 8, so the simplifier should reduce both sides by 2.
2315    #[test]
2316    fn reduce_div_by_common_factor_with_divisor() {
2317        let lhs = (A.to_dim() * 14 + 6) / 8;
2318        let rhs = (A.to_dim() * 7 + 3) / 4;
2319        assert_eq!(lhs, rhs);
2320    }
2321
2322    /// Common factor that fully divides the divisor → drop the divisor.
2323    /// (4·a + 8) / 4  ==  a + 2.
2324    #[test]
2325    fn reduce_div_when_factor_equals_divisor() {
2326        let lhs = (A.to_dim() * 4 + 8) / 4;
2327        let rhs = A.to_dim() + 2;
2328        assert_eq!(lhs, rhs);
2329    }
2330
2331    /// No common factor → no reduction (identity check).
2332    /// (3 + 7·a) / 4 stays as-is (gcd(3, 7, 4) = 1).
2333    #[test]
2334    fn no_reduce_when_terms_coprime_with_divisor() {
2335        let e = (A.to_dim() * 7 + 3) / 4;
2336        // We just check it didn't reduce to something weird; the
2337        // canonical form is `Div(Add(...), 4)`.
2338        match &e {
2339            Div(_, q) => assert_eq!(*q, 4),
2340            other => panic!("expected Div(_, 4), got {other:?}"),
2341        }
2342    }
2343
2344    /// Sym without an explicit `MulInt` wrapper has implicit coefficient 1.
2345    /// Any common factor gcd including 1 collapses to 1, so the reduction
2346    /// does nothing — the rule must not silently drop the Sym.
2347    #[test]
2348    fn no_reduce_when_sym_has_implicit_unit_coefficient() {
2349        // (a + 4) / 2 must stay non-trivial — gcd(1, 4, 2) = 1.
2350        let e = (A.to_dim() + 4) / 2;
2351        // It can simplify to other forms but it should still depend on `a`.
2352        // Eval at a=2 → (2+4)/2 = 3.  Eval at a=4 → (4+4)/2 = 4.
2353        let sv2 = SymbolValues::default().with(&A, 2);
2354        let sv4 = SymbolValues::default().with(&A, 4);
2355        assert_eq!(e.eval_to_i64(&sv2).unwrap(), 3);
2356        assert_eq!(e.eval_to_i64(&sv4).unwrap(), 4);
2357    }
2358}