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}