Skip to main content

voxgig_struct/
value.rs

1// Copyright (c) 2025-2026 Voxgig Ltd. MIT LICENSE.
2// VERSION: @voxgig/struct 0.1.0
3//
4// The in-memory JSON-shaped value type for the Rust port. See rs/PLAN.md.
5//
6// - `Noval` is the TS `undefined` — property absent. NOT a scalar.
7// - `Null` is JSON null — a real value, distinct from `Noval`.
8// - Lists and maps are `Rc<RefCell<...>>`: reference-stable, mutated in place.
9// - `OrderedMap` preserves insertion order (the inject machinery needs it);
10//   defined inline in `ordered_map.rs` — no third-party dependency.
11// - `Func` carries callables that live inside the data (transform commands, etc.).
12// - `Sentinel` (SKIP / DELETE) is compared by pointer identity.
13
14use std::cell::RefCell;
15use std::fmt;
16use std::rc::Rc;
17
18use crate::major::Inj;
19use crate::ordered_map::OrderedMap;
20
21pub type VList = Rc<RefCell<Vec<Value>>>;
22pub type VMap = Rc<RefCell<OrderedMap<Value>>>;
23
24/// Injector-shaped native function: `(inj, val, ref, store) -> any`.
25/// Thunks (e.g. `$WHEN`) just ignore the arguments. Takes the injection by
26/// `&Inj` (the `Rc<RefCell<…>>`) so injectors can re-borrow it as needed.
27pub type NativeFn = Rc<dyn Fn(&Inj, &Value, &str, &Value) -> Value>;
28
29/// `Modify` hook: mutates `parent[key]` (or `inj`), returns nothing.
30pub type ModifyFn = Rc<dyn Fn(&Value, &Value, &Value, &Inj, &Value)>;
31
32/// Identity-only marker for SKIP / DELETE.
33pub struct Sentinel {
34    pub tag: &'static str,
35}
36
37pub static SKIP: Sentinel = Sentinel { tag: "`$SKIP`" };
38pub static DELETE: Sentinel = Sentinel { tag: "`$DELETE`" };
39
40#[derive(Clone)]
41pub enum Value {
42    Noval,
43    Null,
44    Bool(bool),
45    Num(f64),
46    Str(String),
47    List(VList),
48    Map(VMap),
49    Func(NativeFn),
50    Sentinel(&'static Sentinel),
51}
52
53impl Value {
54    // ---- constructors --------------------------------------------------
55
56    pub fn list(items: Vec<Value>) -> Value {
57        Value::List(Rc::new(RefCell::new(items)))
58    }
59
60    pub fn empty_list() -> Value {
61        Value::list(Vec::new())
62    }
63
64    pub fn map(entries: OrderedMap<Value>) -> Value {
65        Value::Map(Rc::new(RefCell::new(entries)))
66    }
67
68    pub fn empty_map() -> Value {
69        Value::map(OrderedMap::new())
70    }
71
72    pub fn map_of<I: IntoIterator<Item = (String, Value)>>(pairs: I) -> Value {
73        let mut m = OrderedMap::new();
74        for (k, v) in pairs {
75            m.insert(k, v);
76        }
77        Value::map(m)
78    }
79
80    pub fn func<F>(f: F) -> Value
81    where
82        F: Fn(&Inj, &Value, &str, &Value) -> Value + 'static,
83    {
84        Value::Func(Rc::new(f))
85    }
86
87    pub fn str<S: Into<String>>(s: S) -> Value {
88        Value::Str(s.into())
89    }
90
91    pub fn skip() -> Value {
92        Value::Sentinel(&SKIP)
93    }
94
95    pub fn delete() -> Value {
96        Value::Sentinel(&DELETE)
97    }
98
99    // ---- predicates / accessors ---------------------------------------
100
101    pub fn is_noval(&self) -> bool {
102        matches!(self, Value::Noval)
103    }
104
105    pub fn is_null(&self) -> bool {
106        matches!(self, Value::Null)
107    }
108
109    /// JS `null == val` — true for both `undefined` and JSON `null`.
110    pub fn is_nullish(&self) -> bool {
111        matches!(self, Value::Noval | Value::Null)
112    }
113
114    pub fn is_skip(&self) -> bool {
115        matches!(self, Value::Sentinel(s) if std::ptr::eq(*s, &SKIP))
116    }
117
118    pub fn is_delete(&self) -> bool {
119        matches!(self, Value::Sentinel(s) if std::ptr::eq(*s, &DELETE))
120    }
121
122    pub fn as_str(&self) -> Option<&str> {
123        match self {
124            Value::Str(s) => Some(s.as_str()),
125            _ => None,
126        }
127    }
128
129    pub fn as_bool(&self) -> Option<bool> {
130        match self {
131            Value::Bool(b) => Some(*b),
132            _ => None,
133        }
134    }
135
136    pub fn as_num(&self) -> Option<f64> {
137        match self {
138            Value::Num(n) => Some(*n),
139            _ => None,
140        }
141    }
142
143    pub fn as_list(&self) -> Option<&VList> {
144        match self {
145            Value::List(l) => Some(l),
146            _ => None,
147        }
148    }
149
150    pub fn as_map(&self) -> Option<&VMap> {
151        match self {
152            Value::Map(m) => Some(m),
153            _ => None,
154        }
155    }
156
157    pub fn as_func(&self) -> Option<&NativeFn> {
158        match self {
159            Value::Func(f) => Some(f),
160            _ => None,
161        }
162    }
163
164    /// Truthy in the JS sense (used rarely; mostly for predicate returns).
165    pub fn truthy(&self) -> bool {
166        match self {
167            Value::Noval | Value::Null => false,
168            Value::Bool(b) => *b,
169            Value::Num(n) => *n != 0.0 && !n.is_nan(),
170            Value::Str(s) => !s.is_empty(),
171            _ => true,
172        }
173    }
174}
175
176// Deep, order-independent (for maps) equality — matches `deepStrictEqual`
177// semantics used by the corpus runner and the JSON-string fallback used by
178// `validate_EXACT`. Functions are never equal (pointer-eq would also do).
179impl PartialEq for Value {
180    fn eq(&self, other: &Value) -> bool {
181        match (self, other) {
182            (Value::Noval, Value::Noval) => true,
183            (Value::Null, Value::Null) => true,
184            (Value::Bool(a), Value::Bool(b)) => a == b,
185            (Value::Num(a), Value::Num(b)) => a == b,
186            (Value::Str(a), Value::Str(b)) => a == b,
187            (Value::Sentinel(a), Value::Sentinel(b)) => std::ptr::eq(*a, *b),
188            (Value::List(a), Value::List(b)) => {
189                if Rc::ptr_eq(a, b) {
190                    return true;
191                }
192                let a = a.borrow();
193                let b = b.borrow();
194                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x == y)
195            }
196            (Value::Map(a), Value::Map(b)) => {
197                if Rc::ptr_eq(a, b) {
198                    return true;
199                }
200                let a = a.borrow();
201                let b = b.borrow();
202                a.len() == b.len()
203                    && a.iter()
204                        .all(|(k, v)| b.get(k).map(|w| v == w).unwrap_or(false))
205            }
206            _ => false,
207        }
208    }
209}
210
211impl fmt::Debug for Value {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Value::Noval => write!(f, "Noval"),
215            Value::Null => write!(f, "Null"),
216            Value::Bool(b) => write!(f, "Bool({b})"),
217            Value::Num(n) => write!(f, "Num({n})"),
218            Value::Str(s) => write!(f, "Str({s:?})"),
219            Value::List(l) => write!(f, "List({:?})", l.borrow()),
220            Value::Map(m) => write!(f, "Map({:?})", m.borrow()),
221            Value::Func(_) => write!(f, "Func(..)"),
222            Value::Sentinel(s) => write!(f, "Sentinel({})", s.tag),
223        }
224    }
225}
226
227// Convenient `From` impls for building values in tests / the runner.
228impl From<bool> for Value {
229    fn from(b: bool) -> Value {
230        Value::Bool(b)
231    }
232}
233impl From<i64> for Value {
234    fn from(n: i64) -> Value {
235        Value::Num(n as f64)
236    }
237}
238impl From<i32> for Value {
239    fn from(n: i32) -> Value {
240        Value::Num(n as f64)
241    }
242}
243impl From<usize> for Value {
244    fn from(n: usize) -> Value {
245        Value::Num(n as f64)
246    }
247}
248impl From<f64> for Value {
249    fn from(n: f64) -> Value {
250        Value::Num(n)
251    }
252}
253impl From<&str> for Value {
254    fn from(s: &str) -> Value {
255        Value::Str(s.to_string())
256    }
257}
258impl From<String> for Value {
259    fn from(s: String) -> Value {
260        Value::Str(s)
261    }
262}
263impl<T: Into<Value>> From<Vec<T>> for Value {
264    fn from(v: Vec<T>) -> Value {
265        Value::list(v.into_iter().map(Into::into).collect())
266    }
267}
268
269// ---- JS numeric / string coercions ------------------------------------
270//
271// The canonical leans on JS coercions with no Rust stdlib equivalent.
272// Implemented faithfully for the cases the corpus exercises.
273
274/// `Number.isInteger(v)` — note `Number.isInteger(2.0) === true`.
275pub fn is_integer_f64(v: f64) -> bool {
276    v.is_finite() && v.fract() == 0.0
277}
278
279/// `String(v)` / `"" + v` for the value kinds that get stringified as keys.
280pub fn js_string(v: &Value) -> String {
281    match v {
282        Value::Noval => "undefined".to_string(),
283        Value::Null => "null".to_string(),
284        Value::Bool(b) => b.to_string(),
285        Value::Num(n) => num_to_string(*n),
286        Value::Str(s) => s.clone(),
287        // JS `String([1,2])` => "1,2"; `String({})` => "[object Object]".
288        Value::List(l) => l
289            .borrow()
290            .iter()
291            .map(|x| match x {
292                Value::Noval | Value::Null => String::new(),
293                _ => js_string(x),
294            })
295            .collect::<Vec<_>>()
296            .join(","),
297        Value::Map(_) => "[object Object]".to_string(),
298        Value::Func(_) => "function".to_string(),
299        Value::Sentinel(s) => s.tag.to_string(),
300    }
301}
302
303/// JS number -> string. Differs from JS only for very large / very small
304/// magnitudes where JS switches to exponent notation (documented gap).
305pub fn num_to_string(n: f64) -> String {
306    if n.is_nan() {
307        return "NaN".to_string();
308    }
309    if n.is_infinite() {
310        return if n > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
311    }
312    if n == 0.0 {
313        return "0".to_string();
314    }
315    if n.fract() == 0.0 && n.abs() < 1e21 {
316        // Integer-valued: no decimal point, no exponent.
317        return format!("{}", n as i128);
318    }
319    let s = format!("{n}");
320    s
321}
322
323/// JS unary `+x` / `Number(x)` (ToNumber). Returns NaN on failure.
324pub fn js_to_number(v: &Value) -> f64 {
325    match v {
326        Value::Noval => f64::NAN,
327        Value::Null => 0.0,
328        Value::Bool(b) => {
329            if *b {
330                1.0
331            } else {
332                0.0
333            }
334        }
335        Value::Num(n) => *n,
336        Value::Str(s) => js_string_to_number(s),
337        Value::List(l) => {
338            let b = l.borrow();
339            match b.len() {
340                0 => 0.0,
341                1 => js_to_number(&b[0]),
342                _ => f64::NAN,
343            }
344        }
345        _ => f64::NAN,
346    }
347}
348
349/// `Number("...")` for a string: trims, accepts decimal/hex/empty.
350pub fn js_string_to_number(s: &str) -> f64 {
351    let t = s.trim();
352    if t.is_empty() {
353        return 0.0;
354    }
355    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
356        return i64::from_str_radix(hex, 16)
357            .map(|n| n as f64)
358            .unwrap_or(f64::NAN);
359    }
360    t.parse::<f64>().unwrap_or(f64::NAN)
361}
362
363/// JS `n | 0` (ToInt32): truncate toward zero, wrap mod 2^32, signed.
364pub fn js_to_int32(n: f64) -> i32 {
365    if !n.is_finite() {
366        return 0;
367    }
368    let trunc = n.trunc();
369    // wrap into u32 then reinterpret as i32
370    let m = trunc.rem_euclid(4294967296.0);
371    m as u32 as i32
372}