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.iter() { m.insert(k.to_string(), value_to_json(v)); }
183 J::Object(m)
184 }
185 Value::StackRecord { .. } => J::String("<stack-record-unreachable>".into()),
190 Value::StackTuple { .. } => J::String("<stack-tuple-unreachable>".into()),
194 Value::Variant { name, args } => {
195 let mut m = serde_json::Map::new();
196 m.insert("$variant".into(), J::String(name.clone()));
197 m.insert("args".into(), J::Array(args.iter().map(value_to_json).collect()));
198 J::Object(m)
199 }
200 Value::Closure { body_hash, .. } => {
201 let prefix: String = body_hash.iter().take(4)
206 .map(|b| format!("{b:02x}")).collect();
207 J::String(format!("<closure {prefix}>"))
208 }
209 Value::F64Array { rows, cols, data } => {
210 let mut m = serde_json::Map::new();
211 m.insert("$f64_array".into(), J::Bool(true));
212 m.insert("rows".into(), J::from(*rows));
213 m.insert("cols".into(), J::from(*cols));
214 m.insert("data".into(), J::Array(data.iter().map(|f| J::from(*f)).collect()));
215 J::Object(m)
216 }
217 Value::Map(m) => {
218 let mut o = serde_json::Map::new();
219 o.insert("$map".into(), J::Bool(true));
220 o.insert("entries".into(), J::Array(m.iter().map(|(k, v)| {
221 J::Array(vec![value_to_json(&k.as_value()), value_to_json(v)])
222 }).collect()));
223 J::Object(o)
224 }
225 Value::Set(s) => {
226 let mut o = serde_json::Map::new();
227 o.insert("$set".into(), J::Bool(true));
228 o.insert("items".into(), J::Array(
229 s.iter().map(|k| value_to_json(&k.as_value())).collect()));
230 J::Object(o)
231 }
232 Value::Deque(items) => {
233 let mut o = serde_json::Map::new();
234 o.insert("$deque".into(), J::Bool(true));
235 o.insert("items".into(), J::Array(
236 items.iter().map(value_to_json).collect()));
237 J::Object(o)
238 }
239 Value::Actor(_) => J::String("<actor>".into()),
240 Value::Ticker(_) => J::String("<ticker>".into()),
241 Value::ArrowTable(t) => {
242 let mut o = serde_json::Map::new();
246 o.insert("$arrow_table".into(), J::Bool(true));
247 o.insert("nrows".into(), J::from(t.num_rows() as i64));
248 o.insert("ncols".into(), J::from(t.num_columns() as i64));
249 J::Object(o)
250 }
251 }
252}
253
254pub(crate) fn json_to_value(v: &serde_json::Value) -> Value {
255 use serde_json::Value as J;
256 match v {
257 J::Null => Value::Unit,
258 J::Bool(b) => Value::Bool(*b),
259 J::Number(n) => {
260 if let Some(i) = n.as_i64() { Value::Int(i) }
261 else if let Some(f) = n.as_f64() { Value::Float(f) }
262 else { Value::Unit }
263 }
264 J::String(s) => Value::Str(s.as_str().into()),
265 J::Array(items) => Value::List(items.iter().map(json_to_value).collect()),
266 J::Object(map) => {
267 if let (Some(serde_json::Value::String(name)), Some(serde_json::Value::Array(args))) =
269 (map.get("$variant"), map.get("args"))
270 {
271 return Value::Variant {
272 name: name.clone(),
273 args: args.iter().map(json_to_value).collect(),
274 };
275 }
276 let mut out = indexmap::IndexMap::new();
277 for (k, v) in map { out.insert(k.clone(), json_to_value(v)); }
278 Value::record_dynamic(out)
279 }
280 }
281}
282
283impl Tracer for Recorder {
284 fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]) {
285 push_call_frame(&self.state, node_id, name, args);
286 }
287 fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]) {
288 push_effect_frame(&self.state, node_id, kind, op, args);
289 }
290 fn exit_ok(&mut self, value: &Value) { exit_ok_frame(&self.state, value); }
291 fn exit_err(&mut self, message: &str) { exit_err_frame(&self.state, message); }
292 fn exit_call_tail(&mut self) { exit_tail_frame(&self.state); }
293 fn override_effect(&mut self, node_id: &str) -> Option<Value> {
294 lookup_override(&self.state, node_id)
295 }
296}
297
298impl Tracer for Handle {
304 fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]) {
305 push_call_frame(&self.state, node_id, name, args);
306 }
307 fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]) {
308 push_effect_frame(&self.state, node_id, kind, op, args);
309 }
310 fn exit_ok(&mut self, value: &Value) { exit_ok_frame(&self.state, value); }
311 fn exit_err(&mut self, message: &str) { exit_err_frame(&self.state, message); }
312 fn exit_call_tail(&mut self) { exit_tail_frame(&self.state); }
313 fn override_effect(&mut self, node_id: &str) -> Option<Value> {
314 lookup_override(&self.state, node_id)
315 }
316}
317
318fn push_call_frame(state: &Mutex<RecorderState>, node_id: &str, name: &str, args: &[Value]) {
321 let mut st = state.lock().unwrap();
322 st.open.push(OpenFrame {
323 node: TraceNode {
324 node_id: node_id.to_string(),
325 kind: TraceNodeKind::Call,
326 target: name.to_string(),
327 input: values_to_json(args),
328 output: None,
329 error: None,
330 started_at: now_unix(),
331 ended_at: 0,
332 children: Vec::new(),
333 },
334 children: Vec::new(),
335 });
336}
337
338fn push_effect_frame(state: &Mutex<RecorderState>, node_id: &str, kind: &str, op: &str, args: &[Value]) {
339 let mut st = state.lock().unwrap();
340 st.open.push(OpenFrame {
341 node: TraceNode {
342 node_id: node_id.to_string(),
343 kind: TraceNodeKind::Effect,
344 target: format!("{kind}.{op}"),
345 input: values_to_json(args),
346 output: None,
347 error: None,
348 started_at: now_unix(),
349 ended_at: 0,
350 children: Vec::new(),
351 },
352 children: Vec::new(),
353 });
354}
355
356fn exit_ok_frame(state: &Mutex<RecorderState>, value: &Value) {
357 let mut st = state.lock().unwrap();
358 if let Some(mut frame) = st.open.pop() {
359 frame.node.ended_at = now_unix();
360 frame.node.output = Some(value_to_json(value));
361 frame.node.children = frame.children;
362 attach_completed(&mut st, frame.node);
363 }
364}
365
366fn exit_err_frame(state: &Mutex<RecorderState>, message: &str) {
367 let mut st = state.lock().unwrap();
368 if let Some(mut frame) = st.open.pop() {
369 frame.node.ended_at = now_unix();
370 frame.node.error = Some(message.to_string());
371 frame.node.children = frame.children;
372 attach_completed(&mut st, frame.node);
373 }
374}
375
376fn exit_tail_frame(state: &Mutex<RecorderState>) {
377 let mut st = state.lock().unwrap();
378 if let Some(mut frame) = st.open.pop() {
379 frame.node.ended_at = now_unix();
380 frame.node.output = Some(serde_json::Value::Null);
381 frame.node.children = frame.children;
382 attach_completed(&mut st, frame.node);
383 }
384}
385
386fn lookup_override(state: &Mutex<RecorderState>, node_id: &str) -> Option<Value> {
387 let st = state.lock().unwrap();
388 st.overrides.get(node_id).map(json_to_value)
389}
390
391fn attach_completed(st: &mut RecorderState, node: TraceNode) {
392 if let Some(parent) = st.open.last_mut() {
393 parent.children.push(node);
394 } else {
395 st.completed.push(node);
396 }
397}