alkahest-cas 3.12.0

High-performance computer algebra kernel: symbolic expressions, polynomials, Gröbner bases, JIT, and Arb ball arithmetic.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
//! Explicit, conservative assumptions for condition-gated simplification.

use super::idmap::IdSet;
use crate::deriv::SideCondition;
use crate::errors::AlkahestError;
use crate::kernel::expr::PredicateKind;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::logic::{satisfiable, Satisfiability};
use crate::simplify::{rules_for_config, simplify_with, SimplifyConfig};
use crate::DerivedExpr;

/// A contradiction in an explicit assumption context.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssumptionError {
    /// The asserted predicate contradicts the context's supported arithmetic facts.
    Contradiction,
}

impl std::fmt::Display for AssumptionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Contradiction => write!(f, "assumption contradicts the current context"),
        }
    }
}

impl std::error::Error for AssumptionError {}

impl AlkahestError for AssumptionError {
    fn code(&self) -> &'static str {
        "E-SIMPLIFY-001"
    }

    fn remediation(&self) -> Option<&'static str> {
        Some("remove the conflicting refinement or create a separate AssumptionContext")
    }
}

/// User-provided predicates plus the small set of facts safe to use in rewrites.
///
/// Only positivity and non-zero facts are normalized. Other predicates are
/// retained for contradiction checks but do not authorize a simplification.
#[derive(Debug, Clone, Default)]
pub struct AssumptionContext {
    predicates: Vec<ExprId>,
    facts: Vec<SideCondition>,
}

impl AssumptionContext {
    /// Create an empty context.
    pub fn new() -> Self {
        Self::default()
    }

    /// All predicates explicitly supplied to this context.
    pub fn predicates(&self) -> &[ExprId] {
        &self.predicates
    }

    /// Facts that the conditional simplifier may use.
    pub fn facts(&self) -> &[SideCondition] {
        &self.facts
    }

    /// Add a predicate atomically.
    ///
    /// A definitive `Unsat` result rejects the predicate and leaves this context
    /// unchanged. `Unknown` is retained as provenance but grants no rewrite fact.
    pub fn refine(&mut self, predicate: ExprId, pool: &ExprPool) -> Result<(), AssumptionError> {
        let mut predicates = self.predicates.clone();
        predicates.push(predicate);
        let conjunction = if predicates.len() == 1 {
            predicate
        } else {
            pool.predicate(PredicateKind::And, predicates.clone())
        };
        if matches!(satisfiable(conjunction, pool), Satisfiability::Unsat) {
            return Err(AssumptionError::Contradiction);
        }

        self.predicates = predicates;
        for fact in normalize_predicate(predicate, pool) {
            push_unique(&mut self.facts, fact);
        }
        Ok(())
    }

    /// Simplify under this explicit context.
    ///
    /// Static symbol domains in the expression are added as facts for this call.
    pub fn simplify(&self, expr: ExprId, pool: &ExprPool) -> DerivedExpr<ExprId> {
        let mut facts = self.facts.clone();
        collect_static_domain_facts(expr, pool, &mut facts);
        let config = SimplifyConfig {
            assumptions: facts,
            ..SimplifyConfig::default()
        };
        let rules = rules_for_config(&config);
        simplify_with(expr, pool, &rules, config)
    }
}

/// Simplify using explicit assumptions without exposing the context internals.
pub fn simplify_with_assumptions(
    expr: ExprId,
    pool: &ExprPool,
    assumptions: &AssumptionContext,
) -> DerivedExpr<ExprId> {
    assumptions.simplify(expr, pool)
}

// ---------------------------------------------------------------------------
// Ambient assumption scope
// ---------------------------------------------------------------------------
//
// An [`AssumptionContext`] is a value the caller threads into the one call that
// wants it, which works for [`AssumptionContext::simplify`] and for `solve`,
// because the caller talks to those directly. It does not work for a fact that
// only a *sub*-engine needs: `integrate(exp(-k·t), t, 0, ∞)` bottoms out in
// `lim_{t→∞}` sixteen frames down, and threading an `&AssumptionContext`
// through every intermediate signature would be a breaking change to most of
// this crate's public surface for the benefit of one leaf.
//
// So the scope is ambient and thread-local, exactly like [`crate::budget`]:
// `alkahest.context(assumptions=…)` pushes a frame for the duration of its
// `with` block and every engine on that thread can consult it. The asymmetry
// noted in ARCHITECTURE.md applies here too — the stack is per-thread, so a
// `rayon` worker does not inherit its parent's assumptions and will simply
// refuse for want of a fact rather than answer with one it cannot see.
//
// # Why the frame records a pool address
//
// An `ExprId` is an index into one `ExprPool` and means something different in
// another. A frame therefore records the address of the pool its predicates
// were interned in, and a query against any other pool ignores it. The address
// is never dereferenced — it is compared, and nothing else — and
// [`enter_assumptions`] borrows the pool for the guard's lifetime, so the
// address cannot be recycled while a frame that mentions it is live.

