datalogic_rs/
trace.rs

1//! Execution tracing for step-by-step debugging.
2//!
3//! This module provides execution tracing capabilities for debugging JSONLogic
4//! expressions. It generates an expression tree with unique IDs and records
5//! each evaluation step for replay in the Web UI.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11use crate::{CompiledNode, OpCode};
12
13/// The result of a traced evaluation, containing both the result and execution trace.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TracedResult {
16    /// The evaluation result
17    pub result: Value,
18    /// Expression tree with unique IDs for flow diagram rendering
19    pub expression_tree: ExpressionNode,
20    /// Ordered execution steps for replay
21    pub steps: Vec<ExecutionStep>,
22}
23
24/// Represents a node in the expression tree for flow diagram rendering.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ExpressionNode {
27    /// Unique identifier for this node
28    pub id: u32,
29    /// JSON string of this sub-expression
30    pub expression: String,
31    /// Child nodes (arguments/operands that are operators, not literals)
32    pub children: Vec<ExpressionNode>,
33}
34
35impl ExpressionNode {
36    /// Build an expression tree from a CompiledNode, assigning unique IDs.
37    ///
38    /// Returns the expression tree and a mapping from node pointers to IDs.
39    pub fn build_from_compiled(node: &CompiledNode) -> (ExpressionNode, HashMap<usize, u32>) {
40        let mut id_counter = 0u32;
41        let mut node_id_map = HashMap::new();
42        let tree = Self::build_node(node, &mut id_counter, &mut node_id_map);
43        (tree, node_id_map)
44    }
45
46    fn build_node(
47        node: &CompiledNode,
48        id_counter: &mut u32,
49        node_id_map: &mut HashMap<usize, u32>,
50    ) -> ExpressionNode {
51        let id = *id_counter;
52        *id_counter += 1;
53
54        // Store the mapping from node pointer to ID
55        let node_ptr = node as *const CompiledNode as usize;
56        node_id_map.insert(node_ptr, id);
57
58        match node {
59            CompiledNode::Value { value, .. } => {
60                // Literals don't have children but we still need to represent them
61                // in the tree for completeness
62                ExpressionNode {
63                    id,
64                    expression: value.to_string(),
65                    children: vec![],
66                }
67            }
68            CompiledNode::Array { nodes, .. } => {
69                let children: Vec<ExpressionNode> = nodes
70                    .iter()
71                    .filter(|n| Self::is_operator_node(n))
72                    .map(|n| Self::build_node(n, id_counter, node_id_map))
73                    .collect();
74
75                ExpressionNode {
76                    id,
77                    expression: Self::node_to_json_string(node),
78                    children,
79                }
80            }
81            CompiledNode::BuiltinOperator { opcode, args, .. } => {
82                let children: Vec<ExpressionNode> = args
83                    .iter()
84                    .filter(|n| Self::is_operator_node(n))
85                    .map(|n| Self::build_node(n, id_counter, node_id_map))
86                    .collect();
87
88                ExpressionNode {
89                    id,
90                    expression: Self::builtin_to_json_string(opcode, args),
91                    children,
92                }
93            }
94            CompiledNode::CustomOperator { name, args, .. } => {
95                let children: Vec<ExpressionNode> = args
96                    .iter()
97                    .filter(|n| Self::is_operator_node(n))
98                    .map(|n| Self::build_node(n, id_counter, node_id_map))
99                    .collect();
100
101                ExpressionNode {
102                    id,
103                    expression: Self::custom_to_json_string(name, args),
104                    children,
105                }
106            }
107            CompiledNode::StructuredObject { fields, .. } => {
108                let children: Vec<ExpressionNode> = fields
109                    .iter()
110                    .filter(|(_, n)| Self::is_operator_node(n))
111                    .map(|(_, n)| Self::build_node(n, id_counter, node_id_map))
112                    .collect();
113
114                ExpressionNode {
115                    id,
116                    expression: Self::structured_to_json_string(fields),
117                    children,
118                }
119            }
120        }
121    }
122
123    /// Check if a node is an operator (not a literal value)
124    fn is_operator_node(node: &CompiledNode) -> bool {
125        !matches!(node, CompiledNode::Value { .. })
126    }
127
128    /// Convert a CompiledNode to its JSON string representation
129    fn node_to_json_string(node: &CompiledNode) -> String {
130        match node {
131            CompiledNode::Value { value, .. } => value.to_string(),
132            CompiledNode::Array { nodes, .. } => {
133                let items: Vec<String> = nodes.iter().map(Self::node_to_json_string).collect();
134                format!("[{}]", items.join(", "))
135            }
136            CompiledNode::BuiltinOperator { opcode, args, .. } => {
137                Self::builtin_to_json_string(opcode, args)
138            }
139            CompiledNode::CustomOperator { name, args, .. } => {
140                Self::custom_to_json_string(name, args)
141            }
142            CompiledNode::StructuredObject { fields, .. } => {
143                Self::structured_to_json_string(fields)
144            }
145        }
146    }
147
148    fn builtin_to_json_string(opcode: &OpCode, args: &[CompiledNode]) -> String {
149        let op_str = opcode.as_str();
150        let args_str = if args.len() == 1 {
151            Self::node_to_json_string(&args[0])
152        } else {
153            let items: Vec<String> = args.iter().map(Self::node_to_json_string).collect();
154            format!("[{}]", items.join(", "))
155        };
156        format!("{{\"{}\": {}}}", op_str, args_str)
157    }
158
159    fn custom_to_json_string(name: &str, args: &[CompiledNode]) -> String {
160        let args_str = if args.len() == 1 {
161            Self::node_to_json_string(&args[0])
162        } else {
163            let items: Vec<String> = args.iter().map(Self::node_to_json_string).collect();
164            format!("[{}]", items.join(", "))
165        };
166        format!("{{\"{}\": {}}}", name, args_str)
167    }
168
169    fn structured_to_json_string(fields: &[(String, CompiledNode)]) -> String {
170        let items: Vec<String> = fields
171            .iter()
172            .map(|(key, node)| format!("\"{}\": {}", key, Self::node_to_json_string(node)))
173            .collect();
174        format!("{{{}}}", items.join(", "))
175    }
176}
177
178/// Captures state at each evaluation step.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ExecutionStep {
181    /// Sequential step number
182    pub id: u32,
183    /// ID of the node being evaluated
184    pub node_id: u32,
185    /// Current context/scope data at this step
186    pub context: Value,
187    /// Result after evaluating this node (None if error)
188    pub result: Option<Value>,
189    /// Error message if evaluation failed (None if success)
190    pub error: Option<String>,
191    /// Current iteration index (only for iterator body evaluations)
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub iteration_index: Option<u32>,
194    /// Total iteration count (only for iterator body evaluations)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub iteration_total: Option<u32>,
197}
198
199/// Collector for execution steps during traced evaluation.
200pub struct TraceCollector {
201    /// Recorded execution steps
202    steps: Vec<ExecutionStep>,
203    /// Counter for generating step IDs
204    step_counter: u32,
205    /// Stack of iteration info (index, total) for nested iterations
206    iteration_stack: Vec<(u32, u32)>,
207}
208
209impl TraceCollector {
210    /// Create a new trace collector
211    pub fn new() -> Self {
212        Self {
213            steps: Vec::new(),
214            step_counter: 0,
215            iteration_stack: Vec::new(),
216        }
217    }
218
219    /// Record a successful execution step
220    pub fn record_step(&mut self, node_id: u32, context: Value, result: Value) {
221        let (iteration_index, iteration_total) = self.current_iteration();
222        let step = ExecutionStep {
223            id: self.step_counter,
224            node_id,
225            context,
226            result: Some(result),
227            error: None,
228            iteration_index,
229            iteration_total,
230        };
231        self.steps.push(step);
232        self.step_counter += 1;
233    }
234
235    /// Record an error execution step
236    pub fn record_error(&mut self, node_id: u32, context: Value, error: String) {
237        let (iteration_index, iteration_total) = self.current_iteration();
238        let step = ExecutionStep {
239            id: self.step_counter,
240            node_id,
241            context,
242            result: None,
243            error: Some(error),
244            iteration_index,
245            iteration_total,
246        };
247        self.steps.push(step);
248        self.step_counter += 1;
249    }
250
251    /// Push iteration context for map/filter/reduce operations
252    pub fn push_iteration(&mut self, index: u32, total: u32) {
253        self.iteration_stack.push((index, total));
254    }
255
256    /// Pop iteration context
257    pub fn pop_iteration(&mut self) {
258        self.iteration_stack.pop();
259    }
260
261    /// Get current iteration info if inside an iteration
262    fn current_iteration(&self) -> (Option<u32>, Option<u32>) {
263        self.iteration_stack
264            .last()
265            .map(|(i, t)| (Some(*i), Some(*t)))
266            .unwrap_or((None, None))
267    }
268
269    /// Consume the collector and return the recorded steps
270    pub fn into_steps(self) -> Vec<ExecutionStep> {
271        self.steps
272    }
273}
274
275impl Default for TraceCollector {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::OpCode;
285
286    #[test]
287    fn test_expression_node_from_simple_operator() {
288        // Create a simple {"var": "age"} node
289        let node = CompiledNode::BuiltinOperator {
290            opcode: OpCode::Var,
291            args: vec![CompiledNode::Value {
292                value: serde_json::json!("age"),
293            }],
294        };
295
296        let (tree, node_id_map) = ExpressionNode::build_from_compiled(&node);
297
298        assert_eq!(tree.id, 0);
299        assert_eq!(tree.expression, r#"{"var": "age"}"#);
300        assert!(tree.children.is_empty()); // "age" is a literal, not a child
301        assert_eq!(node_id_map.len(), 1);
302    }
303
304    #[test]
305    fn test_expression_node_from_nested_operator() {
306        // Create {">=": [{"var": "age"}, 18]}
307        let var_node = CompiledNode::BuiltinOperator {
308            opcode: OpCode::Var,
309            args: vec![CompiledNode::Value {
310                value: serde_json::json!("age"),
311            }],
312        };
313        let node = CompiledNode::BuiltinOperator {
314            opcode: OpCode::GreaterThanEqual,
315            args: vec![
316                var_node,
317                CompiledNode::Value {
318                    value: serde_json::json!(18),
319                },
320            ],
321        };
322
323        let (tree, node_id_map) = ExpressionNode::build_from_compiled(&node);
324
325        assert_eq!(tree.id, 0);
326        assert!(tree.expression.contains(">="));
327        assert_eq!(tree.children.len(), 1); // var node is a child
328        assert_eq!(tree.children[0].id, 1);
329        assert!(tree.children[0].expression.contains("var"));
330        assert_eq!(node_id_map.len(), 2);
331    }
332
333    #[test]
334    fn test_trace_collector_records_steps() {
335        let mut collector = TraceCollector::new();
336
337        collector.record_step(0, serde_json::json!({"age": 25}), serde_json::json!(25));
338        collector.record_step(1, serde_json::json!({"age": 25}), serde_json::json!(true));
339
340        let steps = collector.into_steps();
341        assert_eq!(steps.len(), 2);
342        assert_eq!(steps[0].id, 0);
343        assert_eq!(steps[0].node_id, 0);
344        assert_eq!(steps[1].id, 1);
345        assert_eq!(steps[1].node_id, 1);
346    }
347
348    #[test]
349    fn test_trace_collector_iteration_context() {
350        let mut collector = TraceCollector::new();
351
352        collector.push_iteration(0, 3);
353        collector.record_step(2, serde_json::json!(1), serde_json::json!(2));
354
355        let steps = collector.into_steps();
356        assert_eq!(steps[0].iteration_index, Some(0));
357        assert_eq!(steps[0].iteration_total, Some(3));
358    }
359}