Skip to main content

datalogic_rs/engine/
mod.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3
4use crate::config::EvaluationConfig;
5
6use crate::{CompiledNode, Logic, Result};
7
8thread_local! {
9    /// Per-thread re-entry counter for the `Engine::evaluate` boundary.
10    /// Bumped by `enter_dispatch_boundary` on entry and restored by the
11    /// `DepthGuard` on drop (not touched by `dispatch_node` itself), so the
12    /// value reflects how many nested `Engine::evaluate(...)` calls are
13    /// currently live on the sync call stack.
14    ///
15    /// Why thread-local rather than a `ContextStack` field: a custom
16    /// operator can hold `Arc<Engine>` and call `engine.evaluate(...)`
17    /// recursively from inside its own `evaluate(...)` — each top-level
18    /// call constructs a fresh `ContextStack` (depth resets to 0) but
19    /// the C call stack keeps growing. A thread-local survives across
20    /// those boundaries and catches the runaway recursion before stack
21    /// overflow.
22    ///
23    /// Tokio safety: `dispatch_node` is sync, so a task can't `.await`
24    /// while holding the counter raised. Between dispatch calls the
25    /// value returns to zero, so cross-thread task migration starts
26    /// fresh on whatever thread it lands.
27    static DISPATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
28}
29
30/// Restores [`DISPATCH_DEPTH`] to its prior value on drop. Used by
31/// the boundary entry points (`Engine::evaluate`, `TracedSession::evaluate`)
32/// so early returns and panics leave the counter consistent.
33///
34/// `DepthGuard(u32::MAX)` is a no-op sentinel — used when the engine has
35/// no custom operators registered, so cross-evaluate recursion is
36/// impossible and the boundary skips the TLS bookkeeping entirely. The
37/// drop check makes the guard zero-cost in that case.
38pub(crate) struct DepthGuard(u32);
39
40impl DepthGuard {
41    const NOOP: u32 = u32::MAX;
42}
43
44impl Drop for DepthGuard {
45    #[inline]
46    fn drop(&mut self) {
47        if self.0 != Self::NOOP {
48            DISPATCH_DEPTH.with(|d| d.set(self.0));
49        }
50    }
51}
52
53/// JSONLogic compile/evaluate engine.
54///
55/// Holds the immutable engine state — registered [`crate::CustomOperator`]
56/// implementations, the [`EvaluationConfig`], the optional
57/// preserve-structure flag — and exposes the public surface for parsing
58/// rules ([`Self::compile`]), evaluating them ([`Self::eval`] /
59// `Self::eval_into` is feature-gated on `serde_json`; link it
60// conditionally so default-features `cargo doc` doesn't break.
61#[cfg_attr(
62    feature = "serde_json",
63    doc = "[`Self::eval_str`], [`Self::eval_into`]), and opening hot-loop"
64)]
65#[cfg_attr(
66    not(feature = "serde_json"),
67    doc = "[`Self::eval_str`], `Self::eval_into`), and opening hot-loop"
68)]
69/// sessions ([`Self::session`]).
70// The `trace` feature adds [`Self::trace`]; reference it conditionally so
71// `cargo doc` without `--all-features` doesn't break on the intra-doc link.
72#[cfg_attr(
73    feature = "trace",
74    doc = "Enabling the `trace` feature also exposes [`Self::trace`] for traced sessions."
75)]
76///
77/// `Engine` is `Send + Sync` (every field is); the typical pattern is to
78/// build one at startup, wrap it in `Arc<Engine>`, and clone the `Arc`
79/// across threads or async tasks.
80///
81/// # Example
82///
83/// ```rust
84/// use datalogic_rs::Engine;
85///
86/// // 1. Build the engine.
87/// let engine = Engine::new();
88///
89/// // 2. Compile a rule once.
90/// let logic = engine
91///     .compile(r#"{"if": [{">=": [{"var": "age"}, 18]}, "adult", "minor"]}"#)
92///     .unwrap();
93///
94/// // 3. Evaluate against many inputs (here via `Session::eval_str`;
95/// //    drop to `Engine::evaluate` if you want zero-copy borrowed results).
96/// let mut session = engine.session();
97/// for age in [12, 18, 42] {
98///     let payload = format!(r#"{{"age": {age}}}"#);
99///     let result = session.eval_str(&logic, &payload).unwrap();
100///     assert!(result == "\"adult\"" || result == "\"minor\"");
101///     session.reset();
102/// }
103/// ```
104///
105/// # Choosing an evaluate method
106///
107/// **Start here.** Use [`Self::eval_str`] for one-shot calls. Switch
108/// to [`crate::Session`] once you're evaluating the same compiled rule
109/// many times — it reuses one arena instead of allocating per call.
110/// Drop down to [`Self::evaluate`] only when you're managing your own
111/// `bumpalo::Bump` (custom pools, integration with arena-aware
112/// downstream code).
113///
114/// Result-shape suffixes work the same on every tier: `(none)` returns
115/// [`datavalue::OwnedDataValue`], `_str` returns [`String`] (JSON),
116/// `_into::<T>` returns `T: DeserializeOwned` (requires `serde_json`).
117/// The raw [`Self::evaluate`] is the only method that exposes
118/// `&'a DataValue<'a>` and a caller-owned `&Bump`.
119///
120/// Three tiers, in order of caller control:
121///
122/// | Method | Arena ownership | Result type | When to use |
123/// |---|---|---|---|
124// `eval_into` is feature-gated on `serde_json`; emit a linked or plain
125// reference depending on the active features so default-features
126// `cargo doc` doesn't break the table row.
127#[cfg_attr(
128    feature = "serde_json",
129    doc = "| [`Self::eval`] / [`Self::eval_str`] / [`Self::eval_into`] | engine creates a fresh `Bump::with_capacity(4096)` per call | [`OwnedDataValue`](datavalue::OwnedDataValue) / `String` / `T` | One-shot. Any caller that doesn't want to think about arenas. Allocates each call — for hot loops, drop to `Session`. |"
130)]
131#[cfg_attr(
132    not(feature = "serde_json"),
133    doc = "| [`Self::eval`] / [`Self::eval_str`] / `Self::eval_into` | engine creates a fresh `Bump::with_capacity(4096)` per call | [`OwnedDataValue`](datavalue::OwnedDataValue) / `String` / `T` | One-shot. Any caller that doesn't want to think about arenas. Allocates each call — for hot loops, drop to `Session`. |"
134)]
135#[cfg_attr(
136    feature = "serde_json",
137    doc = "| [`crate::Session::eval`] / [`crate::Session::eval_str`] / [`crate::Session::eval_into`] / [`crate::Session::eval_borrowed`] | session-owned `Bump`, caller calls [`crate::Session::reset`] between batches | owned / `String` / `T` / borrowed `&'a DataValue<'a>` | Hot loop with a long-lived engine. The `Session` hides `bumpalo` from the call site and pre-sizes the arena via [`crate::Session::reset_with_capacity`] when needed. |"
138)]
139#[cfg_attr(
140    not(feature = "serde_json"),
141    doc = "| [`crate::Session::eval`] / [`crate::Session::eval_str`] / `Session::eval_into` / [`crate::Session::eval_borrowed`] | session-owned `Bump`, caller calls [`crate::Session::reset`] between batches | owned / `String` / `T` / borrowed `&'a DataValue<'a>` | Hot loop with a long-lived engine. The `Session` hides `bumpalo` from the call site and pre-sizes the arena via [`crate::Session::reset_with_capacity`] when needed. |"
142)]
143/// | [`Self::evaluate`] | caller-passed `&Bump`; library never resets | `&'a DataValue<'a>` (borrowed) | Zero-copy result paths, custom pool/allocator strategies, integration with arena-aware downstream code. |
144///
145/// All routes share the same dispatcher; the differences are who owns
146/// the arena, what the result type looks like, and whether the
147/// boundary parses / serialises JSON for you. There is no perf
148/// difference between the arena-aware paths once the bump is warm.
149///
150/// See the crate-level docs for the two-phase architecture, threading
151/// model, and walk-through examples; see the `Session` and
152/// `EvaluationConfig` rustdoc for arena-management and behaviour-tuning
153/// options respectively.
154pub struct Engine {
155    /// Custom `CustomOperator` implementations registered with the engine.
156    pub(super) custom_operators: HashMap<String, Box<dyn crate::CustomOperator>>,
157    /// Whether templating mode is enabled — multi-key objects compile
158    /// to output-shaping templates and unknown operator keys pass through.
159    #[cfg(feature = "templating")]
160    templating: bool,
161    /// Whether `Engine::compile` runs the constant-folding pass.
162    /// Defaults to `true`; toggled via
163    /// [`crate::EngineBuilder::with_constant_folding`]. The trace surface
164    /// always disables folding regardless of this flag (handled in
165    /// `TracedSession`).
166    constant_folding: bool,
167    /// Configuration for evaluation behavior
168    config: EvaluationConfig,
169}
170
171mod dispatch;
172
173/// Convert an `OwnedDataValue` literal to an arena-resident `DataValue`
174/// reference. Reached from the `dispatch_node` literal path for any
175/// `CompiledNode::Value` whose `lit` was not precomputed — in practice
176/// only ad-hoc `synthetic_value` wrappers built outside the compile
177/// pipeline, since `populate_lits` pre-builds every literal reachable from
178/// a `Logic`. Trivial cases (Null, Bool, empty primitives) hit shared
179/// singletons with no allocation; a non-empty String allocates a single
180/// `DataValue` wrapper into the per-call arena (the `&str` is borrowed from
181/// the owned source); non-empty Arrays/Objects rebuild their spine in the
182/// arena via [`borrow_to_arena`], borrowing string bytes from the owned
183/// source instead of copying them.
184///
185/// `#[cold]` + `#[inline(never)]`: `dispatch_node` is `#[inline(always)]`,
186/// so its literal path is stamped into every operator's dispatch site —
187/// keeping this fallback outlined keeps those sites small (I-cache), and
188/// the cold hint steers the branch layout toward the pre-built `lit` path
189/// that every compiled literal takes.
190#[cold]
191#[inline(never)]
192fn literal_fallback<'a>(
193    value: &'a datavalue::OwnedDataValue,
194    arena: &'a bumpalo::Bump,
195) -> &'a crate::arena::DataValue<'a> {
196    use datavalue::OwnedDataValue;
197    match value {
198        OwnedDataValue::Null => crate::arena::singletons::singleton_null(),
199        OwnedDataValue::Bool(b) => crate::arena::singletons::singleton_bool(*b),
200        OwnedDataValue::String(s) if s.is_empty() => {
201            crate::arena::singletons::singleton_empty_string()
202        }
203        OwnedDataValue::Array(a) if a.is_empty() => {
204            crate::arena::singletons::singleton_empty_array()
205        }
206        OwnedDataValue::Object(o) if o.is_empty() => {
207            crate::arena::singletons::singleton_empty_object()
208        }
209        OwnedDataValue::String(s) => arena.alloc(crate::arena::DataValue::String(s.as_str())),
210        _ => arena.alloc(borrow_to_arena(value, arena)),
211    }
212}
213
214/// Convert an owned composite literal into an arena `DataValue`, borrowing
215/// string bytes (element strings and object keys) from the owned source
216/// instead of copying them into the arena the way
217/// `OwnedDataValue::to_arena` does. Only the array/object spine is built in
218/// the arena. Sound because the compiled node — and therefore `value` —
219/// outlives the evaluation: `dispatch_node` borrows the node at the same
220/// `'a` as the arena. Recursion depth mirrors the value's nesting, which is
221/// bounded by the JSON parser / compile-time depth caps upstream.
222fn borrow_to_arena<'a>(
223    value: &'a datavalue::OwnedDataValue,
224    arena: &'a bumpalo::Bump,
225) -> crate::arena::DataValue<'a> {
226    use crate::arena::DataValue;
227    use datavalue::OwnedDataValue;
228    match value {
229        OwnedDataValue::Null => DataValue::Null,
230        OwnedDataValue::Bool(b) => DataValue::Bool(*b),
231        OwnedDataValue::Number(n) => DataValue::Number(*n),
232        OwnedDataValue::String(s) => DataValue::String(s.as_str()),
233        OwnedDataValue::Array(items) => DataValue::Array(
234            arena.alloc_slice_fill_with(items.len(), |i| borrow_to_arena(&items[i], arena)),
235        ),
236        OwnedDataValue::Object(pairs) => {
237            DataValue::Object(arena.alloc_slice_fill_with(pairs.len(), |i| {
238                let (k, v) = &pairs[i];
239                (k.as_str(), borrow_to_arena(v, arena))
240            }))
241        }
242        #[cfg(feature = "datetime")]
243        OwnedDataValue::DateTime(d) => DataValue::DateTime(*d),
244        #[cfg(feature = "datetime")]
245        OwnedDataValue::Duration(d) => DataValue::Duration(*d),
246    }
247}
248
249impl Default for Engine {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255impl std::fmt::Debug for Engine {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        // Print the operator *count* rather than names: names are user
258        // registration data, and `Engine::custom_operator_names()` exposes
259        // them already for callers who want them. The trait objects
260        // themselves can't render a meaningful Debug.
261        let mut s = f.debug_struct("Engine");
262        s.field("custom_operators", &self.custom_operators.len());
263        #[cfg(feature = "templating")]
264        s.field("templating", &self.templating);
265        s.field("config", &self.config);
266        s.finish_non_exhaustive()
267    }
268}
269
270impl Engine {
271    /// Start a [`crate::EngineBuilder`] for fluent construction.
272    ///
273    /// Use the builder when you need a non-default [`EvaluationConfig`],
274    /// templating mode, or pre-registered custom operators.
275    /// For a stock engine, [`Self::new`] is shorter.
276    #[inline]
277    pub fn builder() -> crate::EngineBuilder {
278        crate::EngineBuilder::new()
279    }
280
281    /// Open a [`crate::Session`] handle that owns a reusable arena and
282    /// returns owned results, so callers don't need to manage a
283    /// [`bumpalo::Bump`] themselves.
284    ///
285    /// Use this when you want the throughput of arena reuse without the
286    /// lifetime juggling of [`Self::evaluate`]. Results are deep-cloned out
287    /// of the arena before returning, so they survive later calls and resets.
288    /// The session does **not** auto-reset: allocations accumulate until you
289    /// call [`crate::Session::reset`], which you should do between logical
290    /// batches to bound peak memory in long-running services.
291    ///
292    /// # Example
293    ///
294    /// ```rust
295    /// use datalogic_rs::Engine;
296    ///
297    /// let engine = Engine::new();
298    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
299    /// let mut session = engine.session();
300    /// let result = session.eval_str(&compiled, r#"{"x": 41}"#).unwrap();
301    /// assert_eq!(result, "42");
302    /// ```
303    #[inline]
304    pub fn session(&self) -> crate::Session<'_> {
305        crate::Session::new(self)
306    }
307
308    /// Internal seam used by the builder. `pub(crate)` is enough — no
309    /// `#[doc(hidden)]` needed since it's not externally reachable.
310    #[inline]
311    pub(crate) fn from_builder_parts(
312        config: EvaluationConfig,
313        _templating: bool,
314        constant_folding: bool,
315        operators: HashMap<String, Box<dyn crate::CustomOperator>>,
316    ) -> Self {
317        Self {
318            custom_operators: operators,
319            #[cfg(feature = "templating")]
320            templating: _templating,
321            constant_folding,
322            config,
323        }
324    }
325
326    /// Creates a new Engine with all built-in operators.
327    ///
328    /// The engine includes every built-in operator compiled into this build
329    /// (64 canonical operators with all operator features on; see
330    /// [`Self::builtin_operator_names`]), dispatched via `OpCode`.
331    /// Templating mode is disabled by default. For non-default
332    /// configuration (custom [`EvaluationConfig`], templating mode,
333    /// pre-registered custom operators) prefer [`Self::builder`].
334    ///
335    /// # Example
336    ///
337    /// ```rust
338    /// use datalogic_rs::Engine;
339    ///
340    /// let engine = Engine::new();
341    /// ```
342    pub fn new() -> Self {
343        Self::from_builder_parts(EvaluationConfig::default(), false, true, HashMap::new())
344    }
345
346    /// Gets a reference to the current evaluation configuration.
347    pub fn config(&self) -> &EvaluationConfig {
348        &self.config
349    }
350
351    /// Internal: whether the constant-folding pass runs during
352    /// [`Self::compile`]. Reads the field set by
353    /// [`crate::EngineBuilder::with_constant_folding`].
354    #[inline]
355    pub(crate) fn constant_folding_enabled(&self) -> bool {
356        self.constant_folding
357    }
358
359    /// Internal: whether templating mode is on. Always returns `false`
360    /// when the crate is built without `feature = "templating"` (the
361    /// underlying field doesn't exist off-feature). Folded here so the
362    /// single call site in `compile/` doesn't repeat the `#[cfg]` ceremony.
363    #[inline]
364    pub(crate) fn is_templating_enabled(&self) -> bool {
365        #[cfg(feature = "templating")]
366        {
367            self.templating
368        }
369        #[cfg(not(feature = "templating"))]
370        {
371            false
372        }
373    }
374
375    /// Checks if a custom operator with the given name is registered.
376    ///
377    /// Operator registration is builder-only; this is a read-only check
378    /// against the frozen set produced by [`crate::EngineBuilder`].
379    pub fn has_custom_operator(&self, name: &str) -> bool {
380        self.custom_operators.contains_key(name)
381    }
382
383    /// Iterator over the names of every *custom* operator registered on
384    /// this engine (built-ins are not included). Order is unspecified
385    /// (HashMap iteration order). Useful for tooling, UIs, and tests
386    /// that need to introspect what's available.
387    pub fn custom_operator_names(&self) -> impl Iterator<Item = &str> {
388        self.custom_operators.keys().map(String::as_str)
389    }
390
391    /// Iterator over every *built-in* operator name this build evaluates:
392    /// the JSONLogic baseline plus whichever extension families
393    /// (`ext-string`, `datetime`, `ext-control`, …) were compiled in.
394    ///
395    /// The list is derived from the same table the compiler resolves
396    /// operator keys against, so it cannot drift from dispatch. It
397    /// includes input aliases (`var` for `val`, `?:` for `if`, `match`
398    /// for `switch`) because those keys are live calls too; order is
399    /// canonical name first, then its aliases, per operator.
400    ///
401    /// Custom registrations are not included; see
402    /// [`Self::custom_operator_names`]. The union of the two is the full
403    /// vocabulary of this engine. A custom operator registered under a
404    /// built-in name is never reached: built-in resolution wins at
405    /// compile time.
406    ///
407    /// Under templating mode an unknown key is not an error (the object
408    /// echoes back as data), so this is the vocabulary authoring-side
409    /// tooling needs to tell a live call from a literal.
410    ///
411    /// # Example
412    ///
413    /// ```rust
414    /// use datalogic_rs::Engine;
415    ///
416    /// let engine = Engine::new();
417    /// let names: Vec<&str> = engine.builtin_operator_names().collect();
418    /// assert!(names.contains(&"val"));
419    /// assert!(names.contains(&"var")); // alias of `val`
420    /// assert!(!names.contains(&"lenght"));
421    /// ```
422    pub fn builtin_operator_names(&self) -> impl Iterator<Item = &'static str> + use<> {
423        crate::opcode::builtin_operator_names()
424    }
425
426    // ============================================================
427    // V5 PUBLIC API
428    //   - One-shot:   `eval` / `eval_str` / `eval_into`   (engine-owned arena per call)
429    //   - Power tier: `evaluate(&Logic, D, &Bump)`        (caller-owned arena, borrowed result)
430    //   - Hot loop:   `engine.session().eval*(...)`       (pooled arena, manual reset)
431    //   - Trace:      `engine.trace().eval*(...)`         (same shapes wrapped in TracedRun<R>; eval_str/eval_into take a rule source)
432    // ============================================================
433
434    /// Compile a rule source into reusable [`Logic`].
435    ///
436    /// `rule` accepts any [`crate::IntoLogic`] shape: `&str` (JSON-parsed),
437    /// `&OwnedDataValue` / `OwnedDataValue` (cloned/moved), or
438    /// `&serde_json::Value` (gated on `serde_json`). For cross-thread
439    /// sharing prefer [`Self::compile_arc`].
440    ///
441    /// # Example
442    ///
443    /// ```rust
444    /// use datalogic_rs::Engine;
445    ///
446    /// let engine = Engine::new();
447    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
448    /// ```
449    pub fn compile<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
450        let owned = rule.into_owned_logic()?;
451        Logic::compile_with(&owned, self)
452    }
453
454    /// Compile and wrap in an [`Arc`](std::sync::Arc) in one call. Convenience for the
455    /// dominant cross-thread-sharing pattern; equivalent to
456    /// `Arc::new(engine.compile(rule)?)`.
457    pub fn compile_arc<R: crate::IntoLogic>(&self, rule: R) -> Result<std::sync::Arc<Logic>> {
458        Ok(std::sync::Arc::new(self.compile(rule)?))
459    }
460
461    /// Open a [`crate::TracedSession`] over this engine. Calls made through
462    /// the session collect a per-call trace; the bare `eval*` methods on
463    /// `Engine` itself pay no trace overhead.
464    ///
465    /// Available only when the crate is built with `feature = "trace"`.
466    ///
467    /// # Trace coverage
468    ///
469    /// The session's one-shot [`crate::TracedSession::eval_str`] compiles
470    /// the rule internally with optimization disabled, so every operator
471    /// in the rule surfaces a trace step.
472    ///
473    /// The pre-compiled paths ([`crate::TracedSession::eval`] taking a
474    /// `&Logic`) inherit whatever shape that `Logic` was compiled into —
475    /// constant sub-expressions folded by [`Self::compile`] won't appear,
476    /// since there is no operator left to execute. Use `eval_str` for
477    /// full coverage on a one-shot run.
478    ///
479    /// # Example
480    ///
481    /// ```rust
482    /// # #[cfg(feature = "trace")] {
483    /// use datalogic_rs::Engine;
484    ///
485    /// let engine = Engine::new();
486    /// let run = engine
487    ///     .trace()
488    ///     .eval_str(r#"{"+": [1, 2]}"#, "null");
489    /// assert_eq!(run.result.unwrap(), "3");
490    /// // run.steps is the per-node execution log;
491    /// // run.expression_tree is the rule's compile-time tree shape.
492    /// # }
493    /// ```
494    #[cfg(feature = "trace")]
495    #[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
496    #[inline]
497    pub fn trace(&self) -> crate::trace::TracedSession<'_> {
498        crate::trace::TracedSession::new(self)
499    }
500
501    /// Evaluate compiled logic against arena-resident data — **raw tier**.
502    ///
503    /// The caller owns the [`bumpalo::Bump`] lifecycle and may `reset()`
504    /// it between calls; the returned `&DataValue<'a>` borrows from the
505    /// arena, so it must be dropped before the next reset (enforced by
506    /// the borrow checker). For ergonomic owned/typed/JSON-string output,
507    /// prefer [`Self::eval`] / [`Self::eval_str`]
508    // `Self::eval_into` is gated behind `serde_json`; link conditionally.
509    #[cfg_attr(feature = "serde_json", doc = "/ [`Self::eval_into`].")]
510    #[cfg_attr(
511        not(feature = "serde_json"),
512        doc = "(plus `Self::eval_into` with the `serde_json` feature)."
513    )]
514    ///
515    /// # Example
516    ///
517    /// ```rust
518    /// use bumpalo::Bump;
519    /// use datalogic_rs::{Engine, DataValue};
520    ///
521    /// let engine = Engine::new();
522    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 2]}"#).unwrap();
523    ///
524    /// let arena = Bump::new();
525    /// let data = DataValue::from_str(r#"{"x": 40}"#, &arena).unwrap();
526    /// let result = engine.evaluate(&compiled, data, &arena).unwrap();
527    /// assert_eq!(result.as_i64(), Some(42));
528    /// ```
529    ///
530    /// `data` accepts any input shape understood by [`crate::EvalInput`]:
531    /// `&'a DataValue<'a>` (zero-cost passthrough), `DataValue<'a>`
532    /// (single arena alloc), `&str` (JSON-parsed), `&OwnedDataValue`
533    /// (deep-borrowed), [`&ParsedData`](crate::ParsedData) (zero-cost
534    /// passthrough of a parse-once handle), or `&serde_json::Value`
535    /// (gated on `serde_json`).
536    #[inline(always)]
537    pub fn evaluate<'a, D: crate::EvalInput<'a>>(
538        &self,
539        compiled: &'a Logic,
540        data: D,
541        arena: &'a bumpalo::Bump,
542    ) -> Result<&'a crate::arena::DataValue<'a>> {
543        let _depth_guard = self.enter_dispatch_boundary()?;
544        let data_ref = data.into_arena_value(arena)?;
545        let mut ctx = crate::arena::ContextStack::new(data_ref);
546        match self.dispatch_node(&compiled.root, &mut ctx, arena) {
547            Ok(av) => Ok(av),
548            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled, true)),
549        }
550    }
551
552    /// Apply the engine's configured truthiness rules
553    /// ([`crate::TruthyEvaluator`]) to an evaluated value.
554    ///
555    /// This is the same coercion `if` / `and` / `or` / `!` apply to
556    /// their operands, exposed so callers (and bindings) can collapse
557    /// any rule result to a boolean without re-implementing the
558    /// engine's configured semantics.
559    ///
560    /// # Example
561    ///
562    /// ```rust
563    /// use bumpalo::Bump;
564    /// use datalogic_rs::Engine;
565    ///
566    /// let engine = Engine::new();
567    /// let compiled = engine.compile(r#"{"var": "items"}"#).unwrap();
568    /// let arena = Bump::new();
569    /// let result = engine.evaluate(&compiled, r#"{"items": [1]}"#, &arena).unwrap();
570    /// assert!(engine.truthy(result));
571    /// ```
572    #[inline]
573    pub fn truthy(&self, value: &crate::arena::DataValue<'_>) -> bool {
574        crate::arena::truthy_arena(value, self)
575    }
576
577    /// One-shot evaluation returning [`datavalue::OwnedDataValue`].
578    ///
579    /// Compiles `rule`, parses `data`, evaluates against a fresh
580    /// per-call arena, and deep-clones the result out. For the same
581    /// rule run repeatedly, escalate to [`Self::compile`] + a
582    /// [`Session`](crate::Session).
583    ///
584    /// # Example
585    ///
586    /// ```rust
587    /// use datalogic_rs::Engine;
588    ///
589    /// let engine = Engine::new();
590    /// let result = engine.eval(
591    ///     r#"{"+": [{"var": "x"}, 1]}"#,
592    ///     r#"{"x": 41}"#,
593    /// ).unwrap();
594    /// assert_eq!(result.as_i64(), Some(42));
595    /// ```
596    pub fn eval<R, D>(&self, rule: R, data: D) -> Result<datavalue::OwnedDataValue>
597    where
598        R: crate::IntoLogic,
599        D: crate::OwnedInput,
600    {
601        self.eval_with::<datavalue::OwnedDataValue, _, _>(rule, data)
602    }
603
604    /// One-shot evaluation returning a JSON [`String`].
605    ///
606    /// # Example
607    ///
608    /// ```rust
609    /// use datalogic_rs::Engine;
610    ///
611    /// let engine = Engine::new();
612    /// let result = engine.eval_str(
613    ///     r#"{"==": [{"var": "x"}, 5]}"#,
614    ///     r#"{"x": 5}"#,
615    /// ).unwrap();
616    /// assert_eq!(result, "true");
617    /// ```
618    pub fn eval_str<R, D>(&self, rule: R, data: D) -> Result<String>
619    where
620        R: crate::IntoLogic,
621        D: crate::OwnedInput,
622    {
623        self.eval_with::<String, _, _>(rule, data)
624    }
625
626    /// One-shot evaluation deserialised into a typed `T: DeserializeOwned`.
627    ///
628    /// Use `T = serde_json::Value` for a JSON `Value` result; use a typed
629    /// struct for direct mapping. Internally routes through `serde_json`
630    /// (round-trips the result through a JSON value).
631    ///
632    /// # Example
633    ///
634    /// ```rust
635    /// # #[cfg(feature = "serde_json")] {
636    /// use datalogic_rs::Engine;
637    /// use serde_json::Value;
638    ///
639    /// let engine = Engine::new();
640    /// let result: Value = engine.eval_into(
641    ///     r#"{"+": [{"var": "x"}, 1]}"#,
642    ///     r#"{"x": 41}"#,
643    /// ).unwrap();
644    /// assert_eq!(result, Value::from(42));
645    /// # }
646    /// ```
647    #[cfg(feature = "serde_json")]
648    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
649    pub fn eval_into<T, R, D>(&self, rule: R, data: D) -> Result<T>
650    where
651        T: serde::de::DeserializeOwned,
652        R: crate::IntoLogic,
653        D: crate::OwnedInput,
654    {
655        let value: serde_json::Value = self.eval_with(rule, data)?;
656        serde_json::from_value(value).map_err(crate::Error::from)
657    }
658
659    /// Internal generic shared by `eval` / `eval_str` / `eval_into`.
660    /// Compiles, allocates a fresh per-call arena, evaluates, and
661    /// projects the result through [`crate::FromDataValue`].
662    fn eval_with<O, R, D>(&self, rule: R, data: D) -> Result<O>
663    where
664        O: crate::FromDataValue,
665        R: crate::IntoLogic,
666        D: crate::OwnedInput,
667    {
668        let compiled = self.compile(rule)?;
669        // 4 KB initial capacity covers typical small-rule evaluations.
670        let arena = bumpalo::Bump::with_capacity(4096);
671        let owned_data = data.into_owned_input()?;
672        let result = self.evaluate(&compiled, &owned_data, &arena)?;
673        O::from_arena(result)
674    }
675
676    /// Bump the per-thread dispatch-boundary depth counter, bailing with
677    /// `ConfigurationError` if the configured cap is reached. Returns a
678    /// guard that decrements the counter on drop (covers `?` early returns
679    /// and panics).
680    ///
681    /// Called from every public boundary entry point (`Engine::evaluate`,
682    /// `TracedSession::evaluate`, …). The counter is thread-local rather
683    /// than per-`ContextStack` so it survives across nested
684    /// `engine.evaluate(...)` calls — the scenario a `CustomOperator`
685    /// holding `Arc<Engine>` creates by re-entering the engine from inside
686    /// its own `evaluate(...)`.
687    ///
688    /// Tokio safety: dispatch is sync, so a task cannot `.await` while the
689    /// counter is raised; between dispatches the value is restored to its
690    /// prior level (zero at the outermost call). Cross-thread task migration
691    /// thus starts fresh on whatever thread the task lands on.
692    #[inline(always)]
693    pub(crate) fn enter_dispatch_boundary(&self) -> Result<DepthGuard> {
694        // Built-in operators can't re-enter `Engine::evaluate` (only a
695        // `CustomOperator` holding `Arc<Engine>` can); when the registry
696        // is empty, cross-evaluate recursion is impossible and we skip
697        // the TLS bookkeeping. The pure-built-in benchmarks pay zero.
698        if self.custom_operators.is_empty() {
699            return Ok(DepthGuard(DepthGuard::NOOP));
700        }
701        self.enter_dispatch_boundary_checked()
702    }
703
704    /// Slow path of [`Self::enter_dispatch_boundary`] — hit only when
705    /// the engine has at least one custom operator registered. Marked
706    /// `#[cold]` and `#[inline(never)]` so the hot fast-path stays
707    /// inline-friendly.
708    #[cold]
709    #[inline(never)]
710    fn enter_dispatch_boundary_checked(&self) -> Result<DepthGuard> {
711        let prev_depth = DISPATCH_DEPTH.with(Cell::get);
712        if prev_depth >= self.config.max_recursion_depth {
713            return Err(crate::Error::configuration_error(format!(
714                "max recursion depth exceeded ({})",
715                self.config.max_recursion_depth
716            )));
717        }
718        DISPATCH_DEPTH.with(|d| d.set(prev_depth + 1));
719        Ok(DepthGuard(prev_depth))
720    }
721
722    /// Arena-mode dispatch hub. Returns `&'a DataValue<'a>` for every
723    /// `CompiledNode` shape — exhaustive match, no value-mode fallback.
724    ///
725    /// On error, accumulates the failing node's id onto the context stack's
726    /// breadcrumb so [`Error`] consumers can surface the failing
727    /// path. When a tracer is attached to `ctx`, records a step per
728    /// non-literal node (entry context + result/error).
729    #[inline(always)]
730    pub(crate) fn dispatch_node<'a>(
731        &self,
732        node: &'a CompiledNode,
733        ctx: &mut crate::arena::ContextStack<'a>,
734        arena: &'a bumpalo::Bump,
735    ) -> Result<&'a crate::arena::DataValue<'a>> {
736        // Literal fast path — no breadcrumb push, no trace step.
737        if let CompiledNode::Value { value, lit, .. } = node {
738            // Every literal reachable from a `Logic` carries a pre-built
739            // `PreLit` — trivial ones from `precompute_lit` at node
740            // construction, composites from the `populate_lits` pass —
741            // so the hot path is a borrow, not a conversion.
742            if let Some(av) = lit {
743                return Ok(av.as_ref());
744            }
745            // Synthetic composite wrappers built outside the compile
746            // pipeline fall through to per-call arena conversion here.
747            return Ok(literal_fallback(value, arena));
748        }
749
750        // Snapshot context for trace BEFORE recursing — children will
751        // mutate iteration frames. Cheap when no tracer is attached.
752        #[cfg(feature = "trace")]
753        let ctx_snapshot: Option<serde_json::Value> =
754            ctx.has_tracer().then(|| ctx.current_data_as_value());
755
756        let result = dispatch::dispatch_node_inner(self, node, ctx, arena);
757
758        // Accumulate the failing node's id on every Err. We always pay
759        // the (single) Vec::push since errors are rare and structured-error
760        // consumers need the breadcrumb.
761        if result.is_err() {
762            ctx.push_error_step(node.id());
763        }
764
765        #[cfg(feature = "trace")]
766        if let Some(ctx_data) = ctx_snapshot {
767            ctx.record_node_result(node.id(), ctx_data, &result);
768        }
769
770        result
771    }
772
773    /// Evaluate an iteration body (map/filter/reduce/all/some/none) with the
774    /// trace collector's iteration index/total markers set around it. The
775    /// markers are no-ops when no tracer is attached, so plain-mode callers
776    /// pay only one branch per iteration.
777    #[inline]
778    pub(crate) fn run_iter_body<'a>(
779        &self,
780        body: &'a CompiledNode,
781        ctx: &mut crate::arena::ContextStack<'a>,
782        arena: &'a bumpalo::Bump,
783        _index: u32,
784        _total: u32,
785    ) -> Result<&'a crate::arena::DataValue<'a>> {
786        #[cfg(feature = "trace")]
787        ctx.trace_push_iteration(_index, _total);
788        let res = self.dispatch_node(body, ctx, arena);
789        #[cfg(feature = "trace")]
790        ctx.trace_pop_iteration();
791        res
792    }
793}