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 50+ built-in operators optimized with OpCode dispatch.
329    /// Templating mode is disabled by default. For non-default
330    /// configuration (custom [`EvaluationConfig`], templating mode,
331    /// pre-registered custom operators) prefer [`Self::builder`].
332    ///
333    /// # Example
334    ///
335    /// ```rust
336    /// use datalogic_rs::Engine;
337    ///
338    /// let engine = Engine::new();
339    /// ```
340    pub fn new() -> Self {
341        Self::from_builder_parts(EvaluationConfig::default(), false, true, HashMap::new())
342    }
343
344    /// Gets a reference to the current evaluation configuration.
345    pub fn config(&self) -> &EvaluationConfig {
346        &self.config
347    }
348
349    /// Internal: whether the constant-folding pass runs during
350    /// [`Self::compile`]. Reads the field set by
351    /// [`crate::EngineBuilder::with_constant_folding`].
352    #[inline]
353    pub(crate) fn constant_folding_enabled(&self) -> bool {
354        self.constant_folding
355    }
356
357    /// Internal: whether templating mode is on. Always returns `false`
358    /// when the crate is built without `feature = "templating"` (the
359    /// underlying field doesn't exist off-feature). Folded here so the
360    /// single call site in `compile/` doesn't repeat the `#[cfg]` ceremony.
361    #[inline]
362    pub(crate) fn is_templating_enabled(&self) -> bool {
363        #[cfg(feature = "templating")]
364        {
365            self.templating
366        }
367        #[cfg(not(feature = "templating"))]
368        {
369            false
370        }
371    }
372
373    /// Checks if a custom operator with the given name is registered.
374    ///
375    /// Operator registration is builder-only; this is a read-only check
376    /// against the frozen set produced by [`crate::EngineBuilder`].
377    pub fn has_custom_operator(&self, name: &str) -> bool {
378        self.custom_operators.contains_key(name)
379    }
380
381    /// Iterator over the names of every *custom* operator registered on
382    /// this engine (built-ins are not included). Order is unspecified
383    /// (HashMap iteration order). Useful for tooling, UIs, and tests
384    /// that need to introspect what's available.
385    pub fn custom_operator_names(&self) -> impl Iterator<Item = &str> {
386        self.custom_operators.keys().map(String::as_str)
387    }
388
389    // ============================================================
390    // V5 PUBLIC API
391    //   - One-shot:   `eval` / `eval_str` / `eval_into`   (engine-owned arena per call)
392    //   - Power tier: `evaluate(&Logic, D, &Bump)`        (caller-owned arena, borrowed result)
393    //   - Hot loop:   `engine.session().eval*(...)`       (pooled arena, manual reset)
394    //   - Trace:      `engine.trace().eval*(...)`         (Session mirror with TracedRun<R>)
395    // ============================================================
396
397    /// Compile a rule source into reusable [`Logic`].
398    ///
399    /// `rule` accepts any [`crate::IntoLogic`] shape: `&str` (JSON-parsed),
400    /// `&OwnedDataValue` / `OwnedDataValue` (cloned/moved), or
401    /// `&serde_json::Value` (gated on `serde_json`). For cross-thread
402    /// sharing prefer [`Self::compile_arc`].
403    ///
404    /// # Example
405    ///
406    /// ```rust
407    /// use datalogic_rs::Engine;
408    ///
409    /// let engine = Engine::new();
410    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
411    /// ```
412    pub fn compile<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
413        let owned = rule.into_owned_logic()?;
414        Logic::compile_with(&owned, self)
415    }
416
417    /// Compile and wrap in an [`Arc`](std::sync::Arc) in one call. Convenience for the
418    /// dominant cross-thread-sharing pattern; equivalent to
419    /// `Arc::new(engine.compile(rule)?)`.
420    pub fn compile_arc<R: crate::IntoLogic>(&self, rule: R) -> Result<std::sync::Arc<Logic>> {
421        Ok(std::sync::Arc::new(self.compile(rule)?))
422    }
423
424    /// Open a [`crate::TracedSession`] over this engine. Calls made through
425    /// the session collect a per-call trace; the bare `eval*` methods on
426    /// `Engine` itself pay no trace overhead.
427    ///
428    /// Available only when the crate is built with `feature = "trace"`.
429    ///
430    /// # Trace coverage
431    ///
432    /// The session's one-shot [`crate::TracedSession::eval_str`] compiles
433    /// the rule internally with optimization disabled, so every operator
434    /// in the rule surfaces a trace step.
435    ///
436    /// The pre-compiled paths ([`crate::TracedSession::eval`] taking a
437    /// `&Logic`) inherit whatever shape that `Logic` was compiled into —
438    /// constant sub-expressions folded by [`Self::compile`] won't appear,
439    /// since there is no operator left to execute. Use `eval_str` for
440    /// full coverage on a one-shot run.
441    ///
442    /// # Example
443    ///
444    /// ```rust
445    /// # #[cfg(feature = "trace")] {
446    /// use datalogic_rs::Engine;
447    ///
448    /// let engine = Engine::new();
449    /// let run = engine
450    ///     .trace()
451    ///     .eval_str(r#"{"+": [1, 2]}"#, "null");
452    /// assert_eq!(run.result.unwrap(), "3");
453    /// // run.steps is the per-node execution log;
454    /// // run.expression_tree is the rule's compile-time tree shape.
455    /// # }
456    /// ```
457    #[cfg(feature = "trace")]
458    #[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
459    #[inline]
460    pub fn trace(&self) -> crate::trace::TracedSession<'_> {
461        crate::trace::TracedSession::new(self)
462    }
463
464    /// Evaluate compiled logic against arena-resident data — **raw tier**.
465    ///
466    /// The caller owns the [`bumpalo::Bump`] lifecycle and may `reset()`
467    /// it between calls; the returned `&DataValue<'a>` borrows from the
468    /// arena, so it must be dropped before the next reset (enforced by
469    /// the borrow checker). For ergonomic owned/typed/JSON-string output,
470    /// prefer [`Self::eval`] / [`Self::eval_str`]
471    // `Self::eval_into` is gated behind `serde_json`; link conditionally.
472    #[cfg_attr(feature = "serde_json", doc = "/ [`Self::eval_into`].")]
473    #[cfg_attr(
474        not(feature = "serde_json"),
475        doc = "(plus `Self::eval_into` with the `serde_json` feature)."
476    )]
477    ///
478    /// # Example
479    ///
480    /// ```rust
481    /// use bumpalo::Bump;
482    /// use datalogic_rs::{Engine, DataValue};
483    ///
484    /// let engine = Engine::new();
485    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 2]}"#).unwrap();
486    ///
487    /// let arena = Bump::new();
488    /// let data = DataValue::from_str(r#"{"x": 40}"#, &arena).unwrap();
489    /// let result = engine.evaluate(&compiled, data, &arena).unwrap();
490    /// assert_eq!(result.as_i64(), Some(42));
491    /// ```
492    ///
493    /// `data` accepts any input shape understood by [`crate::EvalInput`]:
494    /// `&'a DataValue<'a>` (zero-cost passthrough), `DataValue<'a>`
495    /// (single arena alloc), `&str` (JSON-parsed), `&OwnedDataValue`
496    /// (deep-borrowed), [`&ParsedData`](crate::ParsedData) (zero-cost
497    /// passthrough of a parse-once handle), or `&serde_json::Value`
498    /// (gated on `serde_json`).
499    #[inline(always)]
500    pub fn evaluate<'a, D: crate::EvalInput<'a>>(
501        &self,
502        compiled: &'a Logic,
503        data: D,
504        arena: &'a bumpalo::Bump,
505    ) -> Result<&'a crate::arena::DataValue<'a>> {
506        let _depth_guard = self.enter_dispatch_boundary()?;
507        let data_ref = data.into_arena_value(arena)?;
508        let mut ctx = crate::arena::ContextStack::new(data_ref);
509        match self.dispatch_node(&compiled.root, &mut ctx, arena) {
510            Ok(av) => Ok(av),
511            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled, true)),
512        }
513    }
514
515    /// Apply the engine's configured truthiness rules
516    /// ([`crate::TruthyEvaluator`]) to an evaluated value.
517    ///
518    /// This is the same coercion `if` / `and` / `or` / `!` apply to
519    /// their operands, exposed so callers (and bindings) can collapse
520    /// any rule result to a boolean without re-implementing the
521    /// engine's configured semantics.
522    ///
523    /// # Example
524    ///
525    /// ```rust
526    /// use bumpalo::Bump;
527    /// use datalogic_rs::Engine;
528    ///
529    /// let engine = Engine::new();
530    /// let compiled = engine.compile(r#"{"var": "items"}"#).unwrap();
531    /// let arena = Bump::new();
532    /// let result = engine.evaluate(&compiled, r#"{"items": [1]}"#, &arena).unwrap();
533    /// assert!(engine.truthy(result));
534    /// ```
535    #[inline]
536    pub fn truthy(&self, value: &crate::arena::DataValue<'_>) -> bool {
537        crate::arena::truthy_arena(value, self)
538    }
539
540    /// One-shot evaluation returning [`datavalue::OwnedDataValue`].
541    ///
542    /// Compiles `rule`, parses `data`, evaluates against a fresh
543    /// per-call arena, and deep-clones the result out. For the same
544    /// rule run repeatedly, escalate to [`Self::compile`] + a
545    /// [`Session`](crate::Session).
546    ///
547    /// # Example
548    ///
549    /// ```rust
550    /// use datalogic_rs::Engine;
551    ///
552    /// let engine = Engine::new();
553    /// let result = engine.eval(
554    ///     r#"{"+": [{"var": "x"}, 1]}"#,
555    ///     r#"{"x": 41}"#,
556    /// ).unwrap();
557    /// assert_eq!(result.as_i64(), Some(42));
558    /// ```
559    pub fn eval<R, D>(&self, rule: R, data: D) -> Result<datavalue::OwnedDataValue>
560    where
561        R: crate::IntoLogic,
562        D: crate::OwnedInput,
563    {
564        self.eval_with::<datavalue::OwnedDataValue, _, _>(rule, data)
565    }
566
567    /// One-shot evaluation returning a JSON [`String`].
568    ///
569    /// # Example
570    ///
571    /// ```rust
572    /// use datalogic_rs::Engine;
573    ///
574    /// let engine = Engine::new();
575    /// let result = engine.eval_str(
576    ///     r#"{"==": [{"var": "x"}, 5]}"#,
577    ///     r#"{"x": 5}"#,
578    /// ).unwrap();
579    /// assert_eq!(result, "true");
580    /// ```
581    pub fn eval_str<R, D>(&self, rule: R, data: D) -> Result<String>
582    where
583        R: crate::IntoLogic,
584        D: crate::OwnedInput,
585    {
586        self.eval_with::<String, _, _>(rule, data)
587    }
588
589    /// One-shot evaluation deserialised into a typed `T: DeserializeOwned`.
590    ///
591    /// Use `T = serde_json::Value` for a JSON `Value` result; use a typed
592    /// struct for direct mapping. Internally routes through `serde_json`
593    /// (round-trips the result through a JSON value).
594    ///
595    /// # Example
596    ///
597    /// ```rust
598    /// # #[cfg(feature = "serde_json")] {
599    /// use datalogic_rs::Engine;
600    /// use serde_json::Value;
601    ///
602    /// let engine = Engine::new();
603    /// let result: Value = engine.eval_into(
604    ///     r#"{"+": [{"var": "x"}, 1]}"#,
605    ///     r#"{"x": 41}"#,
606    /// ).unwrap();
607    /// assert_eq!(result, Value::from(42));
608    /// # }
609    /// ```
610    #[cfg(feature = "serde_json")]
611    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
612    pub fn eval_into<T, R, D>(&self, rule: R, data: D) -> Result<T>
613    where
614        T: serde::de::DeserializeOwned,
615        R: crate::IntoLogic,
616        D: crate::OwnedInput,
617    {
618        let value: serde_json::Value = self.eval_with(rule, data)?;
619        serde_json::from_value(value).map_err(crate::Error::from)
620    }
621
622    /// Internal generic shared by `eval` / `eval_str` / `eval_into`.
623    /// Compiles, allocates a fresh per-call arena, evaluates, and
624    /// projects the result through [`crate::FromDataValue`].
625    fn eval_with<O, R, D>(&self, rule: R, data: D) -> Result<O>
626    where
627        O: crate::FromDataValue,
628        R: crate::IntoLogic,
629        D: crate::OwnedInput,
630    {
631        let compiled = self.compile(rule)?;
632        // 4 KB initial capacity covers typical small-rule evaluations.
633        let arena = bumpalo::Bump::with_capacity(4096);
634        let owned_data = data.into_owned_input()?;
635        let result = self.evaluate(&compiled, &owned_data, &arena)?;
636        O::from_arena(result)
637    }
638
639    /// Bump the per-thread dispatch-boundary depth counter, bailing with
640    /// `ConfigurationError` if the configured cap is reached. Returns a
641    /// guard that decrements the counter on drop (covers `?` early returns
642    /// and panics).
643    ///
644    /// Called from every public boundary entry point (`Engine::evaluate`,
645    /// `TracedSession::evaluate`, …). The counter is thread-local rather
646    /// than per-`ContextStack` so it survives across nested
647    /// `engine.evaluate(...)` calls — the scenario a `CustomOperator`
648    /// holding `Arc<Engine>` creates by re-entering the engine from inside
649    /// its own `evaluate(...)`.
650    ///
651    /// Tokio safety: dispatch is sync, so a task cannot `.await` while the
652    /// counter is raised; between dispatches the value is restored to its
653    /// prior level (zero at the outermost call). Cross-thread task migration
654    /// thus starts fresh on whatever thread the task lands on.
655    #[inline(always)]
656    pub(crate) fn enter_dispatch_boundary(&self) -> Result<DepthGuard> {
657        // Built-in operators can't re-enter `Engine::evaluate` (only a
658        // `CustomOperator` holding `Arc<Engine>` can); when the registry
659        // is empty, cross-evaluate recursion is impossible and we skip
660        // the TLS bookkeeping. The pure-built-in benchmarks pay zero.
661        if self.custom_operators.is_empty() {
662            return Ok(DepthGuard(DepthGuard::NOOP));
663        }
664        self.enter_dispatch_boundary_checked()
665    }
666
667    /// Slow path of [`Self::enter_dispatch_boundary`] — hit only when
668    /// the engine has at least one custom operator registered. Marked
669    /// `#[cold]` and `#[inline(never)]` so the hot fast-path stays
670    /// inline-friendly.
671    #[cold]
672    #[inline(never)]
673    fn enter_dispatch_boundary_checked(&self) -> Result<DepthGuard> {
674        let prev_depth = DISPATCH_DEPTH.with(Cell::get);
675        if prev_depth >= self.config.max_recursion_depth {
676            return Err(crate::Error::configuration_error(format!(
677                "max recursion depth exceeded ({})",
678                self.config.max_recursion_depth
679            )));
680        }
681        DISPATCH_DEPTH.with(|d| d.set(prev_depth + 1));
682        Ok(DepthGuard(prev_depth))
683    }
684
685    /// Arena-mode dispatch hub. Returns `&'a DataValue<'a>` for every
686    /// `CompiledNode` shape — exhaustive match, no value-mode fallback.
687    ///
688    /// On error, accumulates the failing node's id onto the context stack's
689    /// breadcrumb so [`Error`] consumers can surface the failing
690    /// path. When a tracer is attached to `ctx`, records a step per
691    /// non-literal node (entry context + result/error).
692    #[inline(always)]
693    pub(crate) fn dispatch_node<'a>(
694        &self,
695        node: &'a CompiledNode,
696        ctx: &mut crate::arena::ContextStack<'a>,
697        arena: &'a bumpalo::Bump,
698    ) -> Result<&'a crate::arena::DataValue<'a>> {
699        // Literal fast path — no breadcrumb push, no trace step.
700        if let CompiledNode::Value { value, lit, .. } = node {
701            // Every literal reachable from a `Logic` carries a pre-built
702            // `PreLit` — trivial ones from `precompute_lit` at node
703            // construction, composites from the `populate_lits` pass —
704            // so the hot path is a borrow, not a conversion.
705            if let Some(av) = lit {
706                return Ok(av.as_ref());
707            }
708            // Synthetic composite wrappers built outside the compile
709            // pipeline fall through to per-call arena conversion here.
710            return Ok(literal_fallback(value, arena));
711        }
712
713        // Snapshot context for trace BEFORE recursing — children will
714        // mutate iteration frames. Cheap when no tracer is attached.
715        #[cfg(feature = "trace")]
716        let ctx_snapshot: Option<serde_json::Value> =
717            ctx.has_tracer().then(|| ctx.current_data_as_value());
718
719        let result = dispatch::dispatch_node_inner(self, node, ctx, arena);
720
721        // Accumulate the failing node's id on every Err. We always pay
722        // the (single) Vec::push since errors are rare and structured-error
723        // consumers need the breadcrumb.
724        if result.is_err() {
725            ctx.push_error_step(node.id());
726        }
727
728        #[cfg(feature = "trace")]
729        if let Some(ctx_data) = ctx_snapshot {
730            ctx.record_node_result(node.id(), ctx_data, &result);
731        }
732
733        result
734    }
735
736    /// Evaluate an iteration body (map/filter/reduce/all/some/none) with the
737    /// trace collector's iteration index/total markers set around it. The
738    /// markers are no-ops when no tracer is attached, so plain-mode callers
739    /// pay only one branch per iteration.
740    #[inline]
741    pub(crate) fn run_iter_body<'a>(
742        &self,
743        body: &'a CompiledNode,
744        ctx: &mut crate::arena::ContextStack<'a>,
745        arena: &'a bumpalo::Bump,
746        _index: u32,
747        _total: u32,
748    ) -> Result<&'a crate::arena::DataValue<'a>> {
749        #[cfg(feature = "trace")]
750        ctx.trace_push_iteration(_index, _total);
751        let res = self.dispatch_node(body, ctx, arena);
752        #[cfg(feature = "trace")]
753        ctx.trace_pop_iteration();
754        res
755    }
756}