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