behavior_contracts/value.rs
1//! Runtime value model — shared across all COMMON primitives.
2//!
3//! Mirrors the TS/Python reference model: int = checked i64, float = IEEE754 f64
4//! (NaN/±Inf are Failures at the boundary), string, bool, null, arr, obj.
5//! The distinction between `Int` and `Float` is normative: `int 1 != float 1.0`.
6
7use std::collections::BTreeMap;
8
9/// A runtime value. Object key ordering is preserved via a `Vec` of pairs so that
10/// canonical serialization can apply an explicit code-point sort (a `BTreeMap`
11/// would impose `String` Ord which — while it happens to equal code-point order —
12/// we keep explicit for clarity and insertion-order retention where required).
13#[derive(Debug, Clone)]
14pub enum Value {
15 Null,
16 Bool(bool),
17 Int(i64),
18 Float(f64),
19 Str(String),
20 Arr(Vec<Value>),
21 /// Object: insertion-ordered key/value pairs.
22 Obj(Vec<(String, Value)>),
23}
24
25impl Value {
26 pub fn type_name(&self) -> &'static str {
27 match self {
28 Value::Null => "null",
29 Value::Bool(_) => "bool",
30 Value::Int(_) => "int",
31 Value::Float(_) => "float",
32 Value::Str(_) => "string",
33 Value::Arr(_) => "arr",
34 Value::Obj(_) => "obj",
35 }
36 }
37
38 /// Look up a key in an object value (linear scan; objects are small).
39 pub fn obj_get(&self, key: &str) -> Option<&Value> {
40 if let Value::Obj(pairs) = self {
41 pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v)
42 } else {
43 None
44 }
45 }
46}
47
48/// Deep structural equality with strict int/float type distinction (PROTOCOL §4.1).
49///
50/// - `Int(1)` is NOT equal to `Float(1.0)`.
51/// - NaN == NaN is treated as equal (used only for Failure-path fixtures).
52/// - strings compare by content (code points), arrays are ordered, objects are
53/// key-set + value equal (order-independent).
54pub fn deep_equals(a: &Value, b: &Value) -> bool {
55 match (a, b) {
56 (Value::Null, Value::Null) => true,
57 (Value::Bool(x), Value::Bool(y)) => x == y,
58 (Value::Int(x), Value::Int(y)) => x == y,
59 (Value::Float(x), Value::Float(y)) => x == y || (x.is_nan() && y.is_nan()),
60 (Value::Str(x), Value::Str(y)) => x == y,
61 (Value::Arr(x), Value::Arr(y)) => {
62 x.len() == y.len() && x.iter().zip(y).all(|(u, v)| deep_equals(u, v))
63 }
64 (Value::Obj(x), Value::Obj(y)) => {
65 if x.len() != y.len() {
66 return false;
67 }
68 // key-set + per-key value equality (order independent)
69 let mx: BTreeMap<&str, &Value> = x.iter().map(|(k, v)| (k.as_str(), v)).collect();
70 let my: BTreeMap<&str, &Value> = y.iter().map(|(k, v)| (k.as_str(), v)).collect();
71 if mx.len() != my.len() {
72 return false; // duplicate keys collapsed — treat as unequal shape
73 }
74 mx.len() == my.len()
75 && mx
76 .iter()
77 .all(|(k, v)| my.get(k).map(|w| deep_equals(v, w)).unwrap_or(false))
78 }
79 _ => false,
80 }
81}