Skip to main content

fv_value/
value.rs

1//! The runtime value, the scope an expression reads from, and the [`Lookup`] abstraction over it.
2
3use std::collections::{BTreeMap, HashMap};
4use std::fmt;
5
6pub(crate) use crate::error::ExprError;
7
8/// A value the dialect computes over.
9///
10/// There is one numeric type, `f64`: every numeric literal and every JSON number widens to it, so
11/// `42` and `42.0` are the same value. The derived `PartialEq` is structural (for tests and round
12/// trips); the dialect's own `==` — `null == null`, `true != 1`, NaN never equal — is
13/// [`Value::dialect_eq`].
14#[derive(Debug, Clone, PartialEq, Default)]
15pub enum Value {
16    /// Absent / unknown. Propagates through arithmetic and most functions.
17    #[default]
18    Null,
19    /// A boolean — distinct from numbers: `true == 1` is false.
20    Bool(bool),
21    /// A number (always `f64`).
22    Num(f64),
23    /// A string.
24    Str(String),
25    /// A list of values.
26    List(Vec<Value>),
27    /// A string-keyed object.
28    Obj(BTreeMap<String, Value>),
29}
30
31impl Value {
32    /// `true` for `Null`.
33    pub fn is_null(&self) -> bool {
34        matches!(self, Value::Null)
35    }
36
37    /// `true` for `Num` (never for `Bool`).
38    pub fn is_num(&self) -> bool {
39        matches!(self, Value::Num(_))
40    }
41
42    /// The number, if this is one.
43    pub fn as_num(&self) -> Option<f64> {
44        match self {
45            Value::Num(n) => Some(*n),
46            _ => None,
47        }
48    }
49
50    /// The boolean, if this is one.
51    pub fn as_bool(&self) -> Option<bool> {
52        match self {
53            Value::Bool(b) => Some(*b),
54            _ => None,
55        }
56    }
57
58    /// The string, if this is one.
59    pub fn as_str(&self) -> Option<&str> {
60        match self {
61            Value::Str(s) => Some(s),
62            _ => None,
63        }
64    }
65
66    /// The list, if this is one.
67    pub fn as_list(&self) -> Option<&[Value]> {
68        match self {
69            Value::List(l) => Some(l),
70            _ => None,
71        }
72    }
73
74    /// The object, if this is one.
75    pub fn as_obj(&self) -> Option<&BTreeMap<String, Value>> {
76        match self {
77            Value::Obj(o) => Some(o),
78            _ => None,
79        }
80    }
81
82    /// The dialect's name for this value's type: `null`, `boolean`, `number`, `string`, `list`
83    /// or `object` (what the `typeOf` function returns).
84    pub fn type_name(&self) -> &'static str {
85        match self {
86            Value::Null => "null",
87            Value::Bool(_) => "boolean",
88            Value::Num(_) => "number",
89            Value::Str(_) => "string",
90            Value::List(_) => "list",
91            Value::Obj(_) => "object",
92        }
93    }
94
95    /// The dialect's `==`: `null == null` is true; a boolean equals only the same boolean; any
96    /// other comparison involving `null` is false; numbers compare by value (so NaN is never equal);
97    /// lists and objects compare element-wise with these same rules.
98    pub fn dialect_eq(&self, other: &Value) -> bool {
99        match (self, other) {
100            (Value::Null, Value::Null) => true,
101            (Value::Bool(x), Value::Bool(y)) => x == y,
102            (Value::Bool(_), _) | (_, Value::Bool(_)) => false,
103            (Value::Null, _) | (_, Value::Null) => false,
104            (Value::Num(x), Value::Num(y)) => x == y,
105            (Value::Str(x), Value::Str(y)) => x == y,
106            (Value::List(x), Value::List(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| p.dialect_eq(q)),
107            (Value::Obj(x), Value::Obj(y)) => {
108                x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|w| v.dialect_eq(w)))
109            }
110            _ => false,
111        }
112    }
113}
114
115impl fmt::Display for Value {
116    /// The value as JSON text (a non-finite number prints as `null`).
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{}", crate::json::value_to_json(self))
119    }
120}
121
122impl From<bool> for Value {
123    fn from(b: bool) -> Self {
124        Value::Bool(b)
125    }
126}
127impl From<f64> for Value {
128    fn from(n: f64) -> Self {
129        Value::Num(n)
130    }
131}
132impl From<i64> for Value {
133    fn from(n: i64) -> Self {
134        Value::Num(n as f64)
135    }
136}
137impl From<i32> for Value {
138    fn from(n: i32) -> Self {
139        Value::Num(f64::from(n))
140    }
141}
142impl From<&str> for Value {
143    fn from(s: &str) -> Self {
144        Value::Str(s.to_string())
145    }
146}
147impl From<String> for Value {
148    fn from(s: String) -> Self {
149        Value::Str(s)
150    }
151}
152impl<T: Into<Value>> From<Vec<T>> for Value {
153    fn from(v: Vec<T>) -> Self {
154        Value::List(v.into_iter().map(Into::into).collect())
155    }
156}
157impl<T: Into<Value>> From<Option<T>> for Value {
158    fn from(v: Option<T>) -> Self {
159        v.map_or(Value::Null, Into::into)
160    }
161}
162impl From<BTreeMap<String, Value>> for Value {
163    fn from(o: BTreeMap<String, Value>) -> Self {
164        Value::Obj(o)
165    }
166}
167
168/// The scope an expression evaluates against: column name → value. A missing name reads as
169/// `Null`. Any [`Lookup`] works; this is the convenient owned form.
170pub type Scope = BTreeMap<String, Value>;
171
172/// Where an expression's identifiers are resolved. Implemented for [`Scope`], `HashMap`, and
173/// `[(String, Value)]` row slices, so a hot loop can evaluate straight against a row without
174/// building a map per row. Return `Value::Null` for a name you do not have.
175pub trait Lookup {
176    /// The value bound to `name`, or `Value::Null`.
177    fn lookup(&self, name: &str) -> Value;
178}
179
180impl Lookup for Scope {
181    fn lookup(&self, name: &str) -> Value {
182        self.get(name).cloned().unwrap_or(Value::Null)
183    }
184}
185
186impl Lookup for HashMap<String, Value> {
187    fn lookup(&self, name: &str) -> Value {
188        self.get(name).cloned().unwrap_or(Value::Null)
189    }
190}
191
192/// Row-slice lookup: a linear scan (rows are narrow). First match wins.
193impl Lookup for [(String, Value)] {
194    fn lookup(&self, name: &str) -> Value {
195        self.iter()
196            .find(|(k, _)| k == name)
197            .map(|(_, v)| v.clone())
198            .unwrap_or(Value::Null)
199    }
200}
201
202impl Lookup for Vec<(String, Value)> {
203    fn lookup(&self, name: &str) -> Value {
204        self.as_slice().lookup(name)
205    }
206}
207
208impl<T: Lookup + ?Sized> Lookup for &T {
209    fn lookup(&self, name: &str) -> Value {
210        (**self).lookup(name)
211    }
212}
213
214/// A scope with no bindings: every identifier reads as `Null`.
215#[derive(Debug, Clone, Copy, Default)]
216pub struct Empty;
217
218impl Lookup for Empty {
219    fn lookup(&self, _name: &str) -> Value {
220        Value::Null
221    }
222}
223
224/// The result type functions return.
225pub type EResult = Result<Value, ExprError>;
226
227/// A function's failure (see [`ExprError::Call`]).
228pub(crate) fn err<T>(msg: impl Into<String>) -> Result<T, ExprError> {
229    Err(ExprError::call(msg))
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn conversions_and_accessors() {
238        assert_eq!(Value::from(3), Value::Num(3.0));
239        assert_eq!(Value::from(2.5_f64).as_num(), Some(2.5));
240        assert_eq!(Value::from("x").as_str(), Some("x"));
241        assert_eq!(Value::from(String::from("y")).type_name(), "string");
242        assert_eq!(
243            Value::from(vec![1, 2]),
244            Value::List(vec![Value::Num(1.0), Value::Num(2.0)])
245        );
246        assert_eq!(Value::from(None::<bool>), Value::Null);
247        assert_eq!(Value::from(Some(true)).as_bool(), Some(true));
248        assert!(Value::default().is_null() && !Value::Bool(true).is_num());
249        let o: BTreeMap<String, Value> = [("k".to_string(), Value::from(1))].into();
250        assert_eq!(Value::from(o.clone()).as_obj(), Some(&o));
251        assert_eq!(Value::from(vec![Value::Null]).as_list().map(<[Value]>::len), Some(1));
252        assert_eq!(Value::from("s").to_string(), "\"s\"");
253        assert_eq!(Value::Num(f64::NAN).to_string(), "null");
254    }
255
256    #[test]
257    fn dialect_equality_rules() {
258        assert!(Value::Null.dialect_eq(&Value::Null));
259        assert!(!Value::Bool(true).dialect_eq(&Value::Num(1.0)));
260        assert!(!Value::Null.dialect_eq(&Value::Num(0.0)));
261        assert!(!Value::Num(f64::NAN).dialect_eq(&Value::Num(f64::NAN)));
262        assert!(Value::from(vec![1]).dialect_eq(&Value::from(vec![1.0])));
263        assert!(!Value::from(vec![1]).dialect_eq(&Value::from(vec![1, 2])));
264    }
265
266    #[test]
267    fn lookups_agree() {
268        let row = vec![("a".to_string(), Value::from(1))];
269        let scope: Scope = row.iter().cloned().collect();
270        let hash: HashMap<String, Value> = row.iter().cloned().collect();
271        for l in [&row as &dyn Lookup, &scope, &hash, &row.as_slice()] {
272            assert_eq!(l.lookup("a"), Value::Num(1.0));
273            assert_eq!(l.lookup("missing"), Value::Null);
274        }
275        assert_eq!(Empty.lookup("a"), Value::Null);
276        assert_eq!(Lookup::lookup(&&scope, "a"), Value::Num(1.0));
277    }
278}