use std::cell::RefCell;
use std::marker::PhantomData;

/// A sign established by the ambient assumptions (or by structure).
///
/// Deliberately three-valued and wrapped in an `Option` at every use site:
/// `None` means *unknown*, which is a different claim from
/// [`Sign::Zero`], and conflating the two is how a limit engine ends up
/// guessing `+∞` for `lim_{x→∞} k·x`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sign {
    /// Strictly less than zero.
    Negative,
    /// Exactly zero.
    Zero,
    /// Strictly greater than zero.
    Positive,
}

impl Sign {
    /// `-1`, `0`, `1` — the encoding the older `structural_sign` helpers use.
    pub fn to_i8(self) -> i8 {
        match self {
            Sign::Negative => -1,
            Sign::Zero => 0,
            Sign::Positive => 1,
        }
    }

    fn of_ordering(ord: std::cmp::Ordering) -> Sign {
        match ord {
            std::cmp::Ordering::Less => Sign::Negative,
            std::cmp::Ordering::Equal => Sign::Zero,
            std::cmp::Ordering::Greater => Sign::Positive,
        }
    }

    fn negate(self) -> Sign {
        match self {
            Sign::Negative => Sign::Positive,
            Sign::Zero => Sign::Zero,
            Sign::Positive => Sign::Negative,
        }
    }
}

struct AmbientFrame {
    /// Address of the pool `predicates` were interned in. Compared, never read.
    pool: usize,
    predicates: Vec<ExprId>,
}

thread_local! {
    static AMBIENT: RefCell<Vec<AmbientFrame>> = const { RefCell::new(Vec::new()) };
}

/// RAII handle for one ambient assumption frame.
///
/// Not `Send`: the frame lives on a thread-local stack and must be dropped on
/// the thread that pushed it, the same contract [`crate::budget::BudgetGuard`]
/// carries.
pub struct AssumptionScope {
    _not_send: PhantomData<*const ()>,
}

impl Drop for AssumptionScope {
    fn drop(&mut self) {
        AMBIENT.with(|s| {
            s.borrow_mut().pop();
        });
    }
}

/// Make `assumptions` visible to every engine on this thread until the
/// returned guard is dropped.
///
/// `pool` must be the pool `assumptions`' predicates were interned in; queries
/// made against a different pool ignore this frame.
pub fn enter_assumptions(assumptions: &AssumptionContext, pool: &ExprPool) -> AssumptionScope {
    AMBIENT.with(|s| {
        s.borrow_mut().push(AmbientFrame {
            pool: pool as *const ExprPool as usize,
            predicates: assumptions.predicates().to_vec(),
        })
    });
    AssumptionScope {
        _not_send: PhantomData,
    }
}

/// `true` when some ambient frame on this thread was entered with `pool`.
///
/// The hot-path escape hatch: every query below is a no-op without one, and
/// this is a single thread-local read.
pub fn ambient_assumptions_active(pool: &ExprPool) -> bool {
    let addr = pool as *const ExprPool as usize;
    AMBIENT.with(|s| s.borrow().iter().any(|f| f.pool == addr))
}

/// Symbols the ambient scope asserts are exactly equal to a numeric constant.
///
/// Returned as `(symbol, value)` pairs so a caller can substitute them before
/// doing any analysis. Only equalities against a literal are reported: they are
/// the only ones for which substitution is unconditionally sound and
/// terminating.
pub fn ambient_equalities(pool: &ExprPool) -> Vec<(ExprId, ExprId)> {
    let addr = pool as *const ExprPool as usize;
    let mut out: Vec<(ExprId, ExprId)> = Vec::new();
    AMBIENT.with(|s| {
        for frame in s.borrow().iter().filter(|f| f.pool == addr) {
            for &p in &frame.predicates {
                collect_equalities(p, pool, &mut out);
            }
        }
    });
    out
}

