Skip to main content

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//!
7//! # Feature gating
8//!
9//! Gated on `feature = "trace"`. Trace transitively pulls in
10//! `feature = "serde_json"` (the `Cargo.toml` declares
11//! `trace = ["serde_json"]`) because the per-step expression tree and
12//! recorded values are `serde_json::Value`-shaped — the structured-trace
13//! consumers (the Web UI, JSON exporters) need the JSON↔arena bridge to
14//! render steps. `--features trace` implicitly enables `serde_json`.
15
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19use crate::node_serialize;
20use crate::{CompiledNode, Error};
21
22/// Represents a node in the expression tree for flow diagram rendering.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ExpressionNode {
25    /// Unique identifier for this node
26    pub id: u32,
27    /// JSON string of this sub-expression
28    pub expression: String,
29    /// Child nodes (arguments/operands that are operators, not literals)
30    pub children: Vec<ExpressionNode>,
31}
32
33impl ExpressionNode {
34    /// Build an expression tree from a CompiledNode.
35    ///
36    /// Every tree node inherits its compile-time id from the source
37    /// [`CompiledNode::id`]. No side-table is needed: both tracing and error
38    /// reporting look the id up directly on the node.
39    pub(crate) fn build_from_compiled(node: &CompiledNode) -> ExpressionNode {
40        Self::build_node(node)
41    }
42
43    fn build_node(node: &CompiledNode) -> ExpressionNode {
44        let id = node.id();
45        match node {
46            CompiledNode::Value { value, .. } => Self::leaf(id, value.to_json_string()),
47            CompiledNode::Array { nodes, .. } => ExpressionNode {
48                id,
49                expression: node_serialize::node_to_json_string(node),
50                children: Self::op_children(nodes),
51            },
52            CompiledNode::BuiltinOperator { opcode, args, .. } => ExpressionNode {
53                id,
54                expression: node_serialize::builtin_to_json_string(opcode, args),
55                children: Self::op_children(args),
56            },
57            CompiledNode::CustomOperator(data) => ExpressionNode {
58                id,
59                expression: node_serialize::custom_to_json_string(&data.name, &data.args),
60                children: Self::op_children(&data.args),
61            },
62            #[cfg(feature = "templating")]
63            CompiledNode::StructuredObject(data) => ExpressionNode {
64                id,
65                expression: node_serialize::structured_to_json_string(&data.fields),
66                children: Self::op_children_from_fields(&data.fields),
67            },
68            CompiledNode::Var {
69                scope_level,
70                segments,
71                default_value,
72                ..
73            } => Self::build_compiled_var(id, *scope_level, segments, default_value.as_deref()),
74            #[cfg(feature = "ext-control")]
75            CompiledNode::Exists(data) => Self::leaf(
76                id,
77                node_serialize::compiled_exists_to_json_string(&data.segments),
78            ),
79            #[cfg(feature = "error-handling")]
80            CompiledNode::Throw(_) | CompiledNode::Missing(_) | CompiledNode::MissingSome(_) => {
81                Self::leaf(id, node_serialize::node_to_json_string(node))
82            }
83            #[cfg(not(feature = "error-handling"))]
84            CompiledNode::Missing(_) | CompiledNode::MissingSome(_) => {
85                Self::leaf(id, node_serialize::node_to_json_string(node))
86            }
87            CompiledNode::InvalidArgs { .. } => {
88                Self::leaf(id, "{\"<invalid args>\": null}".to_string())
89            }
90        }
91    }
92
93    /// Build a leaf `ExpressionNode` (no children).
94    #[inline]
95    fn leaf(id: u32, expression: String) -> ExpressionNode {
96        ExpressionNode {
97            id,
98            expression,
99            children: vec![],
100        }
101    }
102
103    /// Recurse into a compiled-node slice, keeping only the operator nodes
104    /// (literals don't appear as flow-diagram children).
105    #[inline]
106    fn op_children(nodes: &[CompiledNode]) -> Vec<ExpressionNode> {
107        nodes
108            .iter()
109            .filter(|n| Self::is_operator_node(n))
110            .map(Self::build_node)
111            .collect()
112    }
113
114    /// `op_children` for the `(name, CompiledNode)` shape used by structured
115    /// object fields.
116    #[cfg(feature = "templating")]
117    #[inline]
118    fn op_children_from_fields(fields: &[(String, CompiledNode)]) -> Vec<ExpressionNode> {
119        fields
120            .iter()
121            .filter(|(_, n)| Self::is_operator_node(n))
122            .map(|(_, n)| Self::build_node(n))
123            .collect()
124    }
125
126    /// `CompiledVar`'s expression node — the only operator-shaped variant
127    /// whose "child" is the optional default value rather than a fixed args
128    /// slice.
129    fn build_compiled_var(
130        id: u32,
131        scope_level: u32,
132        segments: &[crate::node::PathSegment],
133        default_value: Option<&CompiledNode>,
134    ) -> ExpressionNode {
135        let mut children = Vec::new();
136        if let Some(def) = default_value {
137            if Self::is_operator_node(def) {
138                children.push(Self::build_node(def));
139            }
140        }
141        ExpressionNode {
142            id,
143            expression: node_serialize::compiled_var_to_json_string(
144                scope_level,
145                segments,
146                default_value,
147            ),
148            children,
149        }
150    }
151
152    /// Check if a node is an operator (not a literal value)
153    fn is_operator_node(node: &CompiledNode) -> bool {
154        !matches!(node, CompiledNode::Value { .. })
155    }
156}
157
158/// Captures state at each evaluation step.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ExecutionStep {
161    /// Sequential step number (assigned by the trace collector in
162    /// recording order). Distinct from `node_id`, which is the
163    /// compiled-node id of the expression being evaluated — `step_id`
164    /// is the *order* this step occurred, `node_id` is *which node* ran.
165    pub step_id: u32,
166    /// ID of the node being evaluated
167    pub node_id: u32,
168    /// Current context/scope data at this step
169    pub context: Value,
170    /// Result after evaluating this node (None if error)
171    pub result: Option<Value>,
172    /// Error message if evaluation failed (None if success)
173    pub error: Option<String>,
174    /// Current iteration index (only for iterator body evaluations)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub iteration_index: Option<u32>,
177    /// Total iteration count (only for iterator body evaluations)
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub iteration_total: Option<u32>,
180}
181
182/// Collector for execution steps during traced evaluation.
183pub(crate) struct TraceCollector {
184    /// Recorded execution steps
185    steps: Vec<ExecutionStep>,
186    /// Counter for generating step IDs
187    step_counter: u32,
188    /// Stack of iteration info (index, total) for nested iterations
189    iteration_stack: Vec<(u32, u32)>,
190}
191
192impl TraceCollector {
193    /// Create a new trace collector
194    pub fn new() -> Self {
195        Self {
196            steps: Vec::new(),
197            step_counter: 0,
198            iteration_stack: Vec::new(),
199        }
200    }
201
202    /// Record a successful execution step
203    pub fn record_step(&mut self, node_id: u32, context: Value, result: Value) {
204        let (iteration_index, iteration_total) = self.current_iteration();
205        let step = ExecutionStep {
206            step_id: self.step_counter,
207            node_id,
208            context,
209            result: Some(result),
210            error: None,
211            iteration_index,
212            iteration_total,
213        };
214        self.steps.push(step);
215        self.step_counter += 1;
216    }
217
218    /// Record an error execution step
219    pub fn record_error(&mut self, node_id: u32, context: Value, error: String) {
220        let (iteration_index, iteration_total) = self.current_iteration();
221        let step = ExecutionStep {
222            step_id: self.step_counter,
223            node_id,
224            context,
225            result: None,
226            error: Some(error),
227            iteration_index,
228            iteration_total,
229        };
230        self.steps.push(step);
231        self.step_counter += 1;
232    }
233
234    /// Push iteration context for map/filter/reduce operations
235    pub fn push_iteration(&mut self, index: u32, total: u32) {
236        self.iteration_stack.push((index, total));
237    }
238
239    /// Pop iteration context
240    pub fn pop_iteration(&mut self) {
241        self.iteration_stack.pop();
242    }
243
244    /// Get current iteration info if inside an iteration
245    fn current_iteration(&self) -> (Option<u32>, Option<u32>) {
246        self.iteration_stack
247            .last()
248            .map(|(i, t)| (Some(*i), Some(*t)))
249            .unwrap_or((None, None))
250    }
251
252    /// Consume the collector and return the recorded steps
253    pub fn into_steps(self) -> Vec<ExecutionStep> {
254        self.steps
255    }
256}
257
258impl Default for TraceCollector {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264// ============================================================================
265// v5 trace surface — `engine.trace().evaluate*(...)` returning `TracedRun`.
266// ============================================================================
267
268/// Result of a traced evaluation produced by [`TracedSession`]. Always
269/// includes the trace data; the value-or-error split lives on
270/// [`Self::result`].
271#[derive(Debug, Clone)]
272pub struct TracedRun<R> {
273    /// `Ok(value)` on success, `Err(error)` on failure. The error always
274    /// carries the operator + path metadata populated by the engine.
275    pub result: Result<R, Error>,
276    /// Per-node execution log captured during the run.
277    pub steps: Vec<ExecutionStep>,
278    /// Compile-time expression tree for flow-diagram rendering.
279    pub expression_tree: ExpressionNode,
280}
281
282/// Trace-enabled view over a [`crate::Engine`] engine. Constructed via
283/// [`crate::Engine::trace`]. Mirrors [`crate::Session`] 1:1 — every
284/// `eval*` returns a [`TracedRun<R>`] carrying the trace alongside the
285/// result, where `R` is the same shape that `Session::eval*` would
286/// return. Owns its own [`bumpalo::Bump`] across calls; reset is
287/// per-call (the trace path always allocates a fresh arena to keep the
288/// borrowed-result lifetime tied to the run).
289pub struct TracedSession<'e> {
290    engine: &'e crate::Engine,
291}
292
293impl<'e> TracedSession<'e> {
294    /// Construct a session over `engine`. Invoked from
295    /// [`crate::Engine::trace`].
296    #[inline]
297    pub(crate) fn new(engine: &'e crate::Engine) -> Self {
298        Self { engine }
299    }
300
301    /// Traced evaluation of a pre-compiled [`crate::Logic`] returning
302    /// [`datavalue::OwnedDataValue`]. The trace surfaces only the
303    /// operators that survived compilation — constant sub-expressions
304    /// folded by [`crate::Engine::compile`] won't appear as steps. For
305    /// full coverage on a one-shot run, prefer [`Self::eval_str`].
306    pub fn eval<D>(&self, compiled: &crate::Logic, data: D) -> TracedRun<datavalue::OwnedDataValue>
307    where
308        D: crate::OwnedInput,
309    {
310        let owned_data = match data.into_owned_input() {
311            Ok(d) => d,
312            Err(e) => return Self::compile_failed(e),
313        };
314        let arena = bumpalo::Bump::new();
315        let inner = self.eval_borrowed_in(compiled, &owned_data, &arena);
316        TracedRun {
317            result: inner.result.and_then(crate::FromDataValue::from_arena),
318            steps: inner.steps,
319            expression_tree: inner.expression_tree,
320        }
321    }
322
323    /// One-shot traced evaluation with JSON-string boundary on both
324    /// sides. Compiles internally with the optimizer + constant-fold
325    /// passes disabled, so the trace surfaces every operator in the
326    /// rule.
327    pub fn eval_str<R, D>(&self, rule: R, data: D) -> TracedRun<String>
328    where
329        R: crate::IntoLogic,
330        D: crate::OwnedInput,
331    {
332        let owned = match rule.into_owned_logic() {
333            Ok(o) => o,
334            Err(e) => return Self::compile_failed(e),
335        };
336        let compiled = match crate::Logic::compile_for_trace(&owned, self.engine) {
337            Ok(c) => c,
338            Err(e) => return Self::compile_failed(e),
339        };
340        let owned_data = match data.into_owned_input() {
341            Ok(d) => d,
342            Err(e) => return Self::compile_failed(e),
343        };
344        let arena = bumpalo::Bump::new();
345        let inner = self.eval_borrowed_in(&compiled, &owned_data, &arena);
346        TracedRun {
347            result: inner.result.map(|v| v.to_string()),
348            steps: inner.steps,
349            expression_tree: inner.expression_tree,
350        }
351    }
352
353    /// Typed traced evaluation: deserialise the result into
354    /// `T: DeserializeOwned`. Routes through `serde_json`.
355    #[cfg(feature = "serde_json")]
356    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
357    pub fn eval_into<T, R, D>(&self, rule: R, data: D) -> TracedRun<T>
358    where
359        T: serde::de::DeserializeOwned,
360        R: crate::IntoLogic,
361        D: crate::OwnedInput,
362    {
363        let owned = match rule.into_owned_logic() {
364            Ok(o) => o,
365            Err(e) => return Self::compile_failed(e),
366        };
367        let compiled = match crate::Logic::compile_for_trace(&owned, self.engine) {
368            Ok(c) => c,
369            Err(e) => return Self::compile_failed(e),
370        };
371        let owned_data = match data.into_owned_input() {
372            Ok(d) => d,
373            Err(e) => return Self::compile_failed(e),
374        };
375        let arena = bumpalo::Bump::new();
376        let inner = self.eval_borrowed_in(&compiled, &owned_data, &arena);
377        let result = inner.result.and_then(|v| {
378            let value: serde_json::Value = crate::FromDataValue::from_arena(v)?;
379            serde_json::from_value(value).map_err(crate::Error::from)
380        });
381        TracedRun {
382            result,
383            steps: inner.steps,
384            expression_tree: inner.expression_tree,
385        }
386    }
387
388    /// Traced borrowed evaluation against a caller-owned arena. Mirrors
389    /// [`crate::Session::eval_borrowed`] / [`crate::Engine::evaluate`]
390    /// — the result references `arena`, while the trace data is owned
391    /// and outlives the arena.
392    pub fn eval_borrowed<'a, D>(
393        &self,
394        compiled: &'a crate::Logic,
395        data: D,
396        arena: &'a bumpalo::Bump,
397    ) -> TracedRun<&'a crate::DataValue<'a>>
398    where
399        D: crate::EvalInput<'a>,
400    {
401        self.eval_borrowed_in(compiled, data, arena)
402    }
403
404    /// Internal: shared body for the borrowed-result trace runs.
405    fn eval_borrowed_in<'a, D>(
406        &self,
407        compiled: &'a crate::Logic,
408        data: D,
409        arena: &'a bumpalo::Bump,
410    ) -> TracedRun<&'a crate::DataValue<'a>>
411    where
412        D: crate::EvalInput<'a>,
413    {
414        let expression_tree = ExpressionNode::build_from_compiled(&compiled.root);
415        let _depth_guard = match self.engine.enter_dispatch_boundary() {
416            Ok(g) => g,
417            Err(e) => {
418                return TracedRun {
419                    expression_tree,
420                    steps: TraceCollector::new().into_steps(),
421                    result: Err(e),
422                };
423            }
424        };
425        let data_ref = match data.into_arena_value(arena) {
426            Ok(av) => av,
427            Err(e) => {
428                return TracedRun {
429                    expression_tree,
430                    steps: TraceCollector::new().into_steps(),
431                    result: Err(e),
432                };
433            }
434        };
435        let mut ctx = crate::arena::ContextStack::new(data_ref);
436        ctx.attach_tracer(TraceCollector::new());
437
438        let outcome = self.engine.dispatch_node(&compiled.root, &mut ctx, arena);
439        let result = match outcome {
440            Ok(av) => Ok(av),
441            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled, false)),
442        };
443        let collector = ctx.detach_tracer().expect("attach_tracer was called above");
444        TracedRun {
445            result,
446            steps: collector.into_steps(),
447            expression_tree,
448        }
449    }
450
451    fn compile_failed<R>(error: crate::Error) -> TracedRun<R> {
452        TracedRun {
453            result: Err(error),
454            steps: Vec::new(),
455            expression_tree: ExpressionNode {
456                id: 0,
457                expression: String::new(),
458                children: Vec::new(),
459            },
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::OpCode;
468
469    #[test]
470    fn test_expression_node_from_simple_operator() {
471        // Create a simple {"val": "age"} node (var is normalized to Val).
472        let node = CompiledNode::BuiltinOperator {
473            id: crate::node::SYNTHETIC_ID,
474            opcode: OpCode::Val,
475            args: vec![CompiledNode::synthetic_value(
476                datavalue::OwnedDataValue::from("age"),
477            )]
478            .into_boxed_slice(),
479            predicate_hint: None,
480            iter_arg_kind: crate::operators::array::IterArgKind::General,
481        };
482
483        let tree = ExpressionNode::build_from_compiled(&node);
484
485        // Synthetic test nodes all share SYNTHETIC_ID, which surfaces as 0
486        // through the public `ExpressionNode::id` (u32) shape; the
487        // structural assertions below still hold.
488        assert_eq!(tree.id, 0);
489        assert_eq!(tree.expression, r#"{"val": "age"}"#);
490        assert!(tree.children.is_empty()); // "age" is a literal, not a child
491    }
492
493    #[test]
494    fn test_expression_node_from_nested_operator() {
495        // Create {">=": [{"val": "age"}, 18]}
496        let var_node = CompiledNode::BuiltinOperator {
497            id: crate::node::SYNTHETIC_ID,
498            opcode: OpCode::Val,
499            args: vec![CompiledNode::synthetic_value(
500                datavalue::OwnedDataValue::from("age"),
501            )]
502            .into_boxed_slice(),
503            predicate_hint: None,
504            iter_arg_kind: crate::operators::array::IterArgKind::General,
505        };
506        let node = CompiledNode::BuiltinOperator {
507            id: crate::node::SYNTHETIC_ID,
508            opcode: OpCode::GreaterThanEqual,
509            args: vec![
510                var_node,
511                CompiledNode::synthetic_value(datavalue::OwnedDataValue::Number(
512                    datavalue::NumberValue::Integer(18),
513                )),
514            ]
515            .into_boxed_slice(),
516            predicate_hint: None,
517            iter_arg_kind: crate::operators::array::IterArgKind::General,
518        };
519
520        let tree = ExpressionNode::build_from_compiled(&node);
521
522        assert_eq!(tree.id, 0);
523        assert!(tree.expression.contains(">="));
524        assert_eq!(tree.children.len(), 1); // var node is a child
525        assert!(tree.children[0].expression.contains("val"));
526    }
527
528    #[test]
529    fn test_trace_collector_records_steps() {
530        let mut collector = TraceCollector::new();
531
532        collector.record_step(0, serde_json::json!({"age": 25}), serde_json::json!(25));
533        collector.record_step(1, serde_json::json!({"age": 25}), serde_json::json!(true));
534
535        let steps = collector.into_steps();
536        assert_eq!(steps.len(), 2);
537        assert_eq!(steps[0].step_id, 0);
538        assert_eq!(steps[0].node_id, 0);
539        assert_eq!(steps[1].step_id, 1);
540        assert_eq!(steps[1].node_id, 1);
541    }
542
543    #[test]
544    fn test_trace_collector_iteration_context() {
545        let mut collector = TraceCollector::new();
546
547        collector.push_iteration(0, 3);
548        collector.record_step(2, serde_json::json!(1), serde_json::json!(2));
549
550        let steps = collector.into_steps();
551        assert_eq!(steps[0].iteration_index, Some(0));
552        assert_eq!(steps[0].iteration_total, Some(3));
553    }
554
555    #[test]
556    fn traced_session_evaluate_str_smoke() {
557        let engine = crate::Engine::new();
558        let run = engine.trace().eval_str(r#"{"+": [1, 2, 3]}"#, "null");
559        assert_eq!(run.result.unwrap(), "6");
560        // The one-shot trace path skips static folding internally, so the
561        // `+` operator survives and produces a step.
562        assert!(!run.steps.is_empty(), "expected non-empty steps");
563        assert_ne!(run.expression_tree.id, 0);
564    }
565
566    #[test]
567    fn traced_pre_compiled_inherits_fold() {
568        // Pre-compiled trace inherits the shape from `Engine::compile`, which
569        // folds. A fully-constant rule has no surviving operator → no steps.
570        let engine = crate::Engine::new();
571        let compiled = engine.compile(r#"{"+": [1, 2]}"#).unwrap();
572        let arena = bumpalo::Bump::new();
573        let data = datavalue::DataValue::from_str("null", &arena).unwrap();
574        let run = engine.trace().eval_borrowed(&compiled, data, &arena);
575        assert_eq!(run.result.as_ref().unwrap().as_i64(), Some(3));
576        assert!(
577            run.steps.is_empty(),
578            "folded rule should not produce trace steps"
579        );
580    }
581
582    #[test]
583    fn traced_session_carries_error_metadata() {
584        let engine = crate::Engine::new();
585        let run = engine.trace().eval_str(r#"{"+": ["x", 1]}"#, "null");
586        let err = run.result.expect_err("string-arith should fail");
587        assert_eq!(err.operator(), Some("+"));
588        assert!(!err.node_ids().is_empty(), "expected populated breadcrumb");
589    }
590}