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::StrSlice(r)  => !r.is_empty(),
34        Val::Arr(a)    => !a.is_empty(),
35        Val::IntVec(a) => !a.is_empty(),
36        Val::FloatVec(a) => !a.is_empty(),
37        Val::StrVec(a)       => !a.is_empty(),
38        Val::StrSliceVec(a)  => !a.is_empty(),
39        Val::ObjVec(d)       => !d.rows.is_empty(),
40        Val::Obj(m)       => !m.is_empty(),
41        Val::ObjSmall(p)  => !p.is_empty(),
42    }
43}
44
45#[inline]
46pub fn kind_matches(v: &Val, ty: KindType) -> bool {
47    matches!((v, ty),
48        (Val::Null,         KindType::Null)   |
49        (Val::Bool(_),      KindType::Bool)   |
50        (Val::Int(_),       KindType::Number) |
51        (Val::Float(_),     KindType::Number) |
52        (Val::Str(_),       KindType::Str)    |
53        (Val::Arr(_),       KindType::Array)  |
54        (Val::IntVec(_),    KindType::Array)  |
55        (Val::FloatVec(_),  KindType::Array)  |
56        (Val::Obj(_),       KindType::Object)
57    )
58}
59
60#[inline]
61pub fn vals_eq(a: &Val, b: &Val) -> bool {
62    match (a, b) {
63        (Val::Null,     Val::Null)     => true,
64        (Val::Bool(x),  Val::Bool(y))  => x == y,
65        (Val::Str(x),   Val::Str(y))   => x == y,
66        (Val::Int(x),   Val::Int(y))   => x == y,
67        (Val::Float(x), Val::Float(y)) => x == y,
68        (Val::Int(x),   Val::Float(y)) => (*x as f64) == *y,
69        (Val::Float(x), Val::Int(y))   => *x == (*y as f64),
70        _ => false,
71    }
72}
73
74#[inline]
75pub fn cmp_vals(a: &Val, b: &Val) -> std::cmp::Ordering {
76    match (a, b) {
77        (Val::Int(x),   Val::Int(y))   => x.cmp(y),
78        (Val::Float(x), Val::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
79        (Val::Int(x),   Val::Float(y)) => (*x as f64).partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
80        (Val::Float(x), Val::Int(y))   => x.partial_cmp(&(*y as f64)).unwrap_or(std::cmp::Ordering::Equal),
81        (Val::Str(x),   Val::Str(y))   => x.cmp(y),
82        (Val::Bool(x),  Val::Bool(y))  => x.cmp(y),
83        _ => std::cmp::Ordering::Equal,
84    }
85}
86
87// ── Value conversions ─────────────────────────────────────────────────────────
88
89/// Canonical string key for use in HashSets / dedup (never allocates for Str).
90#[inline]
91pub fn val_to_key(v: &Val) -> String {
92    match v {
93        Val::Str(s)    => s.to_string(),
94        Val::Int(n)    => n.to_string(),
95        Val::Float(f)  => f.to_string(),
96        Val::Bool(b)   => b.to_string(),
97        Val::Null      => "null".to_string(),
98        other          => val_to_string(other),
99    }
100}
101
102#[inline]
103pub fn val_to_string(v: &Val) -> String {
104    match v {
105        Val::Str(s)    => s.to_string(),
106        Val::Int(n)    => n.to_string(),
107        Val::Float(f)  => f.to_string(),
108        Val::Bool(b)   => b.to_string(),
109        Val::Null      => "null".to_string(),
110        other          => {
111            let sv: serde_json::Value = other.clone().into();
112            serde_json::to_string(&sv).unwrap_or_default()
113        }
114    }
115}
116
117// ── Constructors ──────────────────────────────────────────────────────────────
118
119#[inline] pub fn val_int(n: i64)  -> Val { Val::Int(n) }
120#[inline] pub fn val_float(f: f64) -> Val { Val::Float(f) }
121#[inline] pub fn val_str(s: &str) -> Val { Val::Str(Arc::from(s)) }
122#[inline] pub fn val_key(s: &str) -> Arc<str> { Arc::from(s) }
123
124// ── Arithmetic ────────────────────────────────────────────────────────────────
125
126pub fn add_vals(a: Val, b: Val) -> Result<Val, EvalError> {
127    match (a, b) {
128        (Val::Int(x),   Val::Int(y))   => Ok(Val::Int(x + y)),
129        (Val::Float(x), Val::Float(y)) => Ok(Val::Float(x + y)),
130        (Val::Int(x),   Val::Float(y)) => Ok(Val::Float(x as f64 + y)),
131        (Val::Float(x), Val::Int(y))   => Ok(Val::Float(x + y as f64)),
132        (Val::Str(x), Val::Str(y)) => {
133            // `format!` would allocate a temporary `String` for argument
134            // formatting, on top of the `Arc::<str>::from` allocation.
135            // Direct `push_str` halves the allocation count.
136            let mut s = String::with_capacity(x.len() + y.len());
137            s.push_str(&x);
138            s.push_str(&y);
139            Ok(Val::Str(Arc::<str>::from(s)))
140        }
141        (Val::Arr(x), Val::Arr(y)) => {
142            let mut v = Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone());
143            v.extend_from_slice(&y);
144            Ok(Val::arr(v))
145        }
146        _ => Err(EvalError("+ not supported between these types".into())),
147    }
148}
149
150pub fn num_op<Fi, Ff>(a: Val, b: Val, fi: Fi, ff: Ff) -> Result<Val, EvalError>
151where
152    Fi: Fn(i64, i64) -> i64,
153    Ff: Fn(f64, f64) -> f64,
154{
155    match (a, b) {
156        (Val::Int(x),   Val::Int(y))   => Ok(Val::Int(fi(x, y))),
157        (Val::Float(x), Val::Float(y)) => Ok(Val::Float(ff(x, y))),
158        (Val::Int(x),   Val::Float(y)) => Ok(Val::Float(ff(x as f64, y))),
159        (Val::Float(x), Val::Int(y))   => Ok(Val::Float(ff(x, y as f64))),
160        _ => Err(EvalError("arithmetic on non-numbers".into())),
161    }
162}
163
164// ── Array helpers ─────────────────────────────────────────────────────────────
165
166pub fn flatten_val(v: Val, depth: usize) -> Val {
167    match v {
168        Val::Arr(a) if depth > 0 => {
169            let items = Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone());
170            // Columnar fast-path: Arr of IntVec (or Int scalars) → IntVec out.
171            // Skips per-item Val::Int allocation and keeps the result in the
172            // typed lane for downstream aggregates / accumulate.
173            let all_int_children = !items.is_empty() && items.iter().all(|it| matches!(it,
174                Val::IntVec(_) | Val::Int(_)
175            ));
176            if all_int_children {
177                let cap: usize = items.iter().map(|it| match it {
178                    Val::IntVec(inner) => inner.len(),
179                    _ => 1,
180                }).sum();
181                let mut out: Vec<i64> = Vec::with_capacity(cap);
182                for item in items {
183                    match item {
184                        Val::IntVec(inner) => out.extend(inner.iter().copied()),
185                        Val::Int(n)        => out.push(n),
186                        _ => unreachable!(),
187                    }
188                }
189                return Val::int_vec(out);
190            }
191            let all_float_children = !items.is_empty() && items.iter().all(|it| matches!(it,
192                Val::FloatVec(_) | Val::Float(_) | Val::Int(_)
193            ));
194            if all_float_children {
195                let cap: usize = items.iter().map(|it| match it {
196                    Val::FloatVec(inner) => inner.len(),
197                    _ => 1,
198                }).sum();
199                let mut out: Vec<f64> = Vec::with_capacity(cap);
200                for item in items {
201                    match item {
202                        Val::FloatVec(inner) => out.extend(inner.iter().copied()),
203                        Val::Float(f)        => out.push(f),
204                        Val::Int(n)          => out.push(n as f64),
205                        _ => unreachable!(),
206                    }
207                }
208                return Val::float_vec(out);
209            }
210            // Precompute exact capacity in one pass — eliminates Vec doubling
211            // reallocations on the hot `$.flatten()` / `.map(...).flatten()`
212            // paths.
213            let cap: usize = items.iter().map(|it| match it {
214                Val::Arr(inner) => inner.len(),
215                Val::IntVec(inner) => inner.len(),
216                Val::FloatVec(inner) => inner.len(),
217                Val::StrVec(inner) => inner.len(),
218                _ => 1,
219            }).sum();
220            let mut out = Vec::with_capacity(cap);
221            for item in items {
222                match item {
223                    Val::Arr(_) => match flatten_val(item, depth - 1) {
224                        Val::Arr(inner) => {
225                            out.extend(Arc::try_unwrap(inner).unwrap_or_else(|a| (*a).clone()));
226                        }
227                        Val::IntVec(inner) => {
228                            out.extend(inner.iter().map(|n| Val::Int(*n)));
229                        }
230                        Val::FloatVec(inner) => {
231                            out.extend(inner.iter().map(|f| Val::Float(*f)));
232                        }
233                        Val::StrVec(inner) => {
234                            out.extend(inner.iter().map(|s| Val::Str(s.clone())));
235                        }
236                        _ => {}
237                    },
238                    Val::IntVec(inner) => {
239                        // IntVec is already flat-of-scalars — extend once.
240                        out.extend(inner.iter().map(|n| Val::Int(*n)));
241                    }
242                    Val::FloatVec(inner) => {
243                        out.extend(inner.iter().map(|f| Val::Float(*f)));
244                    }
245                    Val::StrVec(inner) => {
246                        out.extend(inner.iter().map(|s| Val::Str(s.clone())));
247                    }
248                    other => out.push(other),
249                }
250            }
251            Val::arr(out)
252        }
253        // Columnar receiver — already flat of scalars.
254        Val::IntVec(_) | Val::FloatVec(_) | Val::StrVec(_) => v,
255        other => other,
256    }
257}
258
259pub fn zip_arrays(a: Val, b: Val, longest: bool, fill: Val) -> Result<Val, EvalError> {
260    let av = a.as_vals().map(|c| c.into_owned()).unwrap_or_default();
261    let bv = b.as_vals().map(|c| c.into_owned()).unwrap_or_default();
262    let len = if longest { av.len().max(bv.len()) } else { av.len().min(bv.len()) };
263    Ok(Val::arr((0..len).map(|i| Val::arr(vec![
264        av.get(i).cloned().unwrap_or_else(|| fill.clone()),
265        bv.get(i).cloned().unwrap_or_else(|| fill.clone()),
266    ])).collect()))
267}
268
269pub fn cartesian(arrays: &[Vec<Val>]) -> Vec<Vec<Val>> {
270    arrays.iter().fold(vec![vec![]], |acc, arr| {
271        acc.into_iter().flat_map(|prefix| {
272            arr.iter().map(move |item| {
273                let mut row = prefix.clone();
274                row.push(item.clone());
275                row
276            }).collect::<Vec<_>>()
277        }).collect()
278    })
279}
280
281// ── Field existence ───────────────────────────────────────────────────────────
282
283pub fn field_exists_nested(v: &Val, path: &str) -> bool {
284    let mut parts = path.splitn(2, '.');
285    let first = parts.next().unwrap_or("");
286    match (v.get(first), parts.next()) {
287        (Some(v), _) if v.is_null() => false,
288        (Some(_), None)             => true,
289        (Some(child), Some(rest))   => field_exists_nested(child, rest),
290        (None, _)                   => false,
291    }
292}
293
294// ── Deep merge ────────────────────────────────────────────────────────────────
295
296pub fn deep_merge(base: Val, other: Val) -> Val {
297    match (base, other) {
298        (Val::Obj(bm), Val::Obj(om)) => {
299            let mut map = Arc::try_unwrap(bm).unwrap_or_else(|m| (*m).clone());
300            for (k, v) in Arc::try_unwrap(om).unwrap_or_else(|m| (*m).clone()) {
301                let existing = map.shift_remove(&k);
302                map.insert(k, match existing {
303                    Some(e) => deep_merge(e, v),
304                    None    => v,
305                });
306            }
307            Val::obj(map)
308        }
309        (_, other) => other,
310    }
311}
312
313/// Deep-merge where arrays concatenate instead of replace.  Used by the
314/// `...**` spread operator.  Objects recurse, arrays concat, scalars rhs-wins.
315pub fn deep_merge_concat(base: Val, other: Val) -> Val {
316    match (base, other) {
317        (Val::Obj(bm), Val::Obj(om)) => {
318            let mut map = Arc::try_unwrap(bm).unwrap_or_else(|m| (*m).clone());
319            for (k, v) in Arc::try_unwrap(om).unwrap_or_else(|m| (*m).clone()) {
320                let existing = map.shift_remove(&k);
321                map.insert(k, match existing {
322                    Some(e) => deep_merge_concat(e, v),
323                    None    => v,
324                });
325            }
326            Val::obj(map)
327        }
328        (Val::Arr(ba), Val::Arr(oa)) => {
329            let mut a = Arc::try_unwrap(ba).unwrap_or_else(|a| (*a).clone());
330            for v in Arc::try_unwrap(oa).unwrap_or_else(|a| (*a).clone()) { a.push(v); }
331            Val::arr(a)
332        }
333        (base, other)
334            if (base.is_array() && other.is_array())
335            && (matches!(&base, Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_))
336                || matches!(&other, Val::StrVec(_) | Val::IntVec(_) | Val::FloatVec(_))) =>
337        {
338            let mut a = base.into_vec().unwrap_or_default();
339            if let Some(v) = other.into_vec() { a.extend(v); }
340            Val::arr(a)
341        }
342        (_, other) => other,
343    }
344}
345
346// ── Object building helpers ───────────────────────────────────────────────────
347
348/// Build a Val::Obj with two string-key entries (common pattern for itertools output).
349pub fn obj2(k1: &str, v1: Val, k2: &str, v2: Val) -> Val {
350    let mut m = IndexMap::with_capacity(2);
351    m.insert(val_key(k1), v1);
352    m.insert(val_key(k2), v2);
353    Val::obj(m)
354}