Skip to main content

probl_engine/
ops.rs

1//! Operators, and applying operations to every outcome of a distribution.
2
3use crate::dist::{Budget, Dist};
4use crate::error::{Fault, OpError, OpResult};
5use crate::value::{EnumValue, Record, Value};
6use probl_number::Integer;
7use probl_syntax::ast::{BinOp, UnOp};
8use std::borrow::Cow;
9use std::sync::Arc;
10
11/// Checked integer context: preserve ints, convert exactly integral finite
12/// floats, and never round or admit other numeric types implicitly.
13pub fn integer<'a>(v: &'a Value, context: &str, budget: &mut Budget) -> OpResult<Cow<'a, Integer>> {
14    match v {
15        Value::Int(n) => Ok(Cow::Borrowed(n)),
16        Value::Float(f) if f.is_finite() && f.fract() == 0.0 => {
17            let n = Integer::from_f64(*f).expect("finite integral float fits the integer hard limit");
18            budget.integer_allocation(n.bits(), 1)?;
19            budget.work(n.bits().div_ceil(64).max(1))?;
20            Ok(Cow::Owned(n))
21        }
22        _ => {
23            let found = match v {
24                Value::Float(f) => format!("the float {f:?}"),
25                _ => article(&v.kind()),
26            };
27            Err(OpError::new(format!(
28                "`{context}` needs an int (or an exactly integral finite float), found {found}"
29            )))
30        }
31    }
32}
33
34/// The outcomes of a value: those of a distribution, or the value itself.
35pub fn outcomes(v: &Value) -> std::borrow::Cow<'_, [(Value, f64)]> {
36    match v {
37        Value::Dist(d) => std::borrow::Cow::Borrowed(&d.outcomes),
38        other => std::borrow::Cow::Owned(vec![(other.clone(), 1.0)]),
39    }
40}
41
42fn missing(v: &Value) -> f64 {
43    match v {
44        Value::Dist(d) => d.missing,
45        _ => 0.0,
46    }
47}
48
49/// Gather the results of applying an operation to each outcome of some
50/// distributions into a distribution. Results that are themselves
51/// distributions are mixed in. The result is always a distribution, even when
52/// only one outcome is possible.
53pub fn combine(results: Vec<(Value, f64)>, missing: f64, budget: &mut Budget) -> OpResult<Value> {
54    if results.is_empty() {
55        return Err(OpError::new("the result would be an empty distribution"));
56    }
57    let mut flat = Vec::with_capacity(results.len());
58    let mut missing = missing;
59    for (v, w) in results {
60        match v {
61            Value::Dist(d) => {
62                missing += w * d.missing;
63                flat.extend(d.outcomes.iter().map(|(x, p)| (x.clone(), w * p)));
64            }
65            other => flat.push((other, w)),
66        }
67    }
68    budget.outcomes(flat.len() as u128)?;
69    budget.work(flat.len() as u64)?;
70    Ok(Dist::from_pairs(flat, missing.min(1.0)).into_value())
71}
72
73/// Apply `f` to a value, or to every outcome of a distribution.
74pub fn lift1(a: &Value, budget: &mut Budget, f: impl Fn(&Value, &mut Budget) -> OpResult<Value>) -> OpResult<Value> {
75    if !a.is_dist() {
76        return f(a, budget);
77    }
78    let mut results = Vec::new();
79    for (v, w) in outcomes(a).iter() {
80        results.push((f(v, budget)?, *w));
81    }
82    combine(results, missing(a), budget)
83}
84
85/// Apply `f` to two values; distributions are independent draws.
86pub fn lift2(
87    a: &Value,
88    b: &Value,
89    budget: &mut Budget,
90    f: impl Fn(&Value, &Value, &mut Budget) -> OpResult<Value>,
91) -> OpResult<Value> {
92    if !a.is_dist() && !b.is_dist() {
93        return f(a, b, budget);
94    }
95    let (oa, ob) = (outcomes(a), outcomes(b));
96    budget.outcomes(oa.len() as u128 * ob.len() as u128)?;
97    budget.work((oa.len() * ob.len()) as u64)?;
98    let mut results = Vec::with_capacity(oa.len() * ob.len());
99    for (x, p) in oa.iter() {
100        for (y, q) in ob.iter() {
101            results.push((f(x, y, budget)?, p * q));
102        }
103    }
104    let m = missing(a) + (1.0 - missing(a)) * missing(b);
105    combine(results, m, budget)
106}
107
108/// A function lifted by [`lift_n`]; it may need the budget itself (to build
109/// distributions).
110pub type NaryFn<'a> = &'a dyn Fn(&[Value], &mut Budget) -> OpResult<Value>;
111
112/// Apply `f` to a list of values, lifting over any that are distributions.
113pub fn lift_n(args: &[Value], budget: &mut Budget, f: NaryFn) -> OpResult<Value> {
114    if !args.iter().any(Value::is_dist) {
115        return f(args, budget);
116    }
117    let size = args
118        .iter()
119        .fold(1u128, |acc, a| acc.saturating_mul(outcomes(a).len() as u128));
120    budget.outcomes(size)?;
121    budget.work(size as u64)?;
122    let missing = args.iter().fold(0.0, |m, a| m + (1.0 - m) * missing(a));
123    let mut results = Vec::new();
124    let mut current = Vec::with_capacity(args.len());
125    product(args, 0, &mut current, 1.0, f, budget, &mut results)?;
126    combine(results, missing, budget)
127}
128
129fn product(
130    args: &[Value],
131    i: usize,
132    current: &mut Vec<Value>,
133    weight: f64,
134    f: NaryFn,
135    budget: &mut Budget,
136    results: &mut Vec<(Value, f64)>,
137) -> OpResult<()> {
138    if i == args.len() {
139        results.push((f(current, budget)?, weight));
140        return Ok(());
141    }
142    for (v, w) in outcomes(&args[i]).iter() {
143        current.push(v.clone());
144        product(args, i + 1, current, weight * w, f, budget, results)?;
145        current.pop();
146    }
147    Ok(())
148}
149
150// ── Probabilities, conditions and facts ──────────────────────────────────
151
152pub fn article(kind: &str) -> String {
153    let vowel = kind.starts_with(['a', 'e', 'i', 'o', 'u']);
154    format!("{} {kind}", if vowel { "an" } else { "a" })
155}
156
157/// A value used as a probability (chance weights, `bernoulli`, `binomial`…):
158/// a `prob` or a number checked at this boundary. Booleans need `prob(b)`.
159pub fn to_prob(v: &Value) -> OpResult<f64> {
160    if crate::analytic::contains(v) {
161        return Err(crate::analytic::unsupported(
162            "using an analytic outcome as a probability parameter",
163        ));
164    }
165    match v {
166        // A parameter out of range is outside what its function is defined for.
167        Value::Prob(_) | Value::Float(_) | Value::Int(_) => {
168            match make_prob(v).map_err(|e| e.as_fault(Fault::DomainError))? {
169                Value::Prob(p) => Ok(p),
170                _ => unreachable!("make_prob returns a probability"),
171            }
172        }
173        Value::Bool(_) => Err(OpError::new("expected a probability, found a fact (true or false)")
174            .help("convert a boolean explicitly with `prob(fact)`")),
175        Value::Dist(_) => Err(OpError::new(format!("expected a probability, found a {}", v.kind()))
176            .help("`P(…)` gives the probability that a distribution of facts is true")),
177        other => Err(OpError::new(format!(
178            "expected a probability, found {}",
179            article(&other.kind())
180        ))),
181    }
182}
183
184/// Explicit, checked construction. This never draws, lifts or clamps.
185pub fn make_prob(v: &Value) -> OpResult<Value> {
186    let p = match v {
187        Value::Bool(b) => {
188            if *b {
189                1.0
190            } else {
191                0.0
192            }
193        }
194        Value::Prob(p) | Value::Float(p) => *p,
195        Value::Int(n) if *n == 0 => 0.0,
196        Value::Int(n) if *n == 1 => 1.0,
197        Value::Int(_) => {
198            return Err(OpError::fault(
199                Fault::ConversionError,
200                "prob needs a finite number between 0 and 1",
201            ));
202        }
203        _ => {
204            return Err(
205                OpError::new(format!("prob needs a number or bool, found {}", article(&v.kind())))
206                    .help("draw distribution outcomes explicitly; use `P(d)` to query a boolean distribution"),
207            );
208        }
209    };
210    if !p.is_finite() || !(0.0..=1.0).contains(&p) {
211        return Err(OpError::fault(
212            Fault::ConversionError,
213            "prob needs a finite number between 0 and 1",
214        ));
215    }
216    Ok(Value::Prob(p))
217}
218
219/// Computed chances may stray a few ulps outside [0, 1] through rounding.
220/// This correction is only for algorithms known to produce probabilities;
221/// user inputs always go through the strict `make_prob` boundary.
222pub(crate) fn computed_prob(p: f64, what: &str) -> OpResult<Value> {
223    const ROUNDING: f64 = 8.0 * f64::EPSILON;
224    if !p.is_finite() || !(-ROUNDING..=1.0 + ROUNDING).contains(&p) {
225        return Err(OpError::new(format!(
226            "`{what}` could not compute a finite probability between 0 and 1"
227        )));
228    }
229    Ok(Value::Prob(if p <= 0.0 { 0.0 } else { p.min(1.0) }))
230}
231
232pub fn fact(v: &Value, context: &str) -> OpResult<bool> {
233    match v {
234        Value::Bool(b) => Ok(*b),
235        _ => Err(
236            OpError::new(format!("`{context}` needs a bool, found {}", article(&v.kind())))
237                .help("draw an outcome first with `~`; for a probability, draw from `bernoulli(p)`"),
238        ),
239    }
240}
241
242/// How likely a condition is to hold (docs/semantics.md, section 3).
243#[derive(Clone, Copy, Debug, PartialEq)]
244pub struct Condition {
245    /// The probability that it's true.
246    pub yes: f64,
247    /// The probability that it's false.
248    pub no: f64,
249    /// The probability missing from a distribution of facts: it could be
250    /// either. The three add up to one, up to rounding.
251    pub missing: f64,
252}
253
254/// A fact, or a distribution of facts.
255pub enum Truth {
256    Fact(bool),
257    Analytic(Arc<crate::analytic::Event>),
258    Probability(f64),
259    Uncertain(Arc<Dist>),
260}
261
262/// The operand of `and`, `or` or `not` (docs/semantics.md, section 2).
263pub fn truth(v: &Value, op: &str) -> OpResult<Truth> {
264    match v {
265        Value::Bool(b) => Ok(Truth::Fact(*b)),
266        Value::Event(e) => Ok(Truth::Analytic(e.clone())),
267        Value::Dist(d) if d.truth().is_some() => Ok(Truth::Uncertain(d.clone())),
268        Value::Prob(_) => Ok(Truth::Probability(to_prob(v)?)),
269        other => Err(OpError::new(format!(
270            "`{op}` needs bool, prob or dist[bool], found {}",
271            article(&other.kind())
272        ))),
273    }
274}
275
276pub fn not(v: &Value, budget: &mut Budget) -> OpResult<Value> {
277    match truth(v, "not")? {
278        Truth::Fact(b) => Ok(Value::Bool(!b)),
279        Truth::Analytic(e) => Ok(crate::analytic::Event {
280            draw: e.draw.clone(),
281            yes: e.yes.complement(),
282        }
283        .value()),
284        Truth::Probability(p) => computed_prob(1.0 - p, "not"),
285        Truth::Uncertain(d) => lift1(&Value::Dist(d), budget, |x, _| match x {
286            Value::Bool(b) => Ok(Value::Bool(!b)),
287            _ => unreachable!("checked by `truth`"),
288        }),
289    }
290}
291
292/// Compose independent boolean recipes. Bound facts retain their identity.
293pub fn logic(and: bool, a: Truth, b: Truth, budget: &mut Budget) -> OpResult<Value> {
294    if matches!(a, Truth::Analytic(_)) || matches!(b, Truth::Analytic(_)) {
295        let value = |t| match t {
296            Truth::Fact(b) => Value::Bool(b),
297            Truth::Probability(p) => Value::Prob(p),
298            Truth::Uncertain(d) => Value::Dist(d),
299            Truth::Analytic(e) => Value::Event(e),
300        };
301        let (a, b) = (value(a), value(b));
302        for v in [&a, &b] {
303            if let Value::Event(e) = v {
304                budget.collection(e.yes.0.len() as u128 + e.draw.domain.0.len() as u128)?;
305                budget.work((e.yes.0.len() + e.draw.domain.0.len()) as u64)?;
306            }
307        }
308        return crate::analytic::logic(and, &a, &b);
309    }
310    let op = |x: bool, y: bool| if and { x && y } else { x || y };
311    match (a, b) {
312        (Truth::Fact(x), Truth::Fact(y)) => Ok(Value::Bool(op(x, y))),
313        (Truth::Probability(p), Truth::Probability(q)) => computed_prob(
314            if and { p * q } else { p + (1.0 - p) * q },
315            if and { "and" } else { "or" },
316        ),
317        (Truth::Probability(p), Truth::Fact(b)) | (Truth::Fact(b), Truth::Probability(p)) => {
318            Ok(Value::Prob(if b == and {
319                p
320            } else if b {
321                1.0
322            } else {
323                0.0
324            }))
325        }
326        (Truth::Fact(x), Truth::Uncertain(d)) | (Truth::Uncertain(d), Truth::Fact(x)) => {
327            lift1(&Value::Dist(d), budget, |v, _| match v {
328                Value::Bool(y) => Ok(Value::Bool(op(x, *y))),
329                _ => unreachable!("checked by `truth`"),
330            })
331        }
332        (a, b) => {
333            let law = |v| match v {
334                Truth::Uncertain(d) => Value::Dist(d),
335                Truth::Probability(p) => Dist::bernoulli(p).into_value(),
336                Truth::Fact(b) => Value::Bool(b),
337                Truth::Analytic(_) => unreachable!("handled above"),
338            };
339            lift2(&law(a), &law(b), budget, |x, y, _| match (x, y) {
340                (Value::Bool(x), Value::Bool(y)) => Ok(Value::Bool(op(*x, *y))),
341                _ => unreachable!("checked by `truth`"),
342            })
343        }
344    }
345}
346
347/// The boolean law requested by an observed anonymous draw.
348pub fn boolean_law(v: &Value) -> OpResult<Value> {
349    match truth(v, "observe ~")? {
350        Truth::Analytic(e) => Ok(Value::Event(e)),
351        Truth::Fact(b) => Ok(Dist::bernoulli(if b { 1.0 } else { 0.0 }).into_value()),
352        Truth::Probability(p) => Ok(Dist::bernoulli(p).into_value()),
353        Truth::Uncertain(d) => Ok(Value::Dist(d)),
354    }
355}
356
357pub fn condition(v: &Value) -> OpResult<Condition> {
358    if matches!(v, Value::Analytic(_)) {
359        return Err(crate::analytic::unsupported(
360            "using a continuous outcome as a probability condition",
361        ));
362    }
363    if matches!(v, Value::Float(_) | Value::Int(_)) {
364        let p = to_prob(v)?;
365        return Ok(Condition {
366            yes: p,
367            no: 1.0 - p,
368            missing: 0.0,
369        });
370    }
371    match truth(v, "condition")? {
372        Truth::Analytic(e) => Ok(Condition {
373            yes: e.probability(),
374            no: 1.0 - e.probability(),
375            missing: 0.0,
376        }),
377        Truth::Fact(b) => Ok(Condition {
378            yes: if b { 1.0 } else { 0.0 },
379            no: if b { 0.0 } else { 1.0 },
380            missing: 0.0,
381        }),
382        Truth::Probability(p) => Ok(Condition {
383            yes: p,
384            no: 1.0 - p,
385            missing: 0.0,
386        }),
387        Truth::Uncertain(d) => {
388            let (yes, no) = d.truth().expect("checked by truth");
389            Ok(Condition {
390                yes,
391                no,
392                missing: d.missing,
393            })
394        }
395    }
396}
397
398// ── Operators ────────────────────────────────────────────────────────────
399
400pub fn unary(op: UnOp, v: &Value, budget: &mut Budget) -> OpResult<Value> {
401    match op {
402        UnOp::Neg => lift1(v, budget, |x, budget| match x {
403            Value::Int(i) => {
404                budget.integer_work(i, &Integer::ZERO, false)?;
405                {
406                    let n = i.negated();
407                    budget.integer_allocation(n.bits(), 1)?;
408                    Ok(Value::Int(n))
409                }
410            }
411            Value::Float(f) | Value::Prob(f) => Ok(Value::Float(-f)),
412            Value::Complex(z) => Ok(Value::Complex(z.negated())),
413            Value::Analytic(a) => {
414                let mut a = (**a).clone();
415                a.scale = -a.scale;
416                a.offset = -a.offset;
417                a.value()
418            }
419            other => Err(OpError::new(format!("can't negate {}", article(&other.kind())))),
420        }),
421        UnOp::Not => not(v, budget),
422        UnOp::Typeof => unreachable!("typeof is lowered to an interpreter intrinsic"),
423    }
424}
425
426pub fn binary(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
427    match op {
428        BinOp::And | BinOp::Or => unreachable!("`and` and `or` are evaluated lazily by the interpreter"),
429        BinOp::To => unreachable!("`to` is lowered to a built-in"),
430        BinOp::Range | BinOp::RangeExcl => range(op, a, b, budget),
431        BinOp::In => lift2(a, b, budget, |x, coll, budget| {
432            contains(coll, x, budget).map(Value::Bool)
433        }),
434        BinOp::NotIn => lift2(a, b, budget, |x, coll, budget| {
435            contains(coll, x, budget).map(|c| Value::Bool(!c))
436        }),
437        _ => lift2(a, b, budget, |x, y, budget| binary_plain(op, x, y, budget)),
438    }
439}
440
441fn binary_plain(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
442    if matches!(a, Value::Analytic(_)) || matches!(b, Value::Analytic(_)) {
443        return crate::analytic::binary(op, a, b);
444    }
445    if matches!(
446        op,
447        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
448    ) && (crate::analytic::contains(a) || crate::analytic::contains(b))
449    {
450        if matches!(op, BinOp::Eq | BinOp::Ne) {
451            if let (Value::Event(x), Value::Bool(b)) | (Value::Bool(b), Value::Event(x)) = (a, b) {
452                return if *b == (op == BinOp::Eq) {
453                    Ok(Value::Event(x.clone()))
454                } else {
455                    not(&Value::Event(x.clone()), budget)
456                };
457            }
458        }
459        if let (Value::Event(x), Value::Event(y)) = (a, b) {
460            if x.draw.id == y.draw.id && matches!(op, BinOp::Eq | BinOp::Ne) {
461                let both = x.yes.intersect(&y.yes);
462                let neither = x.yes.complement().intersect(&y.yes.complement());
463                let equal = both.complement().intersect(&neither.complement()).complement();
464                return Ok(crate::analytic::Event {
465                    draw: x.draw.clone(),
466                    yes: if op == BinOp::Eq { equal } else { equal.complement() },
467                }
468                .value());
469            }
470        }
471        return Err(crate::analytic::unsupported(
472            "comparing aggregate or boolean analytic outcomes",
473        ));
474    }
475    if let Some(v) = continuous_binary(op, a, b)? {
476        return Ok(v);
477    }
478    if matches!(
479        op,
480        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
481    ) {
482        for v in [a, b] {
483            if let Value::Int(n) = v {
484                budget.integer_work(n, &Integer::ONE, false)?;
485            }
486            if let Value::Str(s) = v {
487                budget.string_work(s)?;
488            }
489        }
490    }
491    match op {
492        BinOp::Eq => Ok(Value::Bool(equals(a, b))),
493        BinOp::Ne => Ok(Value::Bool(!equals(a, b))),
494        BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
495            let ord = compare(a, b)?;
496            Ok(Value::Bool(match op {
497                BinOp::Lt => ord.is_lt(),
498                BinOp::Le => ord.is_le(),
499                BinOp::Gt => ord.is_gt(),
500                _ => ord.is_ge(),
501            }))
502        }
503        BinOp::Add => add(a, b, budget),
504        BinOp::Sub => sub(a, b, budget),
505        _ => arith(op, a, b, budget),
506    }
507}
508
509fn add(a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
510    match (a, b) {
511        (Value::Str(x), Value::Str(y)) => {
512            let size = x
513                .len()
514                .checked_add(y.len())
515                .ok_or_else(|| OpError::limit("string size overflow"))?;
516            budget.string_size(size)?;
517            let mut out = String::new();
518            crate::text::push(&mut out, x, budget)?;
519            crate::text::push(&mut out, y, budget)?;
520            Ok(Value::str(&out))
521        }
522        (Value::List(x), Value::List(y)) => {
523            budget.collection(x.len() as u128 + y.len() as u128)?;
524            budget.work(x.len() as u64 + y.len() as u64)?;
525            let mut items = x.to_vec();
526            items.extend(y.iter().cloned());
527            Ok(Value::list(items))
528        }
529        (Value::Date(d), n @ (Value::Int(_) | Value::Float(_)))
530        | (n @ (Value::Int(_) | Value::Float(_)), Value::Date(d)) => {
531            let n = integer(n, "date offset", budget)?;
532            date_plus(
533                *d,
534                n.to_i64()
535                    .ok_or_else(|| OpError::fault(Fault::NumericOverflow, "date out of range"))?,
536            )
537        }
538        (Value::Str(_), _) | (_, Value::Str(_)) => {
539            Err(
540                OpError::new(format!("can't add {} and {}", article(&a.kind()), article(&b.kind())))
541                    .help("to build text, use interpolation: \"total: {x}\""),
542            )
543        }
544        _ => arith(BinOp::Add, a, b, budget),
545    }
546}
547
548fn sub(a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
549    match (a, b) {
550        (Value::Date(x), Value::Date(y)) => Ok(Value::Int((*x as i64 - *y as i64).into())),
551        (Value::Date(d), n @ (Value::Int(_) | Value::Float(_))) => {
552            match integer(n, "date offset", budget)?.negated().to_i64() {
553                Some(m) => date_plus(*d, m),
554                None => Err(OpError::fault(Fault::NumericOverflow, "date out of range")),
555            }
556        }
557        _ => arith(BinOp::Sub, a, b, budget),
558    }
559}
560
561fn date_plus(d: i32, n: i64) -> OpResult<Value> {
562    crate::dates::add_days(d, n)
563        .map(Value::Date)
564        .ok_or_else(|| OpError::fault(Fault::NumericOverflow, "date out of range"))
565}
566
567/// Numbers for arithmetic: ints, floats and probabilities (not facts).
568fn number(v: &Value) -> Option<f64> {
569    match v {
570        Value::Bool(_) => None,
571        _ => v.as_f64(),
572    }
573}
574
575fn arith(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
576    let bad = || {
577        OpError::new(format!(
578            "can't use `{}` with {} and {}",
579            op.symbol(),
580            article(&a.kind()),
581            article(&b.kind())
582        ))
583    };
584    if let (BinOp::Pow, Value::Complex(z), Value::Int(n)) = (op, a, b) {
585        budget.integer_work(n, &Integer::ONE, false)?;
586        budget.work(n.bits())?;
587        return Ok(Value::Complex(z.pow_integer(n)?));
588    }
589    if matches!(a, Value::Complex(_)) || matches!(b, Value::Complex(_)) {
590        let (Some(x), Some(y)) = (a.as_complex(), b.as_complex()) else {
591            return Err(bad());
592        };
593        let z = match op {
594            BinOp::Add => x.plus(y),
595            BinOp::Sub => x.minus(y),
596            BinOp::Mul => x.times(y),
597            BinOp::Div => x.divided_by(y),
598            BinOp::Pow => match b {
599                Value::Int(n) => {
600                    budget.integer_work(n, &Integer::ONE, false)?;
601                    budget.work(n.bits())?;
602                    x.pow_integer(n)
603                }
604                _ => Err(OpError::new("a complex power needs an int exponent")),
605            },
606            _ => Err(bad()),
607        }?;
608        return Ok(Value::Complex(z));
609    }
610    if let (Value::Int(x), Value::Int(y)) = (a, b) {
611        budget.integer_work(x, y, matches!(op, BinOp::Mul | BinOp::Div | BinOp::IntDiv | BinOp::Mod))?;
612        let n = match op {
613            BinOp::Add => x.add(y)?,
614            BinOp::Sub => x.sub(y)?,
615            BinOp::Mul => {
616                if !x.is_zero() && !y.is_zero() {
617                    budget.integer_bits((x.bits() + y.bits()).saturating_sub(1))?;
618                }
619                x.mul(y)?
620            }
621            BinOp::Div => {
622                if y.is_zero() {
623                    return Err(division_by_zero());
624                }
625                return x.ratio(y).map(Value::Float).ok_or_else(|| {
626                    OpError::fault(
627                        Fault::NumericOverflow,
628                        "division result is too large for a finite float",
629                    )
630                });
631            }
632            BinOp::IntDiv => x.div_mod(y)?.0,
633            BinOp::Mod => x.div_mod(y)?.1,
634            BinOp::Pow => return int_power(x, y, budget),
635            _ => return Err(bad()),
636        };
637        budget.integer_allocation(n.bits(), 1)?;
638        return Ok(Value::Int(n));
639    }
640    let (Some(x), Some(y)) = (number(a), number(b)) else {
641        return Err(bad());
642    };
643    let v = match op {
644        BinOp::Add => x + y,
645        BinOp::Sub => x - y,
646        BinOp::Mul => x * y,
647        BinOp::Div => {
648            if y == 0.0 {
649                return Err(division_by_zero());
650            }
651            x / y
652        }
653        BinOp::IntDiv => {
654            if y == 0.0 {
655                return Err(division_by_zero());
656            }
657            let q = (x / y).floor();
658            let n = Integer::from_f64(q)
659                .ok_or_else(|| OpError::fault(Fault::NumericOverflow, "integer division result is not finite"))?;
660            budget.integer_allocation(n.bits(), 1)?;
661            return Ok(Value::Int(n));
662        }
663        BinOp::Mod => {
664            if y == 0.0 {
665                return Err(division_by_zero());
666            }
667            x - (x / y).floor() * y
668        }
669        BinOp::Pow => libm::pow(x, y),
670        _ => return Err(bad()),
671    };
672    if !v.is_finite() {
673        return Err(OpError::fault(
674            Fault::NumericOverflow,
675            format!("`{}` gave a result that isn't a finite number", op.symbol()),
676        ));
677    }
678    Ok(Value::Float(v))
679}
680
681/// A continuous distribution compared with a number: a distribution of
682/// facts, from its CDF. Nothing else is defined on one yet
683/// (docs/semantics.md, section 13).
684fn continuous_binary(op: BinOp, a: &Value, b: &Value) -> OpResult<Option<Value>> {
685    let (family, other, flipped) = match (a, b) {
686        (Value::Continuous(f), other) => (f, other, false),
687        (other, Value::Continuous(f)) => (f, other, true),
688        _ => return Ok(None),
689    };
690    let needs_value = || {
691        OpError::new(format!(
692            "`{}` needs a value, not a {} distribution",
693            op.symbol(),
694            family.name()
695        ))
696        .help("draw a value first, like `let x ~ normal(0, 1)`; comparing a distribution with a number works too")
697    };
698    let x = match number(other) {
699        Some(x) if !x.is_nan() => x,
700        _ => return Err(needs_value()),
701    };
702    // For a continuous X, P(X < x) = P(X ≤ x) = F(x).
703    let below = family.cdf(x);
704    let yes = match (op, flipped) {
705        (BinOp::Lt | BinOp::Le, false) | (BinOp::Gt | BinOp::Ge, true) => below,
706        (BinOp::Gt | BinOp::Ge, false) | (BinOp::Lt | BinOp::Le, true) => 1.0 - below,
707        (BinOp::Eq, _) => 0.0,
708        (BinOp::Ne, _) => 1.0,
709        _ => return Err(needs_value()),
710    };
711    Ok(Some(Dist::bernoulli(yes).into_value()))
712}
713
714fn division_by_zero() -> OpError {
715    OpError::fault(Fault::DivisionByZero, "division by zero")
716}
717
718fn int_power(base: &Integer, exponent: &Integer, budget: &mut Budget) -> OpResult<Value> {
719    if exponent.is_zero() {
720        return Ok(Value::Int(Integer::ONE));
721    }
722    if base.is_zero() {
723        return if exponent.is_negative() {
724            Err(division_by_zero())
725        } else {
726            Ok(Value::Int(Integer::ZERO))
727        };
728    }
729    if exponent.is_negative() {
730        let reciprocal = Integer::ONE.ratio(&base.abs()).unwrap_or(0.0);
731        let power = exponent.abs().to_f64().unwrap_or(f64::INFINITY);
732        let result = libm::pow(reciprocal, power);
733        return Ok(Value::Float(if base.is_negative() && exponent.is_odd() {
734            -result
735        } else {
736            result
737        }));
738    }
739    if *base == 1 || *base == -1 {
740        return Ok(Value::Int(if *base == -1 && exponent.is_odd() {
741            (-1).into()
742        } else {
743            Integer::ONE
744        }));
745    }
746    let mut n = exponent
747        .to_u64()
748        .ok_or_else(|| OpError::limit("integer power exceeds the integer size limit"))?;
749    budget.integer_bits(base.bits().saturating_sub(1).saturating_mul(n).saturating_add(1))?;
750    let mut result = Integer::ONE;
751    let mut b = base.clone();
752    while n != 0 {
753        if n & 1 != 0 {
754            budget.integer_work(&result, &b, true)?;
755            result = result.mul(&b)?;
756            budget.integer_bits(result.bits())?;
757        }
758        n >>= 1;
759        if n != 0 {
760            budget.integer_work(&b, &b, true)?;
761            b = b.mul(&b)?;
762            budget.integer_bits(b.bits())?;
763        }
764    }
765    budget.integer_allocation(result.bits(), 1)?;
766    Ok(Value::Int(result))
767}
768
769fn numeric_compare(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
770    match (a, b) {
771        (Value::Int(a), Value::Int(b)) => Some(a.cmp(b)),
772        (Value::Int(a), Value::Float(b) | Value::Prob(b)) => a.cmp_f64(*b),
773        (Value::Float(a) | Value::Prob(a), Value::Int(b)) => b.cmp_f64(*a).map(std::cmp::Ordering::reverse),
774        (Value::Float(a) | Value::Prob(a), Value::Float(b) | Value::Prob(b)) => a.partial_cmp(b),
775        _ => None,
776    }
777}
778
779/// The language's `==`: numbers compare by value across int, float and prob.
780pub fn equals(a: &Value, b: &Value) -> bool {
781    if let (Value::Complex(x), Value::Complex(y)) = (a, b) {
782        return x == y;
783    }
784    if let (Value::Complex(z), real) | (real, Value::Complex(z)) = (a, b) {
785        if z.im() != 0.0 {
786            return false;
787        }
788        return match real {
789            // Compare integers without rounding them through f64.
790            Value::Int(n) => {
791                let x = z.re();
792                n.cmp_f64(x).is_some_and(|c| c.is_eq())
793            }
794            Value::Float(x) | Value::Prob(x) => z.re() == *x,
795            _ => false,
796        };
797    }
798    if let Some(c) = numeric_compare(a, b) {
799        return c.is_eq();
800    }
801    if matches!(a, Value::Float(x) | Value::Prob(x) if x.is_nan())
802        || matches!(b, Value::Float(x) | Value::Prob(x) if x.is_nan())
803    {
804        return false;
805    }
806    a == b
807}
808
809/// Ordering for `<`, `>` and friends.
810pub fn compare(a: &Value, b: &Value) -> OpResult<std::cmp::Ordering> {
811    if matches!(a, Value::Complex(_)) || matches!(b, Value::Complex(_)) {
812        return Err(OpError::new("complex values have no ordering").help("compare `abs(z)`, `real(z)` or `imag(z)`"));
813    }
814    if let Some(c) = numeric_compare(a, b) {
815        return Ok(c);
816    }
817    if matches!(a, Value::Float(x) | Value::Prob(x) if x.is_nan())
818        || matches!(b, Value::Float(x) | Value::Prob(x) if x.is_nan())
819    {
820        return Err(OpError::new("can't order NaN"));
821    }
822    match (a, b) {
823        (Value::Str(x), Value::Str(y)) => Ok(x.cmp(y)),
824        (Value::Date(x), Value::Date(y)) => Ok(x.cmp(y)),
825        (Value::Enum(x), Value::Enum(y)) if x.ty == y.ty => Ok(x.variant.cmp(&y.variant)),
826        (Value::List(x), Value::List(y)) => {
827            for (p, q) in x.iter().zip(y.iter()) {
828                let c = compare(p, q)?;
829                if c.is_ne() {
830                    return Ok(c);
831                }
832            }
833            Ok(x.len().cmp(&y.len()))
834        }
835        _ => Err(OpError::new(format!(
836            "can't compare {} with {}",
837            article(&a.kind()),
838            article(&b.kind())
839        ))),
840    }
841}
842
843/// Whether `item` is in `coll` (for `in`).
844pub fn contains(coll: &Value, item: &Value, budget: &mut Budget) -> OpResult<bool> {
845    if crate::analytic::contains(coll) || crate::analytic::contains(item) {
846        return Err(crate::analytic::unsupported("membership involving analytic outcomes"));
847    }
848    if let Value::Str(s) = coll {
849        budget.string_work(s)?;
850        if let Value::Str(s) = item {
851            budget.string_work(s)?;
852        }
853    }
854    Ok(match coll {
855        Value::List(items) => items.iter().any(|x| equals(x, item)),
856        Value::Map(m) => m.contains_key(item),
857        Value::Bag(b) => b.get(item).is_some_and(|n| *n > 0),
858        Value::Range(lo, hi) => match item {
859            Value::Int(n) => n >= lo && n <= hi,
860            Value::Float(x) | Value::Prob(x) if x.is_finite() && x.fract() == 0.0 => {
861                lo.cmp_f64(*x).is_some_and(|c| !c.is_gt()) && hi.cmp_f64(*x).is_some_and(|c| !c.is_lt())
862            }
863            _ => false,
864        },
865        Value::Str(s) => match item {
866            Value::Str(sub) => s.contains(&**sub),
867            _ => {
868                return Err(OpError::new(format!(
869                    "can't look for {} in a string",
870                    article(&item.kind())
871                )));
872            }
873        },
874        other => return Err(OpError::new(format!("can't look inside {}", article(&other.kind())))),
875    })
876}
877
878fn range(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
879    if a.is_uncertain() || b.is_uncertain() {
880        return Err(OpError::new("a range needs plain whole numbers, not distributions")
881            .help("draw a value first, like `let n ~ d6`"));
882    }
883    let lo = integer(a, "range bound", budget)?;
884    let hi = integer(b, "range bound", budget)?;
885    let hi = if op == BinOp::RangeExcl {
886        hi.sub(&Integer::ONE)?
887    } else {
888        hi.into_owned()
889    };
890    budget.integer_work(&lo, &hi, false)?;
891    if op == BinOp::RangeExcl {
892        budget.integer_allocation(hi.bits(), 1)?;
893    }
894    Ok(Value::Range(lo.into_owned(), hi))
895}
896
897/// The number of integers in `lo..=hi`, without overflowing.
898pub fn range_len(lo: &Integer, hi: &Integer) -> OpResult<Integer> {
899    if hi < lo {
900        Ok(Integer::ZERO)
901    } else {
902        Ok(hi.sub(lo)?.add(&Integer::ONE)?)
903    }
904}
905
906/// Bounded materialization count, never truncating a large length.
907pub fn range_count(lo: &Integer, hi: &Integer) -> OpResult<u128> {
908    range_len(lo, hi)?
909        .to_u128()
910        .ok_or_else(|| OpError::limit("range has too many elements to materialize"))
911}
912
913// ── Collections ──────────────────────────────────────────────────────────
914
915pub fn field(v: &Value, name: &str, budget: &mut Budget) -> OpResult<Value> {
916    lift1(v, budget, |x, _| match x {
917        Value::Record(r) => r.get(name).cloned().ok_or_else(|| {
918            let known: Vec<String> = r.fields.iter().map(|(n, _)| format!("`{n}`")).collect();
919            OpError::new(format!("{} has no field `{name}`", article(&x.kind())))
920                .help(format!("its fields are {}", known.join(", ")))
921        }),
922        other => Err(OpError::new(format!(
923            "can't read the field `{name}` of {}",
924            article(&other.kind())
925        ))),
926    })
927}
928
929pub fn index(coll: &Value, i: &Value, budget: &mut Budget) -> OpResult<Value> {
930    lift2(coll, i, budget, |c, i, budget| {
931        let v = index_plain(c, i, budget)?;
932        if let (Value::Range(lo, hi), Value::Int(n)) = (c, &v) {
933            budget.integer_work(lo, hi, false)?;
934            budget.integer_allocation(n.bits(), 1)?;
935        }
936        Ok(v)
937    })
938}
939
940pub fn index_plain(coll: &Value, i: &Value, budget: &mut Budget) -> OpResult<Value> {
941    match coll {
942        Value::List(items) => {
943            let k = as_index(i, items.len() as u128, budget)?;
944            Ok(items[k as usize].clone())
945        }
946        Value::Range(lo, hi) => {
947            let k = integer(i, "index", budget)?;
948            if k.is_negative() {
949                return Err(OpError::fault(Fault::IndexOutOfBounds, "index out of range"));
950            }
951            let n = lo.add(&k)?;
952            if &n > hi {
953                return Err(OpError::fault(Fault::IndexOutOfBounds, "index out of range"));
954            }
955            Ok(Value::Int(n))
956        }
957        Value::Str(s) => {
958            budget.string_work(s)?;
959            let k = as_index(i, s.chars().count() as u128, budget)?;
960            let c = s.chars().nth(k as usize).expect("checked scalar index");
961            crate::text::value(c.encode_utf8(&mut [0; 4]), budget)
962        }
963        Value::Map(m) => m.get(i).cloned().ok_or_else(|| {
964            OpError::fault(Fault::MissingKey, format!("the key {i:?} isn't in the map"))
965                .help("use `get(key, default)` for keys that may be missing")
966        }),
967        other => Err(OpError::new(format!("can't index {}", article(&other.kind())))),
968    }
969}
970
971pub fn as_index(i: &Value, len: u128, budget: &mut Budget) -> OpResult<u128> {
972    let k = integer(i, "index", budget)?.to_u128();
973    k.filter(|k| *k < len).ok_or_else(|| {
974        OpError::fault(
975            Fault::IndexOutOfBounds,
976            format!("index {i} is out of range for a length of {len}"),
977        )
978        .help("indices start at 0")
979    })
980}
981
982pub fn make_record(ty: Option<Arc<str>>, mut fields: Vec<(Arc<str>, Value)>) -> Value {
983    fields.sort_by(|a, b| a.0.cmp(&b.0));
984    Value::record(Record { ty, fields })
985}
986
987pub fn with_fields(base: &Value, updates: &[(Arc<str>, Value)]) -> OpResult<Value> {
988    let Value::Record(r) = base else {
989        return Err(OpError::new(format!(
990            "`with` needs a record, found {}",
991            article(&base.kind())
992        )));
993    };
994    let mut r = Record::clone(r);
995    for (name, v) in updates {
996        match r.get_mut(name) {
997            Some(slot) => *slot = v.clone(),
998            None => {
999                return Err(OpError::new(format!("{} has no field `{name}`", article(&base.kind()))));
1000            }
1001        }
1002    }
1003    Ok(Value::record(r))
1004}
1005
1006pub fn enum_value(ty: u32, variant: u32, name: &str) -> Value {
1007    Value::Enum(Arc::new(EnumValue {
1008        ty,
1009        variant,
1010        name: Arc::from(name),
1011    }))
1012}
1013
1014/// Is `v` certainly `truth`? (For `and`/`or` whose right side has statements.)
1015pub fn is_certain(v: &Value, truth: bool) -> bool {
1016    matches!(v, Value::Bool(b) if *b == truth)
1017}
1018
1019#[cfg(test)]
1020mod probability_tests {
1021    use super::*;
1022
1023    #[test]
1024    fn computed_probabilities_only_correct_boundary_roundoff() {
1025        assert_eq!(computed_prob(1.0 + f64::EPSILON, "test").unwrap(), Value::Prob(1.0));
1026        assert_eq!(computed_prob(-f64::EPSILON, "test").unwrap(), Value::Prob(0.0));
1027        assert_eq!(computed_prob(1e-300, "test").unwrap(), Value::Prob(1e-300));
1028        for p in [1.001, -0.001, f64::INFINITY, f64::NEG_INFINITY, f64::NAN] {
1029            assert!(computed_prob(p, "test").is_err());
1030        }
1031    }
1032}