Skip to main content

logicaffeine_base/
hybrid.rs

1//! Hybrid flows: loops of infinitesimal steps whose state flows between jumps — a bouncing ball, a
2//! thermostat — run exactly, as the ideal hybrid system their steps follow to within an
3//! infinitesimal.
4//!
5//! A loop stepping its clock by `dt` whose body is an Euler step `x ← x + dt·f(x)` of a polynomial
6//! field and one or more jumps `If guard: …` stays, between jumps, within an infinitesimal of the
7//! flow of `x' = f(x)`, and a jump whose guard the flow crosses fires within an infinitesimal of
8//! the crossing. Every value the loop leaves is therefore infinitely close to the value the ideal
9//! hybrid system leaves, and that value is what [`run`] computes, exactly, in real algebraic
10//! numbers:
11//!
12//! - **flights**: a field whose Lie series terminates flows along polynomials in time, so a flight
13//!   is a polynomial in the time since it began;
14//! - **events**: a guard's atoms along a flight are polynomials in time. Their real roots cut the
15//!   flight into intervals on which every atom keeps one sign, and the guard fires at the first
16//!   root after which it holds. A guard that only touches its boundary, two guards that fire at
17//!   once, or a jump in the loop's last step depend on which infinitesimal `dt` is, and are refused
18//!   by name;
19//! - **jumps**: the jump's statements run on the state at the crossing in three-valued logic. Two
20//!   values infinitely close to each other are told apart only by what is known of their
21//!   difference: that the run holds both exactly, or a sign the guard or a reset implies;
22//! - **Zeno tails**: when one jump's events follow each other by a scaling — every quantity the
23//!   guards read scaled by `s^wᵢ` and the time between events by `s^q`, under weights for which the
24//!   field, the guards and the resets are homogeneous ([`check_scaling`]) — every later event is
25//!   the scaled copy of the one before, every value at event `k` is a polynomial in `z = sᵏ` (plus
26//!   a multiple of `k` for a count), and for `s < 1` the events accumulate at the sum of a
27//!   geometric series. The values there are those polynomials at `z = 0`;
28//! - **continuous firing**: a jump whose guard holds again within an infinitesimal of its firing
29//!   fires infinitely often in every standard interval — the ball at rest after its Zeno time. The
30//!   state it writes stays where the jump holds it, the rest flows, and a value the jump records
31//!   takes its value at the last firing where its condition holds.
32//!
33//! Each value the run leaves says whether the loop's own value is exactly it or only infinitely
34//! close to it.
35
36use crate::algebraic::{resultant_in_y, Algebraic};
37use crate::numeric::Rational;
38use crate::poly::Poly;
39
40/// The most firings a run follows one by one before the rest must be read off a scaling. Each
41/// firing's values are exact numbers that grow as the firings go on, so the budget bounds the
42/// work as well as the count.
43pub const EVENT_BUDGET: usize = 1_000;
44
45/// The highest Lie derivative a field may need before its flow is taken not to be a polynomial in
46/// time.
47pub const MAX_LIE_ORDER: usize = 32;
48
49/// The largest denominator a weight `log r / log σ` is looked for with.
50const MAX_WEIGHT_DENOMINATOR: i64 = 64;
51
52/// The largest numerator a weight `log r / log σ` is looked for with.
53const MAX_WEIGHT_NUMERATOR: i64 = 256;
54
55fn alg(n: i64) -> Algebraic {
56    Algebraic::from_i64(n)
57}
58
59fn quotient(a: &Algebraic, b: &Algebraic) -> Algebraic {
60    a.div(b).expect("a divisor the run has checked is nonzero")
61}
62
63fn factorial(m: usize) -> Algebraic {
64    alg((1..=m as i64).product::<i64>().max(1))
65}
66
67/// `base^e` for any integer `e`, `base` nonzero when `e < 0`.
68fn power(base: &Algebraic, e: i64) -> Algebraic {
69    let e = i32::try_from(e).expect("an exponent the run builds fits an i32");
70    base.pow(e).expect("a power the run takes of zero is nonnegative")
71}
72
73// ── polynomials in one variable ──────────────────────────────────────────────────────────────
74
75/// A polynomial in one variable with real algebraic coefficients, lowest power first, with no
76/// trailing zero: a coordinate of a flight in the time since it began, an atom along a flight, a
77/// value along a tail in `z`.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct UPoly(Vec<Algebraic>);
80
81impl UPoly {
82    pub fn zero() -> UPoly {
83        UPoly(Vec::new())
84    }
85
86    pub fn from_coeffs(mut c: Vec<Algebraic>) -> UPoly {
87        while c.last().is_some_and(Algebraic::is_zero) {
88            c.pop();
89        }
90        UPoly(c)
91    }
92
93    pub fn constant(a: Algebraic) -> UPoly {
94        UPoly::from_coeffs(vec![a])
95    }
96
97    /// `a·xᵏ`.
98    pub fn monomial(a: Algebraic, k: usize) -> UPoly {
99        let mut c = vec![alg(0); k + 1];
100        c[k] = a;
101        UPoly::from_coeffs(c)
102    }
103
104    pub fn coeffs(&self) -> &[Algebraic] {
105        &self.0
106    }
107
108    pub fn coeff(&self, k: usize) -> Algebraic {
109        self.0.get(k).cloned().unwrap_or_else(|| alg(0))
110    }
111
112    pub fn is_zero(&self) -> bool {
113        self.0.is_empty()
114    }
115
116    pub fn degree(&self) -> Option<usize> {
117        self.0.len().checked_sub(1)
118    }
119
120    /// The constant this polynomial is, when it is one.
121    pub fn as_constant(&self) -> Option<Algebraic> {
122        match self.0.len() {
123            0 => Some(alg(0)),
124            1 => Some(self.0[0].clone()),
125            _ => None,
126        }
127    }
128
129    pub fn add(&self, o: &UPoly) -> UPoly {
130        let n = self.0.len().max(o.0.len());
131        UPoly::from_coeffs((0..n).map(|k| self.coeff(k).add(&o.coeff(k))).collect())
132    }
133
134    pub fn neg(&self) -> UPoly {
135        UPoly(self.0.iter().map(Algebraic::neg).collect())
136    }
137
138    pub fn sub(&self, o: &UPoly) -> UPoly {
139        self.add(&o.neg())
140    }
141
142    pub fn mul(&self, o: &UPoly) -> UPoly {
143        if self.is_zero() || o.is_zero() {
144            return UPoly::zero();
145        }
146        let mut c = vec![alg(0); self.0.len() + o.0.len() - 1];
147        for (i, a) in self.0.iter().enumerate() {
148            for (j, b) in o.0.iter().enumerate() {
149                c[i + j] = c[i + j].add(&a.mul(b));
150            }
151        }
152        UPoly::from_coeffs(c)
153    }
154
155    pub fn scale(&self, k: &Algebraic) -> UPoly {
156        UPoly::from_coeffs(self.0.iter().map(|a| a.mul(k)).collect())
157    }
158
159    pub fn eval(&self, x: &Algebraic) -> Algebraic {
160        self.0.iter().rev().fold(alg(0), |acc, c| acc.mul(x).add(c))
161    }
162
163    /// The lowest power with a nonzero coefficient, and that coefficient.
164    pub fn lowest(&self) -> Option<(usize, &Algebraic)> {
165        self.0.iter().enumerate().find(|(_, c)| !c.is_zero())
166    }
167
168    /// `self(k·x)`.
169    pub fn dilate(&self, k: &Algebraic) -> UPoly {
170        let mut scale = alg(1);
171        let mut c = Vec::with_capacity(self.0.len());
172        for a in &self.0 {
173            c.push(a.mul(&scale));
174            scale = scale.mul(k);
175        }
176        UPoly::from_coeffs(c)
177    }
178}
179
180// ── real roots ───────────────────────────────────────────────────────────────────────────────
181
182/// The distinct real roots of `p`, ascending; none for a constant.
183pub fn real_roots(p: &UPoly) -> Vec<Algebraic> {
184    let Some((zeros, _)) = p.lowest() else {
185        return Vec::new();
186    };
187    if zeros > 0 {
188        let rest = UPoly::from_coeffs(p.0[zeros..].to_vec());
189        let mut roots = real_roots(&rest);
190        if let Err(at) = roots.binary_search(&alg(0)) {
191            roots.insert(at, alg(0));
192        }
193        return roots;
194    }
195    let degree = match p.degree() {
196        None | Some(0) => return Vec::new(),
197        Some(d) => d,
198    };
199    match degree {
200        1 => return vec![quotient(&p.coeff(0).neg(), &p.coeff(1))],
201        2 => return quadratic_roots(&p.coeff(2), &p.coeff(1), &p.coeff(0)),
202        _ => {}
203    }
204    if let Some(rational) = p.0.iter().map(Algebraic::to_rational).collect::<Option<Vec<Rational>>>() {
205        return rational_roots(&Poly::from_coeffs(rational));
206    }
207    let mut roots: Vec<Algebraic> = rational_roots(&free_norm(p))
208        .into_iter()
209        .filter(|r| p.eval(r).is_zero())
210        .collect();
211    roots.dedup();
212    roots
213}
214
215fn rational_roots(p: &Poly) -> Vec<Algebraic> {
216    let q = p.squarefree_part();
217    (1..=q.count_real_roots()).filter_map(|k| Algebraic::root_of(&q, k)).collect()
218}
219
220fn quadratic_roots(a: &Algebraic, b: &Algebraic, c: &Algebraic) -> Vec<Algebraic> {
221    let disc = b.mul(b).sub(&alg(4).mul(a).mul(c));
222    let two_a = alg(2).mul(a);
223    match disc.signum() {
224        -1 => Vec::new(),
225        0 => vec![quotient(&b.neg(), &two_a)],
226        _ => {
227            let r = disc.sqrt().expect("a positive number has a square root");
228            let mut roots = vec![quotient(&b.neg().sub(&r), &two_a), quotient(&b.neg().add(&r), &two_a)];
229            roots.sort();
230            roots
231        }
232    }
233}
234
235/// A nonzero rational polynomial vanishing at every root of `p`: the product of `Σ σⱼ(aⱼ)·xʲ` over
236/// every choice of a conjugate `σⱼ(aⱼ)` for each nonzero coefficient. Its value at a point `ξ` is
237/// the product of the roots of the polynomial whose roots are the sums `Σ σⱼ(aⱼ)·ξʲ`, which the
238/// resultants that add algebraic numbers build; it is interpolated through `ξ = 1, 2, …`.
239fn free_norm(p: &UPoly) -> Poly {
240    let terms: Vec<(usize, &Algebraic)> = p.0.iter().enumerate().filter(|(_, a)| !a.is_zero()).collect();
241    let n = p.degree().expect("a polynomial with roots is nonzero");
242    let degree = n * terms.iter().map(|(_, a)| a.degree()).product::<usize>();
243    let points: Vec<Rational> = (1..=degree as i64 + 1).map(Rational::from_i64).collect();
244    let values: Vec<Rational> = points
245        .iter()
246        .map(|xi| {
247            let mut sums: Option<Poly> = None;
248            for (j, a) in &terms {
249                let shrink = xi
250                    .pow(*j as i32)
251                    .and_then(|s| s.recip())
252                    .expect("a positive point has nonzero powers");
253                let scaled = a
254                    .minimal_polynomial()
255                    .compose(&Poly::from_coeffs(vec![Rational::zero(), shrink]));
256                sums = Some(match sums {
257                    None => scaled,
258                    Some(s) => {
259                        let d = s.degree().expect("nonzero") * scaled.degree().expect("nonzero");
260                        resultant_in_y(
261                            &s,
262                            |z0| scaled.compose(&Poly::from_coeffs(vec![z0.clone(), Rational::from_i64(-1)])),
263                            d,
264                        )
265                    }
266                });
267            }
268            let s = sums.expect("a polynomial with roots has a nonzero coefficient");
269            let d = s.degree().expect("a polynomial of sums is nonzero");
270            let product = s.coeff(0).div(&s.coeff(d)).expect("a leading coefficient is nonzero");
271            if d % 2 == 1 {
272                product.negated()
273            } else {
274                product
275            }
276        })
277        .collect();
278    interpolate(&points, &values)
279}
280
281/// The polynomial through `(points[i], values[i])`, by Newton's divided differences.
282fn interpolate(points: &[Rational], values: &[Rational]) -> Poly {
283    let n = points.len();
284    let mut d = values.to_vec();
285    for j in 1..n {
286        for i in (j..n).rev() {
287            let num = d[i].sub(&d[i - 1]);
288            let den = points[i].sub(&points[i - j]);
289            d[i] = num.div(&den).expect("interpolation points are distinct");
290        }
291    }
292    let mut poly = Poly::constant(d[n - 1].clone());
293    for j in (0..n - 1).rev() {
294        let factor = Poly::from_coeffs(vec![points[j].negated(), Rational::one()]);
295        poly = poly.mul(&factor).add(&Poly::constant(d[j].clone()));
296    }
297    poly
298}
299
300/// A rational strictly between `a < b`.
301fn rational_between(a: &Algebraic, b: &Algebraic) -> Rational {
302    let mut bits = 8;
303    loop {
304        let (_, a_hi) = a.enclosure(bits);
305        let (b_lo, _) = b.enclosure(bits);
306        if a_hi < b_lo {
307            return a_hi
308                .add(&b_lo)
309                .mul(&Rational::from_ratio_i64(1, 2).expect("2 is nonzero"));
310        }
311        bits = bits.saturating_mul(2);
312    }
313}
314
315// ── polynomials over the slots ───────────────────────────────────────────────────────────────
316
317/// What a polynomial over the slots evaluates in: algebraic numbers, polynomials in one variable,
318/// polynomials over the slots, values along a tail.
319pub trait Ring: Clone {
320    fn lift(a: &Algebraic) -> Self;
321    fn plus(&self, o: &Self) -> Result<Self, String>;
322    fn times(&self, o: &Self) -> Result<Self, String>;
323}
324
325impl Ring for Algebraic {
326    fn lift(a: &Algebraic) -> Self {
327        a.clone()
328    }
329    fn plus(&self, o: &Self) -> Result<Self, String> {
330        Ok(self.add(o))
331    }
332    fn times(&self, o: &Self) -> Result<Self, String> {
333        Ok(self.mul(o))
334    }
335}
336
337impl Ring for UPoly {
338    fn lift(a: &Algebraic) -> Self {
339        UPoly::constant(a.clone())
340    }
341    fn plus(&self, o: &Self) -> Result<Self, String> {
342        Ok(self.add(o))
343    }
344    fn times(&self, o: &Self) -> Result<Self, String> {
345        Ok(self.mul(o))
346    }
347}
348
349/// A polynomial over the run's slots with real algebraic coefficients: each monomial by its
350/// exponents, listed once, in order, none with a zero coefficient.
351#[derive(Clone, Debug, PartialEq, Eq)]
352pub struct MPoly {
353    width: usize,
354    terms: Vec<(Vec<u32>, Algebraic)>,
355}
356
357impl MPoly {
358    pub fn zero(width: usize) -> MPoly {
359        MPoly { width, terms: Vec::new() }
360    }
361
362    pub fn constant(width: usize, a: Algebraic) -> MPoly {
363        MPoly::from_terms(width, vec![(vec![0; width], a)])
364    }
365
366    /// The value of slot `i`.
367    pub fn slot(width: usize, i: usize) -> MPoly {
368        let mut e = vec![0; width];
369        e[i] = 1;
370        MPoly::from_terms(width, vec![(e, alg(1))])
371    }
372
373    pub fn from_terms(width: usize, mut terms: Vec<(Vec<u32>, Algebraic)>) -> MPoly {
374        terms.sort_by(|a, b| a.0.cmp(&b.0));
375        let mut merged: Vec<(Vec<u32>, Algebraic)> = Vec::with_capacity(terms.len());
376        for (e, c) in terms {
377            match merged.last_mut() {
378                Some((last, sum)) if *last == e => *sum = sum.add(&c),
379                _ => merged.push((e, c)),
380            }
381        }
382        merged.retain(|(_, c)| !c.is_zero());
383        MPoly { width, terms: merged }
384    }
385
386    pub fn width(&self) -> usize {
387        self.width
388    }
389
390    pub fn terms(&self) -> &[(Vec<u32>, Algebraic)] {
391        &self.terms
392    }
393
394    pub fn is_zero(&self) -> bool {
395        self.terms.is_empty()
396    }
397
398    pub fn add(&self, o: &MPoly) -> MPoly {
399        MPoly::from_terms(self.width, self.terms.iter().chain(&o.terms).cloned().collect())
400    }
401
402    pub fn neg(&self) -> MPoly {
403        MPoly {
404            width: self.width,
405            terms: self.terms.iter().map(|(e, c)| (e.clone(), c.neg())).collect(),
406        }
407    }
408
409    pub fn sub(&self, o: &MPoly) -> MPoly {
410        self.add(&o.neg())
411    }
412
413    pub fn scale(&self, k: &Algebraic) -> MPoly {
414        MPoly::from_terms(self.width, self.terms.iter().map(|(e, c)| (e.clone(), c.mul(k))).collect())
415    }
416
417    pub fn mul(&self, o: &MPoly) -> MPoly {
418        let mut out = Vec::with_capacity(self.terms.len() * o.terms.len());
419        for (e, c) in &self.terms {
420            for (f, d) in &o.terms {
421                out.push((e.iter().zip(f).map(|(a, b)| a + b).collect(), c.mul(d)));
422            }
423        }
424        MPoly::from_terms(self.width, out)
425    }
426
427    /// `∂/∂xᵢ`.
428    pub fn partial(&self, i: usize) -> MPoly {
429        MPoly::from_terms(
430            self.width,
431            self.terms
432                .iter()
433                .filter(|(e, _)| e[i] > 0)
434                .map(|(e, c)| {
435                    let mut lowered = e.clone();
436                    lowered[i] -= 1;
437                    (lowered, c.mul(&alg(i64::from(e[i]))))
438                })
439                .collect(),
440        )
441    }
442
443    /// The derivative along `field`: `Σᵢ ∂/∂xᵢ · fieldᵢ`.
444    pub fn lie(&self, field: &[MPoly]) -> MPoly {
445        let mut out = MPoly::zero(self.width);
446        for (i, f) in field.iter().enumerate() {
447            if !f.is_zero() && self.reads_slot(i) {
448                out = out.add(&self.partial(i).mul(f));
449            }
450        }
451        out
452    }
453
454    pub fn reads_slot(&self, i: usize) -> bool {
455        self.terms.iter().any(|(e, _)| e[i] > 0)
456    }
457
458    /// The slots some monomial reads, ascending.
459    pub fn reads(&self) -> Vec<usize> {
460        (0..self.width).filter(|i| self.reads_slot(*i)).collect()
461    }
462
463    pub fn eval<R: Ring>(&self, x: &[R]) -> Result<R, String> {
464        let mut total = R::lift(&alg(0));
465        for (e, c) in &self.terms {
466            let mut term = R::lift(c);
467            for (i, k) in e.iter().enumerate() {
468                for _ in 0..*k {
469                    term = term.times(&x[i])?;
470                }
471            }
472            total = total.plus(&term)?;
473        }
474        Ok(total)
475    }
476
477    /// The `k ≠ 0` with `self = k·o`.
478    pub fn proportional_to(&self, o: &MPoly) -> Option<Algebraic> {
479        if self.terms.len() != o.terms.len() || self.terms.is_empty() {
480            return None;
481        }
482        let k = quotient(&self.terms[0].1, &o.terms[0].1);
483        self.terms
484            .iter()
485            .zip(&o.terms)
486            .all(|((e, c), (f, d))| e == f && *c == d.mul(&k))
487            .then_some(k)
488    }
489}
490
491/// `p` with each slot replaced by the polynomial over the slots in `x`.
492fn substitute(p: &MPoly, x: &[MPoly]) -> MPoly {
493    let mut total = MPoly::zero(p.width);
494    for (e, c) in &p.terms {
495        let mut term = MPoly::constant(p.width, c.clone());
496        for (i, k) in e.iter().enumerate() {
497            for _ in 0..*k {
498                term = term.mul(&x[i]);
499            }
500        }
501        total = total.add(&term);
502    }
503    total
504}
505
506// ── the model ────────────────────────────────────────────────────────────────────────────────
507
508/// `left − right` compared with zero.
509#[derive(Clone, Copy, Debug, PartialEq, Eq)]
510pub enum Rel {
511    Lt,
512    Le,
513    Gt,
514    Ge,
515    Eq,
516    Ne,
517}
518
519impl Rel {
520    /// The relation a model names: `<`, `<=`, `>`, `>=`, `=`, `!=`.
521    pub fn named(name: &str) -> Option<Rel> {
522        Some(match name {
523            "<" => Rel::Lt,
524            "<=" => Rel::Le,
525            ">" => Rel::Gt,
526            ">=" => Rel::Ge,
527            "=" => Rel::Eq,
528            "!=" => Rel::Ne,
529            _ => return None,
530        })
531    }
532
533    fn allows(self) -> Signs {
534        match self {
535            Rel::Lt => Signs::NEG,
536            Rel::Le => Signs::NEG.join(Signs::ZERO),
537            Rel::Gt => Signs::POS,
538            Rel::Ge => Signs::ZERO.join(Signs::POS),
539            Rel::Eq => Signs::ZERO,
540            Rel::Ne => Signs::NEG.join(Signs::POS),
541        }
542    }
543
544    fn negated(self) -> Rel {
545        match self {
546            Rel::Lt => Rel::Ge,
547            Rel::Le => Rel::Gt,
548            Rel::Gt => Rel::Le,
549            Rel::Ge => Rel::Lt,
550            Rel::Eq => Rel::Ne,
551            Rel::Ne => Rel::Eq,
552        }
553    }
554
555    fn verdict(self, s: Signs) -> Tri {
556        let allowed = self.allows();
557        if s.0 == 0 {
558            Tri::U
559        } else if s.within(allowed) {
560            Tri::T
561        } else if s.meet(allowed).0 == 0 {
562            Tri::F
563        } else {
564            Tri::U
565        }
566    }
567}
568
569/// A polynomial over the slots whose coefficients are the standard parts of the program's, with
570/// whether they are the program's coefficients exactly.
571#[derive(Clone, Debug)]
572pub struct Term {
573    pub poly: MPoly,
574    pub exact: bool,
575}
576
577/// A condition of a guard or of a jump's statements.
578#[derive(Clone, Debug)]
579pub enum Cond {
580    All(Vec<Cond>),
581    Any(Vec<Cond>),
582    Not(Box<Cond>),
583    /// `poly rel 0`.
584    Compare(Rel, Term),
585    Infinitesimal(Term),
586    Limited(Term),
587    /// The truth value a slot holds.
588    Truth(usize),
589    Constant(bool),
590}
591
592/// What a jump assigns: a polynomial over the slots, or a value no polynomial reads.
593#[derive(Clone, Debug)]
594pub enum Value {
595    Poly(Term),
596    Fixed(Cell),
597}
598
599#[derive(Clone, Debug)]
600pub enum Stmt {
601    Set { slot: usize, value: Value },
602    If { cond: Cond, then: Vec<Stmt>, otherwise: Vec<Stmt> },
603}
604
605/// `If guard: block`.
606#[derive(Clone, Debug)]
607pub struct Jump {
608    pub guard: Cond,
609    pub block: Vec<Stmt>,
610}
611
612/// One part of a loop's body, in its order: a run of flow statements — each slot's rate, the
613/// coefficient of `dt` in its Euler step, and whether the run leaves it exactly unchanged — or the
614/// check of a jump.
615#[derive(Clone, Debug)]
616pub enum Item {
617    Flow { rates: Vec<MPoly>, preserved: Vec<bool> },
618    Check(usize),
619}
620
621/// A loop's flow and jumps over its slots: its body in order, and the flow of a whole step — each
622/// slot's rate, and whether a step leaves it exactly unchanged.
623#[derive(Clone, Debug)]
624pub struct Model {
625    pub rates: Vec<MPoly>,
626    pub preserved: Vec<bool>,
627    pub jumps: Vec<Jump>,
628    pub schedule: Vec<Item>,
629}
630
631impl Model {
632    /// The model of a loop of `width` slots whose body is `schedule`.
633    pub fn new(width: usize, schedule: Vec<Item>, jumps: Vec<Jump>) -> Model {
634        let mut rates = vec![MPoly::zero(width); width];
635        let mut preserved = vec![true; width];
636        for item in &schedule {
637            if let Item::Flow { rates: r, preserved: p } = item {
638                for i in 0..width {
639                    rates[i] = rates[i].add(&r[i]);
640                    preserved[i] &= p[i];
641                }
642            }
643        }
644        Model {
645            rates,
646            preserved,
647            jumps,
648            schedule,
649        }
650    }
651}
652
653/// What a slot holds in the ideal run.
654#[derive(Clone, Debug, PartialEq, Eq)]
655pub enum Cell {
656    /// A number's standard value, and whether the loop's value is exactly it.
657    Num { value: Algebraic, exact: bool },
658    Bool(bool),
659    /// A value no polynomial reads, by its index among the values the caller holds.
660    Other(usize),
661}
662
663impl Cell {
664    fn number(&self) -> Option<&Algebraic> {
665        match self {
666            Cell::Num { value, .. } => Some(value),
667            _ => None,
668        }
669    }
670
671    fn is_exact(&self) -> bool {
672        !matches!(self, Cell::Num { exact: false, .. })
673    }
674
675    fn inexact(&self) -> Cell {
676        match self {
677            Cell::Num { value, .. } => Cell::Num { value: value.clone(), exact: false },
678            other => other.clone(),
679        }
680    }
681}
682
683fn ideal(cells: &[Cell]) -> Vec<Algebraic> {
684    cells.iter().map(|c| c.number().cloned().unwrap_or_else(|| alg(0))).collect()
685}
686
687// ── three-valued logic and signs ─────────────────────────────────────────────────────────────
688
689#[derive(Clone, Copy, Debug, PartialEq, Eq)]
690enum Tri {
691    F,
692    T,
693    U,
694}
695
696impl Tri {
697    fn not(self) -> Tri {
698        match self {
699            Tri::F => Tri::T,
700            Tri::T => Tri::F,
701            Tri::U => Tri::U,
702        }
703    }
704
705    fn and(self, o: Tri) -> Tri {
706        match (self, o) {
707            (Tri::F, _) | (_, Tri::F) => Tri::F,
708            (Tri::T, Tri::T) => Tri::T,
709            _ => Tri::U,
710        }
711    }
712
713    fn or(self, o: Tri) -> Tri {
714        self.not().and(o.not()).not()
715    }
716}
717
718/// The signs a value may have: a set of negative, zero, positive.
719#[derive(Clone, Copy, Debug, PartialEq, Eq)]
720struct Signs(u8);
721
722impl Signs {
723    const NEG: Signs = Signs(1);
724    const ZERO: Signs = Signs(2);
725    const POS: Signs = Signs(4);
726    const ALL: Signs = Signs(7);
727
728    fn of(sign: i32) -> Signs {
729        match sign.signum() {
730            -1 => Signs::NEG,
731            0 => Signs::ZERO,
732            _ => Signs::POS,
733        }
734    }
735
736    fn meet(self, o: Signs) -> Signs {
737        Signs(self.0 & o.0)
738    }
739
740    fn join(self, o: Signs) -> Signs {
741        Signs(self.0 | o.0)
742    }
743
744    fn within(self, o: Signs) -> bool {
745        self.0 & !o.0 == 0
746    }
747
748    fn members(self) -> impl Iterator<Item = i32> {
749        [(-1, Signs::NEG), (0, Signs::ZERO), (1, Signs::POS)]
750            .into_iter()
751            .filter(move |(_, s)| self.0 & s.0 != 0)
752            .map(|(v, _)| v)
753    }
754
755    fn times(self, o: Signs) -> Signs {
756        let mut out = Signs(0);
757        for a in self.members() {
758            for b in o.members() {
759                out = out.join(Signs::of(a * b));
760            }
761        }
762        out
763    }
764
765    fn negated(self) -> Signs {
766        self.times(Signs::NEG)
767    }
768
769    fn power(self, k: u32) -> Signs {
770        (0..k).fold(Signs::POS, |acc, _| acc.times(self))
771    }
772}
773
774/// What is known of the signs of values infinitely close to zero: each fact a polynomial over the
775/// slots and the signs its value may have.
776#[derive(Clone, Debug, Default)]
777struct Facts(Vec<(MPoly, Signs)>);
778
779impl Facts {
780    fn signs_of(&self, p: &MPoly) -> Signs {
781        let mut acc = Signs::ALL;
782        for (q, s) in &self.0 {
783            if let Some(k) = p.proportional_to(q) {
784                acc = acc.meet(if k.signum() > 0 { *s } else { s.negated() });
785            }
786        }
787        if acc.0 == 0 {
788            Signs::ALL
789        } else {
790            acc
791        }
792    }
793
794    fn forget(&mut self, slot: usize) {
795        self.0.retain(|(p, _)| !p.reads_slot(slot));
796    }
797
798    fn add(&mut self, p: MPoly, s: Signs) {
799        if s != Signs::ALL {
800            self.0.push((p, s));
801        }
802    }
803}
804
805/// The facts a guard that holds gives: each comparison among its conjuncts.
806fn guard_facts(cond: &Cond, facts: &mut Facts) {
807    match cond {
808        Cond::All(cs) => cs.iter().for_each(|c| guard_facts(c, facts)),
809        Cond::Compare(rel, t) => facts.add(t.poly.clone(), rel.allows()),
810        Cond::Not(inner) => match inner.as_ref() {
811            Cond::Compare(rel, t) => facts.add(t.poly.clone(), rel.negated().allows()),
812            Cond::Any(cs) => cs
813                .iter()
814                .for_each(|c| guard_facts(&Cond::Not(Box::new(c.clone())), facts)),
815            Cond::Not(c) => guard_facts(c, facts),
816            _ => {}
817        },
818        _ => {}
819    }
820}
821
822/// An atom of a condition.
823enum Atom<'c> {
824    Compare(Rel, &'c Term),
825    Infinitesimal(&'c Term),
826}
827
828/// `cond` decided atom by atom by `atom`, the truth values a slot holds read from `cells`.
829fn decide(cond: &Cond, cells: &[Cell], atom: &mut dyn FnMut(Atom<'_>) -> Result<Tri, String>) -> Result<Tri, String> {
830    Ok(match cond {
831        Cond::All(cs) => {
832            let mut acc = Tri::T;
833            for c in cs {
834                acc = acc.and(decide(c, cells, atom)?);
835            }
836            acc
837        }
838        Cond::Any(cs) => {
839            let mut acc = Tri::F;
840            for c in cs {
841                acc = acc.or(decide(c, cells, atom)?);
842            }
843            acc
844        }
845        Cond::Not(c) => decide(c, cells, atom)?.not(),
846        Cond::Compare(rel, t) => atom(Atom::Compare(*rel, t))?,
847        Cond::Infinitesimal(t) => atom(Atom::Infinitesimal(t))?,
848        Cond::Limited(_) => Tri::T,
849        Cond::Truth(slot) => match &cells[*slot] {
850            Cell::Bool(b) => {
851                if *b {
852                    Tri::T
853                } else {
854                    Tri::F
855                }
856            }
857            _ => return Err("a condition of a hybrid loop reads a value that is not a truth value".to_string()),
858        },
859        Cond::Constant(b) => {
860            if *b {
861                Tri::T
862            } else {
863                Tri::F
864            }
865        }
866    })
867}
868
869/// Every term a condition compares or asks about.
870fn atom_terms(cond: &Cond, out: &mut Vec<Term>) {
871    match cond {
872        Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| atom_terms(c, out)),
873        Cond::Not(c) => atom_terms(c, out),
874        Cond::Compare(_, t) | Cond::Infinitesimal(t) => out.push(t.clone()),
875        Cond::Limited(_) | Cond::Truth(_) | Cond::Constant(_) => {}
876    }
877}
878
879/// Every slot a condition reads.
880fn cond_reads(cond: &Cond, out: &mut Vec<usize>) {
881    match cond {
882        Cond::All(cs) | Cond::Any(cs) => cs.iter().for_each(|c| cond_reads(c, out)),
883        Cond::Not(c) => cond_reads(c, out),
884        Cond::Compare(_, t) | Cond::Infinitesimal(t) | Cond::Limited(t) => out.extend(t.poly.reads()),
885        Cond::Truth(slot) => out.push(*slot),
886        Cond::Constant(_) => {}
887    }
888}
889
890/// Whether a term's value is known exactly from `cells`: exact coefficients over exact slots.
891fn term_exact(term: &Term, cells: &[Cell]) -> bool {
892    term.exact && term.poly.reads().into_iter().all(|i| cells[i].is_exact())
893}
894
895/// The signs a term may have at a state: its standard value's sign, zero when the run holds it
896/// exactly, else what the facts know.
897fn signs_at(term: &Term, cells: &[Cell], facts: &Facts) -> Result<Signs, String> {
898    let value = term.poly.eval(&ideal(cells))?;
899    Ok(if !value.is_zero() {
900        Signs::of(value.signum())
901    } else if term_exact(term, cells) {
902        Signs::ZERO
903    } else {
904        facts.signs_of(&term.poly)
905    })
906}
907
908fn point_atom(atom: Atom<'_>, cells: &[Cell], facts: &Facts) -> Result<Tri, String> {
909    Ok(match atom {
910        Atom::Compare(rel, t) => rel.verdict(signs_at(t, cells, facts)?),
911        Atom::Infinitesimal(t) => {
912            if t.poly.eval(&ideal(cells))?.is_zero() {
913                Tri::T
914            } else {
915                Tri::F
916            }
917        }
918    })
919}
920
921// ── the run ──────────────────────────────────────────────────────────────────────────────────
922
923/// `Lᵐ xᵢ` for `m = 0, 1, …` up to the first that vanishes: the Lie series of every slot.
924fn lie_series(rates: &[MPoly]) -> Result<Vec<Vec<MPoly>>, String> {
925    let width = rates.len();
926    (0..width)
927        .map(|i| {
928            let mut series = vec![MPoly::slot(width, i)];
929            loop {
930                let next = series.last().expect("nonempty").lie(rates);
931                if next.is_zero() {
932                    return Ok(series);
933                }
934                if series.len() > MAX_LIE_ORDER {
935                    return Err(
936                        "the flow between the loop's jumps is not a polynomial in time — its field's derivatives never vanish — so the times its jumps fire at are not found exactly"
937                            .to_string(),
938                    );
939                }
940                series.push(next);
941            }
942        })
943        .collect()
944}
945
946/// The flight from `cells`: each slot as a polynomial in the time since the flight began.
947fn flight(series: &[Vec<MPoly>], cells: &[Cell]) -> Result<Vec<UPoly>, String> {
948    let x = ideal(cells);
949    series
950        .iter()
951        .map(|derivatives| {
952            let mut c = Vec::with_capacity(derivatives.len());
953            for (m, d) in derivatives.iter().enumerate() {
954                c.push(quotient(&d.eval(&x)?, &factorial(m)));
955            }
956            Ok(UPoly::from_coeffs(c))
957        })
958        .collect()
959}
960
961/// A firing: the state after the jump, which branch each condition it met took, and what is known
962/// of the signs of its values infinitely close to zero.
963#[derive(Clone, Debug)]
964struct Firing {
965    post: Vec<Cell>,
966    path: Vec<bool>,
967    facts: Facts,
968    /// Each condition it met, atom by atom, as decided.
969    conditions: Vec<Vec<Tri>>,
970}
971
972/// A firing at a crossing, in the record a tail is read from.
973#[derive(Clone, Debug)]
974struct Event {
975    at: Algebraic,
976    jump: usize,
977    firing: Firing,
978}
979
980enum Next {
981    /// A jump fires now, on the state as it is.
982    FireNow(usize),
983    /// A jump fires within an infinitesimal time, on the state flowed that far.
984    FireSoon(usize),
985    /// A jump that just fired fires again and again.
986    Slide(usize),
987    Flight(Vec<UPoly>),
988}
989
990enum SlideEnd {
991    Bound,
992    LiftOff,
993    Fires(usize),
994}
995
996enum TailMove {
997    None,
998    Moved,
999    Zeno(usize),
1000}
1001
1002/// The values a loop of infinitesimal steps leaves after `span` of its time, starting from
1003/// `start`: each infinitely close to what the loop leaves, and exactly it where the cell says so.
1004pub fn run(model: &Model, start: Vec<Cell>, span: &Algebraic) -> Result<Vec<Cell>, String> {
1005    let width = model.rates.len();
1006    if start.len() != width || model.preserved.len() != width {
1007        return Err("a hybrid loop's model and state disagree in width".to_string());
1008    }
1009    if span.signum() < 0 {
1010        return Err("a hybrid loop runs forward a nonnegative time".to_string());
1011    }
1012    check_numbers(model, &start)?;
1013    let series = lie_series(&model.rates)?;
1014    let mut r = Run {
1015        model,
1016        width,
1017        series,
1018        cells: start,
1019        elapsed: alg(0),
1020        span: span.clone(),
1021        facts: Facts::default(),
1022        history: Vec::new(),
1023        fired: 0,
1024    };
1025    r.go()?;
1026    Ok(r.cells)
1027}
1028
1029/// Every slot a polynomial of the model reads: a rate, a guard, a jump's statement or condition.
1030fn numeric_slots(model: &Model) -> Vec<bool> {
1031    let width = model.rates.len();
1032    let mut out = vec![false; width];
1033    let mut mark = |p: &MPoly| p.reads().into_iter().for_each(|i| out[i] = true);
1034    model.rates.iter().for_each(&mut mark);
1035    fn cond_polys(c: &Cond, mark: &mut dyn FnMut(&MPoly)) {
1036        let mut terms = Vec::new();
1037        atom_terms(c, &mut terms);
1038        if let Cond::Limited(t) = c {
1039            terms.push(t.clone());
1040        }
1041        terms.iter().for_each(|t| mark(&t.poly));
1042    }
1043    fn block_polys(stmts: &[Stmt], mark: &mut dyn FnMut(&MPoly)) {
1044        for s in stmts {
1045            match s {
1046                Stmt::Set { value: Value::Poly(t), .. } => mark(&t.poly),
1047                Stmt::Set { .. } => {}
1048                Stmt::If { cond, then, otherwise } => {
1049                    cond_polys(cond, mark);
1050                    block_polys(then, mark);
1051                    block_polys(otherwise, mark);
1052                }
1053            }
1054        }
1055    }
1056    for jump in &model.jumps {
1057        cond_polys(&jump.guard, &mut mark);
1058        block_polys(&jump.block, &mut mark);
1059    }
1060    out
1061}
1062
1063/// A slot a polynomial reads holds a number from the start, and no jump sets it to anything else.
1064fn check_numbers(model: &Model, start: &[Cell]) -> Result<(), String> {
1065    let numeric = numeric_slots(model);
1066    if (0..start.len()).any(|i| numeric[i] && start[i].number().is_none()) {
1067        return Err("a hybrid loop computes with a value that is not a number".to_string());
1068    }
1069    fn sets_other(stmts: &[Stmt], numeric: &[bool]) -> bool {
1070        stmts.iter().any(|s| match s {
1071            Stmt::Set { slot, value: Value::Fixed(cell) } => numeric[*slot] && cell.number().is_none(),
1072            Stmt::Set { .. } => false,
1073            Stmt::If { then, otherwise, .. } => sets_other(then, numeric) || sets_other(otherwise, numeric),
1074        })
1075    }
1076    if model.jumps.iter().any(|j| sets_other(&j.block, &numeric)) {
1077        return Err("a jump of a hybrid loop sets a value the loop computes with to something that is not a number".to_string());
1078    }
1079    Ok(())
1080}
1081
1082struct Run<'m> {
1083    model: &'m Model,
1084    width: usize,
1085    series: Vec<Vec<MPoly>>,
1086    cells: Vec<Cell>,
1087    elapsed: Algebraic,
1088    span: Algebraic,
1089    facts: Facts,
1090    history: Vec<Event>,
1091    fired: usize,
1092}
1093
1094impl<'m> Run<'m> {
1095    fn remaining(&self) -> Algebraic {
1096        self.span.sub(&self.elapsed)
1097    }
1098
1099    fn count(&mut self) -> Result<(), String> {
1100        self.fired += 1;
1101        if self.fired > EVENT_BUDGET {
1102            let gaps: Vec<Algebraic> = self
1103                .history
1104                .windows(2)
1105                .rev()
1106                .take(4)
1107                .map(|w| w[1].at.sub(&w[0].at))
1108                .collect();
1109            let quickening = gaps.len() == 4 && gaps.windows(2).all(|g| g[0] < g[1]);
1110            return Err(format!(
1111                "the loop's jumps fire more than {} times before its end{} and follow no scaling that settles the rest, so its end state is not found exactly",
1112                thousands(EVENT_BUDGET),
1113                if quickening { ", each time sooner after the last," } else { "" }
1114            ));
1115        }
1116        Ok(())
1117    }
1118
1119    fn go(&mut self) -> Result<(), String> {
1120        let mut last: Option<usize> = None;
1121        let mut after_zeno: Option<usize> = None;
1122        loop {
1123            if self.elapsed == self.span {
1124                return Ok(());
1125            }
1126            match self.next(last, after_zeno)? {
1127                Next::FireNow(k) => {
1128                    self.fire_here(k, self.cells.clone())?;
1129                    last = Some(k);
1130                    after_zeno = None;
1131                    self.history.clear();
1132                }
1133                Next::FireSoon(k) => {
1134                    let pre = self.flowed_an_instant();
1135                    self.fire_here(k, pre)?;
1136                    last = Some(k);
1137                    after_zeno = None;
1138                    self.history.clear();
1139                }
1140                Next::Slide(k) => {
1141                    self.history.clear();
1142                    after_zeno = None;
1143                    match self.slide(k)? {
1144                        SlideEnd::Bound => return Ok(()),
1145                        SlideEnd::LiftOff => last = Some(k),
1146                        SlideEnd::Fires(other) => {
1147                            self.fire_here(other, self.cells.clone())?;
1148                            last = Some(other);
1149                        }
1150                    }
1151                }
1152                Next::Flight(flight) => {
1153                    let remaining = self.remaining();
1154                    match self.crossing(&flight, &remaining)? {
1155                        None => {
1156                            self.advance(&flight, &remaining);
1157                            return Ok(());
1158                        }
1159                        Some((at, k)) => {
1160                            self.advance(&flight, &at);
1161                            let firing = self.fire(k, self.cells.clone())?;
1162                            self.count()?;
1163                            self.cells = firing.post.clone();
1164                            self.facts = firing.facts.clone();
1165                            self.history.push(Event {
1166                                at: self.elapsed.clone(),
1167                                jump: k,
1168                                firing,
1169                            });
1170                            last = Some(k);
1171                            after_zeno = None;
1172                            match self.tail()? {
1173                                TailMove::None => {}
1174                                TailMove::Moved => {}
1175                                TailMove::Zeno(j) => {
1176                                    after_zeno = Some(j);
1177                                    last = None;
1178                                }
1179                            }
1180                        }
1181                    }
1182                }
1183            }
1184        }
1185    }
1186
1187    /// The state an infinitesimal time later: every slot that flows known only to within an
1188    /// infinitesimal.
1189    fn flowed_an_instant(&self) -> Vec<Cell> {
1190        self.cells
1191            .iter()
1192            .enumerate()
1193            .map(|(i, c)| if self.model.preserved[i] { c.clone() } else { c.inexact() })
1194            .collect()
1195    }
1196
1197    fn advance(&mut self, flight: &[UPoly], by: &Algebraic) {
1198        if by.is_zero() {
1199            return;
1200        }
1201        for i in 0..self.width {
1202            if let Cell::Num { exact, .. } = &self.cells[i] {
1203                let exact = *exact && self.model.preserved[i];
1204                self.cells[i] = Cell::Num {
1205                    value: flight[i].eval(by),
1206                    exact,
1207                };
1208            }
1209        }
1210        self.elapsed = self.elapsed.add(by);
1211        self.facts = Facts::default();
1212    }
1213
1214    fn fire_here(&mut self, k: usize, pre: Vec<Cell>) -> Result<(), String> {
1215        let firing = self.fire(k, pre)?;
1216        self.count()?;
1217        self.cells = firing.post;
1218        self.facts = firing.facts;
1219        Ok(())
1220    }
1221
1222    /// Jump `k` fired on `pre`: its statements run on the state, the guard's conjuncts known to
1223    /// hold.
1224    fn fire(&self, k: usize, pre: Vec<Cell>) -> Result<Firing, String> {
1225        let jump = &self.model.jumps[k];
1226        let mut facts = Facts::default();
1227        guard_facts(&jump.guard, &mut facts);
1228        let mut cells = pre;
1229        let mut path = Vec::new();
1230        let mut conditions = Vec::new();
1231        self.exec(&jump.block, &mut cells, &mut facts, &mut path, &mut conditions)?;
1232        Ok(Firing {
1233            post: cells,
1234            path,
1235            facts,
1236            conditions,
1237        })
1238    }
1239
1240    fn exec(
1241        &self,
1242        stmts: &[Stmt],
1243        cells: &mut Vec<Cell>,
1244        facts: &mut Facts,
1245        path: &mut Vec<bool>,
1246        conditions: &mut Vec<Vec<Tri>>,
1247    ) -> Result<(), String> {
1248        for s in stmts {
1249            match s {
1250                Stmt::Set { slot, value } => match value {
1251                    Value::Poly(term) => {
1252                        let value = term.poly.eval(&ideal(cells))?;
1253                        let exact = term_exact(term, cells);
1254                        let derived = if value.is_zero() && !exact {
1255                            derived_signs(term, cells, facts)?
1256                        } else {
1257                            Signs::ALL
1258                        };
1259                        cells[*slot] = Cell::Num { value, exact };
1260                        facts.forget(*slot);
1261                        facts.add(MPoly::slot(self.width, *slot), derived);
1262                    }
1263                    Value::Fixed(cell) => {
1264                        cells[*slot] = cell.clone();
1265                        facts.forget(*slot);
1266                    }
1267                },
1268                Stmt::If { cond, then, otherwise } => {
1269                    let mut atoms = Vec::new();
1270                    let verdict = decide(cond, cells, &mut |a| {
1271                        let v = point_atom(a, cells, facts)?;
1272                        atoms.push(v);
1273                        Ok(v)
1274                    })?;
1275                    conditions.push(atoms);
1276                    match verdict {
1277                        Tri::T => {
1278                            path.push(true);
1279                            self.exec(then, cells, facts, path, conditions)?;
1280                        }
1281                        Tri::F => {
1282                            path.push(false);
1283                            self.exec(otherwise, cells, facts, path, conditions)?;
1284                        }
1285                        Tri::U => {
1286                            return Err(format!(
1287                                "whether a condition in the loop's jump holds when it fires {} into the loop depends on which infinitesimal dt is: it compares values infinitely close to each other",
1288                                self.elapsed
1289                            ))
1290                        }
1291                    }
1292                }
1293            }
1294        }
1295        Ok(())
1296    }
1297
1298    /// Each jump's next check after `last` fired, or after the loop began, in the order the body
1299    /// checks them: the rates of the flow statements that run before it and whether they leave each
1300    /// slot exactly unchanged, `None` when none run.
1301    fn first_checks(&self, last: Option<usize>) -> Vec<(usize, Option<(Vec<MPoly>, Vec<bool>)>)> {
1302        let schedule = &self.model.schedule;
1303        let start = match last {
1304            Some(l) => schedule
1305                .iter()
1306                .position(|item| matches!(item, Item::Check(k) if *k == l))
1307                .map_or(0, |p| p + 1),
1308            None => 0,
1309        };
1310        let mut rates: Vec<MPoly> = vec![MPoly::zero(self.width); self.width];
1311        let mut preserved = vec![true; self.width];
1312        let mut flowed = false;
1313        let mut out = Vec::with_capacity(self.model.jumps.len());
1314        for n in 0..schedule.len() {
1315            match &schedule[(start + n) % schedule.len()] {
1316                Item::Flow { rates: r, preserved: p } => {
1317                    flowed = true;
1318                    for i in 0..self.width {
1319                        rates[i] = rates[i].add(&r[i]);
1320                        preserved[i] &= p[i];
1321                    }
1322                }
1323                Item::Check(k) => out.push((*k, flowed.then(|| (rates.clone(), preserved.clone())))),
1324            }
1325        }
1326        out
1327    }
1328
1329    /// What happens next: a jump that fires at its next check, or within an infinitesimal time
1330    /// because its guard holds just after now — again and again when it is the jump that just
1331    /// fired — or a flight.
1332    fn next(&self, last: Option<usize>, after_zeno: Option<usize>) -> Result<Next, String> {
1333        let flight = flight(&self.series, &self.cells)?;
1334        let mut open = Vec::with_capacity(self.model.jumps.len());
1335        for jump in &self.model.jumps {
1336            open.push(decide(&jump.guard, &self.cells, &mut |a| self.open_atom(a, &flight))?);
1337        }
1338        let again = |k: usize| last == Some(k) || after_zeno == Some(k);
1339        for (k, flowed) in self.first_checks(last) {
1340            let guard = &self.model.jumps[k].guard;
1341            let verdict = match &flowed {
1342                None => decide(guard, &self.cells, &mut |a| point_atom(a, &self.cells, &self.facts))?,
1343                Some((rates, preserved)) => decide(guard, &self.cells, &mut |a| self.window_atom(a, rates, preserved))?,
1344            };
1345            let fires = match verdict {
1346                Tri::T => true,
1347                Tri::U if open[k] == Tri::T => true,
1348                Tri::U => {
1349                    return Err(format!(
1350                        "whether a jump of the loop fires {} into it depends on which infinitesimal dt is: its guard compares values infinitely close to each other",
1351                        self.elapsed
1352                    ))
1353                }
1354                Tri::F => false,
1355            };
1356            if fires {
1357                return Ok(if again(k) {
1358                    Next::Slide(k)
1359                } else if verdict == Tri::T && flowed.is_none() {
1360                    Next::FireNow(k)
1361                } else {
1362                    Next::FireSoon(k)
1363                });
1364            }
1365        }
1366        if open.contains(&Tri::U) {
1367            return Err(format!(
1368                "the loop's flow runs along a jump's boundary {} into it, so whether the jump fires depends on which infinitesimal dt is",
1369                self.elapsed
1370            ));
1371        }
1372        let firing: Vec<usize> = (0..open.len()).filter(|k| open[*k] == Tri::T).collect();
1373        match firing.as_slice() {
1374            [] => {}
1375            [k] => return Ok(if again(*k) { Next::Slide(*k) } else { Next::FireSoon(*k) }),
1376            _ => {
1377                return Err(format!(
1378                    "two of the loop's jumps fire at once {} into it, so which fires first depends on which infinitesimal dt is",
1379                    self.elapsed
1380                ))
1381            }
1382        }
1383        for jump in &self.model.jumps {
1384            let verdict = decide(&jump.guard, &self.cells, &mut |a| {
1385                self.window_atom(a, &self.model.rates, &self.model.preserved)
1386            })?;
1387            if verdict != Tri::F {
1388                return Err(format!(
1389                    "a jump of the loop may fire again within an infinitesimal time of {} into it, so its end state depends on which infinitesimal dt is",
1390                    self.elapsed
1391                ));
1392            }
1393        }
1394        Ok(Next::Flight(flight))
1395    }
1396
1397    /// An atom on the open interval just after now, along the flight.
1398    fn open_atom(&self, atom: Atom<'_>, flight: &[UPoly]) -> Result<Tri, String> {
1399        let (term, rel) = match atom {
1400            Atom::Compare(rel, t) => (t, Some(rel)),
1401            Atom::Infinitesimal(t) => (t, None),
1402        };
1403        let along = term.poly.eval(flight)?;
1404        let signs = match along.lowest() {
1405            Some((_, c)) => Signs::of(c.signum()),
1406            None if self.constant_exactly(term) => Signs::ZERO,
1407            None => Signs::ALL,
1408        };
1409        Ok(match rel {
1410            Some(rel) => rel.verdict(signs),
1411            None => {
1412                if along.is_zero() {
1413                    Tri::T
1414                } else {
1415                    Tri::F
1416                }
1417            }
1418        })
1419    }
1420
1421    /// Whether a term keeps its value exactly while the loop flows: exact coefficients over exact
1422    /// slots the flow leaves unchanged.
1423    fn constant_exactly(&self, term: &Term) -> bool {
1424        term.exact
1425            && term
1426                .poly
1427                .reads()
1428                .into_iter()
1429                .all(|i| self.cells[i].is_exact() && self.model.preserved[i])
1430    }
1431
1432    /// An atom over the steps an infinitesimal time after now, each having run flow statements of
1433    /// `rates`: a value at zero moves first by its rate, from where the facts put it, and a value
1434    /// they leave unchanged stays as it is.
1435    fn window_atom(&self, atom: Atom<'_>, rates: &[MPoly], preserved: &[bool]) -> Result<Tri, String> {
1436        let (term, rel) = match atom {
1437            Atom::Compare(rel, t) => (t, rel),
1438            Atom::Infinitesimal(t) => {
1439                return Ok(if t.poly.eval(&ideal(&self.cells))?.is_zero() {
1440                    Tri::T
1441                } else {
1442                    Tri::F
1443                })
1444            }
1445        };
1446        let x = ideal(&self.cells);
1447        let value = term.poly.eval(&x)?;
1448        if !value.is_zero() {
1449            return Ok(rel.verdict(Signs::of(value.signum())));
1450        }
1451        let start = if term_exact(term, &self.cells) {
1452            Signs::ZERO
1453        } else {
1454            self.facts.signs_of(&term.poly)
1455        };
1456        if term.poly.reads().into_iter().all(|i| preserved[i]) {
1457            return Ok(rel.verdict(start));
1458        }
1459        let rate = term.poly.lie(rates).eval(&x)?;
1460        if rate.is_zero() {
1461            return Ok(rel.verdict(Signs::ALL));
1462        }
1463        let moving = Signs::of(rate.signum());
1464        Ok(rel.verdict(if start.within(Signs::ZERO.join(moving)) {
1465            moving
1466        } else {
1467            Signs::ALL
1468        }))
1469    }
1470
1471    /// The first time in `(0, remaining]` a jump's guard crosses into holding along `flight`, and
1472    /// which jump; `None` when none does before the loop ends.
1473    fn crossing(&self, flight: &[UPoly], remaining: &Algebraic) -> Result<Option<(Algebraic, usize)>, String> {
1474        let atoms = self.guard_atoms(flight)?;
1475        let zero = alg(0);
1476        let mut points: Vec<Algebraic> = atoms
1477            .iter()
1478            .flatten()
1479            .flat_map(real_roots)
1480            .filter(|r| r > &zero && r <= remaining)
1481            .collect();
1482        points.sort();
1483        points.dedup();
1484        if points.last() != Some(remaining) {
1485            points.push(remaining.clone());
1486        }
1487        let mut lo = zero;
1488        for (n, at) in points.iter().enumerate() {
1489            let inside = rational_between(&lo, at);
1490            let before = self.verdicts(flight, &Algebraic::from_rational(inside))?;
1491            if before.iter().any(|v| *v != Tri::F) {
1492                return Err(format!(
1493                    "the loop's flow runs along a jump's boundary {} into it, so whether the jump fires depends on which infinitesimal dt is",
1494                    self.elapsed.add(&lo)
1495                ));
1496            }
1497            let here = self.verdicts(flight, at)?;
1498            if at == remaining {
1499                if here.iter().any(|v| *v != Tri::F) {
1500                    return Err(format!(
1501                        "a jump of the loop meets its guard's boundary at the loop's last step, {} into it, so whether it fires before the loop ends depends on which infinitesimal dt is",
1502                        self.elapsed.add(at)
1503                    ));
1504                }
1505                return Ok(None);
1506            }
1507            let next = &points[n + 1];
1508            let after = self.verdicts(flight, &Algebraic::from_rational(rational_between(at, next)))?;
1509            let mut crossing = Vec::new();
1510            for k in 0..here.len() {
1511                match (here[k], after[k]) {
1512                    (_, Tri::T) => crossing.push(k),
1513                    (_, Tri::U) => {
1514                        return Err(format!(
1515                            "the loop's flow runs along a jump's boundary {} into it, so whether the jump fires depends on which infinitesimal dt is",
1516                            self.elapsed.add(at)
1517                        ))
1518                    }
1519                    (Tri::F, Tri::F) => {}
1520                    (_, Tri::F) => {
1521                        return Err(format!(
1522                            "the loop's flow touches a jump's guard without crossing it {} into the loop, so whether the jump fires depends on which infinitesimal dt is",
1523                            self.elapsed.add(at)
1524                        ))
1525                    }
1526                }
1527            }
1528            match crossing.as_slice() {
1529                [] => {}
1530                [k] => return Ok(Some((at.clone(), *k))),
1531                _ => {
1532                    return Err(format!(
1533                        "two of the loop's jumps fire at once {} into it, so which fires first depends on which infinitesimal dt is",
1534                        self.elapsed.add(at)
1535                    ))
1536                }
1537            }
1538            lo = at.clone();
1539        }
1540        Ok(None)
1541    }
1542
1543    /// Every guard atom along a flight, jump by jump.
1544    fn guard_atoms(&self, flight: &[UPoly]) -> Result<Vec<Vec<UPoly>>, String> {
1545        self.model
1546            .jumps
1547            .iter()
1548            .map(|jump| {
1549                let mut terms = Vec::new();
1550                atom_terms(&jump.guard, &mut terms);
1551                terms.iter().map(|t| t.poly.eval(flight)).collect()
1552            })
1553            .collect()
1554    }
1555
1556    /// Each jump's guard at time `at` of a flight: an atom that vanishes there is decided only if it
1557    /// keeps its value exactly.
1558    fn verdicts(&self, flight: &[UPoly], at: &Algebraic) -> Result<Vec<Tri>, String> {
1559        let mut out = Vec::with_capacity(self.model.jumps.len());
1560        for jump in &self.model.jumps {
1561            out.push(decide(&jump.guard, &self.cells, &mut |a| {
1562                let (term, rel) = match a {
1563                    Atom::Compare(rel, t) => (t, Some(rel)),
1564                    Atom::Infinitesimal(t) => (t, None),
1565                };
1566                let value = term.poly.eval(flight)?.eval(at);
1567                let signs = if !value.is_zero() {
1568                    Signs::of(value.signum())
1569                } else if self.constant_exactly(term) {
1570                    Signs::ZERO
1571                } else {
1572                    Signs::ALL
1573                };
1574                Ok(match rel {
1575                    Some(rel) => rel.verdict(signs),
1576                    None => {
1577                        if value.is_zero() {
1578                            Tri::T
1579                        } else {
1580                            Tri::F
1581                        }
1582                    }
1583                })
1584            })?);
1585        }
1586        Ok(out)
1587    }
1588}
1589
1590/// The signs a reset leaves a value it sets to an infinitesimal: a single monomial's sign is the
1591/// product of its coefficient's and its factors'.
1592fn derived_signs(term: &Term, cells: &[Cell], facts: &Facts) -> Result<Signs, String> {
1593    let [(exponents, coefficient)] = term.poly.terms() else {
1594        return Ok(Signs::ALL);
1595    };
1596    let width = term.poly.width();
1597    let mut signs = Signs::of(coefficient.signum());
1598    for (i, k) in exponents.iter().enumerate() {
1599        if *k == 0 {
1600            continue;
1601        }
1602        let factor = Term {
1603            poly: MPoly::slot(width, i),
1604            exact: true,
1605        };
1606        signs = signs.times(signs_at(&factor, cells, facts)?.power(*k));
1607    }
1608    Ok(signs)
1609}
1610
1611fn thousands(n: usize) -> String {
1612    let digits = n.to_string();
1613    let mut out = String::new();
1614    for (i, ch) in digits.chars().enumerate() {
1615        if i > 0 && (digits.len() - i) % 3 == 0 {
1616            out.push(',');
1617        }
1618        out.push(ch);
1619    }
1620    out
1621}
1622
1623fn magnitude(a: &Algebraic) -> Algebraic {
1624    if a.signum() < 0 {
1625        a.neg()
1626    } else {
1627        a.clone()
1628    }
1629}
1630
1631// ── continuous firing ────────────────────────────────────────────────────────────────────────
1632
1633/// A firing's statements in the order it ran them: the slot each sets, what it sets, and the
1634/// conditions it ran under with the branch each took.
1635type PathWrite<'b> = (usize, &'b Value, Vec<(&'b Cond, bool)>);
1636
1637fn path_writes<'b>(block: &'b [Stmt], path: &[bool]) -> Vec<PathWrite<'b>> {
1638    fn walk<'b>(
1639        stmts: &'b [Stmt],
1640        bits: &mut std::slice::Iter<'_, bool>,
1641        conds: &mut Vec<(&'b Cond, bool)>,
1642        out: &mut Vec<PathWrite<'b>>,
1643    ) {
1644        for s in stmts {
1645            match s {
1646                Stmt::Set { slot, value } => out.push((*slot, value, conds.clone())),
1647                Stmt::If { cond, then, otherwise } => {
1648                    let taken = bits.next().copied().unwrap_or(false);
1649                    conds.push((cond, taken));
1650                    walk(if taken { then } else { otherwise }, bits, conds, out);
1651                    conds.pop();
1652                }
1653            }
1654        }
1655    }
1656    let mut out = Vec::new();
1657    walk(block, &mut path.iter(), &mut Vec::new(), &mut out);
1658    out
1659}
1660
1661/// A truth over the open interval `(0, end)` of a regime: one value on each interval between
1662/// `points`, and one at each point.
1663#[derive(Clone, Debug)]
1664struct Piecewise {
1665    points: Vec<Algebraic>,
1666    between: Vec<Tri>,
1667    at: Vec<Tri>,
1668}
1669
1670impl Piecewise {
1671    fn value_at(&self, x: &Algebraic) -> Tri {
1672        match self.points.binary_search(x) {
1673            Ok(k) => self.at[k],
1674            Err(k) => self.between[k],
1675        }
1676    }
1677
1678    /// The conjunction of `parts`, each taken as it is or negated.
1679    fn all(parts: &[(&Piecewise, bool)], end: &Algebraic) -> Piecewise {
1680        let mut points: Vec<Algebraic> = parts.iter().flat_map(|(p, _)| p.points.iter().cloned()).collect();
1681        points.sort();
1682        points.dedup();
1683        let eval = |x: &Algebraic| {
1684            parts.iter().fold(Tri::T, |acc, (p, polarity)| {
1685                let v = p.value_at(x);
1686                acc.and(if *polarity { v } else { v.not() })
1687            })
1688        };
1689        let mut between = Vec::with_capacity(points.len() + 1);
1690        let mut lo = alg(0);
1691        for p in points.iter().chain(std::iter::once(end)) {
1692            between.push(eval(&Algebraic::from_rational(rational_between(&lo, p))));
1693            lo = p.clone();
1694        }
1695        let at = points.iter().map(eval).collect();
1696        Piecewise { points, between, at }
1697    }
1698
1699    fn holds_throughout(&self) -> bool {
1700        self.between.iter().all(|v| *v == Tri::T)
1701    }
1702
1703    fn never_holds(&self) -> bool {
1704        self.between.iter().chain(&self.at).all(|v| *v == Tri::F)
1705    }
1706
1707    /// The end of the last interval it holds on — the time of the last firing under it — or
1708    /// `None` when it holds on none. A truth that holds only at isolated instants, or is unknown
1709    /// on an interval, has no such time.
1710    fn last_hold(&self, end: &Algebraic) -> Result<Option<Algebraic>, String> {
1711        if self.between.contains(&Tri::U) {
1712            return Err(
1713                "whether a condition of the jump holds while it fires again and again depends on which infinitesimal dt is"
1714                    .to_string(),
1715            );
1716        }
1717        for (k, v) in self.at.iter().enumerate() {
1718            if *v != Tri::F && self.between[k] != Tri::T && self.between[k + 1] != Tri::T {
1719                return Err(
1720                    "a condition of the jump holds only at an instant while it fires again and again, so whether a firing meets it depends on which infinitesimal dt is"
1721                        .to_string(),
1722                );
1723            }
1724        }
1725        Ok(self
1726            .between
1727            .iter()
1728            .rposition(|v| *v == Tri::T)
1729            .map(|k| self.points.get(k).cloned().unwrap_or_else(|| end.clone())))
1730    }
1731}
1732
1733/// The state a firing in a regime sees, as polynomials in the time since the regime began.
1734#[derive(Clone, Debug)]
1735struct SymState {
1736    values: Vec<UPoly>,
1737    exact: Vec<bool>,
1738    cells: Vec<Cell>,
1739    /// The conditions each slot was last set under, by their place in the jump; a read under
1740    /// conditions that do not extend them sees a value it cannot know.
1741    scope: Vec<Vec<usize>>,
1742}
1743
1744/// A statement a regime's firings run, with the truth of the conditions it runs under.
1745struct RegimeWrite {
1746    slot: usize,
1747    value: Option<UPoly>,
1748    fixed: Option<Cell>,
1749    exact: bool,
1750    path: Piecewise,
1751    /// Whether it adds to the value it sets rather than replacing it.
1752    accumulates: bool,
1753    /// Whether it leaves the value it sets exactly as it was.
1754    unchanged: bool,
1755}
1756
1757const UNREADABLE: usize = usize::MAX;
1758
1759impl<'m> Run<'m> {
1760    /// Jump `k` fires again and again: the slots it sets that flow stay where it holds them, the
1761    /// rest flow, and the values it records take their value at the last firing under their
1762    /// conditions. The regime lasts until the jump's guard no longer holds an instant after a
1763    /// firing, another jump fires, or the loop ends.
1764    fn slide(&mut self, k: usize) -> Result<SlideEnd, String> {
1765        let width = self.width;
1766        let jump = &self.model.jumps[k];
1767        let pre = self.flowed_an_instant();
1768        let firing = self.fire(k, pre)?;
1769        self.count()?;
1770        let writes = path_writes(&jump.block, &firing.path);
1771        let mut effect: Vec<MPoly> = (0..width).map(|i| MPoly::slot(width, i)).collect();
1772        for (slot, value, _) in &writes {
1773            if let Value::Poly(t) = value {
1774                effect[*slot] = substitute(&t.poly, &effect);
1775            }
1776        }
1777        let x = ideal(&self.cells);
1778        let mut pinned: Vec<usize> = writes
1779            .iter()
1780            .filter(|(slot, value, _)| matches!(value, Value::Poly(_)) && !self.model.preserved[*slot])
1781            .map(|(slot, _, _)| *slot)
1782            .filter(|slot| effect[*slot] != MPoly::slot(width, *slot))
1783            .collect();
1784        pinned.sort_unstable();
1785        pinned.dedup();
1786        for p in &pinned {
1787            if effect[*p].eval(&x)? != x[*p] {
1788                return Err(format!(
1789                    "a jump of the loop fires again at once {} into it but moves the state it fires on, so its firings have no standard rhythm",
1790                    self.elapsed
1791                ));
1792            }
1793        }
1794        let mut pull = alg(0);
1795        for p in &pinned {
1796            let mut row = alg(0);
1797            for q in &pinned {
1798                row = row.add(&magnitude(&effect[*p].partial(*q).eval(&x)?));
1799            }
1800            pull = pull.max(row);
1801        }
1802        if pull >= alg(1) {
1803            return Err(format!(
1804                "a jump of the loop holds its state where it fires again and again {} into it, but does not pull the state back as it does, so the infinitesimal motion between firings may grow",
1805                self.elapsed
1806            ));
1807        }
1808        let mut rates = self.model.rates.clone();
1809        for p in &pinned {
1810            rates[*p] = MPoly::zero(width);
1811        }
1812        let sliding = lie_series(&rates)?;
1813        let y = flight(&sliding, &self.cells)?;
1814        let remaining = self.remaining();
1815        let (end, kind) = self.regime_end(k, &y, &remaining)?;
1816        let mut state = SymState {
1817            values: y.clone(),
1818            exact: (0..width)
1819                .map(|i| self.cells[i].is_exact() && self.model.preserved[i] && !pinned.contains(&i))
1820                .collect(),
1821            cells: self.cells.clone(),
1822            scope: vec![Vec::new(); width],
1823        };
1824        let mut facts = Facts::default();
1825        guard_facts(&jump.guard, &mut facts);
1826        let mut regime = Vec::new();
1827        let mut scope = Vec::new();
1828        let mut path: Vec<(Piecewise, bool)> = Vec::new();
1829        let mut counter = 0;
1830        regime_walk(
1831            &jump.block,
1832            &mut state,
1833            &mut facts,
1834            &mut path,
1835            &mut scope,
1836            &mut counter,
1837            &mut regime,
1838            &end,
1839        )?;
1840        let read_by_flow_or_guard = |slot: usize| {
1841            self.model.rates.iter().any(|r| r.reads_slot(slot))
1842                || self.model.jumps.iter().any(|j| {
1843                    let mut reads = Vec::new();
1844                    cond_reads(&j.guard, &mut reads);
1845                    reads.contains(&slot)
1846                })
1847        };
1848        let mut last: Vec<Option<(Algebraic, usize)>> = vec![None; width];
1849        for (order, w) in regime.iter().enumerate() {
1850            let slot = w.slot;
1851            if !self.model.preserved[slot] && w.value.is_some() {
1852                if pinned.contains(&slot) {
1853                    let held = w.value.as_ref().and_then(UPoly::as_constant);
1854                    if !w.path.holds_throughout() || held.as_ref() != Some(&x[slot]) {
1855                        return Err(format!(
1856                            "a jump of the loop holds its state where it fires again and again {} into it only under a condition that changes, so its firings have no standard rhythm",
1857                            self.elapsed
1858                        ));
1859                    }
1860                } else if !w.unchanged && !w.path.never_holds() {
1861                    return Err(format!(
1862                        "a jump of the loop would move a flowing value while it fires again and again {} into it, so its firings have no standard rhythm",
1863                        self.elapsed
1864                    ));
1865                }
1866                continue;
1867            }
1868            if w.accumulates && !w.path.never_holds() {
1869                return Err(format!(
1870                    "a jump of the loop fires infinitely often {} into it and adds to a value at each firing, so that value is not known to any standard precision",
1871                    self.elapsed
1872                ));
1873            }
1874            if read_by_flow_or_guard(slot) && !w.path.never_holds() {
1875                let unchanged = match (&w.value, &w.fixed) {
1876                    (Some(v), _) => v.as_constant().as_ref() == Some(&x[slot]) && w.exact == self.cells[slot].is_exact(),
1877                    (None, Some(c)) => *c == self.cells[slot],
1878                    _ => false,
1879                };
1880                if !unchanged {
1881                    return Err(format!(
1882                        "a jump of the loop fires infinitely often {} into it and changes a value its flow or a guard reads, so its firings have no standard rhythm",
1883                        self.elapsed
1884                    ));
1885                }
1886            }
1887            if let Some(at) = w.path.last_hold(&end)? {
1888                let later = match &last[slot] {
1889                    Some((t, _)) => at >= *t,
1890                    None => true,
1891                };
1892                if later {
1893                    last[slot] = Some((at, order));
1894                }
1895            }
1896        }
1897        for i in 0..width {
1898            if pinned.contains(&i) {
1899                self.cells[i] = self.cells[i].inexact();
1900            } else if let (Cell::Num { .. }, false) = (&self.cells[i], self.model.preserved[i]) {
1901                self.cells[i] = Cell::Num {
1902                    value: y[i].eval(&end),
1903                    exact: false,
1904                };
1905            }
1906        }
1907        for (slot, entry) in last.iter().enumerate() {
1908            let Some((at, order)) = entry else { continue };
1909            let w = &regime[*order];
1910            self.cells[slot] = match (&w.value, &w.fixed) {
1911                (Some(v), _) => Cell::Num {
1912                    value: v.eval(at),
1913                    exact: w.exact && v.as_constant().is_some(),
1914                },
1915                (None, Some(c)) => c.clone(),
1916                _ => unreachable!("a write sets a polynomial or a fixed value"),
1917            };
1918        }
1919        self.elapsed = self.elapsed.add(&end);
1920        self.facts = firing.facts;
1921        Ok(kind)
1922    }
1923
1924    /// Where a regime of jump `k` along `y` ends: when its guard no longer holds an instant after
1925    /// a firing, when another jump's guard crosses into holding, or at the loop's end.
1926    fn regime_end(&self, k: usize, y: &[UPoly], remaining: &Algebraic) -> Result<(Algebraic, SlideEnd), String> {
1927        let mut own_terms = Vec::new();
1928        atom_terms(&self.model.jumps[k].guard, &mut own_terms);
1929        let mut micro: Vec<Vec<UPoly>> = Vec::with_capacity(own_terms.len());
1930        for t in &own_terms {
1931            let mut derivatives = Vec::new();
1932            let mut d = t.poly.clone();
1933            while !d.is_zero() {
1934                if derivatives.len() > MAX_LIE_ORDER {
1935                    return Err(
1936                        "the flow between the loop's jumps is not a polynomial in time, so the times its jumps fire at are not found exactly"
1937                            .to_string(),
1938                    );
1939                }
1940                derivatives.push(d.eval(y)?);
1941                d = d.lie(&self.model.rates);
1942            }
1943            micro.push(derivatives);
1944        }
1945        let others: Vec<usize> = (0..self.model.jumps.len()).filter(|j| *j != k).collect();
1946        let other_atoms = self.guard_atoms(y)?;
1947        let zero = alg(0);
1948        let mut points: Vec<Algebraic> = micro
1949            .iter()
1950            .flatten()
1951            .chain(others.iter().flat_map(|j| other_atoms[*j].iter()))
1952            .flat_map(real_roots)
1953            .filter(|r| r > &zero && r < remaining)
1954            .collect();
1955        points.sort();
1956        points.dedup();
1957        points.push(remaining.clone());
1958        let micro_at = |x: &Algebraic| -> Result<Tri, String> {
1959            let mut n = 0;
1960            decide(&self.model.jumps[k].guard, &self.cells, &mut |a| {
1961                let derivatives = &micro[n];
1962                n += 1;
1963                let first = derivatives.iter().map(|d| d.eval(x)).find(|v| !v.is_zero());
1964                Ok(match a {
1965                    Atom::Compare(rel, t) => rel.verdict(match first {
1966                        Some(v) => Signs::of(v.signum()),
1967                        None if self.constant_exactly(t) => Signs::ZERO,
1968                        None => Signs::ALL,
1969                    }),
1970                    Atom::Infinitesimal(_) => {
1971                        if derivatives.first().is_none_or(|d| d.eval(x).is_zero()) {
1972                            Tri::T
1973                        } else {
1974                            Tri::F
1975                        }
1976                    }
1977                })
1978            })
1979        };
1980        let mut lo = zero;
1981        for (n, at) in points.iter().enumerate() {
1982            let inside = Algebraic::from_rational(rational_between(&lo, at));
1983            let own = micro_at(&inside)?;
1984            let rest = self.verdicts(y, &inside)?;
1985            if own != Tri::T || others.iter().any(|j| rest[*j] != Tri::F) {
1986                return Err(format!(
1987                    "a jump of the loop that fires again and again {} into it meets a boundary the regime cannot follow",
1988                    self.elapsed.add(&lo)
1989                ));
1990            }
1991            if at == remaining {
1992                return Ok((at.clone(), SlideEnd::Bound));
1993            }
1994            let next = Algebraic::from_rational(rational_between(at, &points[n + 1]));
1995            let own_here = micro_at(at)?;
1996            let own_after = micro_at(&next)?;
1997            let here = self.verdicts(y, at)?;
1998            let after = self.verdicts(y, &next)?;
1999            let crossing: Vec<usize> = others.iter().copied().filter(|j| after[*j] == Tri::T).collect();
2000            for j in &others {
2001                if after[*j] == Tri::U || (after[*j] == Tri::F && here[*j] != Tri::F) {
2002                    return Err(format!(
2003                        "another jump of the loop meets its guard's boundary without crossing it {} into the loop, so whether it fires depends on which infinitesimal dt is",
2004                        self.elapsed.add(at)
2005                    ));
2006                }
2007            }
2008            match crossing.as_slice() {
2009                [] => {}
2010                [j] => return Ok((at.clone(), SlideEnd::Fires(*j))),
2011                _ => {
2012                    return Err(format!(
2013                        "two of the loop's jumps fire at once {} into it, so which fires first depends on which infinitesimal dt is",
2014                        self.elapsed.add(at)
2015                    ))
2016                }
2017            }
2018            match own_after {
2019                Tri::F => return Ok((at.clone(), SlideEnd::LiftOff)),
2020                Tri::U => {
2021                    return Err(format!(
2022                        "whether the jump that fires again and again keeps firing {} into the loop depends on which infinitesimal dt is",
2023                        self.elapsed.add(at)
2024                    ))
2025                }
2026                Tri::T => {}
2027            }
2028            if own_here != Tri::T {
2029                return Err(format!(
2030                    "the jump that fires again and again lets go for an instant {} into the loop, so whether it fires then depends on which infinitesimal dt is",
2031                    self.elapsed.add(at)
2032                ));
2033            }
2034            lo = at.clone();
2035        }
2036        unreachable!("the loop's end is the last point")
2037    }
2038}
2039
2040/// A condition's truth over `(0, end)` from the state its statement sees.
2041fn piecewise(cond: &Cond, state: &SymState, facts: &Facts, end: &Algebraic) -> Result<Piecewise, String> {
2042    let mut terms = Vec::new();
2043    atom_terms(cond, &mut terms);
2044    let along: Vec<UPoly> = terms.iter().map(|t| t.poly.eval(&state.values)).collect::<Result<_, _>>()?;
2045    let structural: Vec<Signs> = terms
2046        .iter()
2047        .map(|t| {
2048            if t.exact && t.poly.reads().into_iter().all(|i| state.exact[i]) {
2049                Signs::ZERO
2050            } else {
2051                facts.signs_of(&t.poly)
2052            }
2053        })
2054        .collect();
2055    let zero = alg(0);
2056    let mut points: Vec<Algebraic> = along
2057        .iter()
2058        .flat_map(real_roots)
2059        .filter(|r| r > &zero && r < end)
2060        .collect();
2061    points.sort();
2062    points.dedup();
2063    let eval = |x: &Algebraic| -> Result<Tri, String> {
2064        let mut n = 0;
2065        decide(cond, &state.cells, &mut |a| {
2066            let (p, signs) = (&along[n], structural[n]);
2067            n += 1;
2068            let value = p.eval(x);
2069            Ok(match a {
2070                Atom::Compare(rel, _) => rel.verdict(if !value.is_zero() {
2071                    Signs::of(value.signum())
2072                } else if p.is_zero() {
2073                    signs
2074                } else {
2075                    Signs::ALL
2076                }),
2077                Atom::Infinitesimal(_) => {
2078                    if value.is_zero() {
2079                        Tri::T
2080                    } else {
2081                        Tri::F
2082                    }
2083                }
2084            })
2085        })
2086    };
2087    let mut between = Vec::with_capacity(points.len() + 1);
2088    let mut lo = zero;
2089    for p in points.iter().chain(std::iter::once(end)) {
2090        between.push(eval(&Algebraic::from_rational(rational_between(&lo, p)))?);
2091        lo = p.clone();
2092    }
2093    let at = points.iter().map(eval).collect::<Result<Vec<Tri>, String>>()?;
2094    Ok(Piecewise { points, between, at })
2095}
2096
2097fn check_scope(reads: &[usize], state: &SymState, scope: &[usize]) -> Result<(), String> {
2098    for r in reads {
2099        let written = &state.scope[*r];
2100        if written.len() > scope.len() || written.iter().zip(scope).any(|(a, b)| a != b) {
2101            return Err(
2102                "a statement of a jump that fires again and again reads a value the jump sets only under another condition, so which value it reads depends on the firing"
2103                    .to_string(),
2104            );
2105        }
2106    }
2107    Ok(())
2108}
2109
2110/// The statements of a jump that fires again and again, run on the regime's state: both branches
2111/// of each condition, each statement recorded with the truth of the conditions it runs under.
2112#[allow(clippy::too_many_arguments)]
2113fn regime_walk(
2114    stmts: &[Stmt],
2115    state: &mut SymState,
2116    facts: &mut Facts,
2117    path: &mut Vec<(Piecewise, bool)>,
2118    scope: &mut Vec<usize>,
2119    counter: &mut usize,
2120    out: &mut Vec<RegimeWrite>,
2121    end: &Algebraic,
2122) -> Result<(), String> {
2123    let width = state.values.len();
2124    for s in stmts {
2125        match s {
2126            Stmt::Set { slot, value } => {
2127                let parts: Vec<(&Piecewise, bool)> = path.iter().map(|(p, b)| (p, *b)).collect();
2128                let truth = Piecewise::all(&parts, end);
2129                match value {
2130                    Value::Poly(t) => {
2131                        check_scope(&t.poly.reads(), state, scope)?;
2132                        let v = t.poly.eval(&state.values)?;
2133                        let exact = t.exact && t.poly.reads().into_iter().all(|i| state.exact[i]);
2134                        let unchanged = v == state.values[*slot] && exact == state.exact[*slot];
2135                        let accumulates = t.poly.reads_slot(*slot) && !unchanged;
2136                        let derived = if v.is_zero() && !exact {
2137                            regime_signs(t, state, facts)
2138                        } else {
2139                            Signs::ALL
2140                        };
2141                        out.push(RegimeWrite {
2142                            slot: *slot,
2143                            value: Some(v.clone()),
2144                            fixed: None,
2145                            exact,
2146                            path: truth,
2147                            accumulates,
2148                            unchanged,
2149                        });
2150                        state.values[*slot] = v;
2151                        state.exact[*slot] = exact;
2152                        facts.forget(*slot);
2153                        facts.add(MPoly::slot(width, *slot), derived);
2154                    }
2155                    Value::Fixed(c) => {
2156                        out.push(RegimeWrite {
2157                            slot: *slot,
2158                            value: None,
2159                            fixed: Some(c.clone()),
2160                            exact: true,
2161                            path: truth,
2162                            accumulates: false,
2163                            unchanged: state.cells[*slot] == *c,
2164                        });
2165                        state.cells[*slot] = c.clone();
2166                        facts.forget(*slot);
2167                    }
2168                }
2169                state.scope[*slot] = scope.clone();
2170            }
2171            Stmt::If { cond, then, otherwise } => {
2172                let mut reads = Vec::new();
2173                cond_reads(cond, &mut reads);
2174                check_scope(&reads, state, scope)?;
2175                let truth = piecewise(cond, state, facts, end)?;
2176                let id = *counter;
2177                *counter += 1;
2178                let saved = state.clone();
2179                let saved_facts = facts.clone();
2180                let mut written = vec![false; width];
2181                for (branch, block) in [(true, then), (false, otherwise)] {
2182                    let mut st = saved.clone();
2183                    let mut fa = saved_facts.clone();
2184                    path.push((truth.clone(), branch));
2185                    scope.push(id * 2 + usize::from(branch));
2186                    regime_walk(block, &mut st, &mut fa, path, scope, counter, out, end)?;
2187                    scope.pop();
2188                    path.pop();
2189                    for i in 0..width {
2190                        if st.scope[i] != saved.scope[i] {
2191                            written[i] = true;
2192                        }
2193                    }
2194                }
2195                *state = saved;
2196                *facts = saved_facts;
2197                for (i, w) in written.iter().enumerate() {
2198                    if *w {
2199                        state.scope[i] = vec![UNREADABLE];
2200                        facts.forget(i);
2201                    }
2202                }
2203            }
2204        }
2205    }
2206    Ok(())
2207}
2208
2209/// The signs a regime's reset leaves a value it sets to an infinitesimal, as at a single firing.
2210fn regime_signs(term: &Term, state: &SymState, facts: &Facts) -> Signs {
2211    let [(exponents, coefficient)] = term.poly.terms() else {
2212        return Signs::ALL;
2213    };
2214    let width = term.poly.width();
2215    let mut signs = Signs::of(coefficient.signum());
2216    for (i, k) in exponents.iter().enumerate() {
2217        if *k == 0 {
2218            continue;
2219        }
2220        let v = &state.values[i];
2221        let factor = match v.as_constant() {
2222            Some(c) if !c.is_zero() => Signs::of(c.signum()),
2223            Some(_) if state.exact[i] => Signs::ZERO,
2224            Some(_) => facts.signs_of(&MPoly::slot(width, i)),
2225            None => Signs::ALL,
2226        };
2227        signs = signs.times(factor.power(*k));
2228    }
2229    signs
2230}
2231
2232// ── self-similar tails ───────────────────────────────────────────────────────────────────────
2233
2234/// The longest cycle of firings a tail is looked for in.
2235const MAX_CYCLE: usize = 8;
2236
2237/// One firing of a cycle: which jump fires, and the branch each condition it meets takes.
2238pub type Step = (usize, Vec<bool>);
2239
2240/// A scaling certificate: from one round of the firings in `cycle` to the next, every slot in
2241/// `slots` is multiplied by `s^w` (its weight `w`), and each time between firings by `s^q`.
2242#[derive(Clone, Debug, PartialEq, Eq)]
2243pub struct Scaling {
2244    pub cycle: Vec<Step>,
2245    pub slots: Vec<usize>,
2246    pub weights: Vec<i64>,
2247    pub q: i64,
2248    pub s: Algebraic,
2249}
2250
2251impl Scaling {
2252    fn weight(&self, slot: usize) -> Option<i64> {
2253        self.slots.iter().position(|s| *s == slot).map(|k| self.weights[k])
2254    }
2255
2256    /// Whether `after` is the scaling's image of `before` on its slots.
2257    fn image(&self, before: &[Cell], after: &[Cell]) -> bool {
2258        self.slots.iter().zip(&self.weights).all(|(i, w)| match (&before[*i], &after[*i]) {
2259            (Cell::Num { value: x, .. }, Cell::Num { value: y, .. }) => *y == power(&self.s, *w).mul(x),
2260            (b, a) => b == a,
2261        })
2262    }
2263}
2264
2265/// Each firing's statements of a cycle, in order.
2266fn cycle_writes<'m>(model: &'m Model, cycle: &[Step]) -> Vec<Vec<PathWrite<'m>>> {
2267    cycle
2268        .iter()
2269        .map(|(j, path)| path_writes(&model.jumps[*j].block, path))
2270        .collect()
2271}
2272
2273/// The slots a scaling of a cycle covers: every slot a guard reads, closed under what feeds them
2274/// — the slots their rates read, and the slots a statement of the cycle setting one of them, or a
2275/// condition it runs under, reads.
2276pub fn scaled_slots(model: &Model, cycle: &[Step]) -> Vec<usize> {
2277    let width = model.rates.len();
2278    let mut set = vec![false; width];
2279    for j in &model.jumps {
2280        let mut reads = Vec::new();
2281        cond_reads(&j.guard, &mut reads);
2282        reads.into_iter().for_each(|r| set[r] = true);
2283    }
2284    let writes = cycle_writes(model, cycle);
2285    loop {
2286        let mut grown = false;
2287        let mut mark = |r: usize, set: &mut Vec<bool>| {
2288            if !set[r] {
2289                set[r] = true;
2290                grown = true;
2291            }
2292        };
2293        for i in 0..width {
2294            if set[i] {
2295                for r in model.rates[i].reads() {
2296                    mark(r, &mut set);
2297                }
2298            }
2299        }
2300        for (slot, value, conds) in writes.iter().flatten() {
2301            if !set[*slot] {
2302                continue;
2303            }
2304            if let Value::Poly(t) = value {
2305                for r in t.poly.reads() {
2306                    mark(r, &mut set);
2307                }
2308            }
2309            for (cond, _) in conds {
2310                let mut reads = Vec::new();
2311                cond_reads(cond, &mut reads);
2312                for r in reads {
2313                    mark(r, &mut set);
2314                }
2315            }
2316        }
2317        if !grown {
2318            return (0..width).filter(|i| set[*i]).collect();
2319        }
2320    }
2321}
2322
2323/// The equations a scaling's weights satisfy, each `Σ cᵢ·wᵢ + c_q·q = 0`: every rate of a scaled
2324/// slot is homogeneous of its weight less `q`, every guard atom and every condition a scaled
2325/// slot's statement runs under is homogeneous, and every statement setting a scaled slot is
2326/// homogeneous of its weight.
2327fn homogeneity(model: &Model, cycle: &[Step], slots: &[usize]) -> Vec<(Vec<i64>, i64)> {
2328    let monomial = |e: &[u32]| -> Vec<i64> { e.iter().map(|k| i64::from(*k)).collect() };
2329    let mut out = Vec::new();
2330    let same = |p: &MPoly, out: &mut Vec<(Vec<i64>, i64)>| {
2331        for pair in p.terms().windows(2) {
2332            let (a, b) = (monomial(&pair[0].0), monomial(&pair[1].0));
2333            out.push((a.iter().zip(&b).map(|(x, y)| x - y).collect(), 0));
2334        }
2335    };
2336    for &i in slots {
2337        for (e, _) in model.rates[i].terms() {
2338            let mut c = monomial(e);
2339            c[i] -= 1;
2340            out.push((c, 1));
2341        }
2342    }
2343    for j in &model.jumps {
2344        let mut terms = Vec::new();
2345        atom_terms(&j.guard, &mut terms);
2346        terms.iter().for_each(|t| same(&t.poly, &mut out));
2347    }
2348    for (slot, value, conds) in cycle_writes(model, cycle).iter().flatten() {
2349        if !slots.contains(slot) {
2350            continue;
2351        }
2352        if let Value::Poly(t) = value {
2353            for (e, _) in t.poly.terms() {
2354                let mut c = monomial(e);
2355                c[*slot] -= 1;
2356                out.push((c, 0));
2357            }
2358        }
2359        for (cond, _) in conds {
2360            let mut terms = Vec::new();
2361            atom_terms(cond, &mut terms);
2362            terms.iter().for_each(|t| same(&t.poly, &mut out));
2363        }
2364    }
2365    out
2366}
2367
2368/// The certificate a tail rests on, checked on its own: the slots are closed under what feeds
2369/// them, the field, the guards and the cycle's statements setting them are homogeneous for the
2370/// weights, `after` is the scaling's image of `before`, and each flight of the round after is
2371/// `s^q` times the same flight of the round before. A run whose state the scaling maps to its state
2372/// a round later is then, round after round, the scaled copy of the round before. A scaling by one
2373/// is the identity, which every model is homogeneous for: its certificate is that the state
2374/// repeats.
2375pub fn check_scaling(
2376    model: &Model,
2377    scaling: &Scaling,
2378    before: &[Cell],
2379    after: &[Cell],
2380    flights_before: &[Algebraic],
2381    flights_after: &[Algebraic],
2382) -> Result<(), String> {
2383    if scaling.cycle.is_empty() {
2384        return Err("a scaling maps a round of at least one firing".to_string());
2385    }
2386    if scaling.q < 1 {
2387        return Err("a scaling's time weight is a positive whole number".to_string());
2388    }
2389    if scaling.s.signum() <= 0 {
2390        return Err("a scaling's factor is positive".to_string());
2391    }
2392    if scaling.weights.len() != scaling.slots.len() {
2393        return Err("a scaling weighs every slot it covers".to_string());
2394    }
2395    if scaled_slots(model, &scaling.cycle) != scaling.slots {
2396        return Err("a scaling covers exactly the slots the guards read and what feeds them".to_string());
2397    }
2398    let identity = scaling.s == alg(1);
2399    for (c, cq) in homogeneity(model, &scaling.cycle, &scaling.slots).into_iter().filter(|_| !identity) {
2400        let mut total = cq * scaling.q;
2401        for (i, ci) in c.iter().enumerate() {
2402            if *ci == 0 {
2403                continue;
2404            }
2405            match scaling.weight(i) {
2406                Some(w) => total += ci * w,
2407                None => return Err("a scaled slot is fed by a slot the scaling does not cover".to_string()),
2408            }
2409        }
2410        if total != 0 {
2411            return Err("the field, a guard or a reset is not homogeneous for the scaling's weights".to_string());
2412        }
2413    }
2414    if !scaling.image(before, after) {
2415        return Err("the state after is not the scaling's image of the state before".to_string());
2416    }
2417    if flights_before.len() != scaling.cycle.len() || flights_after.len() != scaling.cycle.len() {
2418        return Err("a scaling maps one flight before each firing of its round".to_string());
2419    }
2420    let step = power(&scaling.s, scaling.q);
2421    if flights_before.iter().zip(flights_after).any(|(b, a)| *a != step.mul(b)) {
2422        return Err("a flight after is not the scaling's image of the flight before".to_string());
2423    }
2424    Ok(())
2425}
2426
2427/// `p/q` with `r = σ^(p/q)` exactly, `q` at most [`MAX_WEIGHT_DENOMINATOR`] and `|p|` at most
2428/// [`MAX_WEIGHT_NUMERATOR`].
2429fn exponent_ratio(r: &Algebraic, sigma: &Algebraic) -> Option<Rational> {
2430    let target = r.to_f64().ln() / sigma.to_f64().ln();
2431    if !target.is_finite() {
2432        return None;
2433    }
2434    let (mut h0, mut h1, mut k0, mut k1) = (0i64, 1i64, 1i64, 0i64);
2435    let mut x = target;
2436    for _ in 0..40 {
2437        let a = x.floor();
2438        if a.abs() > 1e9 {
2439            return None;
2440        }
2441        let a = a as i64;
2442        let h = a.checked_mul(h1)?.checked_add(h0)?;
2443        let k = a.checked_mul(k1)?.checked_add(k0)?;
2444        if k > MAX_WEIGHT_DENOMINATOR || h.abs() > MAX_WEIGHT_NUMERATOR {
2445            return None;
2446        }
2447        if k > 0 && power(r, k) == power(sigma, h) {
2448            return Rational::from_ratio_i64(h, k);
2449        }
2450        (h0, h1, k0, k1) = (h1, h, k1, k);
2451        let frac = x - a as f64;
2452        if frac.abs() < 1e-12 {
2453            return None;
2454        }
2455        x = 1.0 / frac;
2456    }
2457    None
2458}
2459
2460fn lcm(a: i64, b: i64) -> i64 {
2461    let (mut x, mut y) = (a, b);
2462    while y != 0 {
2463        (x, y) = (y, x % y);
2464    }
2465    a / x * b
2466}
2467
2468/// The scaling that maps `before` to `after` over a round whose length is multiplied by `sigma`,
2469/// its weights read off the ratios of the values and, for a value zero at both, off the
2470/// homogeneity the scaling must have; `None` when no scaling does.
2471fn find_scaling(
2472    model: &Model,
2473    cycle: &[Step],
2474    slots: &[usize],
2475    before: &[Cell],
2476    after: &[Cell],
2477    sigma: &Algebraic,
2478) -> Option<Scaling> {
2479    let one = alg(1);
2480    if *sigma == one {
2481        let same = slots.iter().all(|i| match (&before[*i], &after[*i]) {
2482            (Cell::Num { value: x, .. }, Cell::Num { value: y, .. }) => x == y,
2483            (b, a) => b == a,
2484        });
2485        return same.then(|| Scaling {
2486            cycle: cycle.to_vec(),
2487            slots: slots.to_vec(),
2488            weights: vec![0; slots.len()],
2489            q: 1,
2490            s: one,
2491        });
2492    }
2493    let mut ratio: Vec<Option<Rational>> = Vec::with_capacity(slots.len());
2494    for i in slots {
2495        match (&before[*i], &after[*i]) {
2496            (Cell::Num { value: x, .. }, Cell::Num { value: y, .. }) => {
2497                if x.is_zero() {
2498                    if !y.is_zero() {
2499                        return None;
2500                    }
2501                    ratio.push(None);
2502                } else {
2503                    let r = quotient(y, x);
2504                    if r.signum() <= 0 {
2505                        return None;
2506                    }
2507                    ratio.push(Some(exponent_ratio(&r, sigma)?));
2508                }
2509            }
2510            (b, a) => {
2511                if b != a {
2512                    return None;
2513                }
2514                ratio.push(Some(Rational::zero()));
2515            }
2516        }
2517    }
2518    let constraints = homogeneity(model, cycle, slots);
2519    let at = |slot: usize| slots.iter().position(|s| *s == slot);
2520    loop {
2521        let mut solved = false;
2522        for (c, cq) in &constraints {
2523            let unknown: Vec<usize> = (0..c.len())
2524                .filter(|i| c[*i] != 0 && at(*i).is_some_and(|k| ratio[k].is_none()))
2525                .collect();
2526            if let [u] = unknown.as_slice() {
2527                let mut sum = Rational::from_i64(*cq);
2528                for (i, ci) in c.iter().enumerate() {
2529                    if *ci != 0 && i != *u {
2530                        sum = sum.add(&Rational::from_i64(*ci).mul(ratio[at(i)?].as_ref()?));
2531                    }
2532                }
2533                ratio[at(*u)?] = sum.negated().div(&Rational::from_i64(c[*u]));
2534                solved = true;
2535            }
2536        }
2537        if !solved {
2538            break;
2539        }
2540    }
2541    let ratio: Vec<Rational> = ratio.into_iter().map(|r| r.unwrap_or_else(Rational::zero)).collect();
2542    let mut q = 1i64;
2543    for r in &ratio {
2544        q = lcm(q, r.denominator().to_i64()?);
2545    }
2546    let weights = ratio
2547        .iter()
2548        .map(|r| r.mul(&Rational::from_i64(q)).to_bigint().and_then(|b| b.to_i64()))
2549        .collect::<Option<Vec<i64>>>()?;
2550    let s = sigma.nth_root(u32::try_from(q).ok()?)?;
2551    Some(Scaling {
2552        cycle: cycle.to_vec(),
2553        slots: slots.to_vec(),
2554        weights,
2555        q,
2556        s,
2557    })
2558}
2559
2560/// A value along a tail: a polynomial in `z = sᵏ` plus a multiple of the round index `k`.
2561#[derive(Clone, Debug, PartialEq, Eq)]
2562struct TailVal {
2563    p: UPoly,
2564    lin: Algebraic,
2565}
2566
2567impl TailVal {
2568    fn constant(a: Algebraic) -> TailVal {
2569        TailVal { p: UPoly::constant(a), lin: alg(0) }
2570    }
2571
2572    fn is_zero(&self) -> bool {
2573        self.p.is_zero() && self.lin.is_zero()
2574    }
2575
2576    fn is_constant(&self) -> bool {
2577        self.p.as_constant().is_some() && self.lin.is_zero()
2578    }
2579
2580    fn at(&self, s: &Algebraic, k: usize) -> Algebraic {
2581        self.p.eval(&power(s, k as i64)).add(&self.lin.mul(&alg(k as i64)))
2582    }
2583
2584    /// A value computed from the state at the end of one round, as a value of the round it is
2585    /// computed in.
2586    fn next_round(&self, s: &Algebraic) -> TailVal {
2587        TailVal {
2588            p: self
2589                .p
2590                .dilate(&s.recip().expect("a scaling's factor is positive"))
2591                .sub(&UPoly::constant(self.lin.clone())),
2592            lin: self.lin.clone(),
2593        }
2594    }
2595}
2596
2597impl Ring for TailVal {
2598    fn lift(a: &Algebraic) -> Self {
2599        TailVal::constant(a.clone())
2600    }
2601    fn plus(&self, o: &Self) -> Result<Self, String> {
2602        Ok(TailVal {
2603            p: self.p.add(&o.p),
2604            lin: self.lin.add(&o.lin),
2605        })
2606    }
2607    fn times(&self, o: &Self) -> Result<Self, String> {
2608        if self.lin.is_zero() && o.lin.is_zero() {
2609            return Ok(TailVal {
2610                p: self.p.mul(&o.p),
2611                lin: alg(0),
2612            });
2613        }
2614        let (c, v) = match (self.p.as_constant(), o.p.as_constant()) {
2615            (Some(c), _) if self.lin.is_zero() => (c, o),
2616            (_, Some(c)) if o.lin.is_zero() => (c, self),
2617            _ => {
2618                return Err(
2619                    "a count of the jumps multiplies a value that changes with them, so the values along the jumps are not polynomials in their scaling"
2620                        .to_string(),
2621                )
2622            }
2623        };
2624        Ok(TailVal {
2625            p: v.p.scale(&c),
2626            lin: v.lin.mul(&c),
2627        })
2628    }
2629}
2630
2631/// A polynomial in one unknown value with coefficients along a tail.
2632#[derive(Clone, Debug)]
2633struct YPoly(Vec<TailVal>);
2634
2635impl YPoly {
2636    fn trimmed(mut self) -> YPoly {
2637        while self.0.last().is_some_and(TailVal::is_zero) {
2638            self.0.pop();
2639        }
2640        self
2641    }
2642}
2643
2644impl Ring for YPoly {
2645    fn lift(a: &Algebraic) -> Self {
2646        YPoly(vec![TailVal::constant(a.clone())]).trimmed()
2647    }
2648    fn plus(&self, o: &Self) -> Result<Self, String> {
2649        let n = self.0.len().max(o.0.len());
2650        let zero = TailVal::constant(alg(0));
2651        let mut c = Vec::with_capacity(n);
2652        for k in 0..n {
2653            c.push(self.0.get(k).unwrap_or(&zero).plus(o.0.get(k).unwrap_or(&zero))?);
2654        }
2655        Ok(YPoly(c).trimmed())
2656    }
2657    fn times(&self, o: &Self) -> Result<Self, String> {
2658        if self.0.is_empty() || o.0.is_empty() {
2659            return Ok(YPoly(Vec::new()));
2660        }
2661        let mut c = vec![TailVal::constant(alg(0)); self.0.len() + o.0.len() - 1];
2662        for (i, a) in self.0.iter().enumerate() {
2663            for (j, b) in o.0.iter().enumerate() {
2664                c[i + j] = c[i + j].plus(&a.times(b)?)?;
2665            }
2666        }
2667        Ok(YPoly(c).trimmed())
2668    }
2669}
2670
2671/// A slot's value along a tail, from the round it holds from.
2672#[derive(Clone, Debug)]
2673struct TailSlot {
2674    val: TailVal,
2675    from: usize,
2676}
2677
2678enum TailSolve {
2679    Values(Vec<Option<TailSlot>>),
2680    /// The values hold only from a round the run has not reached yet.
2681    Wait,
2682    Unsupported(String),
2683}
2684
2685fn union(sets: impl Iterator<Item = Vec<usize>>) -> Vec<usize> {
2686    let mut out: Vec<usize> = sets.flatten().collect();
2687    out.sort_unstable();
2688    out.dedup();
2689    out
2690}
2691
2692/// The slots each slot's value at the end of a round depends on, at the round's start.
2693fn round_dependencies(series: &[Vec<MPoly>], cycle: &[Vec<PathWrite<'_>>]) -> Vec<Vec<usize>> {
2694    let width = series.len();
2695    let flown: Vec<Vec<usize>> = series
2696        .iter()
2697        .map(|derivatives| union(derivatives.iter().map(MPoly::reads)))
2698        .collect();
2699    let mut deps: Vec<Vec<usize>> = (0..width).map(|i| vec![i]).collect();
2700    for writes in cycle {
2701        deps = (0..width)
2702            .map(|i| union(flown[i].iter().map(|r| deps[*r].clone())))
2703            .collect();
2704        for (slot, value, _) in writes {
2705            deps[*slot] = match value {
2706                Value::Poly(t) => union(t.poly.reads().into_iter().map(|r| deps[r].clone())),
2707                Value::Fixed(_) => Vec::new(),
2708            };
2709        }
2710    }
2711    deps
2712}
2713
2714/// `x` flown for `tau`.
2715fn fly<R: Ring>(series: &[Vec<MPoly>], x: &[R], tau: &R) -> Result<Vec<R>, String> {
2716    let mut out = Vec::with_capacity(series.len());
2717    for derivatives in series {
2718        let mut total = R::lift(&alg(0));
2719        let mut t_power = R::lift(&alg(1));
2720        for (m, d) in derivatives.iter().enumerate() {
2721            let term = d
2722                .eval(x)?
2723                .times(&t_power)?
2724                .times(&R::lift(&quotient(&alg(1), &factorial(m))))?;
2725            total = total.plus(&term)?;
2726            t_power = t_power.times(tau)?;
2727        }
2728        out.push(total);
2729    }
2730    Ok(out)
2731}
2732
2733/// The values at the end of a round from the values at its start, `x`, each firing after a
2734/// flight of `taus[r]`.
2735fn round_map<R: Ring>(series: &[Vec<MPoly>], cycle: &[Vec<PathWrite<'_>>], x: &[R], taus: &[R]) -> Result<Vec<R>, String> {
2736    let mut cur = x.to_vec();
2737    for (writes, tau) in cycle.iter().zip(taus) {
2738        cur = fly(series, &cur, tau)?;
2739        for (slot, value, _) in writes {
2740            if let Value::Poly(t) = value {
2741                cur[*slot] = t.poly.eval(&cur)?;
2742            }
2743        }
2744    }
2745    Ok(cur)
2746}
2747
2748/// The value of every numeric slot at the end of every round of a tail, as a polynomial in
2749/// `z = sᵏ` plus a multiple of `k`, rounds counted from the first of `known`.
2750fn tail_values(
2751    model: &Model,
2752    series: &[Vec<MPoly>],
2753    scaling: &Scaling,
2754    known: &[Vec<Cell>],
2755    flights0: &[Algebraic],
2756) -> TailSolve {
2757    let width = model.rates.len();
2758    let m = known.len() - 1;
2759    let base = &known[0];
2760    let s = &scaling.s;
2761    let one = alg(1);
2762    let mut out: Vec<Option<TailSlot>> = vec![None; width];
2763    for (i, w) in scaling.slots.iter().zip(&scaling.weights) {
2764        if let Cell::Num { value, .. } = &base[*i] {
2765            if value.is_zero() {
2766                out[*i] = Some(TailSlot { val: TailVal::constant(alg(0)), from: 0 });
2767            } else if *w < 0 {
2768                return TailSolve::Unsupported("and a value the guards read grows without bound as they do".to_string());
2769            } else {
2770                out[*i] = Some(TailSlot {
2771                    val: TailVal {
2772                        p: UPoly::monomial(value.clone(), *w as usize),
2773                        lin: alg(0),
2774                    },
2775                    from: 0,
2776                });
2777            }
2778        }
2779    }
2780    let writes = cycle_writes(model, &scaling.cycle);
2781    let deps = round_dependencies(series, &writes);
2782    let mut pending: Vec<usize> = (0..width)
2783        .filter(|i| matches!(base[*i], Cell::Num { .. }) && out[*i].is_none())
2784        .collect();
2785    let taus: Vec<YPoly> = flights0
2786        .iter()
2787        .map(|f| {
2788            YPoly(vec![TailVal {
2789                p: UPoly::monomial(f.clone(), scaling.q as usize),
2790                lin: alg(0),
2791            }])
2792        })
2793        .collect();
2794    while !pending.is_empty() {
2795        let Some(at) = pending
2796            .iter()
2797            .position(|k| deps[*k].iter().all(|d| *d == *k || !pending.contains(d)))
2798        else {
2799            return TailSolve::Unsupported(
2800                "and values that feed each other through the jumps change along them in a way the engine does not sum".to_string(),
2801            );
2802        };
2803        let k = pending.remove(at);
2804        let x: Vec<YPoly> = (0..width)
2805            .map(|i| {
2806                if i == k {
2807                    YPoly(vec![TailVal::constant(alg(0)), TailVal::constant(alg(1))])
2808                } else {
2809                    match &out[i] {
2810                        Some(t) => YPoly(vec![t.val.clone()]).trimmed(),
2811                        None => YPoly(Vec::new()),
2812                    }
2813                }
2814            })
2815            .collect();
2816        let next = match round_map(series, &writes, &x, &taus) {
2817            Ok(v) => v[k].clone(),
2818            Err(why) => return TailSolve::Unsupported(format!("and {why}")),
2819        };
2820        let inputs_from = deps[k]
2821            .iter()
2822            .filter(|d| **d != k)
2823            .filter_map(|d| out[*d].as_ref().map(|t| t.from))
2824            .max()
2825            .unwrap_or(0);
2826        let zero = TailVal::constant(alg(0));
2827        let c0 = next.0.first().cloned().unwrap_or_else(|| zero.clone());
2828        let c1 = next.0.get(1).cloned().unwrap_or_else(|| zero.clone());
2829        if next.0.len() > 2 {
2830            return TailSolve::Unsupported("and a value the jumps raise to a power changes with them".to_string());
2831        }
2832        if !c1.is_constant() {
2833            return TailSolve::Unsupported(
2834                "and a value the jumps multiply by a changing factor changes with them".to_string(),
2835            );
2836        }
2837        let a = c1.p.coeff(0);
2838        let anchor = |i0: usize| -> Option<Algebraic> {
2839            (i0 <= m).then(|| known[i0][k].number().cloned().unwrap_or_else(|| alg(0)))
2840        };
2841        let solved = if a.is_zero() {
2842            TailSlot {
2843                val: c0.next_round(s),
2844                from: inputs_from + 1,
2845            }
2846        } else if a == one {
2847            if !c0.lin.is_zero() {
2848                return TailSolve::Unsupported("and a value that adds up a count of the jumps grows without bound".to_string());
2849            }
2850            let Some(start) = anchor(inputs_from) else { return TailSolve::Wait };
2851            let i0 = inputs_from;
2852            if *s == one {
2853                let step = c0.p.eval(&one);
2854                TailSlot {
2855                    val: TailVal {
2856                        p: UPoly::constant(start.sub(&step.mul(&alg(i0 as i64)))),
2857                        lin: step,
2858                    },
2859                    from: i0,
2860                }
2861            } else {
2862                let lin = c0.p.coeff(0);
2863                let mut coeffs = vec![alg(0)];
2864                let z0 = power(s, i0 as i64);
2865                let mut kappa = start.sub(&lin.mul(&alg(i0 as i64)));
2866                for d in 1..=c0.p.degree().unwrap_or(0) {
2867                    let c = quotient(&c0.p.coeff(d), &power(s, d as i64).sub(&one));
2868                    kappa = kappa.sub(&c.mul(&power(&z0, d as i64)));
2869                    coeffs.push(c);
2870                }
2871                coeffs[0] = kappa;
2872                TailSlot {
2873                    val: TailVal {
2874                        p: UPoly::from_coeffs(coeffs),
2875                        lin,
2876                    },
2877                    from: i0,
2878                }
2879            }
2880        } else {
2881            let Some(mu) = scale_exponent(&a, s) else {
2882                return TailSolve::Unsupported(
2883                    "and a value the jumps multiply by a factor their scaling does not produce changes with them".to_string(),
2884                );
2885            };
2886            if !c0.lin.is_zero() || !c0.p.coeff(mu).is_zero() {
2887                return TailSolve::Unsupported(
2888                    "and a value the jumps multiply by their own scaling is driven at that same rate, so it is not a polynomial in the scaling".to_string(),
2889                );
2890            }
2891            let Some(start) = anchor(inputs_from) else { return TailSolve::Wait };
2892            let i0 = inputs_from;
2893            let s_mu = power(s, mu as i64);
2894            let mut coeffs: Vec<Algebraic> = (0..=c0.p.degree().unwrap_or(0).max(mu))
2895                .map(|d| {
2896                    if d == mu {
2897                        alg(0)
2898                    } else {
2899                        quotient(&c0.p.coeff(d), &power(s, d as i64).sub(&s_mu))
2900                    }
2901                })
2902                .collect();
2903            let z0 = power(s, i0 as i64);
2904            let particular = UPoly::from_coeffs(coeffs.clone()).eval(&z0);
2905            coeffs[mu] = quotient(&start.sub(&particular), &power(&z0, mu as i64));
2906            TailSlot {
2907                val: TailVal {
2908                    p: UPoly::from_coeffs(coeffs),
2909                    lin: alg(0),
2910                },
2911                from: i0,
2912            }
2913        };
2914        out[k] = Some(solved);
2915    }
2916    if out.iter().flatten().any(|t| t.from > m + 1) {
2917        return TailSolve::Wait;
2918    }
2919    TailSolve::Values(out)
2920}
2921
2922/// The `μ ≥ 1` with `sᵘ = a`.
2923fn scale_exponent(a: &Algebraic, s: &Algebraic) -> Option<usize> {
2924    if a.signum() <= 0 || *s == alg(1) {
2925        return None;
2926    }
2927    let estimate = (a.to_f64().ln() / s.to_f64().ln()).round();
2928    if !estimate.is_finite() || !(1.0..=4096.0).contains(&estimate) {
2929        return None;
2930    }
2931    let mu = estimate as usize;
2932    (mu.saturating_sub(1)..=mu + 1)
2933        .filter(|m| *m >= 1)
2934        .find(|m| power(s, *m as i64) == *a)
2935}
2936
2937/// The conditions a firing along `path` meets, in order, each with the branch it takes.
2938fn path_conditions<'b>(block: &'b [Stmt], path: &[bool]) -> Vec<(&'b Cond, bool)> {
2939    fn walk<'b>(stmts: &'b [Stmt], bits: &mut std::slice::Iter<'_, bool>, out: &mut Vec<(&'b Cond, bool)>) {
2940        for s in stmts {
2941            if let Stmt::If { cond, then, otherwise } = s {
2942                let taken = bits.next().copied().unwrap_or(false);
2943                out.push((cond, taken));
2944                walk(if taken { then } else { otherwise }, bits, out);
2945            }
2946        }
2947    }
2948    let mut out = Vec::new();
2949    walk(block, &mut path.iter(), &mut out);
2950    out
2951}
2952
2953/// For each firing of a round, each condition it meets, each atom's value along the tail as of
2954/// the round it is met in.
2955fn tail_conditions(
2956    model: &Model,
2957    series: &[Vec<MPoly>],
2958    scaling: &Scaling,
2959    values: &[Option<TailSlot>],
2960    flights0: &[Algebraic],
2961) -> Result<Vec<Vec<Vec<TailVal>>>, String> {
2962    let width = model.rates.len();
2963    let mut cur: Vec<TailVal> = (0..width)
2964        .map(|i| values[i].as_ref().map(|t| t.val.clone()).unwrap_or_else(|| TailVal::constant(alg(0))))
2965        .collect();
2966    fn walk(
2967        stmts: &[Stmt],
2968        bits: &mut std::slice::Iter<'_, bool>,
2969        cur: &mut Vec<TailVal>,
2970        out: &mut Vec<Vec<TailVal>>,
2971        s: &Algebraic,
2972    ) -> Result<(), String> {
2973        for st in stmts {
2974            match st {
2975                Stmt::Set { slot, value } => {
2976                    if let Value::Poly(t) = value {
2977                        cur[*slot] = t.poly.eval(cur)?;
2978                    }
2979                }
2980                Stmt::If { cond, then, otherwise } => {
2981                    let mut terms = Vec::new();
2982                    atom_terms(cond, &mut terms);
2983                    out.push(
2984                        terms
2985                            .iter()
2986                            .map(|t| t.poly.eval(cur).map(|v| v.next_round(s)))
2987                            .collect::<Result<Vec<TailVal>, String>>()?,
2988                    );
2989                    let taken = bits.next().copied().unwrap_or(false);
2990                    walk(if taken { then } else { otherwise }, bits, cur, out, s)?;
2991                }
2992            }
2993        }
2994        Ok(())
2995    }
2996    let mut rounds = Vec::with_capacity(scaling.cycle.len());
2997    for ((j, path), flight) in scaling.cycle.iter().zip(flights0) {
2998        let tau = TailVal {
2999            p: UPoly::monomial(flight.clone(), scaling.q as usize),
3000            lin: alg(0),
3001        };
3002        cur = fly(series, &cur, &tau)?;
3003        let mut out = Vec::new();
3004        walk(&model.jumps[*j].block, &mut path.iter(), &mut cur, &mut out, &scaling.s)?;
3005        rounds.push(out);
3006    }
3007    Ok(rounds)
3008}
3009
3010/// The first `k ≥ from` with `pred(sᵏ)`, for a `pred` that, once it holds, holds for every later
3011/// `k`; `None` when it does not hold by an index the run can reach.
3012fn first_index(s: &Algebraic, from: usize, pred: &dyn Fn(&Algebraic) -> bool) -> Option<usize> {
3013    if pred(&power(s, from as i64)) {
3014        return Some(from);
3015    }
3016    let mut lo = from;
3017    let mut hi = from.max(1);
3018    loop {
3019        hi = hi.checked_mul(2)?;
3020        if hi > 1 << 22 {
3021            return None;
3022        }
3023        if pred(&power(s, hi as i64)) {
3024            break;
3025        }
3026        lo = hi;
3027    }
3028    while hi - lo > 1 {
3029        let mid = lo + (hi - lo) / 2;
3030        if pred(&power(s, mid as i64)) {
3031            hi = mid;
3032        } else {
3033            lo = mid;
3034        }
3035    }
3036    Some(hi)
3037}
3038
3039/// The indices `k ≥ from` at which the sign of `f(k)` may differ from its sign at `k − 1`; `None`
3040/// when they are not bounded within the run's reach.
3041fn sign_changes(f: &TailVal, s: &Algebraic, from: usize) -> Option<Vec<usize>> {
3042    if f.is_zero() {
3043        return Some(Vec::new());
3044    }
3045    let one = alg(1);
3046    let around = |k0: &Algebraic| -> Vec<usize> {
3047        let floor = k0.floor().to_i64().unwrap_or(i64::MAX).max(0);
3048        (floor..=floor.saturating_add(2))
3049            .filter_map(|k| usize::try_from(k).ok())
3050            .filter(|k| *k >= from)
3051            .collect()
3052    };
3053    if *s == one {
3054        if f.lin.is_zero() {
3055            return Some(Vec::new());
3056        }
3057        return Some(around(&quotient(&f.p.eval(&one).neg(), &f.lin)));
3058    }
3059    if f.lin.is_zero() {
3060        let mut out = Vec::new();
3061        for r in real_roots(&f.p) {
3062            if r.signum() <= 0 {
3063                continue;
3064            }
3065            let k = if *s < one {
3066                first_index(s, from, &|z| *z <= r)?
3067            } else {
3068                first_index(s, from, &|z| *z >= r)?
3069            };
3070            out.extend([k, k + 1]);
3071        }
3072        return Some(out);
3073    }
3074    let bound = if *s < one {
3075        let b = f.p.coeffs().iter().fold(alg(0), |acc, c| acc.add(&magnitude(c)));
3076        quotient(&b, &magnitude(&f.lin)).floor().to_i64()? + 1
3077    } else {
3078        let d = f.p.degree().unwrap_or(0);
3079        if d == 0 {
3080            return Some(around(&quotient(&f.p.coeff(0).neg(), &f.lin)));
3081        }
3082        let lead = magnitude(&f.p.coeff(d));
3083        let lower = (0..d).fold(alg(0), |acc, k| acc.add(&magnitude(&f.p.coeff(k))));
3084        let drift = quotient(&magnitude(&f.lin), &s.sub(&one));
3085        let threshold = if d >= 2 {
3086            quotient(&lower.add(&drift), &lead)
3087        } else {
3088            let margin = lead.sub(&drift);
3089            if margin.signum() <= 0 {
3090                return None;
3091            }
3092            quotient(&lower, &margin)
3093        };
3094        let threshold = threshold.max(one);
3095        first_index(s, from, &|z| *z > threshold)? as i64
3096    };
3097    let bound = usize::try_from(bound).ok()?;
3098    if bound.saturating_sub(from) > EVENT_BUDGET {
3099        return None;
3100    }
3101    Some((from..=bound.max(from)).collect())
3102}
3103
3104impl<'m> Run<'m> {
3105    /// After a firing: when the last firings repeat a cycle of one to [`MAX_CYCLE`] firings and a
3106    /// scaling maps each round to the next, the rest of the run along it is read off the tail —
3107    /// to the round where a condition of a firing turns, to the loop's last round, or to the time
3108    /// the firings accumulate at.
3109    fn tail(&mut self) -> Result<TailMove, String> {
3110        let n = self.history.len();
3111        for period in 1..=MAX_CYCLE {
3112            if n < 2 * period + 1 {
3113                break;
3114            }
3115            let cycle: Vec<Step> = self.history[n - period..]
3116                .iter()
3117                .map(|e| (e.jump, e.firing.path.clone()))
3118                .collect();
3119            let repeats = (0..period).all(|r| {
3120                let e = &self.history[n - 2 * period + r];
3121                e.jump == cycle[r].0 && e.firing.path == cycle[r].1
3122            });
3123            if !repeats {
3124                continue;
3125            }
3126            if let Some(found) = self.cycle_tail(period, cycle)? {
3127                return Ok(found);
3128            }
3129        }
3130        Ok(TailMove::None)
3131    }
3132
3133    /// The tail of a cycle of `period` firings, when a scaling certifies it.
3134    fn cycle_tail(&mut self, period: usize, cycle: Vec<Step>) -> Result<Option<TailMove>, String> {
3135        let n = self.history.len();
3136        let (ia, ib, ic) = (n - 1 - 2 * period, n - 1 - period, n - 1);
3137        let flights = |from: usize| -> Vec<Algebraic> {
3138            (from..from + period)
3139                .map(|i| self.history[i + 1].at.sub(&self.history[i].at))
3140                .collect()
3141        };
3142        let total = |f: &[Algebraic]| f.iter().fold(alg(0), |acc, x| acc.add(x));
3143        let (fa, fb) = (flights(ia), flights(ib));
3144        let sigma = quotient(&total(&fb), &total(&fa));
3145        let slots = scaled_slots(self.model, &cycle);
3146        let (a, b, c) = (
3147            &self.history[ia].firing.post,
3148            &self.history[ib].firing.post,
3149            &self.history[ic].firing.post,
3150        );
3151        let Some(scaling) = find_scaling(self.model, &cycle, &slots, a, b, &sigma) else {
3152            return Ok(None);
3153        };
3154        if check_scaling(self.model, &scaling, a, b, &fa, &fb).is_err() || !scaling.image(b, c) {
3155            return Ok(None);
3156        }
3157        let step = power(&scaling.s, scaling.q);
3158        let mut base = ia;
3159        while base >= period {
3160            let prev = base - period;
3161            let same_steps = (0..period).all(|r| {
3162                let e = &self.history[prev + 1 + r];
3163                e.jump == cycle[r].0 && e.firing.path == cycle[r].1
3164            });
3165            let fits = same_steps
3166                && scaling.image(&self.history[prev].firing.post, &self.history[base].firing.post)
3167                && flights(prev).iter().zip(&flights(base)).all(|(x, y)| *y == step.mul(x));
3168            if !fits {
3169                break;
3170            }
3171            base = prev;
3172        }
3173        let known: Vec<Vec<Cell>> = (base..n).step_by(period).map(|i| self.history[i].firing.post.clone()).collect();
3174        let conditions: Vec<Vec<Vec<Tri>>> = (n - period..n).map(|i| self.history[i].firing.conditions.clone()).collect();
3175        let facts = self.history[n - 1].firing.facts.clone();
3176        let at0 = self.history[base].at.clone();
3177        let flights0 = flights(base);
3178        self.resolve_tail(&scaling, &known, &conditions, facts, &at0, &flights0).map(Some)
3179    }
3180
3181    fn resolve_tail(
3182        &mut self,
3183        scaling: &Scaling,
3184        known: &[Vec<Cell>],
3185        conditions: &[Vec<Vec<Tri>>],
3186        facts: Facts,
3187        at0: &Algebraic,
3188        flights0: &[Algebraic],
3189    ) -> Result<TailMove, String> {
3190        let m = known.len() - 1;
3191        let one = alg(1);
3192        let round = flights0.iter().fold(alg(0), |acc, x| acc.add(x));
3193        let sigma = power(&scaling.s, scaling.q);
3194        let zeno = (sigma < one).then(|| at0.add(&quotient(&round, &one.sub(&sigma))));
3195        let accumulates_first = zeno.as_ref().is_some_and(|z| *z <= self.span);
3196        let values = match tail_values(self.model, &self.series, scaling, known, flights0) {
3197            TailSolve::Values(v) => v,
3198            TailSolve::Wait => return Ok(TailMove::None),
3199            TailSolve::Unsupported(why) => {
3200                return match &zeno {
3201                    Some(z) if accumulates_first => Err(format!(
3202                        "the loop's jumps fire infinitely often, accumulating {z} into it, {why}"
3203                    )),
3204                    _ => Ok(TailMove::None),
3205                }
3206            }
3207        };
3208        let round_at = |k: usize| -> Algebraic {
3209            if sigma == one {
3210                at0.add(&round.mul(&alg(k as i64)))
3211            } else {
3212                at0.add(&quotient(&round.mul(&one.sub(&power(&sigma, k as i64))), &one.sub(&sigma)))
3213            }
3214        };
3215        let Some(deviation) = self.first_deviation(scaling, &values, conditions, flights0, m)? else {
3216            return match &zeno {
3217                Some(z) if accumulates_first => Err(format!(
3218                    "the loop's jumps fire infinitely often, accumulating {z} into it, and whether a condition of a jump holds along them is not settled exactly"
3219                )),
3220                _ => Ok(TailMove::None),
3221            };
3222        };
3223        let last = if accumulates_first {
3224            None
3225        } else {
3226            match self.last_before_end(&sigma, at0, &round)? {
3227                Some(j) => Some(j),
3228                None => return Ok(TailMove::None),
3229            }
3230        };
3231        let target = match (deviation, last) {
3232            (Some(d), Some(j)) => Some((d - 1).min(j)),
3233            (Some(d), None) => Some(d - 1),
3234            (None, Some(j)) => Some(j),
3235            (None, None) => None,
3236        };
3237        match target {
3238            Some(k) => {
3239                if k <= m {
3240                    return Ok(TailMove::None);
3241                }
3242                self.cells = (0..self.width)
3243                    .map(|i| match (&known[m][i], &values[i]) {
3244                        (Cell::Num { exact, .. }, Some(t)) => Cell::Num {
3245                            value: t.val.at(&scaling.s, k),
3246                            exact: *exact,
3247                        },
3248                        (cell, _) => cell.clone(),
3249                    })
3250                    .collect();
3251                self.elapsed = round_at(k);
3252                self.facts = facts;
3253                self.history.clear();
3254                Ok(TailMove::Moved)
3255            }
3256            None => {
3257                let z = zeno.expect("only firings that accumulate leave no last round");
3258                let mut cells = Vec::with_capacity(self.width);
3259                for i in 0..self.width {
3260                    cells.push(match (&known[m][i], &values[i]) {
3261                        (Cell::Num { exact, .. }, Some(t)) => {
3262                            if !t.val.lin.is_zero() {
3263                                return Err(format!(
3264                                    "the loop's jumps fire infinitely often, accumulating {z} into it, and a value that counts them grows without bound"
3265                                ));
3266                            }
3267                            Cell::Num {
3268                                value: t.val.p.coeff(0),
3269                                exact: *exact && t.val.is_constant(),
3270                            }
3271                        }
3272                        (cell, _) => cell.clone(),
3273                    });
3274                }
3275                self.cells = cells;
3276                self.elapsed = z;
3277                self.facts = Facts::default();
3278                self.history.clear();
3279                Ok(if self.elapsed == self.span {
3280                    TailMove::Moved
3281                } else {
3282                    TailMove::Zeno(scaling.cycle.last().expect("a cycle has a firing").0)
3283                })
3284            }
3285        }
3286    }
3287
3288    /// The first round after the run's last at which a condition of a firing no longer takes the
3289    /// branch the tail takes: `Some(None)` when none ever does, `None` when that is not settled
3290    /// within the run's reach.
3291    fn first_deviation(
3292        &self,
3293        scaling: &Scaling,
3294        values: &[Option<TailSlot>],
3295        conditions: &[Vec<Vec<Tri>>],
3296        flights0: &[Algebraic],
3297        m: usize,
3298    ) -> Result<Option<Option<usize>>, String> {
3299        let Ok(atoms) = tail_conditions(self.model, &self.series, scaling, values, flights0) else {
3300            return Ok(None);
3301        };
3302        let from = m + 1;
3303        let mut first: Option<usize> = None;
3304        for (r, (j, path)) in scaling.cycle.iter().enumerate() {
3305            let conds = path_conditions(&self.model.jumps[*j].block, path);
3306            for (c, ((cond, taken), along)) in conds.iter().zip(&atoms[r]).enumerate() {
3307                let mut candidates = vec![from];
3308                for f in along {
3309                    match sign_changes(f, &scaling.s, from) {
3310                        Some(ks) => candidates.extend(ks),
3311                        None => return Ok(None),
3312                    }
3313                }
3314                candidates.sort_unstable();
3315                candidates.dedup();
3316                let recorded = conditions
3317                    .get(r)
3318                    .and_then(|rc| rc.get(c))
3319                    .cloned()
3320                    .unwrap_or_default();
3321                for k in candidates {
3322                    if first.is_some_and(|f| k >= f) {
3323                        break;
3324                    }
3325                    let mut n = 0;
3326                    let verdict = decide(cond, &self.cells, &mut |a| {
3327                        let f = &along[n];
3328                        let structural = recorded.get(n).copied().unwrap_or(Tri::U);
3329                        n += 1;
3330                        let value = f.at(&scaling.s, k);
3331                        Ok(match a {
3332                            Atom::Compare(rel, _) => {
3333                                if f.is_zero() {
3334                                    structural
3335                                } else if value.is_zero() {
3336                                    Tri::U
3337                                } else {
3338                                    rel.verdict(Signs::of(value.signum()))
3339                                }
3340                            }
3341                            Atom::Infinitesimal(_) => {
3342                                if value.is_zero() {
3343                                    Tri::T
3344                                } else {
3345                                    Tri::F
3346                                }
3347                            }
3348                        })
3349                    })?;
3350                    let expected = if *taken { Tri::T } else { Tri::F };
3351                    if verdict != expected {
3352                        first = Some(k);
3353                        break;
3354                    }
3355                }
3356            }
3357        }
3358        Ok(Some(first))
3359    }
3360
3361    /// The index of the tail's last round to end before the loop does, counted from its first;
3362    /// `None` when it lies beyond the run's reach. A round ending exactly at the loop's end is
3363    /// refused.
3364    fn last_before_end(&self, sigma: &Algebraic, at0: &Algebraic, round: &Algebraic) -> Result<Option<usize>, String> {
3365        let one = alg(1);
3366        let left = self.span.sub(at0);
3367        let k = if *sigma == one {
3368            match quotient(&left, round).ceil().to_i64().and_then(|c| usize::try_from(c).ok()) {
3369                Some(c) => c,
3370                None => return Ok(None),
3371            }
3372        } else {
3373            let threshold = one.sub(&quotient(&left.mul(&one.sub(sigma)), round));
3374            let found = if *sigma < one {
3375                first_index(sigma, 0, &|p| *p <= threshold)
3376            } else {
3377                first_index(sigma, 0, &|p| *p >= threshold)
3378            };
3379            match found {
3380                Some(k) => k,
3381                None => return Ok(None),
3382            }
3383        };
3384        let at_k = if *sigma == one {
3385            at0.add(&round.mul(&alg(k as i64)))
3386        } else {
3387            at0.add(&quotient(&round.mul(&one.sub(&power(sigma, k as i64))), &one.sub(sigma)))
3388        };
3389        if at_k == self.span {
3390            return Err(format!(
3391                "a jump of the loop fires at its last step, {} into it, so whether it fires before the loop ends depends on which infinitesimal dt is",
3392                self.span
3393            ));
3394        }
3395        Ok(Some(k.saturating_sub(1)))
3396    }
3397}
3398
3399#[cfg(test)]
3400#[path = "../../logicaffeine_tests/unit/logicaffeine_base/hybrid/tests.rs"]
3401mod tests;