Skip to main content

alkahest_cas/kernel/
expr.rs

1use crate::kernel::domain::Domain;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4
5/// Opaque index into an [`crate::kernel::ExprPool`]. `Copy` — expressions are values, not owned objects.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
7pub struct ExprId(pub(crate) u32);
8
9// ---------------------------------------------------------------------------
10// Atom wrappers.
11//
12// `Hash` for the number atoms is value-based and allocation-free: it reads the
13// limbs directly.  It used to format the number as a hex string on every call,
14// which made hashing a machine-size integer ~7x slower than the lookup around
15// it — and the intern table hashes every atom on every lookup and rehashes
16// every key when it grows.  Nothing persists these hashes (the pool file and
17// the Merkle prototype use their own encodings), so changing them is free.
18// ---------------------------------------------------------------------------
19
20/// Arbitrary-precision integer atom.
21#[derive(Debug, Clone)]
22pub struct BigInt(pub rug::Integer);
23
24impl PartialEq for BigInt {
25    fn eq(&self, other: &Self) -> bool {
26        self.0 == other.0
27    }
28}
29impl Eq for BigInt {}
30
31impl Hash for BigInt {
32    fn hash<H: Hasher>(&self, state: &mut H) {
33        // rug hashes the signed limb count and the limbs.  GMP keeps an
34        // `Integer` normalised (no high zero limbs, zero has size 0), so equal
35        // values always have identical (size, limbs) — consistent with `Eq`.
36        self.0.hash(state);
37    }
38}
39
40impl fmt::Display for BigInt {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46/// Arbitrary-precision rational atom. Stored in canonical reduced form by rug.
47#[derive(Debug, Clone)]
48pub struct BigRat(pub rug::Rational);
49
50impl PartialEq for BigRat {
51    fn eq(&self, other: &Self) -> bool {
52        self.0 == other.0
53    }
54}
55impl Eq for BigRat {}
56
57impl Hash for BigRat {
58    fn hash<H: Hasher>(&self, state: &mut H) {
59        // rug keeps a `Rational` canonical (reduced, positive denominator), so
60        // equal values have equal numerators and denominators.
61        self.0.hash(state);
62    }
63}
64
65impl fmt::Display for BigRat {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        if *self.0.denom() == 1 {
68            write!(f, "{}", self.0.numer())
69        } else {
70            write!(f, "{}/{}", self.0.numer(), self.0.denom())
71        }
72    }
73}
74
75/// Arbitrary-precision floating-point atom. `prec` (precision in bits) is
76/// part of structural identity: `Float(1.0, 53) != Float(1.0, 64)`.
77#[derive(Debug, Clone)]
78pub struct BigFloat {
79    pub inner: rug::Float,
80    pub prec: u32,
81}
82
83impl PartialEq for BigFloat {
84    fn eq(&self, other: &Self) -> bool {
85        if self.prec != other.prec {
86            return false;
87        }
88        match (self.inner.is_nan(), other.inner.is_nan()) {
89            (true, true) => true,
90            (false, false) => self.inner == other.inner,
91            _ => false,
92        }
93    }
94}
95impl Eq for BigFloat {}
96
97impl Hash for BigFloat {
98    /// Hashes exactly what [`PartialEq`] compares: the `prec` field and the
99    /// *value* of `inner`.  In particular, matching `eq`:
100    ///
101    /// * every NaN hashes alike (`eq` treats any two NaNs as equal);
102    /// * `+0` and `-0` hash alike (`rug`'s `==` says they are equal);
103    /// * `inner`'s own MPFR precision is not hashed — `eq` compares values, so
104    ///   `1.0` held at 53 and at 64 mantissa bits must collide.  MPFR stores
105    ///   the significand left-aligned with the unused low bits zero, so equal
106    ///   values differ only by extra all-zero low limbs, which are skipped.
107    fn hash<H: Hasher>(&self, state: &mut H) {
108        self.prec.hash(state);
109        let f = &self.inner;
110        if f.is_nan() {
111            0u8.hash(state);
112        } else if f.is_zero() {
113            1u8.hash(state);
114        } else if f.is_infinite() {
115            2u8.hash(state);
116            f.is_sign_negative().hash(state);
117        } else {
118            3u8.hash(state);
119            f.is_sign_negative().hash(state);
120            f.get_exp().hash(state);
121            if let Some(sig) = f.get_significand() {
122                let limbs = sig.as_limbs();
123                let first = limbs.iter().position(|&l| l != 0).unwrap_or(limbs.len());
124                limbs[first..].hash(state);
125            }
126        }
127    }
128}
129
130impl fmt::Display for BigFloat {
131    /// `rug`'s own rendering, made to read back as a float.
132    ///
133    /// `rug` prints a zero as `0`, which every parser in this crate reads as
134    /// the *integer* `0` — a different node, and a different kind of number.
135    /// A finite value whose rendering has neither a point nor an exponent is
136    /// therefore given a `.0`.  The digit count is `rug`'s, which grows with
137    /// `prec`, so the parsers can recover the precision from it (see
138    /// [`crate::parse::float_literal_prec`]).
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        let s = self.inner.to_string();
141        if self.inner.is_finite() && !s.contains(['.', 'e', 'E']) {
142            write!(f, "{s}.0")
143        } else {
144            f.write_str(&s)
145        }
146    }
147}
148
149// ---------------------------------------------------------------------------
150// Predicate — symbolic boolean conditions for Piecewise expressions
151// ---------------------------------------------------------------------------
152
153/// Kind of a symbolic predicate.
154///
155/// Predicates are stored in the intern table as
156/// `ExprData::Predicate { kind, args }`.  The `args` field holds the
157/// operands as `ExprId` nodes.
158///
159/// | Kind | Arity | Meaning |
160/// |------|-------|---------|
161/// | `Lt` | 2     | `args[0]` < `args[1]` |
162/// | `Le` | 2     | `args[0]` ≤ `args[1]` |
163/// | `Gt` | 2     | `args[0]` > `args[1]` |
164/// | `Ge` | 2     | `args[0]` ≥ `args[1]` |
165/// | `Eq` | 2     | `args[0]` = `args[1]` (symbolic equality) |
166/// | `Ne` | 2     | `args[0]` ≠ `args[1]` |
167/// | `And`| n     | conjunction of n predicate ExprIds |
168/// | `Or` | n     | disjunction of n predicate ExprIds |
169/// | `Not`| 1     | negation |
170/// | `True` | 0   | always-true |
171/// | `False`| 0   | always-false |
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173pub enum PredicateKind {
174    Lt,
175    Le,
176    Gt,
177    Ge,
178    Eq,
179    Ne,
180    And,
181    Or,
182    Not,
183    True,
184    False,
185}
186
187impl fmt::Display for PredicateKind {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        let s = match self {
190            PredicateKind::Lt => "<",
191            PredicateKind::Le => "≤",
192            PredicateKind::Gt => ">",
193            PredicateKind::Ge => "≥",
194            PredicateKind::Eq => "=",
195            PredicateKind::Ne => "≠",
196            PredicateKind::And => "∧",
197            PredicateKind::Or => "∨",
198            PredicateKind::Not => "¬",
199            PredicateKind::True => "True",
200            PredicateKind::False => "False",
201        };
202        write!(f, "{s}")
203    }
204}
205
206// ---------------------------------------------------------------------------
207// Expression data — the structural content stored in the intern table.
208// ---------------------------------------------------------------------------
209
210/// Structural content of an expression node.
211///
212/// All compound nodes hold [`ExprId`] children, not owned sub-trees.
213/// This keeps `ExprData` small and allows sharing via the intern table.
214#[derive(Debug, Clone, PartialEq, Eq, Hash)]
215pub enum ExprData {
216    // Atoms
217    Symbol {
218        name: String,
219        domain: Domain,
220        /// When `false`, this generator does not commute under multiplication; see V3-2.
221        commutative: bool,
222    },
223    Integer(BigInt),
224    Rational(BigRat),
225    Float(BigFloat),
226    // Compound (n-ary for Add/Mul; binary for Pow; variadic for Func)
227    Add(Vec<ExprId>),
228    Mul(Vec<ExprId>),
229    Pow {
230        base: ExprId,
231        exp: ExprId,
232    },
233    Func {
234        name: String,
235        args: Vec<ExprId>,
236    },
237    // PA-9 — symbolic conditionals
238    /// A piecewise expression: evaluates to `value_i` when `cond_i` holds,
239    /// and to `default` when no condition matches.
240    ///
241    /// Conditions are `ExprData::Predicate` nodes stored in the pool.
242    /// Branches are tried in order; the first matching condition wins.
243    Piecewise {
244        branches: Vec<(ExprId /* cond */, ExprId /* value */)>,
245        default: ExprId,
246    },
247    /// A symbolic predicate (boolean condition over symbolic reals).
248    Predicate {
249        kind: PredicateKind,
250        args: Vec<ExprId>,
251    },
252    /// Universal quantification (`∀ var . body`).  Used by first-order logic (V3-3).
253    Forall {
254        var: ExprId,
255        body: ExprId,
256    },
257    /// Existential quantification (`∃ var . body`).
258    Exists {
259        var: ExprId,
260        body: ExprId,
261    },
262    /// Landau big-O remainder: `O(arg)` as a symbolic order bound (V2-15 series API).
263    BigO(ExprId),
264    /// Sum over the roots of a polynomial: `Σ_{c : poly(c)=0} body[var := c]`.
265    ///
266    /// A binder (like [`ExprData::Exists`]): `var` is the bound root placeholder,
267    /// `poly` is a univariate polynomial in `var`, and `body` is the summand
268    /// (an expression in `var` and the free variables).  Used to represent the
269    /// logarithmic part of a rational-function integral whose residues are
270    /// algebraic numbers of degree ≥ 2 (Rothstein–Trager / Lazard–Rioboo–Trager).
271    RootSum {
272        poly: ExprId,
273        var: ExprId,
274        body: ExprId,
275    },
276}
277
278#[cfg(test)]
279mod number_hash_tests {
280    //! `Hash` must agree with `Eq` for the number atoms: the intern table
281    //! relies on it for hash-consing.
282    use super::*;
283    use proptest::prelude::*;
284    use rug::{Assign, Float, Integer, Rational};
285    use std::collections::hash_map::RandomState;
286    use std::hash::BuildHasher;
287
288    fn h<T: Hash>(rs: &RandomState, x: &T) -> u64 {
289        rs.hash_one(x)
290    }
291
292    /// `a == b` must imply equal hashes (and `==` must be symmetric).
293    fn assert_consistent<T: Hash + Eq + fmt::Debug>(a: &T, b: &T) {
294        let rs = RandomState::new();
295        if a == b {
296            assert!(b == a, "Eq not symmetric: {a:?} vs {b:?}");
297            assert_eq!(
298                h(&rs, a),
299                h(&rs, b),
300                "equal values hash apart: {a:?} vs {b:?}"
301            );
302        }
303    }
304
305    fn float(inner_prec: u32, v: f64, prec: u32) -> BigFloat {
306        BigFloat {
307            inner: Float::with_val(inner_prec, v),
308            prec,
309        }
310    }
311
312    #[test]
313    fn integer_hash_ignores_allocation() {
314        // Same value, very different internal capacity / construction path.
315        let a = BigInt(Integer::from(-42));
316        let mut grown = Integer::with_capacity(10_000);
317        grown.assign(Integer::from(1) << 5000u32);
318        grown -= Integer::from(1) << 5000u32;
319        grown -= 42;
320        let b = BigInt(grown);
321        assert_eq!(a, b);
322        assert_consistent(&a, &b);
323        let z1 = BigInt(Integer::new());
324        let z2 = BigInt(Integer::from(7) - 7);
325        assert_eq!(z1, z2);
326        assert_consistent(&z1, &z2);
327        // Sign is part of the value.
328        let rs = RandomState::new();
329        assert_ne!(
330            h(&rs, &BigInt(Integer::from(5))),
331            h(&rs, &BigInt(Integer::from(-5)))
332        );
333    }
334
335    #[test]
336    fn rational_hash_uses_canonical_form() {
337        let a = BigRat(Rational::from((6, -4)));
338        let b = BigRat(Rational::from((-3, 2)));
339        assert_eq!(a, b);
340        assert_consistent(&a, &b);
341        let c = BigRat(Rational::from((10, 5)));
342        let d = BigRat(Rational::from(2));
343        assert_eq!(c, d);
344        assert_consistent(&c, &d);
345    }
346
347    #[test]
348    fn float_hash_matches_eq_on_special_values() {
349        // ±0 are equal under `Eq`, so they must hash alike.
350        let pz = float(53, 0.0, 53);
351        let nz = float(53, -0.0, 53);
352        assert!(nz.inner.is_sign_negative());
353        assert_eq!(pz, nz);
354        assert_consistent(&pz, &nz);
355        // Every NaN equals every NaN (same `prec`), whatever its sign.
356        let n1 = float(53, f64::NAN, 53);
357        let n2 = BigFloat {
358            inner: -Float::with_val(113, f64::NAN),
359            prec: 53,
360        };
361        assert_eq!(n1, n2);
362        assert_consistent(&n1, &n2);
363        // ±∞ differ.
364        assert_ne!(
365            float(53, f64::INFINITY, 53),
366            float(53, f64::NEG_INFINITY, 53)
367        );
368        // `prec` is part of identity.
369        assert_ne!(float(53, 1.0, 53), float(53, 1.0, 64));
370    }
371
372    #[test]
373    fn float_hash_ignores_inner_mpfr_precision() {
374        // `Eq` compares values, not the MPFR precision `inner` happens to
375        // carry, so the extra all-zero low limbs must not reach the hash.
376        for v in [
377            1.0,
378            -1.5,
379            0.1,
380            1e300,
381            -3.0e-300,
382            f64::MIN_POSITIVE,
383            f64::MAX,
384        ] {
385            for p in [53u32, 64, 65, 128, 129, 1000] {
386                let a = float(53, v, 53);
387                let b = float(p, v, 53);
388                assert_eq!(a, b, "{v} at inner prec {p}");
389                assert_consistent(&a, &b);
390            }
391        }
392    }
393
394    #[test]
395    fn hashes_spread_over_distinct_values() {
396        // Not a correctness requirement, but a degenerate hash would silently
397        // turn the intern table into a linked list.
398        let rs = RandomState::new();
399        let mut seen = std::collections::HashSet::new();
400        for k in -500i64..500 {
401            seen.insert(h(&rs, &BigInt(Integer::from(k))));
402            seen.insert(h(&rs, &BigRat(Rational::from((k, 7)))));
403            seen.insert(h(&rs, &float(53, k as f64 * 0.37, 53)));
404        }
405        // 3000 values, minus the collisions under `Eq`: 0/7 == 0 is a
406        // BigRat vs BigInt (different types, fine), -0.0 == 0.0 is one value.
407        assert!(seen.len() >= 2990, "only {} distinct hashes", seen.len());
408    }
409
410    fn big_integer() -> impl Strategy<Value = Integer> {
411        prop_oneof![
412            any::<i64>().prop_map(Integer::from),
413            any::<i128>().prop_map(Integer::from),
414            (any::<bool>(), proptest::collection::vec(any::<u64>(), 0..6)).prop_map(
415                |(neg, limbs)| {
416                    let v = Integer::from_digits(&limbs, rug::integer::Order::Lsf);
417                    if neg {
418                        -v
419                    } else {
420                        v
421                    }
422                }
423            ),
424        ]
425    }
426
427    proptest! {
428        #[test]
429        fn prop_integer_eq_implies_hash(a in big_integer(), b in big_integer(), k in big_integer()) {
430            // A second route to the same value, (a + k) - k, in an
431            // over-allocated buffer.
432            let mut round = Integer::with_capacity(4096);
433            round.assign(&a + &k);
434            round -= &k;
435            prop_assert_eq!(&a, &round);
436            assert_consistent(&BigInt(a.clone()), &BigInt(round));
437            assert_consistent(&BigInt(a), &BigInt(b));
438        }
439
440        #[test]
441        fn prop_rational_eq_implies_hash(
442            n in big_integer(),
443            d in big_integer(),
444            k in big_integer(),
445        ) {
446            prop_assume!(d != 0 && k != 0);
447            let r = BigRat(Rational::from((n.clone(), d.clone())));
448            // Unreduced input with the same value: (n·k)/(d·k).
449            let scaled = BigRat(Rational::from((n * &k, d * &k)));
450            prop_assert_eq!(&r, &scaled);
451            assert_consistent(&r, &scaled);
452        }
453
454        #[test]
455        fn prop_float_eq_implies_hash(
456            a in any::<f64>(),
457            b in any::<f64>(),
458            pa in 2u32..300,
459            pb in 2u32..300,
460            prec in prop_oneof![Just(53u32), Just(64u32), 2u32..200],
461        ) {
462            // Arbitrary pairs, including NaNs, ±0, ±∞ and subnormals.
463            let x = BigFloat { inner: Float::with_val(pa, a), prec };
464            let y = BigFloat { inner: Float::with_val(pb, b), prec };
465            assert_consistent(&x, &y);
466            // The same (rounded) value re-held at a wider MPFR precision.
467            let wider = BigFloat {
468                inner: Float::with_val(pa.max(pb) + 64, &x.inner),
469                prec,
470            };
471            prop_assert_eq!(&x, &wider);
472            assert_consistent(&x, &wider);
473        }
474
475        #[test]
476        fn prop_distinct_floats_hash_apart(a in any::<f64>(), b in any::<f64>()) {
477            let x = float(53, a, 53);
478            let y = float(53, b, 53);
479            let rs = RandomState::new();
480            if x != y {
481                prop_assert_ne!(h(&rs, &x), h(&rs, &y));
482            }
483        }
484    }
485}