Skip to main content

relon_eval_api/
value.rs

1use crate::scope::Scope;
2use crate::smol_str::SmolStr;
3use ordered_float::OrderedFloat;
4use relon_parser::Node;
5use serde::de::{self, MapAccess, SeqAccess, Visitor};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
11pub struct ValueDict {
12    /// P2-17: dict keys land in a `SmolStr` so ≤ 22-byte field names
13    /// (the overwhelming majority — see corpus telemetry) ride the
14    /// inline slot and skip the per-key `String` allocation. `SmolStr`
15    /// implements `Borrow<str>` so existing `.get(&str)` /
16    /// `.contains_key(&str)` callsites keep working unchanged.
17    pub map: BTreeMap<SmolStr, Value>,
18    pub brand: Option<String>,
19    /// Name of the parent sum-type Enum when this dict is a tagged-enum
20    /// variant. `Some("Notification")` distinguishes a `Notification.Email`
21    /// payload from a plain `#schema Email { ... }` value (both have
22    /// `brand = Some("Email")`); the JSON serializer uses it to wrap the
23    /// payload as `{ Email: { ... } }` only for the variant case.
24    pub variant_of: Option<String>,
25}
26
27impl ValueDict {
28    /// Build a `ValueDict` from any iterable of key/value pairs. Accepts
29    /// both `SmolStr` (zero-cost) and `String` (consumed and SSO'd via
30    /// `SmolStr::from`) keys; see [`Value::dict`] for the wider
31    /// constructor.
32    pub fn new<K, I>(map: I) -> Self
33    where
34        K: Into<SmolStr>,
35        I: IntoIterator<Item = (K, Value)>,
36    {
37        Self {
38            map: map.into_iter().map(|(k, v)| (k.into(), v)).collect(),
39            brand: None,
40            variant_of: None,
41        }
42    }
43
44    /// Build a branded `ValueDict`. See [`ValueDict::new`] for the
45    /// key-type contract.
46    pub fn with_brand<K, I>(map: I, brand: Option<String>) -> Self
47    where
48        K: Into<SmolStr>,
49        I: IntoIterator<Item = (K, Value)>,
50    {
51        Self {
52            map: map.into_iter().map(|(k, v)| (k.into(), v)).collect(),
53            brand,
54            variant_of: None,
55        }
56    }
57}
58
59impl Serialize for ValueDict {
60    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61    where
62        S: serde::Serializer,
63    {
64        self.map.serialize(serializer)
65    }
66}
67
68impl<'de> Deserialize<'de> for ValueDict {
69    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70    where
71        D: serde::Deserializer<'de>,
72    {
73        let map = BTreeMap::deserialize(deserializer)?;
74        Ok(ValueDict {
75            map,
76            brand: None,
77            variant_of: None,
78        })
79    }
80}
81
82impl PartialEq for ValueDict {
83    fn eq(&self, other: &Self) -> bool {
84        self.map == other.map && self.brand == other.brand && self.variant_of == other.variant_of
85    }
86}
87
88#[derive(Debug, PartialEq, Clone)]
89pub struct SchemaField {
90    pub type_hint: relon_parser::TypeNode,
91    /// Predicates that the field's value must satisfy.
92    ///
93    /// Multiple predicates are AND-combined at validation time. `Wildcard`
94    /// entries are skipped. Stored as a `Vec` (rather than a single `Value`)
95    /// so `Schema + Schema` composition can accumulate constraints from both
96    /// sides instead of letting the right-hand operand silently overwrite the
97    /// left.
98    pub predicates: Vec<Value>,
99    pub custom_error: Option<String>,
100    pub default_value: Option<Value>,
101}
102
103/// Inline payload for `Value::Closure`, boxed out of the enum so the
104/// `Value` discriminant width is governed by the cheap variants. The
105/// closure body (`Node`) plus captured scope dwarf the other variants
106/// by tens of bytes — keeping them inline widens every `Value` on the
107/// stack, every HashMap bucket holding `Value`, and every list slot.
108#[derive(Debug, Clone)]
109pub struct ClosureData {
110    pub params: Vec<String>,
111    /// P2-2: closure body shared via `Arc<Node>` so `xs.map(f)` and
112    /// `Value::Closure::clone()` only bump the body's refcount instead
113    /// of deep-cloning the AST per element. The reference shape
114    /// (`&closure.body`) keeps existing consumers source-compatible —
115    /// `Arc<Node>` auto-derefs to `&Node` for `eval_node` calls.
116    pub body: Arc<Node>,
117    pub captured_env: Arc<Scope>,
118}
119
120/// Inline payload for `Value::Schema`, refcounted out of the enum for
121/// the same width rationale as [`ClosureData`]: the inner
122/// `HashMap<String, SchemaField>` keeps a raw-table header that pushes
123/// the enum width into the >100-byte range when stored inline. The
124/// payload rides an `Arc` (P2-5) so cloning a `Value::Schema` — which
125/// `check_type` does on every typed-field access — only bumps a
126/// refcount instead of deep-cloning the field map.
127#[derive(Debug, Clone)]
128pub struct SchemaData {
129    pub generics: Vec<String>,
130    pub fields: std::collections::HashMap<String, SchemaField>,
131    pub tuple_elements: Option<Vec<relon_parser::TypeNode>>,
132}
133
134/// Inline payload for `Value::EnumSchema`, refcounted for the same
135/// reason: the nested `HashMap<String, HashMap<String, SchemaField>>`
136/// is the largest variant we hold today, and `Arc` indirection
137/// collapses it to a single pointer in the enum layout while keeping
138/// clones O(1).
139#[derive(Debug, Clone)]
140pub struct EnumSchemaData {
141    pub name: String,
142    pub generics: Vec<String>,
143    pub variants: std::collections::HashMap<String, std::collections::HashMap<String, SchemaField>>,
144}
145
146/// Aggregate value type produced by the evaluator.
147///
148/// `List`, `Tuple`, and `Dict` payloads are reference-counted: cloning a
149/// `Value::List`, `Value::Tuple`, or `Value::Dict` only bumps an `Arc` and
150/// does not copy the underlying
151/// collection. Mutations go through `Arc::make_mut` (see [`Value::list_mut`]
152/// and [`Value::dict_mut`]), which clones the inner value lazily on first
153/// write — so existing aliases keep their snapshot semantics. This matters
154/// because the evaluator caches resolved paths and module results in shared
155/// `path_cache`/`module_cache` maps; without `Arc`-sharing every cache hit
156/// would deep-clone the cached structure.
157///
158/// The "heavy" variants (`Closure`, `Schema`, `EnumSchema`) live behind
159/// pointers so the enum stays narrow: the comprehension hot loop stores
160/// `Value`s in per-iteration scope HashMaps, and the bucket size scales
161/// with the enum width. `Schema` / `EnumSchema` use `Arc` (P2-5) — the
162/// `check_type` path clones the schema value out of the type table per
163/// typed-field validation, and a deep field-map clone there was a
164/// measurable cost; refcount-clone collapses it to a single atomic bump
165/// while keeping immutable-snapshot semantics.
166#[derive(Debug, Clone, Serialize)]
167#[serde(untagged)]
168pub enum Value {
169    Bool(bool),
170    Int(i64),
171    Float(OrderedFloat<f64>),
172    /// Short-string-optimized: ≤ 22 byte payloads inline in the value
173    /// slot (no heap alloc), longer payloads ride a refcounted
174    /// `Arc<str>` so clones stay O(1). See [`SmolStr`].
175    String(SmolStr),
176    List(Arc<Vec<Value>>),
177    Tuple(Arc<Vec<Value>>),
178    Dict(Arc<ValueDict>),
179    /// A unified closure (can be used as a function or a decorator).
180    /// Payload is boxed; see [`ClosureData`].
181    #[serde(skip)]
182    Closure(Box<ClosureData>),
183    /// A user-defined type schema. Payload is refcounted; see [`SchemaData`].
184    #[serde(skip)]
185    Schema(Arc<SchemaData>),
186    /// A tagged-enum (sum-type) schema: variants by name, each with its
187    /// own field set. Built from `#enum Name { Var1 { ... }, ... }`
188    /// declarations. Construction with `Name.Var1 { ... }` is
189    /// dispatched via this value.
190    /// Payload is refcounted; see [`EnumSchemaData`].
191    #[serde(skip)]
192    EnumSchema(Arc<EnumSchemaData>),
193    /// A single type description. The payload (`TypeNode`) carries a
194    /// `TokenRange` plus a `Vec<TypeNode>` of generics that together push
195    /// the inline size past 100 bytes; boxing keeps the enum compact
196    /// (matching the rationale for [`ClosureData`] et al.).
197    #[serde(skip)]
198    Type(Box<relon_parser::TypeNode>),
199    /// A wildcard predicate (*)
200    Wildcard,
201}
202
203impl<'de> Deserialize<'de> for Value {
204    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205    where
206        D: serde::Deserializer<'de>,
207    {
208        deserializer.deserialize_any(ValueVisitor)
209    }
210}
211
212struct ValueVisitor;
213
214impl<'de> Visitor<'de> for ValueVisitor {
215    type Value = Value;
216
217    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        formatter.write_str("a Relon value; JSON null is only valid with an Option<T> target")
219    }
220
221    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
222        Ok(Value::Bool(value))
223    }
224
225    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
226        Ok(Value::Int(value))
227    }
228
229    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
230    where
231        E: de::Error,
232    {
233        let value =
234            i64::try_from(value).map_err(|_| E::custom("integer is out of range for Int"))?;
235        Ok(Value::Int(value))
236    }
237
238    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
239        Ok(Value::Float(OrderedFloat(value)))
240    }
241
242    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
243        Ok(Value::String(SmolStr::from(value)))
244    }
245
246    fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E> {
247        Ok(Value::String(SmolStr::from(value)))
248    }
249
250    fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
251        Ok(Value::String(SmolStr::from(value)))
252    }
253
254    fn visit_none<E>(self) -> Result<Self::Value, E>
255    where
256        E: de::Error,
257    {
258        Err(E::custom(
259            "JSON null is not a Relon value; use an Option<T> target type so it decodes as None",
260        ))
261    }
262
263    fn visit_unit<E>(self) -> Result<Self::Value, E>
264    where
265        E: de::Error,
266    {
267        Err(E::custom(
268            "JSON null is not a Relon value; use an Option<T> target type so it decodes as None",
269        ))
270    }
271
272    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
273    where
274        A: SeqAccess<'de>,
275    {
276        let mut values = Vec::new();
277        while let Some(value) = seq.next_element::<Value>()? {
278            values.push(value);
279        }
280        Ok(Value::list(values))
281    }
282
283    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
284    where
285        A: MapAccess<'de>,
286    {
287        let mut values = BTreeMap::new();
288        while let Some((key, value)) = map.next_entry::<String, Value>()? {
289            values.insert(SmolStr::from(key), value);
290        }
291        Ok(Value::Dict(Arc::new(ValueDict {
292            map: values,
293            brand: None,
294            variant_of: None,
295        })))
296    }
297}
298
299impl PartialEq for Value {
300    fn eq(&self, other: &Self) -> bool {
301        match (self, other) {
302            (Self::Bool(l), Self::Bool(r)) => l == r,
303            (Self::Int(l), Self::Int(r)) => l == r,
304            (Self::Float(l), Self::Float(r)) => l == r,
305            (Self::String(l), Self::String(r)) => l == r,
306            (Self::List(l), Self::List(r)) => l == r,
307            (Self::Tuple(l), Self::Tuple(r)) => l == r,
308            (Self::Dict(l), Self::Dict(r)) => l == r,
309            (Self::Schema(_), Self::Schema(_)) => false,
310            (Self::EnumSchema(_), Self::EnumSchema(_)) => false,
311            (Self::Type(l), Self::Type(r)) => l == r,
312            (Self::Wildcard, Self::Wildcard) => true,
313            (Self::Closure(a), Self::Closure(b)) => {
314                a.params == b.params
315                    && a.body == b.body
316                    && Arc::ptr_eq(&a.captured_env, &b.captured_env)
317            }
318            _ => false,
319        }
320    }
321}
322
323impl Value {
324    /// Build a `Value::List` from a `Vec`, taking ownership and wrapping it
325    /// in `Arc` so subsequent clones are O(1).
326    pub fn list(items: Vec<Value>) -> Self {
327        Self::List(Arc::new(items))
328    }
329
330    /// Build a `Value::Tuple` from a `Vec`, taking ownership and wrapping it
331    /// in `Arc` so subsequent clones are O(1).
332    pub fn tuple(items: Vec<Value>) -> Self {
333        Self::Tuple(Arc::new(items))
334    }
335
336    /// Build the standard `Some(value)` tagged value.
337    pub fn option_some(value: Value) -> Self {
338        let mut map = BTreeMap::new();
339        map.insert(SmolStr::from("value"), value);
340        Self::variant_dict(map, "Some".to_string(), "Option".to_string())
341    }
342
343    /// Build the standard `None` tagged value.
344    pub fn option_none() -> Self {
345        Self::variant_dict(
346            BTreeMap::<SmolStr, Value>::new(),
347            "None".to_string(),
348            "Option".to_string(),
349        )
350    }
351
352    /// Build the standard `Ok(value)` tagged value.
353    pub fn result_ok(value: Value) -> Self {
354        let mut map = BTreeMap::new();
355        map.insert(SmolStr::from("value"), value);
356        Self::variant_dict(map, "Ok".to_string(), "Result".to_string())
357    }
358
359    /// Build the standard `Err(error)` tagged value.
360    pub fn result_err(error: Value) -> Self {
361        let mut map = BTreeMap::new();
362        map.insert(SmolStr::from("error"), error);
363        Self::variant_dict(map, "Err".to_string(), "Result".to_string())
364    }
365
366    /// True for the standard `None` tagged value.
367    pub fn is_option_none(&self) -> bool {
368        matches!(
369            self,
370            Value::Dict(d)
371                if d.variant_of.as_deref() == Some("Option")
372                    && d.brand.as_deref() == Some("None")
373        )
374    }
375
376    /// Return the payload of a standard `Option.Some { value }` tagged value.
377    pub fn option_some_value(&self) -> Option<&Value> {
378        match self {
379            Value::Dict(d)
380                if d.variant_of.as_deref() == Some("Option")
381                    && d.brand.as_deref() == Some("Some") =>
382            {
383                d.map.get("value")
384            }
385            _ => None,
386        }
387    }
388
389    /// Build a `Value::Dict` from any iterable of key/value pairs. The
390    /// generic key accepts either a `SmolStr` (zero-cost) or a `String`
391    /// (consumed and short-string-optimised via `SmolStr::from`). Use
392    /// [`Value::branded_dict`] when the dict carries a nominal-type brand.
393    pub fn dict<K, I>(map: I) -> Self
394    where
395        K: Into<SmolStr>,
396        I: IntoIterator<Item = (K, Value)>,
397    {
398        Self::Dict(Arc::new(ValueDict {
399            map: map.into_iter().map(|(k, v)| (k.into(), v)).collect(),
400            brand: None,
401            variant_of: None,
402        }))
403    }
404
405    /// Build a `Value::Dict` with an explicit brand (the typed-dict tag set
406    /// after a successful `User x: { ... }` validation, etc.). See
407    /// [`Value::dict`] for the key-type contract.
408    pub fn branded_dict<K, I>(map: I, brand: Option<String>) -> Self
409    where
410        K: Into<SmolStr>,
411        I: IntoIterator<Item = (K, Value)>,
412    {
413        Self::Dict(Arc::new(ValueDict {
414            map: map.into_iter().map(|(k, v)| (k.into(), v)).collect(),
415            brand,
416            variant_of: None,
417        }))
418    }
419
420    /// Build a `Value::Dict` representing a tagged-enum variant: carries a
421    /// `brand` (the variant name) plus `variant_of` (the parent enum name).
422    /// The JSON projector uses `variant_of` to externally tag the output.
423    /// See [`Value::dict`] for the key-type contract.
424    pub fn variant_dict<K, I>(map: I, variant: String, enum_name: String) -> Self
425    where
426        K: Into<SmolStr>,
427        I: IntoIterator<Item = (K, Value)>,
428    {
429        Self::Dict(Arc::new(ValueDict {
430            map: map.into_iter().map(|(k, v)| (k.into(), v)).collect(),
431            brand: Some(variant),
432            variant_of: Some(enum_name),
433        }))
434    }
435
436    /// In-place mutable handle to a `Value::List`'s inner `Vec`. Clones the
437    /// inner allocation only if the `Arc` is shared with another holder.
438    /// Returns `None` for non-list values.
439    pub fn list_mut(&mut self) -> Option<&mut Vec<Value>> {
440        match self {
441            Value::List(arc) => Some(Arc::make_mut(arc)),
442            _ => None,
443        }
444    }
445
446    /// In-place mutable handle to a `Value::Dict`'s inner [`ValueDict`].
447    /// CoW semantics — see [`Value::list_mut`].
448    pub fn dict_mut(&mut self) -> Option<&mut ValueDict> {
449        match self {
450            Value::Dict(arc) => Some(Arc::make_mut(arc)),
451            _ => None,
452        }
453    }
454
455    pub fn is_truthy(&self) -> bool {
456        match self {
457            Value::Bool(b) => *b,
458            Value::Int(i) => *i != 0,
459            Value::Float(f) => f.into_inner() != 0.0,
460            Value::String(s) => !s.is_empty(),
461            Value::List(l) | Value::Tuple(l) => !l.is_empty(),
462            Value::Dict(d) => !d.map.is_empty(),
463            Value::Closure(_) => true,
464            Value::Schema(_) => true,
465            Value::EnumSchema(_) => true,
466            Value::Type(_) => true,
467            Value::Wildcard => true,
468        }
469    }
470
471    pub fn type_name(&self) -> &'static str {
472        match self {
473            Value::Bool(_) => "Bool",
474            Value::Int(_) => "Int",
475            Value::Float(_) => "Float",
476            Value::String(_) => "String",
477            Value::List(_) => "List",
478            Value::Tuple(_) => "Tuple",
479            Value::Dict(_) => "Dict",
480            Value::Closure(_) => "Closure",
481            Value::Schema(_) => "Schema",
482            Value::EnumSchema(_) => "EnumSchema",
483            Value::Type(_) => "Type",
484            Value::Wildcard => "Wildcard",
485        }
486    }
487
488    pub fn deep_merge(&mut self, patch: &Value) {
489        match (self, patch) {
490            (Value::Dict(base), Value::Dict(patch)) => {
491                let base = Arc::make_mut(base);
492                for (k, v) in &patch.map {
493                    if let Some(base_val) = base.map.get_mut(k) {
494                        base_val.deep_merge(v);
495                    } else {
496                        base.map.insert(k.clone(), v.clone());
497                    }
498                }
499            }
500            (Value::Schema(base), Value::Schema(patch)) => {
501                // P2-5: `base` is now `Arc<SchemaData>`. Materialise a
502                // unique handle via `Arc::make_mut` so we only deep-copy
503                // when another holder still aliases this schema; the
504                // typical post-eval merge path holds the only refcount
505                // and stays clone-free.
506                let base = Arc::make_mut(base);
507                let base_fields = &mut base.fields;
508                let patch_fields = &patch.fields;
509                for (k, v) in patch_fields {
510                    if let Some(base_field) = base_fields.get_mut(k) {
511                        base_field.type_hint = v.type_hint.clone();
512                        // AND-merge predicates rather than overwrite, mirroring
513                        // the static `extract_schema_for_node` composition path.
514                        for pred in &v.predicates {
515                            if !matches!(pred, Value::Wildcard) {
516                                base_field.predicates.push(pred.clone());
517                            }
518                        }
519                        if v.custom_error.is_some() {
520                            base_field.custom_error = v.custom_error.clone();
521                        }
522                        if v.default_value.is_some() {
523                            base_field.default_value = v.default_value.clone();
524                        }
525                    } else {
526                        base_fields.insert(k.clone(), v.clone());
527                    }
528                }
529            }
530            (Value::Schema(base), Value::Dict(patch_data)) => {
531                let base = Arc::make_mut(base);
532                let base_fields = &mut base.fields;
533                for (k, v) in &patch_data.map {
534                    if let Some(base_field) = base_fields.get_mut(k.as_str()) {
535                        base_field.default_value = Some(v.clone());
536                    }
537                }
538            }
539            (b, p) => *b = p.clone(),
540        }
541    }
542}
543
544impl std::fmt::Display for Value {
545    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546        match self {
547            Value::Bool(b) => write!(f, "{}", b),
548            Value::Int(i) => write!(f, "{}", i),
549            Value::Float(fl) => write!(f, "{}", fl),
550            Value::String(s) => write!(f, "{}", s),
551            Value::List(l) => write!(f, "{:?}", l),
552            Value::Tuple(l) => {
553                write!(f, "(")?;
554                for (i, item) in l.iter().enumerate() {
555                    if i > 0 {
556                        write!(f, ", ")?;
557                    }
558                    write!(f, "{item}")?;
559                }
560                if l.len() == 1 {
561                    write!(f, ",")?;
562                }
563                write!(f, ")")
564            }
565            Value::Dict(d) => write!(f, "{:?}", d.map),
566            Value::Closure(_) => write!(f, "<closure>"),
567            Value::Schema(_) => write!(f, "<schema>"),
568            Value::EnumSchema(enum_data) => write!(f, "<enum {}>", enum_data.name),
569            Value::Type(t) => write!(f, "Type<{}>", relon_analyzer::format_type(t)),
570            Value::Wildcard => write!(f, "*"),
571        }
572    }
573}
574
575#[cfg(test)]
576mod deserialize_tests {
577    use super::Value;
578
579    #[test]
580    fn rejects_json_null_at_root() {
581        let err = serde_json::from_value::<Value>(serde_json::json!(null)).unwrap_err();
582        assert!(
583            err.to_string().contains("JSON null is not a Relon value"),
584            "unexpected error: {err}"
585        );
586    }
587
588    #[test]
589    fn rejects_json_null_inside_list() {
590        let err = serde_json::from_value::<Value>(serde_json::json!([1, null])).unwrap_err();
591        assert!(
592            err.to_string().contains("JSON null is not a Relon value"),
593            "unexpected error: {err}"
594        );
595    }
596
597    #[test]
598    fn rejects_json_null_inside_dict() {
599        let err = serde_json::from_value::<Value>(serde_json::json!({ "k": null })).unwrap_err();
600        assert!(
601            err.to_string().contains("JSON null is not a Relon value"),
602            "unexpected error: {err}"
603        );
604    }
605}
606
607#[cfg(test)]
608mod size_guard {
609    use super::Value;
610
611    /// Hard ceiling on `Value` enum width. The comprehension hot loop
612    /// stores `Value`s in per-iteration scope HashMaps; bucket size scales
613    /// with the enum width, so a regression here translates directly into
614    /// MB-scale waste on the comprehension workload (dhat profile attributes
615    /// it to `HashMap::insert`'s grow path). 48 bytes leaves headroom for
616    /// the existing `String(String)` (24 B) + 1-byte tag, plus a couple of
617    /// future smallvec / cow-string tweaks before we have to rebox.
618    #[test]
619    fn value_enum_is_compact() {
620        let size = std::mem::size_of::<Value>();
621        eprintln!("Value enum size: {} bytes", size);
622        assert!(size <= 48, "Value enum grew: {} bytes", size);
623    }
624}