fn collect_equalities(predicate: ExprId, pool: &ExprPool, out: &mut Vec<(ExprId, ExprId)>) {
    let ExprData::Predicate { kind, args } = pool.get(predicate) else {
        return;
    };
    match kind {
        PredicateKind::And => {
            for &part in &args {
                collect_equalities(part, pool, out);
            }
        }
        PredicateKind::Eq if args.len() == 2 => {
            let is_sym = |e: ExprId| matches!(pool.get(e), ExprData::Symbol { .. });
            let is_num = |e: ExprId| {
                matches!(
                    pool.get(e),
                    ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_)
                )
            };
            if is_sym(args[0]) && is_num(args[1]) {
                out.push((args[0], args[1]));
            } else if is_num(args[0]) && is_sym(args[1]) {
                out.push((args[1], args[0]));
            }
        }
        _ => {}
    }
}

/// The sign of `expr`, as far as structure and the ambient assumptions
/// establish it. `None` means *not established* — never *zero*.
///
/// Composition is the usual sign algebra, and every rule is conservative: a
/// factor of unknown sign makes the product unknown, and a sum is only decided
/// when every summand agrees. Callers that used to fall back to a guess when
/// this returns `None` must refuse instead; that guess is the defect this
/// function exists to remove.
pub fn assumed_sign(expr: ExprId, pool: &ExprPool) -> Option<Sign> {
    assumed_sign_at(expr, pool, 0)
}

/// Bound on how deep the sign algebra recurses. A sign is decided by the
/// leading structure of an expression; anything this far down is a coefficient
/// the caller is better off refusing than exploring.
const MAX_SIGN_DEPTH: u32 = 24;

fn assumed_sign_at(expr: ExprId, pool: &ExprPool, depth: u32) -> Option<Sign> {
    if depth > MAX_SIGN_DEPTH {
        return None;
    }
    // A stated fact about this exact expression wins outright: the user said so.
    if let Some(s) = stated_sign(expr, pool) {
        return Some(s);
    }
    match pool.get(expr) {
        ExprData::Integer(n) => Some(Sign::of_ordering(n.0.cmp(&rug::Integer::ZERO))),
        ExprData::Rational(r) => Some(Sign::of_ordering(r.0.cmp0())),
        // The sign of the `rug` value itself: `to_f64` flushes a float past
        // the `f64` exponent range (`1e-400` at any precision) to zero, and
        // this used to report such a float as `Sign::Zero`.
        ExprData::Float(f) => {
            if !f.inner.is_finite() {
                return None;
            }
            Some(Sign::of_ordering(f.inner.cmp0()?))
        }
        ExprData::Symbol {
            domain: Domain::Positive,
            ..
        } => Some(Sign::Positive),
        ExprData::Symbol { .. } => None,
        ExprData::Mul(xs) => {
            let mut sign = Sign::Positive;
            for x in xs {
                match assumed_sign_at(x, pool, depth + 1)? {
                    Sign::Zero => return Some(Sign::Zero),
                    Sign::Negative => sign = sign.negate(),
                    Sign::Positive => {}
                }
            }
            Some(sign)
        }
        ExprData::Add(xs) => {
            let mut sign: Option<Sign> = None;
            for x in xs {
                let s = assumed_sign_at(x, pool, depth + 1)?;
                match (sign, s) {
                    (_, Sign::Zero) => {}
                    (None, s) => sign = Some(s),
                    (Some(a), b) if a == b => {}
                    _ => return None,
                }
            }
            sign.or(Some(Sign::Zero))
        }
        ExprData::Pow { base, exp } => {
            let base_sign = assumed_sign_at(base, pool, depth + 1);
            // An even integer power is non-negative whatever the base is, and
            // strictly positive once the base is known non-zero.
            if let ExprData::Integer(n) = pool.get(exp) {
                if n.0.is_even() {
                    return match base_sign {
                        Some(Sign::Zero) => Some(Sign::Zero),
                        Some(_) => Some(Sign::Positive),
                        // A base of unknown sign may still be *zero*, and `0² = 0`
                        // is not positive. `≥ 0` is not a sign, so say nothing. A
                        // caller that separately knows the base is non-zero can
                        // take the stronger step itself.
                        None => None,
                    };
                }
                // An odd power preserves the base's sign; `x^{-1}` too.
                return base_sign;
            }
            // A positive base raised to anything real stays positive.
            match base_sign {
                Some(Sign::Positive) => Some(Sign::Positive),
                _ => None,
            }
        }
        ExprData::Func { name, args } => match name.as_str() {
            "exp" | "cosh" => Some(Sign::Positive),
            "abs" if args.len() == 1 => match assumed_sign_at(args[0], pool, depth + 1) {
                Some(Sign::Zero) => Some(Sign::Zero),
                Some(_) => Some(Sign::Positive),
                None => None,
            },
            "sqrt" | "cbrt" if args.len() == 1 => assumed_sign_at(args[0], pool, depth + 1),
            _ => None,
        },
        _ => None,
    }
}

