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}