Skip to main content

jetro_core/eval/
util.rs

1//! Shared helpers used across eval / vm:
2//!
3//! - `cmp_vals`: total ordering on `Val` (null < bool < num < str <
4//!   arr < obj), used by sort, min, max, top-N, distinct.  Unlike
5//!   `PartialOrd`, this never returns `None` — it uses a lexicographic
6//!   fallback when types are incomparable, which is essential for
7//!   heap-based partial sorts.
8//! - `add_vals` / `num_op`: numeric widening + arithmetic dispatch
9//!   shared by the tree-walker and the VM so both semantics match.
10//! - `val_key`: canonical string key for grouping / dedup.
11//! - `flatten_val` / `zip_arrays` / `cartesian` / `deep_merge`:
12//!   array / object combinators reused by builtins.
13//! - `val_str` / `val_to_string`: coercion helpers for string
14//!   methods and CSV emission.
15
16use std::sync::Arc;
17use indexmap::IndexMap;
18
19use super::value::Val;
20use super::EvalError;
21use super::super::ast::KindType;
22
23// ── Value predicates ──────────────────────────────────────────────────────────
24
25#[inline]
26pub fn is_truthy(v: &Val) -> bool {
27    match v {
28        Val::Null      => false,
29        Val::Bool(b)   => *b,
30        Val::Int(n)    => *n != 0,
31        Val::Float(f)  => *f != 0.0,
32        Val::Str(s)    => !s.is_empty(),
33        Val::Arr(a)    => !a.is_empty(),
34        Val::Obj(m)    => !m.is_empty(),
35    }
36}
37
38#[inline]
39pub fn kind_matches(v: &Val, ty: KindType) -> bool {
40    matches!((v, ty),
41        (Val::Null,         KindType::Null)   |
42        (Val::Bool(_),      KindType::Bool)   |
43        (Val::Int(_),       KindType::Number) |
44        (Val::Float(_),     KindType::Number) |
45        (Val::Str(_),       KindType::Str)    |
46        (Val::Arr(_),       KindType::Array)  |
47        (Val::Obj(_),       KindType::Object)
48    )
49}
50
51#[inline]
52pub fn vals_eq(a: &Val, b: &Val) -> bool {
53    match (a, b) {
54        (Val::Null,     Val::Null)     => true,
55        (Val::Bool(x),  Val::Bool(y))  => x == y,
56        (Val::Str(x),   Val::Str(y))   => x == y,
57        (Val::Int(x),   Val::Int(y))   => x == y,
58        (Val::Float(x), Val::Float(y)) => x == y,
59        (Val::Int(x),   Val::Float(y)) => (*x as f64) == *y,
60        (Val::Float(x), Val::Int(y))   => *x == (*y as f64),
61        _ => false,
62    }
63}
64
65#[inline]
66pub fn cmp_vals(a: &Val, b: &Val) -> std::cmp::Ordering {
67    match (a, b) {
68        (Val::Int(x),   Val::Int(y))   => x.cmp(y),
69        (Val::Float(x), Val::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
70        (Val::Int(x),   Val::Float(y)) => (*x as f64).partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
71        (Val::Float(x), Val::Int(y))   => x.partial_cmp(&(*y as f64)).unwrap_or(std::cmp::Ordering::Equal),
72        (Val::Str(x),   Val::Str(y))   => x.cmp(y),
73        (Val::Bool(x),  Val::Bool(y))  => x.cmp(y),
74        _ => std::cmp::Ordering::Equal,
75    }
76}
77
78// ── Value conversions ─────────────────────────────────────────────────────────
79
80/// Canonical string key for use in HashSets / dedup (never allocates for Str).
81#[inline]
82pub fn val_to_key(v: &Val) -> String {
83    match v {
84        Val::Str(s)    => s.to_string(),
85        Val::Int(n)    => n.to_string(),
86        Val::Float(f)  => f.to_string(),
87        Val::Bool(b)   => b.to_string(),
88        Val::Null      => "null".to_string(),
89        other          => val_to_string(other),
90    }
91}
92
93#[inline]
94pub fn val_to_string(v: &Val) -> String {
95    match v {
96        Val::Str(s)    => s.to_string(),
97        Val::Int(n)    => n.to_string(),
98        Val::Float(f)  => f.to_string(),
99        Val::Bool(b)   => b.to_string(),
100        Val::Null      => "null".to_string(),
101        other          => {
102            let sv: serde_json::Value = other.clone().into();
103            serde_json::to_string(&sv).unwrap_or_default()
104        }
105    }
106}
107
108// ── Constructors ──────────────────────────────────────────────────────────────
109
110#[inline] pub fn val_int(n: i64)  -> Val { Val::Int(n) }
111#[inline] pub fn val_float(f: f64) -> Val { Val::Float(f) }
112#[inline] pub fn val_str(s: &str) -> Val { Val::Str(Arc::from(s)) }
113#[inline] pub fn val_key(s: &str) -> Arc<str> { Arc::from(s) }
114
115// ── Arithmetic ────────────────────────────────────────────────────────────────
116
117pub fn add_vals(a: Val, b: Val) -> Result<Val, EvalError> {
118    match (a, b) {
119        (Val::Int(x),   Val::Int(y))   => Ok(Val::Int(x + y)),
120        (Val::Float(x), Val::Float(y)) => Ok(Val::Float(x + y)),
121        (Val::Int(x),   Val::Float(y)) => Ok(Val::Float(x as f64 + y)),
122        (Val::Float(x), Val::Int(y))   => Ok(Val::Float(x + y as f64)),
123        (Val::Str(x),   Val::Str(y))   => Ok(Val::Str(Arc::from(format!("{}{}", x, y).as_str()))),
124        (Val::Arr(x), Val::Arr(y)) => {
125            let mut v = Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone());
126            v.extend_from_slice(&y);
127            Ok(Val::arr(v))
128        }
129        _ => Err(EvalError("+ not supported between these types".into())),
130    }
131}
132
133pub fn num_op<Fi, Ff>(a: Val, b: Val, fi: Fi, ff: Ff) -> Result<Val, EvalError>
134where
135    Fi: Fn(i64, i64) -> i64,
136    Ff: Fn(f64, f64) -> f64,
137{
138    match (a, b) {
139        (Val::Int(x),   Val::Int(y))   => Ok(Val::Int(fi(x, y))),
140        (Val::Float(x), Val::Float(y)) => Ok(Val::Float(ff(x, y))),
141        (Val::Int(x),   Val::Float(y)) => Ok(Val::Float(ff(x as f64, y))),
142        (Val::Float(x), Val::Int(y))   => Ok(Val::Float(ff(x, y as f64))),
143        _ => Err(EvalError("arithmetic on non-numbers".into())),
144    }
145}
146
147// ── Array helpers ─────────────────────────────────────────────────────────────
148
149pub fn flatten_val(v: Val, depth: usize) -> Val {
150    match v {
151        Val::Arr(a) if depth > 0 => {
152            let mut out = Vec::new();
153            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
154            for item in items {
155                match item {
156                    Val::Arr(_) => if let Val::Arr(inner) = flatten_val(item, depth - 1) {
157                        out.extend(Arc::try_unwrap(inner).unwrap_or_else(|a| (*a).clone()));
158                    },
159                    other => out.push(other),
160                }
161            }
162            Val::arr(out)
163        }
164        other => other,
165    }
166}
167
168pub fn zip_arrays(a: Val, b: Val, longest: bool, fill: Val) -> Result<Val, EvalError> {
169    let av = a.as_array().map(|a| a.to_vec()).unwrap_or_default();
170    let bv = b.as_array().map(|b| b.to_vec()).unwrap_or_default();
171    let len = if longest { av.len().max(bv.len()) } else { av.len().min(bv.len()) };
172    Ok(Val::arr((0..len).map(|i| Val::arr(vec![
173        av.get(i).cloned().unwrap_or_else(|| fill.clone()),
174        bv.get(i).cloned().unwrap_or_else(|| fill.clone()),
175    ])).collect()))
176}
177
178pub fn cartesian(arrays: &[Vec<Val>]) -> Vec<Vec<Val>> {
179    arrays.iter().fold(vec![vec![]], |acc, arr| {
180        acc.into_iter().flat_map(|prefix| {
181            arr.iter().map(move |item| {
182                let mut row = prefix.clone();
183                row.push(item.clone());
184                row
185            }).collect::<Vec<_>>()
186        }).collect()
187    })
188}
189
190// ── Field existence ───────────────────────────────────────────────────────────
191
192pub fn field_exists_nested(v: &Val, path: &str) -> bool {
193    let mut parts = path.splitn(2, '.');
194    let first = parts.next().unwrap_or("");
195    match (v.get(first), parts.next()) {
196        (Some(v), _) if v.is_null() => false,
197        (Some(_), None)             => true,
198        (Some(child), Some(rest))   => field_exists_nested(child, rest),
199        (None, _)                   => false,
200    }
201}
202
203// ── Deep merge ────────────────────────────────────────────────────────────────
204
205pub fn deep_merge(base: Val, other: Val) -> Val {
206    match (base, other) {
207        (Val::Obj(bm), Val::Obj(om)) => {
208            let mut map = Arc::try_unwrap(bm).unwrap_or_else(|m| (*m).clone());
209            for (k, v) in Arc::try_unwrap(om).unwrap_or_else(|m| (*m).clone()) {
210                let existing = map.shift_remove(&k);
211                map.insert(k, match existing {
212                    Some(e) => deep_merge(e, v),
213                    None    => v,
214                });
215            }
216            Val::obj(map)
217        }
218        (_, other) => other,
219    }
220}
221
222/// Deep-merge where arrays concatenate instead of replace.  Used by the
223/// `...**` spread operator.  Objects recurse, arrays concat, scalars rhs-wins.
224pub fn deep_merge_concat(base: Val, other: Val) -> Val {
225    match (base, other) {
226        (Val::Obj(bm), Val::Obj(om)) => {
227            let mut map = Arc::try_unwrap(bm).unwrap_or_else(|m| (*m).clone());
228            for (k, v) in Arc::try_unwrap(om).unwrap_or_else(|m| (*m).clone()) {
229                let existing = map.shift_remove(&k);
230                map.insert(k, match existing {
231                    Some(e) => deep_merge_concat(e, v),
232                    None    => v,
233                });
234            }
235            Val::obj(map)
236        }
237        (Val::Arr(ba), Val::Arr(oa)) => {
238            let mut a = Arc::try_unwrap(ba).unwrap_or_else(|a| (*a).clone());
239            for v in Arc::try_unwrap(oa).unwrap_or_else(|a| (*a).clone()) { a.push(v); }
240            Val::arr(a)
241        }
242        (_, other) => other,
243    }
244}
245
246// ── Object building helpers ───────────────────────────────────────────────────
247
248/// Build a Val::Obj with two string-key entries (common pattern for itertools output).
249pub fn obj2(k1: &str, v1: Val, k2: &str, v2: Val) -> Val {
250    let mut m = IndexMap::with_capacity(2);
251    m.insert(val_key(k1), v1);
252    m.insert(val_key(k2), v2);
253    Val::obj(m)
254}