/// A sign asserted directly about `expr` by some ambient frame, or implied by
/// `expr`'s static [`Domain`].
fn stated_sign(expr: ExprId, pool: &ExprPool) -> Option<Sign> {
    if matches!(
        pool.get(expr),
        ExprData::Symbol {
            domain: Domain::Positive,
            ..
        }
    ) {
        return Some(Sign::Positive);
    }
    let addr = pool as *const ExprPool as usize;
    AMBIENT.with(|s| {
        for frame in s.borrow().iter().rev().filter(|f| f.pool == addr) {
            for &p in &frame.predicates {
                if let Some(sign) = sign_from_predicate(p, expr, pool) {
                    return Some(sign);
                }
            }
        }
        None
    })
}

/// The sign `predicate` states for `subject`, if it states one.
///
/// Only comparisons against literal zero are read. `a > b` with `b` non-zero
/// says nothing about `a`'s sign without also knowing `b`'s, and a partial
/// implementation of that inference is a worse answer than none.
fn sign_from_predicate(predicate: ExprId, subject: ExprId, pool: &ExprPool) -> Option<Sign> {
    let ExprData::Predicate { kind, args } = pool.get(predicate) else {
        return None;
    };
    if matches!(kind, PredicateKind::And) {
        return args
            .iter()
            .find_map(|&part| sign_from_predicate(part, subject, pool));
    }
    if args.len() != 2 {
        return None;
    }
    let (lhs, rhs) = (args[0], args[1]);
    // Orient the comparison so `subject` is on the left, mirroring the operator.
    let kind = if lhs == subject && is_zero(rhs, pool) {
        kind
    } else if rhs == subject && is_zero(lhs, pool) {
        match kind {
            PredicateKind::Lt => PredicateKind::Gt,
            PredicateKind::Le => PredicateKind::Ge,
            PredicateKind::Gt => PredicateKind::Lt,
            PredicateKind::Ge => PredicateKind::Le,
            other => other,
        }
    } else {
        return None;
    };
    match kind {
        PredicateKind::Gt => Some(Sign::Positive),
        PredicateKind::Lt => Some(Sign::Negative),
        PredicateKind::Eq => Some(Sign::Zero),
        // `≥ 0` and `≤ 0` do not establish a *strict* sign, and `≠ 0` does not
        // establish a direction. Reporting one would be a guess.
        _ => None,
    }
}

fn normalize_predicate(predicate: ExprId, pool: &ExprPool) -> Vec<SideCondition> {
    let ExprData::Predicate { kind, args } = pool.get(predicate) else {
        return vec![];
    };
    match kind {
        PredicateKind::And => args
            .iter()
            .flat_map(|&part| normalize_predicate(part, pool))
            .collect(),
        PredicateKind::Gt if args.len() == 2 && is_zero(args[1], pool) => {
            vec![SideCondition::Positive(args[0])]
        }
        PredicateKind::Lt if args.len() == 2 && is_zero(args[0], pool) => {
            vec![SideCondition::Positive(args[1])]
        }
        PredicateKind::Ne if args.len() == 2 && is_zero(args[1], pool) => {
            vec![SideCondition::NonZero(args[0])]
        }
        PredicateKind::Ne if args.len() == 2 && is_zero(args[0], pool) => {
            vec![SideCondition::NonZero(args[1])]
        }
        _ => vec![],
    }
}

fn is_zero(expr: ExprId, pool: &ExprPool) -> bool {
    match pool.get(expr) {
        ExprData::Integer(value) => value.0 == 0,
        ExprData::Rational(value) => value.0 == 0,
        _ => false,
    }
}

