1use indexmap::IndexMap;
5use lex_bytecode::vm::Tracer;
6use lex_bytecode::Value;
7use serde::{Deserialize, Serialize};
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct RunId(pub String);
12
13impl RunId {
14 pub fn new(seed: &str) -> Self {
15 use sha2::{Digest, Sha256};
16 let mut h = Sha256::new();
17 h.update(seed.as_bytes());
18 h.update(format!("{:?}", std::time::SystemTime::now()).as_bytes());
19 let r = h.finalize();
20 let mut hex = String::with_capacity(64);
21 for b in r { hex.push_str(&format!("{:02x}", b)); }
22 RunId(hex)
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27#[serde(rename_all = "snake_case")]
28pub enum TraceNodeKind { Call, Effect }
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct TraceNode {
32 pub node_id: String,
33 pub kind: TraceNodeKind,
34 pub target: String,
36 pub input: serde_json::Value,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub output: Option<serde_json::Value>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub error: Option<String>,
42 pub started_at: u64,
43 pub ended_at: u64,
44 #[serde(default)]
45 pub children: Vec<TraceNode>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub struct TraceTree {
50 pub run_id: String,
51 pub root_target: String,
52 pub root_input: serde_json::Value,
53 pub root_output: Option<serde_json::Value>,
54 pub root_error: Option<String>,
55 pub started_at: u64,
56 pub ended_at: u64,
57 pub nodes: Vec<TraceNode>,
58}
59
60impl TraceTree {
61 pub fn find(&self, node_id: &str) -> Option<&TraceNode> {
63 for n in &self.nodes {
64 if let Some(found) = find_in(n, node_id) { return Some(found); }
65 }
66 None
67 }
68}
69
70fn find_in<'a>(n: &'a TraceNode, target: &str) -> Option<&'a TraceNode> {
71 if n.node_id == target { return Some(n); }
72 for c in &n.children {
73 if let Some(f) = find_in(c, target) { return Some(f); }
74 }
75 None
76}
77
78pub struct Recorder {
81 state: Arc<Mutex<RecorderState>>,
82}
83
84pub(crate) struct RecorderState {
85 open: Vec<OpenFrame>,
90 completed: Vec<TraceNode>,
93 pub(crate) overrides: IndexMap<String, serde_json::Value>,
95}
96
97struct OpenFrame {
98 node: TraceNode,
99 children: Vec<TraceNode>,
101}
102
103impl Recorder {
104 pub fn new() -> Self {
105 Self {
106 state: Arc::new(Mutex::new(RecorderState {
107 open: Vec::new(),
108 completed: Vec::new(),
109 overrides: IndexMap::new(),
110 })),
111 }
112 }
113
114 pub fn handle(&self) -> Handle {
116 Handle { state: Arc::clone(&self.state) }
117 }
118
119 pub fn with_overrides(self, overrides: IndexMap<String, serde_json::Value>) -> Self {
121 self.state.lock().unwrap().overrides = overrides;
122 self
123 }
124}
125
126impl Default for Recorder { fn default() -> Self { Self::new() } }
127
128#[derive(Clone)]
129pub struct Handle {
130 state: Arc<Mutex<RecorderState>>,
131}
132
133impl Handle {
134 pub fn finalize(
138 &self,
139 root_target: impl Into<String>,
140 root_input: serde_json::Value,
141 root_output: Option<serde_json::Value>,
142 root_error: Option<String>,
143 started_at: u64,
144 ended_at: u64,
145 ) -> TraceTree {
146 let st = self.state.lock().unwrap();
147 TraceTree {
148 run_id: RunId::new(&format!("{}-{}", started_at, ended_at)).0,
149 root_target: root_target.into(),
150 root_input,
151 root_output,
152 root_error,
153 started_at,
154 ended_at,
155 nodes: st.completed.clone(),
156 }
157 }
158}
159
160fn now_unix() -> u64 {
161 use std::time::{SystemTime, UNIX_EPOCH};
162 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
163}
164
165fn values_to_json(args: &[Value]) -> serde_json::Value {
166 serde_json::Value::Array(args.iter().map(value_to_json).collect())
167}
168
169fn value_to_json(v: &Value) -> serde_json::Value {
170 use serde_json::Value as J;
171 match v {
172 Value::Int(n) => J::from(*n),
173 Value::Float(f) => J::from(*f),
174 Value::Bool(b) => J::Bool(*b),
175 Value::Str(s) => J::String(s.to_string()),
176 Value::Bytes(b) => J::String(b.iter().map(|b| format!("{:02x}", b)).collect()),
177 Value::Unit => J::Null,
178 Value::List(items) => J::Array(items.iter().map(value_to_json).collect()),
179 Value::Tuple(items) => J::Array(items.iter().map(value_to_json).collect()),
180 Value::Record(fields) => {
181 let mut m = serde_json::Map::new();
182 for (k, v) in fields { m.insert(k.clone(), value_to_json(v)); }
183 J::Object(m)
184 }
185 Value::Variant { name, args } => {
186 let mut m = serde_json::Map::new();
187 m.insert("$variant".into(), J::String(name.clone()));
188 m.insert("args".into(), J::Array(args.iter().map(value_to_json).collect()));
189 J::Object(m)
190 }
191 Value::Closure { body_hash, .. } => {
192 let prefix: String = body_hash.iter().take(4)
197 .map(|b| format!("{b:02x}")).collect();
198 J::String(format!("<closure {prefix}>"))
199 }
200 Value::F64Array { rows, cols, data } => {
201 let mut m = serde_json::Map::new();
202 m.insert("$f64_array".into(), J::Bool(true));
203 m.insert("rows".into(), J::from(*rows));
204 m.insert("cols".into(), J::from(*cols));
205 m.insert("data".into(), J::Array(data.iter().map(|f| J::from(*f)).collect()));
206 J::Object(m)
207 }
208 Value::Map(m) => {
209 let mut o = serde_json::Map::new();
210 o.insert("$map".into(), J::Bool(true));
211 o.insert("entries".into(), J::Array(m.iter().map(|(k, v)| {
212 J::Array(vec![value_to_json(&k.as_value()), value_to_json(v)])
213 }).collect()));
214 J::Object(o)
215 }
216 Value::Set(s) => {
217 let mut o = serde_json::Map::new();
218 o.insert("$set".into(), J::Bool(true));
219 o.insert("items".into(), J::Array(
220 s.iter().map(|k| value_to_json(&k.as_value())).collect()));
221 J::Object(o)
222 }
223 Value::Deque(items) => {
224 let mut o = serde_json::Map::new();
225 o.insert("$deque".into(), J::Bool(true));
226 o.insert("items".into(), J::Array(
227 items.iter().map(value_to_json).collect()));
228 J::Object(o)
229 }
230 Value::Actor(_) => J::String("<actor>".into()),
231 Value::Ticker(_) => J::String("<ticker>".into()),
232 Value::ArrowTable(t) => {
233 let mut o = serde_json::Map::new();
237 o.insert("$arrow_table".into(), J::Bool(true));
238 o.insert("nrows".into(), J::from(t.num_rows() as i64));
239 o.insert("ncols".into(), J::from(t.num_columns() as i64));
240 J::Object(o)
241 }
242 }
243}
244
245pub(crate) fn json_to_value(v: &serde_json::Value) -> Value {
246 use serde_json::Value as J;
247 match v {
248 J::Null => Value::Unit,
249 J::Bool(b) => Value::Bool(*b),
250 J::Number(n) => {
251 if let Some(i) = n.as_i64() { Value::Int(i) }
252 else if let Some(f) = n.as_f64() { Value::Float(f) }
253 else { Value::Unit }
254 }
255 J::String(s) => Value::Str(s.as_str().into()),
256 J::Array(items) => Value::List(items.iter().map(json_to_value).collect()),
257 J::Object(map) => {
258 if let (Some(serde_json::Value::String(name)), Some(serde_json::Value::Array(args))) =
260 (map.get("$variant"), map.get("args"))
261 {
262 return Value::Variant {
263 name: name.clone(),
264 args: args.iter().map(json_to_value).collect(),
265 };
266 }
267 let mut out = indexmap::IndexMap::new();
268 for (k, v) in map { out.insert(k.clone(), json_to_value(v)); }
269 Value::Record(out)
270 }
271 }
272}
273
274impl Tracer for Recorder {
275 fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]) {
276 push_call_frame(&self.state, node_id, name, args);
277 }
278 fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]) {
279 push_effect_frame(&self.state, node_id, kind, op, args);
280 }
281 fn exit_ok(&mut self, value: &Value) { exit_ok_frame(&self.state, value); }
282 fn exit_err(&mut self, message: &str) { exit_err_frame(&self.state, message); }
283 fn exit_call_tail(&mut self) { exit_tail_frame(&self.state); }
284 fn override_effect(&mut self, node_id: &str) -> Option<Value> {
285 lookup_override(&self.state, node_id)
286 }
287}
288
289impl Tracer for Handle {
295 fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]) {
296 push_call_frame(&self.state, node_id, name, args);
297 }
298 fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]) {
299 push_effect_frame(&self.state, node_id, kind, op, args);
300 }
301 fn exit_ok(&mut self, value: &Value) { exit_ok_frame(&self.state, value); }
302 fn exit_err(&mut self, message: &str) { exit_err_frame(&self.state, message); }
303 fn exit_call_tail(&mut self) { exit_tail_frame(&self.state); }
304 fn override_effect(&mut self, node_id: &str) -> Option<Value> {
305 lookup_override(&self.state, node_id)
306 }
307}
308
309fn push_call_frame(state: &Mutex<RecorderState>, node_id: &str, name: &str, args: &[Value]) {
312 let mut st = state.lock().unwrap();
313 st.open.push(OpenFrame {
314 node: TraceNode {
315 node_id: node_id.to_string(),
316 kind: TraceNodeKind::Call,
317 target: name.to_string(),
318 input: values_to_json(args),
319 output: None,
320 error: None,
321 started_at: now_unix(),
322 ended_at: 0,
323 children: Vec::new(),
324 },
325 children: Vec::new(),
326 });
327}
328
329fn push_effect_frame(state: &Mutex<RecorderState>, node_id: &str, kind: &str, op: &str, args: &[Value]) {
330 let mut st = state.lock().unwrap();
331 st.open.push(OpenFrame {
332 node: TraceNode {
333 node_id: node_id.to_string(),
334 kind: TraceNodeKind::Effect,
335 target: format!("{kind}.{op}"),
336 input: values_to_json(args),
337 output: None,
338 error: None,
339 started_at: now_unix(),
340 ended_at: 0,
341 children: Vec::new(),
342 },
343 children: Vec::new(),
344 });
345}
346
347fn exit_ok_frame(state: &Mutex<RecorderState>, value: &Value) {
348 let mut st = state.lock().unwrap();
349 if let Some(mut frame) = st.open.pop() {
350 frame.node.ended_at = now_unix();
351 frame.node.output = Some(value_to_json(value));
352 frame.node.children = frame.children;
353 attach_completed(&mut st, frame.node);
354 }
355}
356
357fn exit_err_frame(state: &Mutex<RecorderState>, message: &str) {
358 let mut st = state.lock().unwrap();
359 if let Some(mut frame) = st.open.pop() {
360 frame.node.ended_at = now_unix();
361 frame.node.error = Some(message.to_string());
362 frame.node.children = frame.children;
363 attach_completed(&mut st, frame.node);
364 }
365}
366
367fn exit_tail_frame(state: &Mutex<RecorderState>) {
368 let mut st = state.lock().unwrap();
369 if let Some(mut frame) = st.open.pop() {
370 frame.node.ended_at = now_unix();
371 frame.node.output = Some(serde_json::Value::Null);
372 frame.node.children = frame.children;
373 attach_completed(&mut st, frame.node);
374 }
375}
376
377fn lookup_override(state: &Mutex<RecorderState>, node_id: &str) -> Option<Value> {
378 let st = state.lock().unwrap();
379 st.overrides.get(node_id).map(json_to_value)
380}
381
382fn attach_completed(st: &mut RecorderState, node: TraceNode) {
383 if let Some(parent) = st.open.last_mut() {
384 parent.children.push(node);
385 } else {
386 st.completed.push(node);
387 }
388}