Skip to main content

lex_bytecode/
value.rs

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