fn push_unique(facts: &mut Vec<SideCondition>, fact: SideCondition) {
    if !facts.contains(&fact) {
        facts.push(fact);
    }
}

/// Walk `expr` and record rewrite facts implied by static symbol domains.
///
/// Only [`Domain::Positive`] (→ Positive + NonZero) and [`Domain::NonZero`] are
/// collected. Other domains do not authorize conditional rewrites.
///
/// # Why this is a worklist and not a recursion
///
/// [`super::engine::simplify_with`] calls this on the *result* of every
/// simplification, so it sees whatever the rules left behind — including an
/// expression the rules could not shrink, at whatever depth the caller built
/// it. As a recursion it descended one stack frame per level with nothing
/// bounding the descent, and running out of stack aborts the process rather
/// than raising anything a caller could catch. That put an abort back on the
/// path `crate::simplify::stack` exists to keep off it, one function later.
///
/// A walk that only *collects* has nothing to compose on the way back up, so
/// it needs no stack at all: the pending nodes live on the heap and depth
/// costs memory instead of stack. `pending` stays empty for an atom and never
/// allocates for one.
///
/// Children are pushed in reverse so popping yields them left to right: the
/// order facts land in `facts` is exactly what the recursion produced, and
/// this is a behaviour-preserving change rather than merely an equivalent one.
///
/// # Why each interior node is expanded once
///
/// The pool is a DAG, and a walk that re-enters a shared subtree every time a
/// parent reaches it visits it once per *path* rather than once per node — a
/// Chebyshev-style recurrence `T(n+1) = 2x·T(n) − T(n−1)`, where every `T(k)`
/// has two parents, has `O(n)` nodes and `O(fib(n))` paths.  Every `simplify`
/// ends here, so re-simplifying an already-simplified 104-node expression of
/// that shape took 726 ms at `n = 34`, nearly all of it in this walk.
/// `expanded` makes the walk linear in the number of distinct nodes.
///
/// Skipping a revisit cannot change `facts`: a node's facts depend on the node
/// alone, its first visit already pushed them, and [`push_unique`] would have
/// discarded a second copy — so the order is still the first-occurrence
/// pre-order the recursion produced.  Only interior nodes are recorded: a leaf
/// is cheaper to re-read than to hash.
///
/// # Why most calls return immediately
///
/// The pool records, per node, whether its subtree holds any `Positive` or
/// `NonZero` symbol at all ([`ExprPool::has_static_domain_fact`]).  A subtree
/// without one contributes no fact, so it is skipped whole — and for the
/// common expression with no such symbol anywhere the walk ends at the root.
pub(crate) fn collect_static_domain_facts(
    expr: ExprId,
    pool: &ExprPool,
    facts: &mut Vec<SideCondition>,
) {
    let mut pending: Vec<ExprId> = Vec::new();
    let mut expanded = IdSet::default();
    let mut current = expr;
    loop {
        pool.with(current, |data| match data {
            _ if !pool.has_static_domain_fact(current) => {}
            ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => {}
            ExprData::Symbol { domain, .. } => match domain {
                Domain::Positive => {
                    push_unique(facts, SideCondition::Positive(current));
                    push_unique(facts, SideCondition::NonZero(current));
                }
                Domain::NonZero => push_unique(facts, SideCondition::NonZero(current)),
                _ => {}
            },
            // Already expanded: every child is queued or done, so nothing
            // below it can add a fact the first visit did not.
            _ if !expanded.insert(current) => {}
            ExprData::Add(args)
            | ExprData::Mul(args)
            | ExprData::Func { args, .. }
            | ExprData::Predicate { args, .. } => pending.extend(args.iter().rev()),
            ExprData::Pow { base, exp } => pending.extend([*exp, *base]),
            ExprData::Piecewise { branches, default } => {
                pending.push(*default);
                for (condition, value) in branches.iter().rev() {
                    pending.extend([*value, *condition]);
                }
            }
            ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
                pending.extend([*body, *var]);
            }
            ExprData::BigO(arg) => pending.push(*arg),
            ExprData::RootSum { poly, var, body } => {
                pending.extend([*body, *var, *poly]);
            }
        });
        match pending.pop() {
            Some(next) => current = next,
            None => return,
        }
    }
}

