Skip to main content

probl_engine/
value.rs

1//! Runtime values.
2//!
3//! Values have value semantics: collections are shared through `Arc` and
4//! copied on write, so forking a world is cheap and worlds never affect each
5//! other. Equality and hashing are *structural* (they decide which worlds
6//! merge); the language's `==` lives in [`crate::ops`] and compares numbers
7//! across types.
8
9use crate::complex::Complex;
10use crate::continuous::Family;
11use crate::dist::Dist;
12use rustc_hash::FxHasher;
13use std::cmp::Ordering;
14use std::collections::BTreeMap;
15use std::fmt;
16use std::hash::{Hash, Hasher};
17use std::ops::{Deref, DerefMut};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering as Atomic};
20
21#[derive(Clone)]
22pub enum Value {
23    /// A slot with no value: not assigned yet, or cleared because it's dead.
24    Dead,
25    Unit,
26    /// A fact: true or false in a world.
27    Bool(bool),
28    Int(probl_number::Integer),
29    Float(f64),
30    Complex(Complex),
31    /// A probability: a number from 0 to 1. It's a parameter, not an event.
32    Prob(f64),
33    Str(Arc<str>),
34    List(Arc<Hashed<Vec<Value>>>),
35    Map(Arc<Hashed<BTreeMap<Value, Value>>>),
36    /// A multiset: value → count.
37    Bag(Arc<Hashed<Multiset>>),
38    /// Integers from `.0` to `.1`, both included.
39    Range(probl_number::Integer, probl_number::Integer),
40    Record(Arc<Hashed<Record>>),
41    Enum(Arc<EnumValue>),
42    Dist(Arc<Dist>),
43    /// A continuous distribution (docs/semantics.md, section 13).
44    Continuous(Arc<Family>),
45    Closure(Arc<Closure>),
46    Builtin(probl_sema::Builtin),
47    /// Days since 1970-01-01.
48    Date(i32),
49    /// A variable's value that isn't drawn yet, internal to the engine
50    /// (docs/semantics.md, section 14). The engine draws it before any
51    /// value read; a direct `typeof` inspection only needs its outcome type.
52    Delayed(Arc<Delayed>),
53    /// A scalar outcome retained analytically during enumeration.
54    Analytic(Arc<crate::analytic::Analytic>),
55    /// A boolean predicate of that same outcome, retaining its identity.
56    Event(Arc<crate::analytic::Event>),
57    /// A count law too broad to list its outcomes, like `geometric(1e-12)`:
58    /// drawn from directly when sampling, and asked by `pmf`, `cdf` and the
59    /// other queries. Anything that needs its outcomes lists it, which
60    /// fails on the outcome limit, as building it once did.
61    Counts(Arc<crate::dist::Counts>),
62    /// A distribution whose outcomes own continuous draws, from `simulate`
63    /// (docs/semantics.md, section 8): drawing from it gives each outcome's
64    /// draws fresh identities (see `crate::joint`).
65    Joint(Arc<crate::dist::Dist>),
66}
67
68/// The distribution of a variable whose draw is delayed, updated exactly by
69/// the observations so far.
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub struct Delayed {
72    pub family: Family,
73    /// Which of the program's variables that may be delayed it is
74    /// (`Conjugacy::variables`), for statistics.
75    pub variable: u32,
76}
77
78/// A collection, and its hash once computed. Worlds are hashed whenever
79/// they merge, and most of their collections haven't changed since the last
80/// time: each is hashed once. Changing a collection (through `DerefMut`)
81/// forgets its hash, and collections with different hashes are unequal
82/// without comparing them.
83pub struct Hashed<T> {
84    /// 0 until computed.
85    hash: AtomicU64,
86    value: T,
87}
88
89impl<T> Hashed<T> {
90    pub fn new(value: T) -> Hashed<T> {
91        Hashed {
92            hash: AtomicU64::new(0),
93            value,
94        }
95    }
96
97    fn known_hash(&self) -> u64 {
98        self.hash.load(Atomic::Relaxed)
99    }
100}
101
102impl<T: Hash> Hashed<T> {
103    /// The collection's hash: computed the first time, then kept.
104    pub fn hash_code(&self) -> u64 {
105        let known = self.known_hash();
106        if known != 0 {
107            return known;
108        }
109        let mut hasher = FxHasher::default();
110        self.value.hash(&mut hasher);
111        let h = hasher.finish().max(1);
112        self.hash.store(h, Atomic::Relaxed);
113        h
114    }
115}
116
117impl<T> Deref for Hashed<T> {
118    type Target = T;
119
120    fn deref(&self) -> &T {
121        &self.value
122    }
123}
124
125impl<T> DerefMut for Hashed<T> {
126    fn deref_mut(&mut self) -> &mut T {
127        *self.hash.get_mut() = 0;
128        &mut self.value
129    }
130}
131
132impl<T: Clone> Clone for Hashed<T> {
133    fn clone(&self) -> Hashed<T> {
134        Hashed {
135            hash: AtomicU64::new(self.known_hash()),
136            value: self.value.clone(),
137        }
138    }
139}
140
141impl<T: PartialEq> PartialEq for Hashed<T> {
142    fn eq(&self, other: &Hashed<T>) -> bool {
143        let (a, b) = (self.known_hash(), other.known_hash());
144        (a == 0 || b == 0 || a == b) && self.value == other.value
145    }
146}
147
148impl<T: Eq> Eq for Hashed<T> {}
149
150impl<T: Hash> Hash for Hashed<T> {
151    fn hash<H: Hasher>(&self, state: &mut H) {
152        state.write_u64(self.hash_code());
153    }
154}
155
156impl<T: Ord> PartialOrd for Hashed<T> {
157    fn partial_cmp(&self, other: &Hashed<T>) -> Option<Ordering> {
158        Some(self.cmp(other))
159    }
160}
161
162impl<T: Ord> Ord for Hashed<T> {
163    fn cmp(&self, other: &Hashed<T>) -> Ordering {
164        self.value.cmp(&other.value)
165    }
166}
167
168impl<T: fmt::Debug> fmt::Debug for Hashed<T> {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        self.value.fmt(f)
171    }
172}
173
174/// A bag's contents: each distinct value, sorted, and how many times it's
175/// in the bag (never zero). A sorted vector rather than a map: bags are
176/// small, copied whenever they change, and compared each time worlds merge,
177/// which is fastest in contiguous memory.
178#[derive(Clone, Debug, Default)]
179pub struct Multiset {
180    entries: Vec<(Value, u64)>,
181    /// The sum of the entries' hashes, which is the bag's hash: taking a
182    /// value out updates it without going through the other entries.
183    sum: u64,
184}
185
186/// One entry's share of a bag's hash. Mixed thoroughly, so that bags with
187/// counts moved from one value to another don't sum to the same hash.
188fn entry_hash(v: &Value, n: u64) -> u64 {
189    let mut hasher = FxHasher::default();
190    v.hash(&mut hasher);
191    n.hash(&mut hasher);
192    crate::continuous::mix(hasher.finish())
193}
194
195impl Multiset {
196    /// From counts, leaving out zeros.
197    pub fn new(counts: BTreeMap<Value, u64>) -> Multiset {
198        let entries: Vec<(Value, u64)> = counts.into_iter().filter(|(_, n)| *n > 0).collect();
199        let sum = entries
200            .iter()
201            .fold(0u64, |sum, (v, n)| sum.wrapping_add(entry_hash(v, *n)));
202        Multiset { entries, sum }
203    }
204
205    pub fn iter(&self) -> impl Iterator<Item = (&Value, &u64)> + Clone {
206        self.entries.iter().map(|(v, n)| (v, n))
207    }
208
209    pub fn keys(&self) -> impl Iterator<Item = &Value> + Clone {
210        self.entries.iter().map(|(v, _)| v)
211    }
212
213    pub fn values(&self) -> impl Iterator<Item = &u64> + Clone {
214        self.entries.iter().map(|(_, n)| n)
215    }
216
217    /// How many times `v` is in the bag, if it is.
218    pub fn get(&self, v: &Value) -> Option<&u64> {
219        let i = self.entries.binary_search_by(|(k, _)| k.cmp(v)).ok()?;
220        Some(&self.entries[i].1)
221    }
222
223    /// The bag without one of its `i`-th distinct value.
224    pub fn without_nth(&self, i: usize) -> Multiset {
225        let mut entries = self.entries.clone();
226        let (v, n) = (&self.entries[i].0, self.entries[i].1);
227        let mut sum = self.sum.wrapping_sub(entry_hash(v, n));
228        if n > 1 {
229            entries[i].1 = n - 1;
230            sum = sum.wrapping_add(entry_hash(v, n - 1));
231        } else {
232            entries.remove(i);
233        }
234        Multiset { entries, sum }
235    }
236
237    /// The bag without one `v`, if it has one.
238    pub fn without(&self, v: &Value) -> Option<Multiset> {
239        let i = self.entries.binary_search_by(|(k, _)| k.cmp(v)).ok()?;
240        Some(self.without_nth(i))
241    }
242}
243
244impl PartialEq for Multiset {
245    fn eq(&self, other: &Multiset) -> bool {
246        self.sum == other.sum && self.entries == other.entries
247    }
248}
249
250impl Eq for Multiset {}
251
252impl Hash for Multiset {
253    fn hash<H: Hasher>(&self, state: &mut H) {
254        state.write_u64(self.sum);
255    }
256}
257
258impl PartialOrd for Multiset {
259    fn partial_cmp(&self, other: &Multiset) -> Option<Ordering> {
260        Some(self.cmp(other))
261    }
262}
263
264impl Ord for Multiset {
265    fn cmp(&self, other: &Multiset) -> Ordering {
266        self.entries.cmp(&other.entries)
267    }
268}
269
270#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
271pub struct Record {
272    pub ty: Option<Arc<str>>,
273    /// Sorted by name.
274    pub fields: Vec<(Arc<str>, Value)>,
275}
276
277impl Record {
278    pub fn get(&self, name: &str) -> Option<&Value> {
279        self.fields.iter().find(|(n, _)| &**n == name).map(|(_, v)| v)
280    }
281
282    pub fn get_mut(&mut self, name: &str) -> Option<&mut Value> {
283        self.fields.iter_mut().find(|(n, _)| &**n == name).map(|(_, v)| v)
284    }
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
288pub struct EnumValue {
289    pub ty: u32,
290    pub variant: u32,
291    pub name: Arc<str>,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
295pub struct Closure {
296    pub func: u32,
297    pub captured: Vec<Value>,
298}
299
300impl Value {
301    pub fn str(s: &str) -> Value {
302        Value::Str(Arc::from(s))
303    }
304
305    pub fn list(items: Vec<Value>) -> Value {
306        Value::List(Arc::new(Hashed::new(items)))
307    }
308
309    pub fn map(entries: BTreeMap<Value, Value>) -> Value {
310        Value::Map(Arc::new(Hashed::new(entries)))
311    }
312
313    pub fn bag(counts: BTreeMap<Value, u64>) -> Value {
314        Value::multiset(Multiset::new(counts))
315    }
316
317    pub fn multiset(contents: Multiset) -> Value {
318        Value::Bag(Arc::new(Hashed::new(contents)))
319    }
320
321    pub fn record(record: Record) -> Value {
322        Value::Record(Arc::new(Hashed::new(record)))
323    }
324
325    /// A copy that shares no collection or string with the original. Values
326    /// are shared through reference counts: threads that all read the same
327    /// data would compete for those, so each sampling thread takes a copy.
328    pub fn unshared(&self) -> Value {
329        self.unshared_with(&mut rustc_hash::FxHashMap::default())
330    }
331
332    /// `names` keeps the copy's names (record types, fields, variants) shared
333    /// within the copy.
334    fn unshared_with(&self, names: &mut rustc_hash::FxHashMap<String, Arc<str>>) -> Value {
335        fn name(names: &mut rustc_hash::FxHashMap<String, Arc<str>>, n: &str) -> Arc<str> {
336            names.entry(n.to_string()).or_insert_with(|| Arc::from(n)).clone()
337        }
338        match self {
339            Value::Str(s) => Value::str(s),
340            Value::Enum(e) => Value::Enum(Arc::new(EnumValue {
341                name: name(names, &e.name),
342                ..(**e).clone()
343            })),
344            Value::Record(r) => {
345                let ty = r.ty.as_deref().map(|t| name(names, t));
346                let mut fields = Vec::with_capacity(r.fields.len());
347                for (n, v) in &r.fields {
348                    fields.push((name(names, n), v.unshared_with(names)));
349                }
350                Value::record(Record { ty, fields })
351            }
352            Value::List(items) => Value::list(items.iter().map(|x| x.unshared_with(names)).collect()),
353            Value::Map(m) => Value::map(
354                m.iter()
355                    .map(|(k, v)| (k.unshared_with(names), v.unshared_with(names)))
356                    .collect(),
357            ),
358            Value::Bag(b) => Value::multiset(Multiset {
359                entries: b.entries.iter().map(|(v, n)| (v.unshared_with(names), *n)).collect(),
360                sum: b.sum,
361            }),
362            other => other.clone(),
363        }
364    }
365
366    /// The kind of value, as named in error messages.
367    pub fn kind(&self) -> String {
368        match self {
369            Value::Dead => "nothing".into(),
370            Value::Unit => "()".into(),
371            Value::Bool(_) => "bool".into(),
372            Value::Int(_) => "int".into(),
373            Value::Float(_) => "float".into(),
374            Value::Complex(_) => "complex".into(),
375            Value::Prob(_) => "prob".into(),
376            Value::Str(_) => "str".into(),
377            Value::List(_) => "list".into(),
378            Value::Map(_) => "map".into(),
379            Value::Bag(_) => "bag".into(),
380            Value::Range(..) => "range".into(),
381            Value::Record(r) => match &r.ty {
382                Some(t) => t.to_string(),
383                None => "record".into(),
384            },
385            Value::Enum(_) => "enum".into(),
386            Value::Dist(d) if d.outcomes.iter().any(|(v, _)| matches!(v, Value::Continuous(_))) => {
387                "mixture of distributions".into()
388            }
389            Value::Dist(d) => match d.outcomes.first() {
390                Some((v, _)) => format!("distribution over {}", plural_kind(&v.kind())),
391                None => "distribution".into(),
392            },
393            Value::Continuous(f) => format!("{} distribution", f.name()),
394            Value::Closure(_) | Value::Builtin(_) => "function".into(),
395            Value::Date(_) => "date".into(),
396            Value::Delayed(_) => "value not drawn yet".into(),
397            Value::Analytic(a) if a.int => "int".into(),
398            Value::Analytic(_) => "float".into(),
399            Value::Event(_) => "bool".into(),
400            Value::Counts(_) => "distribution over ints".into(),
401            Value::Joint(d) => match d.outcomes.first() {
402                Some((v, _)) => format!("distribution over {}", plural_kind(&v.kind())),
403                None => "distribution".into(),
404            },
405        }
406    }
407
408    /// A distribution with outcomes that can be listed.
409    pub fn is_dist(&self) -> bool {
410        matches!(self, Value::Dist(_))
411    }
412
413    /// Any distribution, including continuous ones: not a settled value.
414    pub fn is_uncertain(&self) -> bool {
415        matches!(
416            self,
417            Value::Dist(_) | Value::Continuous(_) | Value::Counts(_) | Value::Joint(_)
418        )
419    }
420
421    /// Numbers as f64: ints, floats and probabilities.
422    pub fn as_f64(&self) -> Option<f64> {
423        match self {
424            Value::Int(i) => i.to_f64(),
425            Value::Float(f) | Value::Prob(f) => Some(*f),
426            _ => None,
427        }
428    }
429
430    /// Promote real numeric data to a complex scalar, without accepting facts
431    /// or converting complex data back into real probabilities.
432    pub fn as_complex(&self) -> Option<Complex> {
433        match self {
434            Value::Complex(z) => Some(*z),
435            _ => self.as_f64().and_then(|x| Complex::new(x, 0.0).ok()),
436        }
437    }
438
439    /// Order of the value kinds, used to sort mixed values.
440    fn rank(&self) -> u8 {
441        match self {
442            Value::Dead => 0,
443            Value::Unit => 1,
444            Value::Bool(_) => 2,
445            Value::Int(_) | Value::Float(_) | Value::Prob(_) => 3,
446            Value::Str(_) => 4,
447            Value::Date(_) => 5,
448            Value::Enum(_) => 6,
449            Value::List(_) => 7,
450            Value::Range(..) => 8,
451            Value::Map(_) => 9,
452            Value::Bag(_) => 10,
453            Value::Record(_) => 11,
454            Value::Dist(_) => 12,
455            Value::Closure(_) => 13,
456            Value::Builtin(_) => 19,
457            Value::Continuous(_) => 14,
458            Value::Delayed(_) => 15,
459            Value::Complex(_) => 16,
460            Value::Analytic(_) => 17,
461            Value::Event(_) => 18,
462            Value::Counts(_) => 20,
463            Value::Joint(_) => 21,
464        }
465    }
466
467    fn number_rank(&self) -> u8 {
468        match self {
469            Value::Int(_) => 0,
470            Value::Float(_) => 1,
471            _ => 2,
472        }
473    }
474}
475
476fn plural_kind(kind: &str) -> String {
477    match kind {
478        "str" => "strings".into(),
479        "prob" => "probabilities".into(),
480        "list" => "lists".into(),
481        k => format!("{k}s"),
482    }
483}
484
485/// What identifies a continuous distribution: its family and parameters.
486pub(crate) fn family_key(f: &Family) -> (&'static str, Vec<u64>) {
487    (f.name(), f.params().into_iter().map(float_key).collect())
488}
489
490/// Float bits with 0.0 and -0.0 merged and a single NaN.
491pub(crate) fn float_key(f: f64) -> u64 {
492    if f == 0.0 {
493        0
494    } else if f.is_nan() {
495        f64::NAN.to_bits()
496    } else {
497        f.to_bits()
498    }
499}
500
501impl PartialEq for Value {
502    /// Worlds are compared slot by slot, and most slots hold small values:
503    /// those are compared here, inlined, and the rest out of line.
504    #[inline]
505    fn eq(&self, other: &Value) -> bool {
506        match (self, other) {
507            (Value::Int(a), Value::Int(b)) => a == b,
508            (Value::Bool(a), Value::Bool(b)) => a == b,
509            (Value::Dead, Value::Dead) => true,
510            _ => eq_other(self, other),
511        }
512    }
513}
514
515#[inline(never)]
516fn eq_other(x: &Value, y: &Value) -> bool {
517    match (x, y) {
518        (Value::Unit, Value::Unit) => true,
519        (Value::Float(a), Value::Float(b)) | (Value::Prob(a), Value::Prob(b)) => float_key(*a) == float_key(*b),
520        (Value::Complex(a), Value::Complex(b)) => a == b,
521        (Value::Str(a), Value::Str(b)) => a == b,
522        (Value::List(a), Value::List(b)) => Arc::ptr_eq(a, b) || a == b,
523        (Value::Map(a), Value::Map(b)) => Arc::ptr_eq(a, b) || a == b,
524        (Value::Bag(a), Value::Bag(b)) => Arc::ptr_eq(a, b) || a == b,
525        (Value::Range(a, b), Value::Range(c, d)) => a == c && b == d,
526        (Value::Record(a), Value::Record(b)) => Arc::ptr_eq(a, b) || a == b,
527        (Value::Enum(a), Value::Enum(b)) => a.ty == b.ty && a.variant == b.variant,
528        (Value::Dist(a), Value::Dist(b)) => Arc::ptr_eq(a, b) || a == b,
529        (Value::Closure(a), Value::Closure(b)) => Arc::ptr_eq(a, b) || a == b,
530        (Value::Builtin(a), Value::Builtin(b)) => a == b,
531        (Value::Date(a), Value::Date(b)) => a == b,
532        (Value::Continuous(a), Value::Continuous(b)) => family_key(a) == family_key(b),
533        (Value::Analytic(a), Value::Analytic(b)) => a.key() == b.key(),
534        (Value::Event(a), Value::Event(b)) => a.key() == b.key(),
535        (Value::Counts(a), Value::Counts(b)) => a.key() == b.key(),
536        (Value::Joint(a), Value::Joint(b)) => Arc::ptr_eq(a, b) || a == b,
537        (Value::Delayed(a), Value::Delayed(b)) => {
538            a.variable == b.variable && family_key(&a.family) == family_key(&b.family)
539        }
540        _ => false,
541    }
542}
543
544impl Eq for Value {}
545
546impl Hash for Value {
547    /// Like equality: small values inlined, the rest out of line.
548    #[inline]
549    fn hash<H: Hasher>(&self, state: &mut H) {
550        std::mem::discriminant(self).hash(state);
551        match self {
552            Value::Dead | Value::Unit => {}
553            Value::Bool(b) => b.hash(state),
554            Value::Int(i) => i.hash(state),
555            other => hash_other(other, state),
556        }
557    }
558}
559
560#[inline(never)]
561fn hash_other<H: Hasher>(value: &Value, state: &mut H) {
562    match value {
563        Value::Dead | Value::Unit | Value::Bool(_) | Value::Int(_) => {}
564        Value::Float(f) | Value::Prob(f) => float_key(*f).hash(state),
565        Value::Complex(z) => (float_key(z.re()), float_key(z.im())).hash(state),
566        Value::Str(s) => s.hash(state),
567        Value::List(items) => items.hash(state),
568        Value::Map(m) => m.hash(state),
569        Value::Bag(b) => b.hash(state),
570        Value::Range(a, b) => (a, b).hash(state),
571        Value::Record(r) => r.hash(state),
572        Value::Enum(e) => (e.ty, e.variant).hash(state),
573        Value::Dist(d) => d.hash(state),
574        Value::Closure(c) => c.hash(state),
575        Value::Builtin(b) => b.hash(state),
576        Value::Date(d) => d.hash(state),
577        Value::Continuous(f) => family_key(f).hash(state),
578        Value::Analytic(a) => a.key().hash(state),
579        Value::Event(a) => a.key().hash(state),
580        Value::Delayed(d) => (d.variable, family_key(&d.family)).hash(state),
581        Value::Counts(c) => c.key().hash(state),
582        Value::Joint(d) => d.hash(state),
583    }
584}
585
586impl Ord for Value {
587    fn cmp(&self, other: &Value) -> Ordering {
588        let (ra, rb) = (self.rank(), other.rank());
589        if ra != rb {
590            return ra.cmp(&rb);
591        }
592        match (self, other) {
593            (Value::Int(a), Value::Int(b)) => a.cmp(b),
594            (Value::Int(a), Value::Float(b) | Value::Prob(b)) => a
595                .cmp_f64(*b)
596                .unwrap_or(Ordering::Less)
597                .then_with(|| self.number_rank().cmp(&other.number_rank())),
598            (Value::Float(a) | Value::Prob(a), Value::Int(b)) => b
599                .cmp_f64(*a)
600                .unwrap_or(Ordering::Less)
601                .reverse()
602                .then_with(|| self.number_rank().cmp(&other.number_rank())),
603            (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
604            (a, b) if ra == 3 => {
605                // Numbers compare by value, then by kind so the order stays total.
606                let (x, y) = (a.as_f64().unwrap(), b.as_f64().unwrap());
607                f64::from_bits(float_key(x))
608                    .total_cmp(&f64::from_bits(float_key(y)))
609                    .then_with(|| a.number_rank().cmp(&b.number_rank()))
610                    .then_with(|| float_key(x).cmp(&float_key(y)))
611            }
612            (Value::Str(a), Value::Str(b)) => a.cmp(b),
613            (Value::Date(a), Value::Date(b)) => a.cmp(b),
614            // Storage order only; the language's comparison operators reject
615            // complex values. Components are finite with canonical zeros.
616            (Value::Complex(a), Value::Complex(b)) => a.re().total_cmp(&b.re()).then(a.im().total_cmp(&b.im())),
617            (Value::Enum(a), Value::Enum(b)) => (a.ty, a.variant).cmp(&(b.ty, b.variant)),
618            (Value::List(a), Value::List(b)) => a.cmp(b),
619            (Value::Range(a, b), Value::Range(c, d)) => (a, b).cmp(&(c, d)),
620            (Value::Map(a), Value::Map(b)) => a.cmp(b),
621            (Value::Bag(a), Value::Bag(b)) => a.cmp(b),
622            (Value::Record(a), Value::Record(b)) => a.cmp(b),
623            (Value::Dist(a), Value::Dist(b)) => a.cmp(b),
624            (Value::Closure(a), Value::Closure(b)) => a.cmp(b),
625            (Value::Builtin(a), Value::Builtin(b)) => a.cmp(b),
626            (Value::Continuous(a), Value::Continuous(b)) => family_key(a).cmp(&family_key(b)),
627            (Value::Analytic(a), Value::Analytic(b)) => a.key().cmp(&b.key()),
628            (Value::Event(a), Value::Event(b)) => a.key().cmp(&b.key()),
629            (Value::Counts(a), Value::Counts(b)) => a.key().cmp(&b.key()),
630            (Value::Joint(a), Value::Joint(b)) => a.cmp(b),
631            (Value::Delayed(a), Value::Delayed(b)) => {
632                (a.variable, family_key(&a.family)).cmp(&(b.variable, family_key(&b.family)))
633            }
634            _ => Ordering::Equal,
635        }
636    }
637}
638
639impl PartialOrd for Value {
640    fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
641        Some(self.cmp(other))
642    }
643}
644
645// ── Display ──────────────────────────────────────────────────────────────
646
647/// A float the way Probl prints it: `3.0`, `0.25`, `1.5e-12`.
648pub fn fmt_float(f: f64) -> String {
649    if f.is_nan() {
650        return "nan".into();
651    }
652    if f.is_infinite() {
653        return if f > 0.0 { "inf".into() } else { "-inf".into() };
654    }
655    let a = f.abs();
656    if a != 0.0 && (a >= 1e15 || a < 1e-4) {
657        return format!("{f:e}");
658    }
659    if f == f.trunc() {
660        format!("{f:.1}")
661    } else {
662        format!("{f}")
663    }
664}
665
666/// A probability as a percentage with up to two decimals: `30%`, `16.67%`.
667pub fn fmt_prob(p: f64) -> String {
668    let pct = p * 100.0;
669    if pct != 0.0 && pct.abs() < 0.01 {
670        return format!("{pct:.1e}%");
671    }
672    let text = format!("{pct:.2}");
673    let text = text.trim_end_matches('0').trim_end_matches('.');
674    format!("{text}%")
675}
676
677impl fmt::Display for Value {
678    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679        write_value(self, f, false)
680    }
681}
682
683impl fmt::Debug for Value {
684    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685        write_value(self, f, true)
686    }
687}
688
689fn write_value(v: &Value, f: &mut fmt::Formatter<'_>, nested: bool) -> fmt::Result {
690    match v {
691        Value::Dead => write!(f, "<no value>"),
692        Value::Unit => write!(f, "()"),
693        Value::Int(i) => write!(f, "{i}"),
694        Value::Float(x) => write!(f, "{}", fmt_float(*x)),
695        Value::Complex(z) => write!(f, "complex({}, {})", fmt_float(z.re()), fmt_float(z.im())),
696        Value::Bool(b) => write!(f, "{b}"),
697        Value::Prob(p) => write!(f, "{}", fmt_prob(*p)),
698        Value::Str(s) if nested => write!(f, "{s:?}"),
699        Value::Str(s) => write!(f, "{s}"),
700        Value::List(items) => {
701            write!(f, "[")?;
702            for (i, item) in items.iter().enumerate() {
703                if i > 0 {
704                    write!(f, ", ")?;
705                }
706                write_value(item, f, true)?;
707            }
708            write!(f, "]")
709        }
710        Value::Map(m) => {
711            if m.is_empty() {
712                return write!(f, "[:]");
713            }
714            write!(f, "[")?;
715            for (i, (k, v)) in m.iter().enumerate() {
716                if i > 0 {
717                    write!(f, ", ")?;
718                }
719                write_value(k, f, true)?;
720                write!(f, ": ")?;
721                write_value(v, f, true)?;
722            }
723            write!(f, "]")
724        }
725        Value::Bag(b) => {
726            write!(f, "bag([")?;
727            for (i, (k, n)) in b.iter().enumerate() {
728                if i > 0 {
729                    write!(f, ", ")?;
730                }
731                write_value(k, f, true)?;
732                write!(f, ": {n}")?;
733            }
734            write!(f, "])")
735        }
736        Value::Range(a, b) => write!(f, "{a}..{b}"),
737        Value::Record(r) => {
738            if let Some(t) = &r.ty {
739                write!(f, "{t} ")?;
740            }
741            write!(f, "{{ ")?;
742            for (i, (name, v)) in r.fields.iter().enumerate() {
743                if i > 0 {
744                    write!(f, ", ")?;
745                }
746                write!(f, "{name}: ")?;
747                write_value(v, f, true)?;
748            }
749            write!(f, " }}")
750        }
751        Value::Enum(e) => write!(f, "{}", e.name),
752        Value::Dist(d) | Value::Joint(d) => {
753            write!(f, "dist(")?;
754            for (i, (v, p)) in d.outcomes.iter().enumerate() {
755                if i == 8 {
756                    write!(f, ", … {} more", d.outcomes.len() - 8)?;
757                    break;
758                }
759                if i > 0 {
760                    write!(f, ", ")?;
761                }
762                write_value(v, f, true)?;
763                write!(f, ": {}", fmt_prob(*p))?;
764            }
765            write!(f, ")")
766        }
767        Value::Closure(_) | Value::Builtin(_) => write!(f, "<function>"),
768        Value::Date(d) => write!(f, "{}", crate::dates::format(*d)),
769        Value::Continuous(family) => write!(f, "{family}"),
770        Value::Counts(c) => write!(f, "{c}"),
771        Value::Analytic(a) => match a.affine() {
772            Some(g) => write!(f, "<analytic float: {} * {} + {}>", g.scale, a.family, g.offset),
773            None => write!(f, "<analytic float: piecewise in {}>", a.family),
774        },
775        Value::Event(_) => write!(f, "<analytic bool>"),
776        Value::Delayed(d) => write!(f, "<not drawn yet: {}>", d.family),
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    fn bag(counts: &[(i64, u64)]) -> Multiset {
785        Multiset::new(counts.iter().map(|&(v, n)| (Value::Int(v.into()), n)).collect())
786    }
787
788    #[test]
789    fn a_bag_keeps_its_hash_up_to_date() {
790        let full = bag(&[(1, 2), (2, 1), (3, 4)]);
791        // Taking values out in any order gives the bag built directly.
792        let taken = full
793            .without(&Value::Int(3.into()))
794            .unwrap()
795            .without(&Value::Int(2.into()))
796            .unwrap();
797        let direct = bag(&[(1, 2), (3, 3)]);
798        assert_eq!(taken, direct);
799        assert_eq!(taken.sum, direct.sum);
800        assert!(taken.without(&Value::Int(2.into())).is_none());
801        // Moving a count from one value to another changes the hash.
802        assert_ne!(bag(&[(1, 2), (2, 3)]).sum, bag(&[(1, 3), (2, 2)]).sum);
803        assert_ne!(bag(&[(1, 1)]).sum, bag(&[(2, 1)]).sum);
804    }
805
806    #[test]
807    fn an_unshared_copy_is_equal_and_separate() {
808        let row = Value::record(Record {
809            ty: Some(Arc::from("Day")),
810            fields: vec![
811                (Arc::from("n"), Value::Int(1.into())),
812                (Arc::from("s"), Value::str("x")),
813            ],
814        });
815        let original = Value::list(vec![row.clone(), row]);
816        let copy = original.unshared();
817        assert_eq!(copy, original);
818        let (Value::List(a), Value::List(b)) = (&original, &copy) else {
819            panic!()
820        };
821        assert!(!Arc::ptr_eq(a, b));
822        let (Value::Record(x), Value::Record(y)) = (&a[0], &b[0]) else {
823            panic!()
824        };
825        assert!(!Arc::ptr_eq(x, y));
826        // Names are shared within the copy, not with the original.
827        let Value::Record(y2) = &b[1] else { panic!() };
828        assert!(Arc::ptr_eq(&y.fields[0].0, &y2.fields[0].0));
829        assert!(!Arc::ptr_eq(&x.fields[0].0, &y.fields[0].0));
830    }
831
832    #[test]
833    fn a_changed_collection_forgets_its_hash() {
834        let mut list = Hashed::new(vec![Value::Int(1.into())]);
835        let before = list.hash_code();
836        list.push(Value::Int(2.into()));
837        assert_ne!(list.hash_code(), before);
838        assert_eq!(
839            list.hash_code(),
840            Hashed::new(vec![Value::Int(1.into()), Value::Int(2.into())]).hash_code()
841        );
842        assert_ne!(list, Hashed::new(vec![Value::Int(1.into())]));
843    }
844}