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}