/// [`collect_static_domain_facts`] as it was before it kept a visited set —
/// one visit per *path* — kept as the oracle for the differential tests.
#[cfg(test)]
pub(crate) fn collect_static_domain_facts_reference(
    expr: ExprId,
    pool: &ExprPool,
    facts: &mut Vec<SideCondition>,
) {
    let mut pending: Vec<ExprId> = Vec::new();
    let mut current = expr;
    loop {
        pool.with(current, |data| match data {
            ExprData::Symbol { domain, .. } => match domain {
                Domain::Positive => {
                    push_unique(facts, SideCondition::Positive(current));
                    push_unique(facts, SideCondition::NonZero(current));
                }
                Domain::NonZero => push_unique(facts, SideCondition::NonZero(current)),
                _ => {}
            },
            ExprData::Add(args)
            | ExprData::Mul(args)
            | ExprData::Func { args, .. }
            | ExprData::Predicate { args, .. } => pending.extend(args.iter().rev()),
            ExprData::Pow { base, exp } => pending.extend([*exp, *base]),
            ExprData::Piecewise { branches, default } => {
                pending.push(*default);
                for (condition, value) in branches.iter().rev() {
                    pending.extend([*value, *condition]);
                }
            }
            ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
                pending.extend([*body, *var]);
            }
            ExprData::BigO(arg) => pending.push(*arg),
            ExprData::RootSum { poly, var, body } => {
                pending.extend([*body, *var, *poly]);
            }
            ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => {}
        });
        match pending.pop() {
            Some(next) => current = next,
            None => return,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn positive_refinement_enables_conditional_rewrites() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let zero = pool.integer(0_i32);
        let mut assumptions = AssumptionContext::new();
        assumptions
            .refine(pool.predicate(PredicateKind::Gt, vec![x, zero]), &pool)
            .unwrap();

        let squared = pool.pow(x, pool.integer(2_i32));
        assert_eq!(
            assumptions
                .simplify(pool.func("sqrt", vec![squared]), &pool)
                .value,
            x
        );
        assert_eq!(
            assumptions
                .simplify(pool.func("exp", vec![pool.func("log", vec![x])]), &pool)
                .value,
            x
        );
    }

    #[test]
    fn nonzero_refinement_enables_cancellation() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let zero = pool.integer(0_i32);
        let mut assumptions = AssumptionContext::new();
        assumptions
            .refine(pool.predicate(PredicateKind::Ne, vec![x, zero]), &pool)
            .unwrap();

        assert_eq!(
            assumptions.simplify(pool.pow(x, zero), &pool).value,
            pool.integer(1_i32)
        );
        let inverse = pool.pow(x, pool.integer(-1_i32));
        assert_eq!(
            assumptions
                .simplify(pool.mul(vec![x, inverse]), &pool)
                .value,
            pool.integer(1_i32)
        );
    }

    #[test]
    fn contradiction_is_rejected_without_mutating_context() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let zero = pool.integer(0_i32);
        let positive = pool.predicate(PredicateKind::Gt, vec![x, zero]);
        let nonpositive = pool.predicate(PredicateKind::Le, vec![x, zero]);
        let mut assumptions = AssumptionContext::new();
        assumptions.refine(positive, &pool).unwrap();

        assert_eq!(
            assumptions.refine(nonpositive, &pool),
            Err(AssumptionError::Contradiction)
        );
        assert_eq!(assumptions.predicates(), &[positive]);
    }

    #[test]
    fn positive_refinement_enables_abs_of_positive() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let zero = pool.integer(0_i32);
        let mut assumptions = AssumptionContext::new();
        assumptions
            .refine(pool.predicate(PredicateKind::Gt, vec![x, zero]), &pool)
            .unwrap();

        assert_eq!(
            assumptions.simplify(pool.func("abs", vec![x]), &pool).value,
            x
        );
    }

    #[test]
    fn abs_without_positive_fact_unchanged() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let expr = pool.func("abs", vec![x]);
        assert_eq!(AssumptionContext::new().simplify(expr, &pool).value, expr);
    }

    #[test]
    fn static_positive_domain_is_available_without_refinement() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Positive);
        let squared = pool.pow(x, pool.integer(2_i32));

        assert_eq!(
            AssumptionContext::new()
                .simplify(pool.func("sqrt", vec![squared]), &pool)
                .value,
            x
        );
    }

    /// Chebyshev recurrence `T(k+1) = 2y·T(k) − T(k−1)`: every `T(k)` has two
    /// parents, so the expression has `O(n)` nodes and `O(fib(n))` paths.
    fn chebyshev(pool: &ExprPool, y: ExprId, n: usize) -> ExprId {
        let two = pool.integer(2_i32);
        let minus_one = pool.integer(-1_i32);
        let (mut a, mut b) = (pool.integer(1_i32), y);
        for _ in 1..n {
            let c = pool.add(vec![
                pool.mul(vec![two, y, b]),
                pool.mul(vec![minus_one, a]),
            ]);
            a = b;
            b = c;
        }
        b
    }

    /// The walk visits each shared node once.  One visit per path is
    /// `fib(60) ≈ 1.5·10¹²` visits here, which never finishes.
    #[test]
    fn static_domain_facts_on_a_shared_dag_are_linear() {
        let pool = ExprPool::new();
        let y = pool.symbol("y", Domain::Positive);
        let e = chebyshev(&pool, y, 60);
        let start = std::time::Instant::now();
        let mut facts = Vec::new();
        collect_static_domain_facts(e, &pool, &mut facts);
        assert!(start.elapsed() < std::time::Duration::from_secs(2));
        assert_eq!(
            facts,
            vec![SideCondition::Positive(y), SideCondition::NonZero(y)]
        );
    }

    /// The intern-time flag the walk prunes on sees a fact-bearing symbol
    /// anywhere the walk would, piecewise conditions and binders included.
    #[test]
    fn static_domain_fact_flag_tracks_the_walk() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let p = pool.symbol("p", Domain::Positive);
        let n = pool.symbol("n", Domain::NonZero);
        let plain = pool.add(vec![x, pool.func("sin", vec![x])]);
        assert!(!pool.has_static_domain_fact(plain));
        assert!(pool.has_static_domain_fact(p));
        assert!(pool.has_static_domain_fact(pool.pow(x, n)));
        let cond = pool.pred_gt(p, pool.integer(0_i32));
        let pw = pool.piecewise(vec![(cond, x)], x);
        assert!(pool.has_static_domain_fact(pw));
        let bound = pool.forall(n, x);
        assert!(pool.has_static_domain_fact(bound));
        for e in [plain, pw, bound] {
            let (mut fast, mut slow) = (Vec::new(), Vec::new());
            collect_static_domain_facts(e, &pool, &mut fast);
            collect_static_domain_facts_reference(e, &pool, &mut slow);
            assert_eq!(fast, slow);
        }
    }

    /// Every `simplify` ends in the fact walk, so re-simplifying an
    /// already-simplified shared DAG was exponential too: 726 ms at `n = 34`
    /// (release), ~13 s extrapolated at `n = 40`.
    #[test]
    fn simplify_of_a_simplified_shared_dag_is_linear() {
        let pool = ExprPool::new();
        let x = pool.symbol("x", Domain::Real);
        let (mut a, mut b) = (pool.integer(1_i32), x);
        for _ in 1..40 {
            let c = pool.add(vec![pool.mul(vec![x, b]), a]);
            a = b;
            b = c;
        }
        let once = crate::simplify::simplify(b, &pool).value;
        let start = std::time::Instant::now();
        let twice = crate::simplify::simplify(once, &pool);
        assert!(start.elapsed() < std::time::Duration::from_secs(2));
        assert_eq!(twice.value, once);
        assert!(twice.log.is_empty());
    }
}

/// A float whose magnitude is past the `f64` exponent range (`±1e-400`).
#[cfg(test)]
fn tiny_float(pool: &ExprPool, negative: bool) -> ExprId {
    let mut f = rug::Float::with_val(64, rug::Float::i_exp(1, -1400));
    if negative {
        f = -f;
    }
    assert_eq!(f.to_f64(), 0.0, "the premise: f64 flushes it to zero");
    pool.intern(ExprData::Float(crate::kernel::BigFloat {
        inner: f,
        prec: 64,
    }))
}

/// Audit A4 sweep: the sign of a float was read through `to_f64`.
#[cfg(test)]
mod float_sign_tests {
    use super::*;

    #[test]
    fn a_float_below_f64_range_is_not_zero() {
        let pool = ExprPool::new();
        assert_eq!(
            assumed_sign(tiny_float(&pool, false), &pool),
            Some(Sign::Positive)
        );
        assert_eq!(
            assumed_sign(tiny_float(&pool, true), &pool),
            Some(Sign::Negative)
        );
        assert_eq!(assumed_sign(pool.float(0.0, 53), &pool), Some(Sign::Zero));
    }
}