Skip to main content

lex_bytecode/
value.rs

1//! Runtime values.
2
3use crate::program::BodyHash;
4use indexmap::IndexMap;
5use std::collections::{BTreeMap, BTreeSet, VecDeque};
6
7#[derive(Debug, Clone)]
8pub enum Value {
9    Int(i64),
10    Float(f64),
11    Bool(bool),
12    Str(String),
13    Bytes(Vec<u8>),
14    Unit,
15    List(Vec<Value>),
16    Tuple(Vec<Value>),
17    Record(IndexMap<String, Value>),
18    Variant { name: String, args: Vec<Value> },
19    /// First-class function value (a lambda + its captured locals). The
20    /// function's first `captures.len()` params bind to `captures`; the
21    /// remaining params are supplied at call time.
22    ///
23    /// `fn_id` is a dense compile-time index into `Program::functions`
24    /// for fast dispatch; `body_hash` is the **canonical identity** —
25    /// two closures with identical bytecode bodies compare equal even
26    /// when their `fn_id`s differ (which they will, when the source
27    /// has the same closure literal at two locations). See `PartialEq`
28    /// below and #222 for the rationale.
29    Closure { fn_id: u32, body_hash: BodyHash, captures: Vec<Value> },
30    /// Dense row-major `f64` matrix. A "fast lane" representation that
31    /// avoids the per-element `Value::Float` boxing of `Value::List`.
32    /// Used by Core's native tensor ops (matmul, dot, …) so end-to-end
33    /// matmul perf hits the §13.7 #1 100ms target without paying for
34    /// 2M Value boxings at the call boundary.
35    F64Array { rows: u32, cols: u32, data: Vec<f64> },
36    /// Persistent map keyed by `MapKey` (`Str` or `Int`). Insertion-
37    /// independent equality (sorted by `BTreeMap`'s `Ord`), so two
38    /// maps built from the same pairs in different orders compare
39    /// equal. Restricting keys to two primitive variants keeps
40    /// `Eq + Hash` requirements off `Value` itself, which has
41    /// closures and floats and can't be hashed soundly.
42    Map(BTreeMap<MapKey, Value>),
43    /// Persistent set with the same key-type discipline as `Map`.
44    Set(BTreeSet<MapKey>),
45    /// Double-ended queue. O(1) push/pop on both ends; otherwise
46    /// behaves like `List` for iteration / equality / JSON shape.
47    /// Lex's type system tracks `Deque[T]` separately from `List[T]`
48    /// so users explicitly opt in to deque semantics; the runtime
49    /// uses this dedicated variant rather than backing a deque on top
50    /// of `Value::List` (which would make `push_front` O(n)).
51    Deque(VecDeque<Value>),
52}
53
54/// Manual `PartialEq` for `Value` (#222). Mirrors the auto-derived
55/// implementation for every variant *except* `Closure`, which compares
56/// on `(body_hash, captures)` only — `fn_id` is a dense compile-time
57/// index that is not stable across source-location-equivalent closure
58/// literals, and including it would defeat the canonicality property
59/// the `body_hash` field exists to provide.
60impl PartialEq for Value {
61    fn eq(&self, other: &Self) -> bool {
62        use Value::*;
63        match (self, other) {
64            (Int(a), Int(b)) => a == b,
65            (Float(a), Float(b)) => a == b,
66            (Bool(a), Bool(b)) => a == b,
67            (Str(a), Str(b)) => a == b,
68            (Bytes(a), Bytes(b)) => a == b,
69            (Unit, Unit) => true,
70            (List(a), List(b)) => a == b,
71            (Tuple(a), Tuple(b)) => a == b,
72            (Record(a), Record(b)) => a == b,
73            (Variant { name: an, args: aa }, Variant { name: bn, args: ba }) =>
74                an == bn && aa == ba,
75            (Closure { body_hash: ah, captures: ac, .. },
76             Closure { body_hash: bh, captures: bc, .. }) =>
77                ah == bh && ac == bc,
78            (F64Array { rows: ar, cols: ac, data: ad },
79             F64Array { rows: br, cols: bc, data: bd }) =>
80                ar == br && ac == bc && ad == bd,
81            (Map(a), Map(b)) => a == b,
82            (Set(a), Set(b)) => a == b,
83            (Deque(a), Deque(b)) => a == b,
84            _ => false,
85        }
86    }
87}
88
89/// Hashable, ordered key for `Value::Map` / `Value::Set`. v1
90/// supports `Str` and `Int`; extending to other primitives or to
91/// records is forward-compatible since the type is not exposed
92/// to user code beyond the surface API.
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum MapKey {
95    Str(String),
96    Int(i64),
97}
98
99impl MapKey {
100    pub fn from_value(v: &Value) -> Result<Self, String> {
101        match v {
102            Value::Str(s) => Ok(MapKey::Str(s.clone())),
103            Value::Int(n) => Ok(MapKey::Int(*n)),
104            other => Err(format!(
105                "map/set key must be Str or Int, got {other:?}")),
106        }
107    }
108    pub fn into_value(self) -> Value {
109        match self {
110            MapKey::Str(s) => Value::Str(s),
111            MapKey::Int(n) => Value::Int(n),
112        }
113    }
114    pub fn as_value(&self) -> Value {
115        match self {
116            MapKey::Str(s) => Value::Str(s.clone()),
117            MapKey::Int(n) => Value::Int(*n),
118        }
119    }
120}
121
122impl Value {
123    pub fn as_int(&self) -> i64 {
124        match self { Value::Int(n) => *n, other => panic!("expected Int, got {other:?}") }
125    }
126    pub fn as_float(&self) -> f64 {
127        match self { Value::Float(n) => *n, other => panic!("expected Float, got {other:?}") }
128    }
129    pub fn as_bool(&self) -> bool {
130        match self { Value::Bool(b) => *b, other => panic!("expected Bool, got {other:?}") }
131    }
132    pub fn as_str(&self) -> &str {
133        match self { Value::Str(s) => s, other => panic!("expected Str, got {other:?}") }
134    }
135
136    /// Render this `Value` as a `serde_json::Value` for emission to
137    /// CLI output, the agent API, conformance harness reports, etc.
138    /// Canonical mapping shared across crates; previously every
139    /// boundary had its own copy.
140    ///
141    /// Encoding:
142    /// - `Variant { name, args }` → `{"$variant": name, "args": [...]}`
143    /// - `F64Array { ... }` → `{"$f64_array": true, rows, cols, data}`
144    /// - `Closure { body_hash, .. }` → `"<closure HEX8>"` (first 8 hex
145    ///   chars of the body hash; equivalent closures across source
146    ///   locations render identically — see #222)
147    /// - `Bytes` → `{"$bytes": "deadbeef"}` (lowercase hex). Round-trips
148    ///   through `from_json`. Bare hex strings decode as `Str`, so the
149    ///   marker is required to disambiguate bytes from a string that
150    ///   happens to look like hex.
151    /// - `Map` with all-`Str` keys → JSON object; otherwise array of
152    ///   `[key, value]` pairs (Int keys can't be JSON-object keys)
153    /// - `Set` → JSON array of elements
154    /// - other variants → their natural JSON shape
155    ///
156    /// Note: this form is **not** round-trippable for traces (see
157    /// `lex-trace`'s recorder, which uses a richer marker form).
158    pub fn to_json(&self) -> serde_json::Value {
159        use serde_json::Value as J;
160        match self {
161            Value::Int(n) => J::from(*n),
162            Value::Float(f) => J::from(*f),
163            Value::Bool(b) => J::Bool(*b),
164            Value::Str(s) => J::String(s.clone()),
165            Value::Bytes(b) => {
166                let hex: String = b.iter().map(|b| format!("{:02x}", b)).collect();
167                let mut m = serde_json::Map::new();
168                m.insert("$bytes".into(), J::String(hex));
169                J::Object(m)
170            }
171            Value::Unit => J::Null,
172            Value::List(items) => J::Array(items.iter().map(Value::to_json).collect()),
173            Value::Tuple(items) => J::Array(items.iter().map(Value::to_json).collect()),
174            Value::Record(fields) => {
175                let mut m = serde_json::Map::new();
176                for (k, v) in fields { m.insert(k.clone(), v.to_json()); }
177                J::Object(m)
178            }
179            Value::Variant { name, args } => {
180                let mut m = serde_json::Map::new();
181                m.insert("$variant".into(), J::String(name.clone()));
182                m.insert("args".into(), J::Array(args.iter().map(Value::to_json).collect()));
183                J::Object(m)
184            }
185            Value::Closure { body_hash, .. } => {
186                // Render the first 4 bytes (8 hex chars) of the body
187                // hash. Trace stability follows: equivalent closures
188                // produced from different source locations get the
189                // same string. See #222.
190                let prefix: String = body_hash.iter().take(4)
191                    .map(|b| format!("{b:02x}")).collect();
192                J::String(format!("<closure {prefix}>"))
193            }
194            Value::F64Array { rows, cols, data } => {
195                let mut m = serde_json::Map::new();
196                m.insert("$f64_array".into(), J::Bool(true));
197                m.insert("rows".into(), J::from(*rows));
198                m.insert("cols".into(), J::from(*cols));
199                m.insert("data".into(), J::Array(data.iter().map(|f| J::from(*f)).collect()));
200                J::Object(m)
201            }
202            Value::Map(m) => {
203                let all_str = m.keys().all(|k| matches!(k, MapKey::Str(_)));
204                if all_str {
205                    let mut out = serde_json::Map::new();
206                    for (k, v) in m {
207                        if let MapKey::Str(s) = k {
208                            out.insert(s.clone(), v.to_json());
209                        }
210                    }
211                    J::Object(out)
212                } else {
213                    J::Array(m.iter().map(|(k, v)| {
214                        J::Array(vec![k.as_value().to_json(), v.to_json()])
215                    }).collect())
216                }
217            }
218            Value::Set(s) => J::Array(
219                s.iter().map(|k| k.as_value().to_json()).collect()),
220            Value::Deque(items) => J::Array(items.iter().map(Value::to_json).collect()),
221        }
222    }
223
224    /// Decode a `serde_json::Value` into a `Value`. The inverse of
225    /// [`to_json`](Self::to_json) for the shapes Lex round-trips:
226    ///
227    /// - `{"$variant": "Name", "args": [...]}` → `Value::Variant`
228    /// - `{"$bytes": "deadbeef"}` → `Value::Bytes` (lowercase hex; an
229    ///   odd-length string or non-hex character falls through to
230    ///   `Value::Record`, matching the malformed-`$variant` fallback)
231    /// - JSON object → `Value::Record`
232    /// - JSON array → `Value::List`
233    /// - JSON null → `Value::Unit`
234    /// - JSON string / bool / number → the corresponding scalar
235    ///
236    /// Map, Set, F64Array, and Closure don't round-trip — they decode
237    /// as their natural JSON shape (Object / Array / Object / Str
238    /// respectively), since the CLI / HTTP / VM callers building Values
239    /// from JSON don't have those shapes in their input vocabulary.
240    pub fn from_json(v: &serde_json::Value) -> Value {
241        use serde_json::Value as J;
242        match v {
243            J::Null => Value::Unit,
244            J::Bool(b) => Value::Bool(*b),
245            J::Number(n) => {
246                if let Some(i) = n.as_i64() { Value::Int(i) }
247                else if let Some(f) = n.as_f64() { Value::Float(f) }
248                else { Value::Unit }
249            }
250            J::String(s) => Value::Str(s.clone()),
251            J::Array(items) => Value::List(items.iter().map(Value::from_json).collect()),
252            J::Object(map) => {
253                if let (Some(J::String(name)), Some(J::Array(args))) =
254                    (map.get("$variant"), map.get("args"))
255                {
256                    return Value::Variant {
257                        name: name.clone(),
258                        args: args.iter().map(Value::from_json).collect(),
259                    };
260                }
261                if map.len() == 1 {
262                    if let Some(J::String(hex)) = map.get("$bytes") {
263                        if let Some(bytes) = decode_hex(hex) {
264                            return Value::Bytes(bytes);
265                        }
266                    }
267                }
268                let mut out = indexmap::IndexMap::new();
269                for (k, v) in map {
270                    out.insert(k.clone(), Value::from_json(v));
271                }
272                Value::Record(out)
273            }
274        }
275    }
276}
277
278/// Lowercase-hex → bytes. Returns `None` for odd length or non-hex chars
279/// (callers fall through to a record decode rather than erroring).
280fn decode_hex(s: &str) -> Option<Vec<u8>> {
281    if !s.len().is_multiple_of(2) { return None; }
282    let mut out = Vec::with_capacity(s.len() / 2);
283    let bytes = s.as_bytes();
284    for pair in bytes.chunks(2) {
285        let hi = (pair[0] as char).to_digit(16)?;
286        let lo = (pair[1] as char).to_digit(16)?;
287        out.push(((hi << 4) | lo) as u8);
288    }
289    Some(out)
290}