Skip to main content

fugue/program/
value.rs

1//! Runtime values of the program language, and the coercion rules the
2//! interpreter applies to them.
3
4use std::fmt;
5use std::sync::Arc;
6
7/// A value computed by a [`Program`](super::Program).
8///
9/// Sample sites bind their **natural** type — `Bernoulli` binds [`Value::Bool`],
10/// `Poisson`/`Binomial` bind [`Value::U64`], `Categorical` binds
11/// [`Value::Usize`], `DiscreteUniform` binds [`Value::Int`], continuous
12/// distributions bind [`Value::F64`] — and a compiled program builds a
13/// `Model<Value>`, so a program returns whichever of these it computes.
14///
15/// The language is dynamically typed with a few numeric coercions, and the
16/// accessors below expose exactly those coercions, so a host function reading
17/// its arguments sees the same conversions the built-ins do:
18///
19/// | accessor | accepts |
20/// |---|---|
21/// | [`as_f64`](Value::as_f64) | any number; `true`/`false` as `1.0`/`0.0` |
22/// | [`as_bool`](Value::as_bool) | a bool; a number as "is nonzero" (`NaN` has no truth value) |
23/// | [`as_i64`](Value::as_i64) | an integer in range, an integral finite `F64`, a bool as `0`/`1` |
24/// | [`as_u64`](Value::as_u64) / [`as_usize`](Value::as_usize) | as `as_i64`, and non-negative |
25/// | [`as_array`](Value::as_array) | an array |
26///
27/// ```rust
28/// use fugue::program::Value;
29///
30/// assert_eq!(Value::Usize(2).as_f64(), Some(2.0));
31/// assert_eq!(Value::F64(3.0).as_usize(), Some(3));
32/// assert_eq!(Value::F64(2.5).as_i64(), None);
33/// assert_eq!(Value::Int(-1).as_u64(), None);
34/// assert_eq!(Value::Bool(true).as_f64(), Some(1.0));
35/// assert_eq!(Value::F64(0.0).as_bool(), Some(false));
36/// assert_eq!(Value::from(vec![0.5, 0.5]).as_array(), Some(&[0.5, 0.5][..]));
37/// ```
38#[derive(Clone, Debug, PartialEq)]
39pub enum Value {
40    /// A float: `2.0`-style literals, continuous sites, float arithmetic.
41    F64(f64),
42    /// A signed integer: `2`-style literals, loop variables, `.len()`,
43    /// integer arithmetic, and `i64` sites (`DiscreteUniform`).
44    Int(i64),
45    /// An unsigned count: the value of a `u64` site (`Poisson`, `Binomial`).
46    U64(u64),
47    /// An index: the value of a `usize` site (`Categorical`).
48    Usize(usize),
49    /// A boolean: `true`/`false`, comparisons, `&&`/`||`/`!`, and `bool`
50    /// sites (`Bernoulli`).
51    Bool(bool),
52    /// An array of numbers: a data array or an array literal `[a, b, c]`.
53    Arr(Arc<Vec<f64>>),
54}
55
56impl Value {
57    /// The value's type as the language names it in messages: `"f64"`,
58    /// `"int"`, `"u64"`, `"usize"`, `"bool"` or `"array"`.
59    pub fn type_name(&self) -> &'static str {
60        match self {
61            Value::F64(_) => "f64",
62            Value::Int(_) => "int",
63            Value::U64(_) => "u64",
64            Value::Usize(_) => "usize",
65            Value::Bool(_) => "bool",
66            Value::Arr(_) => "array",
67        }
68    }
69
70    /// The value as an `f64`: any number converts, and a bool is `1.0`/`0.0`.
71    /// `None` for an array.
72    pub fn as_f64(&self) -> Option<f64> {
73        match self {
74            Value::F64(x) => Some(*x),
75            Value::Int(i) => Some(*i as f64),
76            Value::U64(u) => Some(*u as f64),
77            Value::Usize(u) => Some(*u as f64),
78            Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
79            Value::Arr(_) => None,
80        }
81    }
82
83    /// The value's truth: a bool is itself, a number is "nonzero". `None` for
84    /// `NaN` (which is neither) and for an array.
85    pub fn as_bool(&self) -> Option<bool> {
86        match self {
87            Value::Bool(b) => Some(*b),
88            Value::F64(x) if x.is_nan() => None,
89            Value::F64(x) => Some(*x != 0.0),
90            Value::Int(i) => Some(*i != 0),
91            Value::U64(u) => Some(*u != 0),
92            Value::Usize(u) => Some(*u != 0),
93            Value::Arr(_) => None,
94        }
95    }
96
97    /// The value as an `i64`: an integer in range, a finite integral `F64` in
98    /// range, or a bool as `0`/`1`. `None` otherwise.
99    pub fn as_i64(&self) -> Option<i64> {
100        match self {
101            Value::Int(i) => Some(*i),
102            Value::U64(u) => i64::try_from(*u).ok(),
103            Value::Usize(u) => i64::try_from(*u).ok(),
104            Value::Bool(b) => Some(i64::from(*b)),
105            // [-2^63, 2^63): both bounds are exactly representable as f64.
106            Value::F64(x) if x.fract() == 0.0 && *x >= -9.223_372_036_854_776e18 => {
107                if *x < 9.223_372_036_854_776e18 {
108                    Some(*x as i64)
109                } else {
110                    None
111                }
112            }
113            Value::F64(_) | Value::Arr(_) => None,
114        }
115    }
116
117    /// The value as a `u64`: as [`as_i64`](Value::as_i64), but non-negative
118    /// (and a `U64` above `i64::MAX` converts too).
119    pub fn as_u64(&self) -> Option<u64> {
120        match self {
121            Value::U64(u) => Some(*u),
122            Value::Usize(u) => u64::try_from(*u).ok(),
123            // [0, 2^64): both bounds are exactly representable as f64.
124            Value::F64(x) if x.fract() == 0.0 && *x >= 0.0 => {
125                if *x < 1.844_674_407_370_955_2e19 {
126                    Some(*x as u64)
127                } else {
128                    None
129                }
130            }
131            other => other.as_i64().and_then(|i| u64::try_from(i).ok()),
132        }
133    }
134
135    /// The value as a `usize`: as [`as_u64`](Value::as_u64), and in range for
136    /// the platform's `usize`.
137    pub fn as_usize(&self) -> Option<usize> {
138        match self {
139            Value::Usize(u) => Some(*u),
140            other => other.as_u64().and_then(|u| usize::try_from(u).ok()),
141        }
142    }
143
144    /// The array's elements, or `None` for a scalar.
145    pub fn as_array(&self) -> Option<&[f64]> {
146        match self {
147            Value::Arr(a) => Some(a.as_slice()),
148            _ => None,
149        }
150    }
151
152    /// The value as an `f64` for [`CompiledProgram::build_f64`]: as
153    /// [`as_f64`](Value::as_f64), with an array reading as `NaN` (the playground
154    /// interpreter's historical conversion).
155    ///
156    /// [`CompiledProgram::build_f64`]: super::CompiledProgram::build_f64
157    pub(crate) fn to_f64_lossy(&self) -> f64 {
158        self.as_f64().unwrap_or(f64::NAN)
159    }
160
161    /// The value as an arithmetic operand: integers (and bools, as `0`/`1`)
162    /// stay exact, floats stay floats, an array is an error.
163    pub(crate) fn num(&self) -> Result<Num, String> {
164        match self {
165            Value::F64(x) => Ok(Num::F(*x)),
166            Value::Int(i) => Ok(Num::I(*i)),
167            Value::Bool(b) => Ok(Num::I(i64::from(*b))),
168            Value::U64(u) => Ok(i64::try_from(*u).map_or(Num::F(*u as f64), Num::I)),
169            Value::Usize(u) => Ok(i64::try_from(*u).map_or(Num::F(*u as f64), Num::I)),
170            Value::Arr(_) => Err("expected a number, found an array".to_string()),
171        }
172    }
173}
174
175/// An arithmetic operand: exact integer or float.
176#[derive(Clone, Copy, Debug)]
177pub(crate) enum Num {
178    I(i64),
179    F(f64),
180}
181
182impl Num {
183    pub(crate) fn to_f64(self) -> f64 {
184        match self {
185            Num::I(i) => i as f64,
186            Num::F(x) => x,
187        }
188    }
189}
190
191/// Formats the value the way Rust's `Display` formats the corresponding Rust
192/// value, which is what makes `addr!("x", i)` in a program byte-identical to
193/// `addr!("x", i)` in compiled Rust: `3`, `0.5`, `true`. An array prints as
194/// `[1, 2.5]` (arrays are not valid address indices).
195impl fmt::Display for Value {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match self {
198            Value::F64(x) => write!(f, "{x}"),
199            Value::Int(i) => write!(f, "{i}"),
200            Value::U64(u) => write!(f, "{u}"),
201            Value::Usize(u) => write!(f, "{u}"),
202            Value::Bool(b) => write!(f, "{b}"),
203            Value::Arr(a) => {
204                f.write_str("[")?;
205                for (i, x) in a.iter().enumerate() {
206                    if i > 0 {
207                        f.write_str(", ")?;
208                    }
209                    write!(f, "{x}")?;
210                }
211                f.write_str("]")
212            }
213        }
214    }
215}
216
217impl From<f64> for Value {
218    fn from(x: f64) -> Self {
219        Value::F64(x)
220    }
221}
222
223impl From<i64> for Value {
224    fn from(i: i64) -> Self {
225        Value::Int(i)
226    }
227}
228
229impl From<i32> for Value {
230    fn from(i: i32) -> Self {
231        Value::Int(i64::from(i))
232    }
233}
234
235impl From<u64> for Value {
236    fn from(u: u64) -> Self {
237        Value::U64(u)
238    }
239}
240
241impl From<usize> for Value {
242    fn from(u: usize) -> Self {
243        Value::Usize(u)
244    }
245}
246
247impl From<bool> for Value {
248    fn from(b: bool) -> Self {
249        Value::Bool(b)
250    }
251}
252
253impl From<Vec<f64>> for Value {
254    fn from(v: Vec<f64>) -> Self {
255        Value::Arr(Arc::new(v))
256    }
257}
258
259impl From<&[f64]> for Value {
260    fn from(v: &[f64]) -> Self {
261        Value::Arr(Arc::new(v.to_vec()))
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn integral_conversions_respect_ranges() {
271        assert_eq!(Value::F64(-0.0).as_i64(), Some(0));
272        assert_eq!(Value::F64(9.223_372_036_854_776e18).as_i64(), None);
273        assert_eq!(
274            Value::F64(-9.223_372_036_854_776e18).as_i64(),
275            Some(i64::MIN)
276        );
277        assert_eq!(Value::F64(f64::NAN).as_i64(), None);
278        assert_eq!(Value::F64(f64::INFINITY).as_u64(), None);
279        assert_eq!(Value::F64(1.844_674_407_370_955_2e19).as_u64(), None);
280        assert_eq!(Value::U64(u64::MAX).as_u64(), Some(u64::MAX));
281        assert_eq!(Value::U64(u64::MAX).as_i64(), None);
282        assert_eq!(Value::Bool(true).as_u64(), Some(1));
283        assert_eq!(Value::Int(-3).as_usize(), None);
284        assert_eq!(Value::from(vec![1.0]).as_i64(), None);
285    }
286
287    #[test]
288    fn truthiness_and_display() {
289        assert_eq!(Value::F64(f64::NAN).as_bool(), None);
290        assert_eq!(Value::Int(2).as_bool(), Some(true));
291        assert_eq!(Value::from(vec![1.0]).as_bool(), None);
292        assert_eq!(Value::F64(3.0).to_string(), "3");
293        assert_eq!(Value::F64(0.5).to_string(), "0.5");
294        assert_eq!(Value::Bool(true).to_string(), "true");
295        assert_eq!(Value::from(vec![1.0, 2.5]).to_string(), "[1, 2.5]");
296        assert_eq!(
297            Value::from(vec![1.0]).to_f64_lossy().to_bits(),
298            f64::NAN.to_bits()
299        );
300    }
301}