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/// An evaluation's result paired with what it cost.
31///
32/// Returned by [`Engine::evaluate_metered`],
33/// [`Engine::evaluate_metered_report`],
34/// [`Session::eval_metered`](crate::Session::eval_metered) and
35/// [`Session::eval_metered_report`](crate::Session::eval_metered_report).
36/// `T` is whatever that entry point produces — a borrowed
37/// `&'a DataValue<'a>` from the engine, an owned value from a session, or
38/// a [`Result<...>`](crate::Result) from the failure-inclusive `*_report`
39/// methods.
40///
41/// Both fields are public, so the usual shape is a destructure:
42///
43/// ```rust
44/// # #[cfg(feature = "budget")] {
45/// use bumpalo::Bump;
46/// use datalogic_rs::{Engine, Metered};
47///
48/// let engine = Engine::new();
49/// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 2]}"#).unwrap();
50/// let arena = Bump::new();
51/// let Metered { value, ops } = engine
52///     .evaluate_metered(&compiled, r#"{"x": 1}"#, &arena, u64::MAX)
53///     .unwrap();
54/// assert_eq!(value.as_i64(), Some(3));
55/// assert_eq!(ops, 2, "the `+` and the `var`; the literal 2 costs nothing");
56///
57/// // `{"+": [1, 2]}` costs 0: the compiler folded it to a literal, and a
58/// // literal is returned before dispatch.
59/// let folded = engine.compile(r#"{"+": [1, 2]}"#).unwrap();
60/// let metered = engine.evaluate_metered(&folded, "null", &arena, u64::MAX).unwrap();
61/// assert_eq!(metered.ops, 0);
62/// # }
63/// ```
64#[cfg(feature = "budget")]
65#[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Metered<T> {
68    /// The evaluation's result (or [`Result<...>`](crate::Result) for the
69    /// failure-inclusive `*_report` methods).
70    pub value: T,
71    /// Operations charged. See [`Engine::evaluate_metered`] for what one
72    /// operation is.
73    pub ops: u64,
74}
75
76#[cfg(feature = "budget")]
77impl<T, E> Metered<std::result::Result<T, E>> {
78    /// Transpose a failure-inclusive `Metered<Result<T, E>>` into a
79    /// `Result<Metered<T>, E>`, keeping `ops` on `Ok` and propagating
80    /// `Err(e)`.
81    #[inline]
82    pub fn transpose(self) -> std::result::Result<Metered<T>, E> {
83        let Self { value, ops } = self;
84        value.map(|value| Metered { value, ops })
85    }
86}
87
88/// Restores [`DISPATCH_DEPTH`] to its prior value on drop. Used by
89/// the boundary entry points (`Engine::evaluate`, `TracedSession::evaluate`)
90/// so early returns and panics leave the counter consistent.
91///
92/// `DepthGuard(u32::MAX)` is a no-op sentinel — used when the engine has
93/// no custom operators registered, so cross-evaluate recursion is
94/// impossible and the boundary skips the TLS bookkeeping entirely. The
95/// drop check makes the guard zero-cost in that case.
96pub(crate) struct DepthGuard(u32);
97
98impl DepthGuard {
99    const NOOP: u32 = u32::MAX;
100}
101
102impl Drop for DepthGuard {
103    #[inline]
104    fn drop(&mut self) {
105        if self.0 != Self::NOOP {
106            DISPATCH_DEPTH.with(|d| d.set(self.0));
107        }
108    }
109}
110
111/// JSONLogic compile/evaluate engine.
112///
113/// Holds the immutable engine state — registered [`crate::CustomOperator`]
114/// implementations, the [`EvaluationConfig`], the templating, folding and
115/// family settings — and exposes the public surface for parsing
116/// rules ([`Self::compile`]), evaluating them ([`Self::eval`] /
117// `Self::eval_into` is feature-gated on `serde_json`; link it
118// conditionally so default-features `cargo doc` doesn't break.
119#[cfg_attr(
120    feature = "serde_json",
121    doc = "[`Self::eval_str`], [`Self::eval_into`]), and opening hot-loop"
122)]
123#[cfg_attr(
124    not(feature = "serde_json"),
125    doc = "[`Self::eval_str`], `Self::eval_into`), and opening hot-loop"
126)]
127/// sessions ([`Self::session`]).
128// The `trace` feature adds [`Self::trace`]; reference it conditionally so
129// `cargo doc` without `--all-features` doesn't break on the intra-doc link.
130#[cfg_attr(
131    feature = "trace",
132    doc = "Enabling the `trace` feature also exposes [`Self::trace`] for traced sessions."
133)]
134///
135/// `Engine` is `Send + Sync` (every field is); the typical pattern is to
136/// build one at startup, wrap it in `Arc<Engine>`, and clone the `Arc`
137/// across threads or async tasks.
138///
139/// # Example
140///
141/// ```rust
142/// use datalogic_rs::Engine;
143///
144/// // 1. Build the engine.
145/// let engine = Engine::new();
146///
147/// // 2. Compile a rule once.
148/// let logic = engine
149///     .compile(r#"{"if": [{">=": [{"var": "age"}, 18]}, "adult", "minor"]}"#)
150///     .unwrap();
151///
152/// // 3. Evaluate against many inputs (here via `Session::eval_str`;
153/// //    drop to `Engine::evaluate` if you want zero-copy borrowed results).
154/// let mut session = engine.session();
155/// for age in [12, 18, 42] {
156///     let payload = format!(r#"{{"age": {age}}}"#);
157///     let result = session.eval_str(&logic, &payload).unwrap();
158///     assert!(result == "\"adult\"" || result == "\"minor\"");
159///     session.reset();
160/// }
161/// ```
162///
163/// # Choosing an evaluate method
164///
165/// **Start here.** Use [`Self::eval_str`] for one-shot calls. Switch
166/// to [`crate::Session`] once you're evaluating the same compiled rule
167/// many times — it reuses one arena instead of allocating per call.
168/// Drop down to [`Self::evaluate`] only when you're managing your own
169/// `bumpalo::Bump` (custom pools, integration with arena-aware
170/// downstream code).
171///
172/// Result-shape suffixes work the same on every tier: `(none)` returns
173/// [`datavalue::OwnedDataValue`], `_str` returns [`String`] (JSON),
174/// `_into::<T>` returns `T: DeserializeOwned` (requires `serde_json`).
175/// The raw [`Self::evaluate`] is the only method that exposes
176/// `&'a DataValue<'a>` and a caller-owned `&Bump`.
177///
178/// Three tiers, in order of caller control:
179///
180/// | Method | Arena ownership | Result type | When to use |
181/// |---|---|---|---|
182// `eval_into` is feature-gated on `serde_json`; emit a linked or plain
183// reference depending on the active features so default-features
184// `cargo doc` doesn't break the table row.
185#[cfg_attr(
186    feature = "serde_json",
187    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`. |"
188)]
189#[cfg_attr(
190    not(feature = "serde_json"),
191    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`. |"
192)]
193#[cfg_attr(
194    feature = "serde_json",
195    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. |"
196)]
197#[cfg_attr(
198    not(feature = "serde_json"),
199    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. |"
200)]
201/// | [`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. |
202///
203/// All routes share the same dispatcher; the differences are who owns
204/// the arena, what the result type looks like, and whether the
205/// boundary parses / serialises JSON for you. There is no perf
206/// difference between the arena-aware paths once the bump is warm.
207///
208/// See the crate-level docs for the two-phase architecture, threading
209/// model, and walk-through examples; see the `Session` and
210/// `EvaluationConfig` rustdoc for arena-management and behaviour-tuning
211/// options respectively.
212pub struct Engine {
213    /// Identifies this engine to the rules it compiles: a compiled custom
214    /// operator call records the engine id and slot, and dispatch takes the
215    /// slot only on the same engine. Unique for the process's lifetime.
216    pub(super) id: u64,
217    /// Custom `CustomOperator` implementations registered with the engine.
218    pub(super) custom_operators: CustomOperators,
219    /// Everything the builder set: config, templating mode and escape,
220    /// folding, families. The trace surface always disables folding
221    /// regardless of `settings.constant_folding` (handled in
222    /// `TracedSession`).
223    settings: crate::builder::EngineSettings,
224    /// [`EvaluationConfig::fold_fingerprint`] of the config, computed once:
225    /// a rule folded under another fingerprint is compiled again before
226    /// it runs here (see `Logic::for_engine`).
227    fold_fingerprint: u64,
228}
229
230/// The built-in operator `name` names among the families in `families`.
231#[inline]
232pub(crate) fn builtin_in(families: u32, name: &str) -> Option<crate::OpCode> {
233    name.parse::<crate::OpCode>()
234        .ok()
235        .filter(|op| op.in_families(families))
236}
237
238mod dispatch;
239
240/// Source of [`Engine::id`].
241static NEXT_ENGINE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
242
243/// The custom operators registered on an engine, addressable by name and,
244/// for the rules the engine compiles, by slot.
245pub(super) struct CustomOperators {
246    ops: Vec<std::sync::Arc<dyn crate::CustomOperator>>,
247    slots: HashMap<String, u32>,
248}
249
250impl CustomOperators {
251    fn new(operators: HashMap<String, std::sync::Arc<dyn crate::CustomOperator>>) -> Self {
252        let mut ops = Vec::with_capacity(operators.len());
253        let mut slots = HashMap::with_capacity(operators.len());
254        for (name, op) in operators {
255            slots.insert(name, ops.len() as u32);
256            ops.push(op);
257        }
258        CustomOperators { ops, slots }
259    }
260
261    fn len(&self) -> usize {
262        self.ops.len()
263    }
264
265    fn is_empty(&self) -> bool {
266        self.ops.is_empty()
267    }
268
269    /// The slot `name` is registered in.
270    pub(super) fn slot(&self, name: &str) -> Option<u32> {
271        self.slots.get(name).copied()
272    }
273
274    /// The operator registered as `name`.
275    pub(super) fn get(&self, name: &str) -> Option<&dyn crate::CustomOperator> {
276        self.slot(name).map(|slot| self.at(slot))
277    }
278
279    /// The operator in `slot`, which [`Self::slot`] returned on this engine.
280    #[inline]
281    pub(super) fn at(&self, slot: u32) -> &dyn crate::CustomOperator {
282        &*self.ops[slot as usize]
283    }
284
285    fn names(&self) -> impl Iterator<Item = &str> {
286        self.slots.keys().map(String::as_str)
287    }
288
289    /// Every registration, sharing each operator instance.
290    fn shared(&self) -> HashMap<String, std::sync::Arc<dyn crate::CustomOperator>> {
291        self.slots
292            .iter()
293            .map(|(name, &slot)| {
294                (
295                    name.clone(),
296                    std::sync::Arc::clone(&self.ops[slot as usize]),
297                )
298            })
299            .collect()
300    }
301}
302
303/// Convert an `OwnedDataValue` literal to an arena-resident `DataValue`
304/// reference. Reached from the `dispatch_node` literal path for any
305/// `CompiledNode::Value` whose `lit` was not precomputed — in practice
306/// only ad-hoc `synthetic_value` wrappers built outside the compile
307/// pipeline, since `populate_lits` pre-builds every literal reachable from
308/// a `Logic`. Trivial cases (Null, Bool, empty primitives) hit shared
309/// singletons with no allocation; a non-empty String allocates a single
310/// `DataValue` wrapper into the per-call arena (the `&str` is borrowed from
311/// the owned source); non-empty Arrays/Objects rebuild their spine in the
312/// arena via `OwnedDataValue::view_in`, borrowing strings, keys and tensor
313/// bytes from the owned source instead of copying them. Sound because the
314/// compiled node, and therefore `value`, outlives the evaluation:
315/// `dispatch_node` borrows the node at the same `'a` as the arena.
316///
317/// `#[cold]` + `#[inline(never)]`: `dispatch_node` is `#[inline(always)]`,
318/// so its literal path is stamped into every operator's dispatch site —
319/// keeping this fallback outlined keeps those sites small (I-cache), and
320/// the cold hint steers the branch layout toward the pre-built `lit` path
321/// that every compiled literal takes.
322/// The error path of `dispatch_node`: push `node` onto the breadcrumb and,
323/// when no deeper node has named the error, name it after `node`'s
324/// operator. The first node to see an error on its way up is the innermost
325/// one that failed, so [`crate::Error::operator`] names the operator that
326/// actually failed (a custom operator included), and the plain and traced
327/// paths agree on it. Outlined and cold for the same I-cache reason as
328/// [`literal_fallback`].
329#[cold]
330#[inline(never)]
331fn record_failure(
332    err: &mut crate::Error,
333    node: &CompiledNode,
334    ctx: &mut crate::arena::ContextStack<'_>,
335) {
336    ctx.push_error_step(node.id());
337    err.name_operator_from(node);
338}
339
340#[cold]
341#[inline(never)]
342fn literal_fallback<'a>(
343    value: &'a datavalue::OwnedDataValue,
344    arena: &'a bumpalo::Bump,
345) -> &'a crate::arena::DataValue<'a> {
346    use datavalue::OwnedDataValue;
347    match value {
348        OwnedDataValue::Null => crate::arena::singletons::singleton_null(),
349        OwnedDataValue::Bool(b) => crate::arena::singletons::singleton_bool(*b),
350        OwnedDataValue::String(s) if s.is_empty() => {
351            crate::arena::singletons::singleton_empty_string()
352        }
353        OwnedDataValue::Array(a) if a.is_empty() => {
354            crate::arena::singletons::singleton_empty_array()
355        }
356        OwnedDataValue::Object(o) if o.is_empty() => {
357            crate::arena::singletons::singleton_empty_object()
358        }
359        OwnedDataValue::String(s) => arena.alloc(crate::arena::DataValue::String(s.as_str())),
360        _ => arena.alloc(value.view_in(arena)),
361    }
362}
363
364impl Default for Engine {
365    fn default() -> Self {
366        Self::new()
367    }
368}
369
370impl std::fmt::Debug for Engine {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        // Print the operator *count* rather than names: names are user
373        // registration data, and `Engine::custom_operator_names()` exposes
374        // them already for callers who want them. The trait objects
375        // themselves can't render a meaningful Debug.
376        // Families print by name: the ones this engine has, among those
377        // compiled into the build.
378        let families: Vec<&str> = crate::Family::ALL
379            .iter()
380            .filter(|f| f.is_compiled() && self.settings.families & f.bit() != 0)
381            .map(|f| f.name())
382            .collect();
383        let settings = &self.settings;
384        let mut s = f.debug_struct("Engine");
385        s.field("custom_operators", &self.custom_operators.len());
386        s.field("templating", &settings.templating);
387        s.field("template_key_escape", &settings.template_key_escape);
388        s.field("constant_folding", &settings.constant_folding);
389        s.field("families", &families);
390        s.field("config", &settings.config);
391        s.finish_non_exhaustive()
392    }
393}
394
395impl Engine {
396    /// Start a [`crate::EngineBuilder`] for fluent construction.
397    ///
398    /// Use the builder when you need a non-default [`EvaluationConfig`],
399    /// templating mode, or pre-registered custom operators.
400    /// For a stock engine, [`Self::new`] is shorter.
401    #[inline]
402    pub fn builder() -> crate::EngineBuilder {
403        crate::EngineBuilder::new()
404    }
405
406    /// A builder that starts from this engine: the same custom operators,
407    /// config, templating mode, template key escape and folding setting.
408    /// Change what differs and call [`build`](crate::EngineBuilder::build)
409    /// for a new engine; this one is untouched.
410    ///
411    /// Both engines share each operator instance (and any state it holds),
412    /// so a host that rebuilds on every reload does not register its
413    /// operators again. Rules compiled on this engine still evaluate on the
414    /// new one, looking their custom operators up by name.
415    ///
416    /// # Example
417    ///
418    /// ```rust
419    /// use datalogic_rs::{Engine, EvaluationConfig, MissingVar};
420    ///
421    /// let running = Engine::new();
422    /// let reloaded = running
423    ///     .to_builder()
424    ///     .with_config(EvaluationConfig::default().with_missing_var(MissingVar::Error))
425    ///     .build();
426    /// assert!(reloaded.eval_str(r#"{"var": "x"}"#, "{}").is_err());
427    /// assert_eq!(running.eval_str(r#"{"var": "x"}"#, "{}").unwrap(), "null");
428    /// ```
429    pub fn to_builder(&self) -> crate::EngineBuilder {
430        crate::EngineBuilder::from_engine_parts(
431            self.settings.clone(),
432            self.custom_operators.shared(),
433        )
434    }
435
436    /// Open a [`crate::Session`] handle that owns a reusable arena and
437    /// returns owned results, so callers don't need to manage a
438    /// [`bumpalo::Bump`] themselves.
439    ///
440    /// Use this when you want the throughput of arena reuse without the
441    /// lifetime juggling of [`Self::evaluate`]. Results are deep-cloned out
442    /// of the arena before returning, so they survive later calls and resets.
443    /// The session does **not** auto-reset: allocations accumulate until you
444    /// call [`crate::Session::reset`], which you should do between logical
445    /// batches to bound peak memory in long-running services.
446    ///
447    /// # Example
448    ///
449    /// ```rust
450    /// use datalogic_rs::Engine;
451    ///
452    /// let engine = Engine::new();
453    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
454    /// let mut session = engine.session();
455    /// let result = session.eval_str(&compiled, r#"{"x": 41}"#).unwrap();
456    /// assert_eq!(result, "42");
457    /// ```
458    #[inline]
459    pub fn session(&self) -> crate::Session<'_> {
460        crate::Session::borrowing(self)
461    }
462
463    /// Internal seam used by the builder. `pub(crate)` is enough — no
464    /// `#[doc(hidden)]` needed since it's not externally reachable.
465    #[inline]
466    pub(crate) fn from_builder_parts(
467        mut settings: crate::builder::EngineSettings,
468        operators: HashMap<String, std::sync::Arc<dyn crate::CustomOperator>>,
469    ) -> Self {
470        settings.templating &= cfg!(feature = "templating");
471        Self {
472            id: NEXT_ENGINE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
473            custom_operators: CustomOperators::new(operators),
474            fold_fingerprint: settings.config.fold_fingerprint(),
475            settings,
476        }
477    }
478
479    /// Creates a new Engine with all built-in operators.
480    ///
481    /// The engine includes every built-in operator compiled into this build
482    /// (84 canonical operators with all operator features on; see
483    /// [`Self::builtin_operator_names`]), dispatched via `OpCode`.
484    /// Templating mode is disabled by default. For non-default
485    /// configuration (custom [`EvaluationConfig`], templating mode,
486    /// pre-registered custom operators) prefer [`Self::builder`].
487    ///
488    /// # Example
489    ///
490    /// ```rust
491    /// use datalogic_rs::Engine;
492    ///
493    /// let engine = Engine::new();
494    /// ```
495    pub fn new() -> Self {
496        Self::from_builder_parts(crate::builder::EngineSettings::default(), HashMap::new())
497    }
498
499    /// Gets a reference to the current evaluation configuration.
500    pub fn config(&self) -> &EvaluationConfig {
501        &self.settings.config
502    }
503
504    /// Internal: whether the constant-folding pass runs during
505    /// [`Self::compile`]. Reads the field set by
506    /// [`crate::EngineBuilder::with_constant_folding`].
507    #[inline]
508    pub(crate) fn constant_folding_enabled(&self) -> bool {
509        self.settings.constant_folding
510    }
511
512    /// Internal: whether templating mode is on. Always `false` when the
513    /// crate is built without `feature = "templating"`: the builder seam
514    /// clears the flag off-feature.
515    #[inline]
516    pub(crate) fn is_templating_enabled(&self) -> bool {
517        self.settings.templating
518    }
519
520    /// Internal: the template-key escape prefix, or `None` when unset.
521    #[inline]
522    pub(crate) fn template_key_escape(&self) -> Option<char> {
523        self.settings.template_key_escape
524    }
525
526    /// Checks if a custom operator with the given name is registered.
527    ///
528    /// Operator registration is builder-only; this is a read-only check
529    /// against the frozen set produced by [`crate::EngineBuilder`].
530    pub fn has_custom_operator(&self, name: &str) -> bool {
531        self.custom_operators.slot(name).is_some()
532    }
533
534    /// What the custom operator registered as `name` declares about
535    /// itself ([`crate::CustomOperator::info`]), or `None` when no custom
536    /// operator has that name. Built-ins are described by
537    /// [`Self::operators`].
538    pub fn custom_operator_info(&self, name: &str) -> Option<crate::CustomOperatorInfo> {
539        self.custom_operators.get(name).map(|op| op.info())
540    }
541
542    /// This engine's process-unique id.
543    #[inline]
544    pub(crate) fn id(&self) -> u64 {
545        self.id
546    }
547
548    /// The fingerprint of the settings constant folding observes; see
549    /// [`EvaluationConfig::fold_fingerprint`].
550    #[inline]
551    pub(crate) fn fold_fingerprint(&self) -> u64 {
552        self.fold_fingerprint
553    }
554
555    /// The custom operator registered as `name`.
556    pub(crate) fn custom_operator(&self, name: &str) -> Option<&dyn crate::CustomOperator> {
557        self.custom_operators.get(name)
558    }
559
560    /// Whether the iterator fast paths may read fields inline. They read a
561    /// missing field as `null` without dispatching a `var`, which is only
562    /// right while a miss is `null` ([`crate::MissingVar::Null`]).
563    #[inline(always)]
564    pub(crate) fn reads_fields_inline(&self) -> bool {
565        self.settings.config.missing_var == crate::MissingVar::Null
566    }
567
568    /// Where a call to custom operator `name` compiled on this engine finds
569    /// it: this engine's id and the operator's slot.
570    pub(crate) fn custom_operator_slot(&self, name: &str) -> Option<(u64, u32)> {
571        self.custom_operators.slot(name).map(|slot| (self.id, slot))
572    }
573
574    /// Iterator over the names of every *custom* operator registered on
575    /// this engine (built-ins are not included). Order is unspecified
576    /// (HashMap iteration order). Useful for tooling, UIs, and tests
577    /// that need to introspect what's available.
578    pub fn custom_operator_names(&self) -> impl Iterator<Item = &str> {
579        self.custom_operators.names()
580    }
581
582    /// Iterator over every *built-in* operator name this build evaluates:
583    /// the JSONLogic baseline plus whichever extension families
584    /// (`ext-string`, `datetime`, `ext-control`, …) were compiled in.
585    ///
586    /// The list is derived from the same table the compiler resolves
587    /// operator keys against, so it cannot drift from dispatch. It
588    /// includes input aliases (`var` for `val`, `?:` for `if`, `match`
589    /// for `switch`) because those keys are live calls too; order is
590    /// canonical name first, then its aliases, per operator.
591    ///
592    /// Custom registrations are not included; see
593    /// [`Self::custom_operator_names`]. The union of the two is the full
594    /// vocabulary of this engine. A custom operator registered under a
595    /// built-in name is never reached: built-in resolution wins at
596    /// compile time.
597    ///
598    /// Under templating mode an unknown key is not an error (the object
599    /// echoes back as data), so this is the vocabulary authoring-side
600    /// tooling needs to tell a live call from a literal.
601    ///
602    /// # Example
603    ///
604    /// ```rust
605    /// use datalogic_rs::Engine;
606    ///
607    /// let engine = Engine::new();
608    /// let names: Vec<&str> = engine.builtin_operator_names().collect();
609    /// assert!(names.contains(&"val"));
610    /// assert!(names.contains(&"var")); // alias of `val`
611    /// assert!(!names.contains(&"lenght"));
612    /// ```
613    pub fn builtin_operator_names(&self) -> impl Iterator<Item = &'static str> + use<> {
614        crate::operators::table::builtin_operator_names_in(self.settings.families)
615    }
616
617    /// Every built-in operator compiled into this build, described by its
618    /// operator-table row: name and aliases, family and gating feature,
619    /// declared argument counts, whether it reads the data context, its
620    /// effect, its cost class, and which argument (if any) runs under a
621    /// pushed frame.
622    ///
623    /// Read from the same table that drives compilation, dispatch and the
624    /// optimizer, so it cannot drift from what the engine does. Custom
625    /// operators are not included; see [`Self::custom_operator_names`].
626    ///
627    /// # Example
628    ///
629    /// ```rust
630    /// use datalogic_rs::{Engine, ScopedArg};
631    ///
632    /// let engine = Engine::new();
633    /// let map = engine.operators().find(|op| op.name == "map").unwrap();
634    /// assert_eq!(map.family, "Core");
635    /// assert_eq!(map.feature, None);
636    /// assert_eq!(map.scoped_arg, Some(ScopedArg::Index(1)));
637    ///
638    /// let val = engine.operators().find(|op| op.name == "val").unwrap();
639    /// assert_eq!(val.aliases, ["var"]);
640    /// assert!(val.reads_context);
641    /// ```
642    pub fn operators(&self) -> impl Iterator<Item = crate::OperatorInfo> + use<> {
643        crate::operators::info::operators_in(self.settings.families)
644    }
645
646    /// The built-in operator `name` names on this engine: a name of a
647    /// family it has ([`crate::EngineBuilder::with_families`]).
648    #[inline]
649    pub(crate) fn builtin(&self, name: &str) -> Option<crate::OpCode> {
650        builtin_in(self.settings.families, name)
651    }
652
653    // ============================================================
654    // V5 PUBLIC API
655    //   - One-shot:   `eval` / `eval_str` / `eval_into`   (engine-owned arena per call)
656    //   - Power tier: `evaluate(&Logic, D, &Bump)`        (caller-owned arena, borrowed result)
657    //   - Hot loop:   `engine.session().eval*(...)`       (pooled arena, manual reset)
658    //   - Trace:      `engine.trace().eval*(...)`         (same shapes wrapped in TracedRun<R>; eval_str/eval_into take a rule source)
659    // ============================================================
660
661    /// Compile a rule source into reusable [`Logic`].
662    ///
663    /// `rule` accepts any [`crate::IntoLogic`] shape: `&str` (JSON-parsed),
664    /// `&OwnedDataValue` / `OwnedDataValue` (cloned/moved), or
665    /// `&serde_json::Value` (gated on `serde_json`). For cross-thread
666    /// sharing prefer [`Self::compile_arc`].
667    ///
668    /// # Example
669    ///
670    /// ```rust
671    /// use datalogic_rs::Engine;
672    ///
673    /// let engine = Engine::new();
674    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
675    /// ```
676    pub fn compile<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
677        let owned = rule.logic_source()?;
678        Logic::compile_with(owned, self)
679    }
680
681    /// Check `rule` without compiling it: every problem the engine can see
682    /// before it runs, in rule order, each located by a JSON Pointer.
683    ///
684    /// An [`Error`](crate::Severity::Error) is something that will fail: a
685    /// rule that does not compile (an object with several keys outside
686    /// templating mode), or a call that fails whenever it runs (an unknown
687    /// operator, `and` / `or` / `if` without an argument array, an argument
688    /// count the operator rejects, a literal timezone that does not exist).
689    /// A [`Warning`](crate::Severity::Warning) runs, but probably not as
690    /// meant: arguments an operator never evaluates, or in a template an
691    /// output key one edit away from an operator name.
692    ///
693    /// Unlike [`Self::compile`], which stops at the first problem and
694    /// leaves call errors to evaluation, this reports them all, including
695    /// those in branches a given input never reaches. `mode` chooses
696    /// strict or templating mode for this check (see [`crate::CheckMode`]).
697    ///
698    /// # Example
699    ///
700    /// ```rust
701    /// use datalogic_rs::{CheckMode, DiagnosticCode, Engine};
702    ///
703    /// let engine = Engine::new();
704    /// let diags = engine.check(r#"{"if": [true, {"vr": "x"}, {"map": [1]}]}"#, CheckMode::Engine);
705    /// assert_eq!(diags.len(), 2);
706    /// assert_eq!(diags[0].code, DiagnosticCode::UnknownOperator);
707    /// assert_eq!(diags[0].pointer, "/if/1");
708    /// assert!(diags[0].message.contains("did you mean `var`"));
709    /// assert_eq!(diags[1].code, DiagnosticCode::ArgumentCount);
710    /// ```
711    pub fn check<R: crate::IntoLogic>(
712        &self,
713        rule: R,
714        mode: crate::CheckMode,
715    ) -> Vec<crate::Diagnostic> {
716        let owned = match rule.logic_source() {
717            Ok(owned) => owned,
718            Err(err) => return crate::CompileError::from_error(err).diagnostics,
719        };
720        let templating = match mode {
721            crate::CheckMode::Engine => self.is_templating_enabled(),
722            crate::CheckMode::Strict => false,
723            crate::CheckMode::Template => true,
724        };
725        crate::check::check(self, &owned, templating)
726    }
727
728    /// [`Self::compile`], refusing a rule that [`Self::check`] finds any
729    /// error in: strict up front, where `compile` leaves a call that cannot
730    /// succeed to fail at evaluation. The error carries every diagnostic,
731    /// warnings included. A rule with only warnings compiles.
732    ///
733    /// # Errors
734    ///
735    /// [`crate::CompileError`] listing the diagnostics.
736    pub fn compile_checked<R: crate::IntoLogic>(
737        &self,
738        rule: R,
739    ) -> std::result::Result<Logic, crate::CompileError> {
740        let owned = rule
741            .logic_source()
742            .map_err(crate::CompileError::from_error)?;
743        let diagnostics = crate::check::check(self, &owned, self.is_templating_enabled());
744        if diagnostics
745            .iter()
746            .any(|d| d.severity == crate::Severity::Error)
747        {
748            return Err(crate::CompileError { diagnostics });
749        }
750        Logic::compile_with(owned, self).map_err(crate::CompileError::from_error)
751    }
752
753    /// Compile `rule` in templating mode, whatever mode the engine was
754    /// built with: a multi-key object is an output template and an
755    /// unknown operator key is an output field, as on an engine built
756    /// [`with_templating(true)`](crate::EngineBuilder::with_templating).
757    /// The engine's custom operators, template key escape and folding
758    /// setting apply as usual.
759    ///
760    /// With [`Self::compile_strict`] this lets one engine, with one set of
761    /// custom operators, both check rules strictly and compile output
762    /// templates, instead of a host building an engine per mode.
763    ///
764    /// # Example
765    ///
766    /// ```rust
767    /// use datalogic_rs::Engine;
768    ///
769    /// let engine = Engine::new();
770    /// let template = r#"{"user": {"var": "name"}, "source": "api"}"#;
771    /// assert!(engine.compile(template).is_err());
772    ///
773    /// let logic = engine.compile_template(template).unwrap();
774    /// let out = engine.session().eval_str(&logic, r#"{"name": "ana"}"#).unwrap();
775    /// assert_eq!(out, r#"{"user":"ana","source":"api"}"#);
776    /// ```
777    #[cfg(feature = "templating")]
778    #[cfg_attr(docsrs, doc(cfg(feature = "templating")))]
779    pub fn compile_template<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
780        let owned = rule.logic_source()?;
781        Logic::compile_in_mode(owned, self, true)
782    }
783
784    /// Compile `rule` outside templating mode, whatever mode the engine
785    /// was built with: a multi-key object is an error, and an unknown
786    /// operator is not an output field, exactly as on an engine built
787    /// without templating. The engine's custom operators and folding
788    /// setting apply as usual.
789    ///
790    /// On an engine built without templating this is [`Self::compile`].
791    ///
792    /// # Example
793    ///
794    /// ```rust
795    /// # #[cfg(feature = "templating")] {
796    /// use datalogic_rs::Engine;
797    ///
798    /// let engine = Engine::builder().with_templating(true).build();
799    /// let typo = r#"{"user": {"var": "name"}, "sourec": "api"}"#;
800    /// assert!(engine.compile(typo).is_ok());          // an output template
801    /// assert!(engine.compile_strict(typo).is_err());  // not a rule
802    /// # }
803    /// ```
804    pub fn compile_strict<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
805        let owned = rule.logic_source()?;
806        Logic::compile_in_mode(owned, self, false)
807    }
808
809    /// Compile and wrap in an [`Arc`](std::sync::Arc) in one call. Convenience for the
810    /// dominant cross-thread-sharing pattern; equivalent to
811    /// `Arc::new(engine.compile(rule)?)`.
812    pub fn compile_arc<R: crate::IntoLogic>(&self, rule: R) -> Result<std::sync::Arc<Logic>> {
813        Ok(std::sync::Arc::new(self.compile(rule)?))
814    }
815
816    /// Open a [`crate::TracedSession`] over this engine. Calls made through
817    /// the session collect a per-call trace; the bare `eval*` methods on
818    /// `Engine` itself pay no trace overhead.
819    ///
820    /// Available only when the crate is built with `feature = "trace"`.
821    ///
822    /// # Trace coverage
823    ///
824    /// The session's one-shot [`crate::TracedSession::eval_str`] compiles
825    /// the rule internally with optimization disabled, so every operator
826    /// in the rule surfaces a trace step.
827    ///
828    /// The pre-compiled paths ([`crate::TracedSession::eval`] taking a
829    /// `&Logic`) inherit whatever shape that `Logic` was compiled into —
830    /// constant sub-expressions folded by [`Self::compile`] won't appear,
831    /// since there is no operator left to execute. Use `eval_str` for
832    /// full coverage on a one-shot run.
833    ///
834    /// # Example
835    ///
836    /// ```rust
837    /// # #[cfg(feature = "trace")] {
838    /// use datalogic_rs::Engine;
839    ///
840    /// let engine = Engine::new();
841    /// let run = engine
842    ///     .trace()
843    ///     .eval_str(r#"{"+": [1, 2]}"#, "null");
844    /// assert_eq!(run.result.unwrap(), "3");
845    /// // run.steps is the per-node execution log;
846    /// // run.expression_tree is the rule's compile-time tree shape.
847    /// # }
848    /// ```
849    #[cfg(feature = "trace")]
850    #[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
851    #[inline]
852    pub fn trace(&self) -> crate::trace::TracedSession<'_> {
853        crate::trace::TracedSession::new(self)
854    }
855
856    /// Evaluate compiled logic against arena-resident data — **raw tier**.
857    ///
858    /// The caller owns the [`bumpalo::Bump`] lifecycle and may `reset()`
859    /// it between calls; the returned `&DataValue<'a>` borrows from the
860    /// arena, so it must be dropped before the next reset (enforced by
861    /// the borrow checker). For ergonomic owned/typed/JSON-string output,
862    /// prefer [`Self::eval`] / [`Self::eval_str`]
863    // `Self::eval_into` is gated behind `serde_json`; link conditionally.
864    #[cfg_attr(feature = "serde_json", doc = "/ [`Self::eval_into`].")]
865    #[cfg_attr(
866        not(feature = "serde_json"),
867        doc = "(plus `Self::eval_into` with the `serde_json` feature)."
868    )]
869    ///
870    /// # Example
871    ///
872    /// ```rust
873    /// use bumpalo::Bump;
874    /// use datalogic_rs::{Engine, DataValue};
875    ///
876    /// let engine = Engine::new();
877    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 2]}"#).unwrap();
878    ///
879    /// let arena = Bump::new();
880    /// let data = DataValue::from_str(r#"{"x": 40}"#, &arena).unwrap();
881    /// let result = engine.evaluate(&compiled, data, &arena).unwrap();
882    /// assert_eq!(result.as_i64(), Some(42));
883    /// ```
884    ///
885    /// `data` accepts any input shape understood by [`crate::EvalInput`]:
886    /// `&'a DataValue<'a>` (zero-cost passthrough), `DataValue<'a>`
887    /// (single arena alloc), `&str` (JSON-parsed), `&OwnedDataValue`
888    /// (deep-borrowed), [`&ParsedData`](crate::ParsedData) (zero-cost
889    /// passthrough of a parse-once handle), or `&serde_json::Value`
890    /// (gated on `serde_json`).
891    #[inline(always)]
892    pub fn evaluate<'a, D: crate::EvalInput<'a>>(
893        &self,
894        compiled: &'a Logic,
895        data: D,
896        arena: &'a bumpalo::Bump,
897    ) -> Result<&'a crate::arena::DataValue<'a>> {
898        // The steps of [`Self::run`], written out: through `run`'s closures
899        // this body measured ~1.2 ns slower per call (a third of the cost
900        // of evaluating a folded rule), so the hot entry points keep the
901        // straight-line form.
902        let compiled = compiled.for_engine(self);
903        let _depth_guard = self.enter_dispatch_boundary()?;
904        let data_ref = data.into_arena_for(compiled, self, arena)?;
905        let mut ctx = self.new_context(compiled, data_ref);
906        match self.dispatch_node(&compiled.root, &mut ctx, arena) {
907            Ok(av) => Ok(av),
908            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled)),
909        }
910    }
911
912    /// The evaluation body, with hooks: the traced run uses it.
913    /// [`Self::evaluate`] and [`Self::evaluate_metered`] write the same
914    /// steps out by hand (see there why); keep the three in step.
915    ///
916    /// In order: pick the rule as this engine runs it
917    /// ([`Logic::for_engine`]), enter the re-entrancy guard, bring the
918    /// input in with `input` (given that rule, so an owned input can be
919    /// projected onto what it reads), build the context, let `prepare`
920    /// adjust it (a per-call budget, a tracer), dispatch the root, and
921    /// decorate an error with its breadcrumb. `finish` sees the result and
922    /// the context together, for what is read off the context afterwards
923    /// (operations spent, the trace).
924    ///
925    #[cfg(feature = "trace")]
926    #[inline(always)]
927    pub(crate) fn run<'a, R>(
928        &self,
929        compiled: &'a Logic,
930        arena: &'a bumpalo::Bump,
931        input: impl FnOnce(&'a Logic) -> Result<&'a crate::arena::DataValue<'a>>,
932        prepare: impl FnOnce(&mut crate::arena::ContextStack<'a>),
933        finish: impl FnOnce(
934            Result<&'a crate::arena::DataValue<'a>>,
935            &mut crate::arena::ContextStack<'a>,
936        ) -> Result<R>,
937    ) -> Result<R> {
938        let compiled = compiled.for_engine(self);
939        let _depth_guard = self.enter_dispatch_boundary()?;
940        let data_ref = input(compiled)?;
941        let mut ctx = self.new_context(compiled, data_ref);
942        prepare(&mut ctx);
943        let result = match self.dispatch_node(&compiled.root, &mut ctx, arena) {
944            Ok(av) => Ok(av),
945            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled)),
946        };
947        finish(result, &mut ctx)
948    }
949
950    /// Build the context stack for one evaluation of `compiled` over
951    /// `data_ref`: ancestor tracking as the scope pass decided, and the
952    /// engine-wide operation budget when one is configured. Every
953    /// evaluation entry point (plain, metered, traced) starts here so a
954    /// per-evaluation knob is wired in exactly one place.
955    #[inline(always)]
956    pub(crate) fn new_context<'a>(
957        &self,
958        compiled: &'a Logic,
959        data_ref: &'a crate::arena::DataValue<'a>,
960    ) -> crate::arena::ContextStack<'a> {
961        // Only the budget branch mutates; without the feature the binding
962        // is returned as built.
963        #[cfg_attr(not(feature = "budget"), allow(unused_mut))]
964        let mut ctx = crate::arena::ContextStack::new(data_ref, compiled.needs_ancestor_frames);
965        #[cfg(feature = "budget")]
966        if let Some(budget) = self.settings.config.ops_budget {
967            ctx.set_budget(budget);
968        }
969        ctx
970    }
971
972    /// The budget a metered call runs under: `explicit` when the caller
973    /// gave one, else the engine-wide
974    /// [`EvaluationConfig::ops_budget`](crate::EvaluationConfig::ops_budget),
975    /// else unbounded (`u64::MAX`).
976    ///
977    /// This is the precedence [`Self::evaluate`] applies implicitly, made
978    /// available to callers of [`Self::evaluate_metered`] — the language
979    /// bindings' "omit the budget to use the engine's" forms all resolve
980    /// through here, so a rule metered from any of them reports the same
981    /// number.
982    #[cfg(feature = "budget")]
983    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
984    #[inline]
985    pub fn resolve_ops_budget(&self, explicit: Option<u64>) -> u64 {
986        explicit
987            .or(self.settings.config.ops_budget)
988            .unwrap_or(u64::MAX)
989    }
990
991    /// Evaluate under an explicit operation budget, reporting what the
992    /// evaluation spent.
993    ///
994    /// Same evaluation as [`Self::evaluate`], with two differences: the
995    /// `budget` argument overrides
996    /// [`EvaluationConfig::ops_budget`](crate::EvaluationConfig::ops_budget)
997    /// for this call, and the result carries the operation count next to
998    /// the value. Pass `u64::MAX` to meter without bounding.
999    ///
1000    /// # What the count means
1001    ///
1002    /// "Nodes dispatched at runtime, plus whatever operators charge" —
1003    /// not "nodes in the source". Concretely:
1004    ///
1005    /// - **1 per dispatched node.** Literals cost 0 (they return before
1006    ///   dispatch), and so do constant-folded subtrees — the compiler
1007    ///   already did that work, and charging for it would make the count
1008    ///   depend on whether folding was enabled.
1009    /// - **1 per item an iterator examines**, charged by `map`, `filter`,
1010    ///   `reduce`, the quantifiers, `sort` and friends as soon as the
1011    ///   source is resolved. This is what keeps the number honest when a
1012    ///   compile-time fast path evaluates a predicate inline instead of
1013    ///   dispatching its body: the iteration costs at least its input
1014    ///   length whichever path runs.
1015    /// - **1 per item a collection operator copies, examines or
1016    ///   compares**, charged before the work: `merge` per item of each
1017    ///   array argument, `in` per item of an array haystack, `missing` /
1018    ///   `missing_some` per path of a path list from data, `keys` /
1019    ///   `values` / `entries` per key, a stepped `slice` per item produced,
1020    ///   `distinct` / `group_by` per comparison, `sort` n·⌈log₂ n⌉, and
1021    ///   one-argument arithmetic per array item. Iterating an object costs
1022    ///   1 per key.
1023    /// - **The structural walk of an equality between containers**: 1 per
1024    ///   array element, `|a| x |b|` per object level, for each level the
1025    ///   comparison reaches.
1026    /// - **1 per whole 64 bytes of string a string operator reads** (`cat`,
1027    ///   `substr`, `upper`, `lower`, `trim`, `length`, `split`, `in` on a
1028    ///   string, `starts_with` / `ends_with`, string `slice`), so strings
1029    ///   under 64 bytes are free.
1030    /// - **Whatever an operator charges for itself.** The tensor family
1031    ///   prices each operator at `max(elements read, elements produced)`,
1032    ///   before it allocates. A [`crate::CustomOperator`] that walks a
1033    ///   large input should do the same via
1034    ///   [`EvalContext::charge`](crate::operator::EvalContext::charge).
1035    /// - **A CSE-memoised subtree is charged once**, on the evaluation
1036    ///   that fills the slot.
1037    ///
1038    /// The count is deterministic for a pinned crate version and a given
1039    /// (rule, data) pair, whichever evaluation path runs: an iterator fast
1040    /// path charges what the general path would for the same data, so
1041    /// tracing and [`MissingVar::Error`](crate::MissingVar::Error), which
1042    /// turn the fast paths off, leave it unchanged. It is not stable
1043    /// *across* versions: a new fold changes what gets dispatched. Budget
1044    /// for the work you want to allow, not for an exact number you
1045    /// measured.
1046    ///
1047    /// # Example
1048    ///
1049    /// ```rust
1050    /// # #[cfg(feature = "budget")] {
1051    /// use bumpalo::Bump;
1052    /// use datalogic_rs::{Engine, ErrorCode};
1053    ///
1054    /// let engine = Engine::new();
1055    /// let compiled = engine.compile(r#"{"map": [{"var": "xs"}, {"*": [{"var": ""}, 2]}]}"#).unwrap();
1056    /// let arena = Bump::new();
1057    ///
1058    /// let metered = engine
1059    ///     .evaluate_metered(&compiled, r#"{"xs": [1, 2, 3]}"#, &arena, u64::MAX)
1060    ///     .unwrap();
1061    /// assert_eq!(metered.value.to_json_string(), "[2,4,6]");
1062    /// assert!(metered.ops >= 3, "at least one operation per item");
1063    ///
1064    /// // Too tight a budget refuses the evaluation instead of running it.
1065    /// let err = engine
1066    ///     .evaluate_metered(&compiled, r#"{"xs": [1, 2, 3]}"#, &arena, 2)
1067    ///     .unwrap_err();
1068    /// assert_eq!(err.code(), ErrorCode::BudgetExceeded);
1069    /// # }
1070    /// ```
1071    ///
1072    /// # Errors
1073    ///
1074    /// [`ErrorKind::BudgetExceeded`](crate::ErrorKind::BudgetExceeded)
1075    /// when the rule charges past `budget`, plus every error
1076    /// [`Self::evaluate`] can return. To observe the operations spent
1077    /// even when evaluation fails, call [`Self::evaluate_metered_report`].
1078    #[cfg(feature = "budget")]
1079    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
1080    pub fn evaluate_metered<'a, D: crate::EvalInput<'a>>(
1081        &self,
1082        compiled: &'a Logic,
1083        data: D,
1084        arena: &'a bumpalo::Bump,
1085        budget: u64,
1086    ) -> Result<Metered<&'a crate::arena::DataValue<'a>>> {
1087        self.evaluate_metered_report(compiled, data, arena, budget)
1088            .transpose()
1089    }
1090
1091    /// Failure-inclusive counterpart to [`Self::evaluate_metered`]:
1092    /// evaluates under `budget` and returns a [`Metered`] report whose
1093    /// `ops` is populated whether evaluation succeeds (`value: Ok(...)`)
1094    /// or fails (`value: Err(...)`).
1095    ///
1096    /// Hosts that reserve capacity from a shared parent ledger before an
1097    /// expression runs can reconcile the reservation against `report.ops`
1098    /// on both successful and failed evaluations, instead of either
1099    /// dropping failed work or conservatively charging the entire
1100    /// reservation.
1101    ///
1102    /// # Charge semantics on failure
1103    ///
1104    /// - **Ordinary runtime failures** (`Thrown`, `VariableNotFound`,
1105    ///   `TypeError`, `ArithmeticError`, `Custom`, …): `report.ops` is
1106    ///   the exact number of operations charged before the failing node or
1107    ///   operator aborted. Because every node and operator charges
1108    ///   *before* doing its work, the failing node's own dispatch charge
1109    ///   (and any pre-allocation charge it made before raising) is
1110    ///   included. The [`crate::Error`] in `report.value` retains its full
1111    ///   [`Error::code`](crate::Error::code),
1112    ///   [`Error::operator`](crate::Error::operator), and
1113    ///   [`Error::node_ids`](crate::Error::node_ids) breadcrumb.
1114    /// - **Budget exhaustion**
1115    ///   ([`ErrorKind::BudgetExceeded { budget, spent }`](crate::ErrorKind::BudgetExceeded)):
1116    ///   every charge is added to the running counter before comparing
1117    ///   against `budget`, so `report.ops` equals `spent` — the attempted
1118    ///   total that crossed the ceiling. Do not add `report.ops` and
1119    ///   `spent` together (they are the same counter); a host debiting a
1120    ///   reserved slice of size `budget` can debit `report.ops.min(budget)`
1121    ///   for bounded work or `report.ops` for attempted work.
1122    /// - **Pre-dispatch failures** (input adaptation / JSON parse errors
1123    ///   from `data`, or exceeding `max_recursion_depth` at the boundary):
1124    ///   no node is dispatched, so `report.ops == 0`.
1125    ///
1126    /// # Example
1127    ///
1128    /// ```rust
1129    /// # #[cfg(feature = "budget")] {
1130    /// use bumpalo::Bump;
1131    /// use datalogic_rs::{Engine, ErrorCode, Metered};
1132    ///
1133    /// let engine = Engine::new();
1134    /// let compiled = engine.compile(r#"{"/": [1, {"var": "zero"}]}"#).unwrap();
1135    /// let arena = Bump::new();
1136    ///
1137    /// let Metered { value, ops } = engine
1138    ///     .evaluate_metered_report(&compiled, r#"{"zero": 0}"#, &arena, 100);
1139    /// let err = value.unwrap_err();
1140    /// assert_eq!(err.code(), ErrorCode::Thrown);
1141    /// assert_eq!(err.operator(), Some("/"));
1142    /// assert_eq!(ops, 2, "the `/` and the `var` were dispatched before division failed");
1143    /// # }
1144    /// ```
1145    #[cfg(feature = "budget")]
1146    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
1147    pub fn evaluate_metered_report<'a, D: crate::EvalInput<'a>>(
1148        &self,
1149        compiled: &'a Logic,
1150        data: D,
1151        arena: &'a bumpalo::Bump,
1152        budget: u64,
1153    ) -> Metered<Result<&'a crate::arena::DataValue<'a>>> {
1154        // [`Self::evaluate`]'s straight-line body, with the budget set and
1155        // `ctx.ops_spent()` preserved on both `Ok` and `Err`.
1156        let compiled = compiled.for_engine(self);
1157        let _depth_guard = match self.enter_dispatch_boundary() {
1158            Ok(guard) => guard,
1159            Err(e) => {
1160                return Metered {
1161                    value: Err(e),
1162                    ops: 0,
1163                };
1164            }
1165        };
1166        let data_ref = match data.into_arena_for(compiled, self, arena) {
1167            Ok(av) => av,
1168            Err(e) => {
1169                return Metered {
1170                    value: Err(e),
1171                    ops: 0,
1172                };
1173            }
1174        };
1175        let mut ctx = self.new_context(compiled, data_ref);
1176        ctx.set_budget(budget);
1177        let value = match self.dispatch_node(&compiled.root, &mut ctx, arena) {
1178            Ok(value) => Ok(value),
1179            Err(e) => Err(e.decorated(ctx.take_error_path(), compiled)),
1180        };
1181        Metered {
1182            value,
1183            ops: ctx.ops_spent(),
1184        }
1185    }
1186
1187    /// Apply the engine's configured truthiness rules
1188    /// ([`crate::TruthyEvaluator`]) to an evaluated value.
1189    ///
1190    /// This is the same coercion `if` / `and` / `or` / `!` apply to
1191    /// their operands, exposed so callers (and bindings) can collapse
1192    /// any rule result to a boolean without re-implementing the
1193    /// engine's configured semantics.
1194    ///
1195    /// # Example
1196    ///
1197    /// ```rust
1198    /// use bumpalo::Bump;
1199    /// use datalogic_rs::Engine;
1200    ///
1201    /// let engine = Engine::new();
1202    /// let compiled = engine.compile(r#"{"var": "items"}"#).unwrap();
1203    /// let arena = Bump::new();
1204    /// let result = engine.evaluate(&compiled, r#"{"items": [1]}"#, &arena).unwrap();
1205    /// assert!(engine.truthy(result));
1206    /// ```
1207    #[inline]
1208    pub fn truthy(&self, value: &crate::arena::DataValue<'_>) -> bool {
1209        crate::arena::truthy_arena(value, self)
1210    }
1211
1212    /// [`Self::truthy`] for a value in any representation the host holds:
1213    /// `&OwnedDataValue`, `&serde_json::Value`, `&ParsedData` or
1214    /// `&DataValue` (see [`crate::TruthyInput`]).
1215    ///
1216    /// Use it instead of re-implementing truthiness on a result: the
1217    /// answer follows the engine's [`crate::TruthyEvaluator`], and under
1218    /// the default rules an empty object is falsy, like an empty array,
1219    /// which hand-written checks often miss.
1220    ///
1221    /// # Example
1222    ///
1223    /// ```rust
1224    /// # #[cfg(feature = "serde_json")] {
1225    /// use datalogic_rs::Engine;
1226    /// use serde_json::json;
1227    ///
1228    /// let engine = Engine::new();
1229    /// let result: serde_json::Value = engine
1230    ///     .eval_into(r#"{"var": "claims"}"#, &json!({"claims": {}}))
1231    ///     .unwrap();
1232    /// assert!(!engine.truthy_of(&result));
1233    /// assert!(engine.truthy_of(&json!({"sub": "ana"})));
1234    /// # }
1235    /// ```
1236    #[inline]
1237    pub fn truthy_of<V: crate::TruthyInput>(&self, value: V) -> bool {
1238        value.truthy_with(self)
1239    }
1240
1241    /// One-shot evaluation returning [`datavalue::OwnedDataValue`].
1242    ///
1243    /// Compiles `rule`, parses `data`, evaluates against a fresh
1244    /// per-call arena, and deep-clones the result out. For the same
1245    /// rule run repeatedly, escalate to [`Self::compile`] + a
1246    /// [`Session`](crate::Session).
1247    ///
1248    /// # Example
1249    ///
1250    /// ```rust
1251    /// use datalogic_rs::Engine;
1252    ///
1253    /// let engine = Engine::new();
1254    /// let result = engine.eval(
1255    ///     r#"{"+": [{"var": "x"}, 1]}"#,
1256    ///     r#"{"x": 41}"#,
1257    /// ).unwrap();
1258    /// assert_eq!(result.as_i64(), Some(42));
1259    /// ```
1260    pub fn eval<R, D>(&self, rule: R, data: D) -> Result<datavalue::OwnedDataValue>
1261    where
1262        R: crate::IntoLogic,
1263        D: crate::OwnedInput,
1264    {
1265        self.eval_with::<datavalue::OwnedDataValue, _, _>(rule, data)
1266    }
1267
1268    /// One-shot evaluation returning a JSON [`String`].
1269    ///
1270    /// # Example
1271    ///
1272    /// ```rust
1273    /// use datalogic_rs::Engine;
1274    ///
1275    /// let engine = Engine::new();
1276    /// let result = engine.eval_str(
1277    ///     r#"{"==": [{"var": "x"}, 5]}"#,
1278    ///     r#"{"x": 5}"#,
1279    /// ).unwrap();
1280    /// assert_eq!(result, "true");
1281    /// ```
1282    pub fn eval_str<R, D>(&self, rule: R, data: D) -> Result<String>
1283    where
1284        R: crate::IntoLogic,
1285        D: crate::OwnedInput,
1286    {
1287        self.eval_with::<String, _, _>(rule, data)
1288    }
1289
1290    /// One-shot evaluation deserialised into a typed `T: DeserializeOwned`.
1291    ///
1292    /// Use `T = serde_json::Value` for a JSON `Value` result; use a typed
1293    /// struct for direct mapping. Internally routes through `serde_json`
1294    /// (round-trips the result through a JSON value).
1295    ///
1296    /// # Example
1297    ///
1298    /// ```rust
1299    /// # #[cfg(feature = "serde_json")] {
1300    /// use datalogic_rs::Engine;
1301    /// use serde_json::Value;
1302    ///
1303    /// let engine = Engine::new();
1304    /// let result: Value = engine.eval_into(
1305    ///     r#"{"+": [{"var": "x"}, 1]}"#,
1306    ///     r#"{"x": 41}"#,
1307    /// ).unwrap();
1308    /// assert_eq!(result, Value::from(42));
1309    /// # }
1310    /// ```
1311    #[cfg(feature = "serde_json")]
1312    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
1313    pub fn eval_into<T, R, D>(&self, rule: R, data: D) -> Result<T>
1314    where
1315        T: serde::de::DeserializeOwned,
1316        R: crate::IntoLogic,
1317        D: crate::OwnedInput,
1318    {
1319        let value: serde_json::Value = self.eval_with(rule, data)?;
1320        serde_json::from_value(value).map_err(crate::Error::from)
1321    }
1322
1323    /// One-shot evaluation converting the result straight into `O`, any
1324    /// [`crate::FromDataValue`] output: `OwnedDataValue`, a JSON
1325    /// [`String`], or a `serde_json::Value` (with `serde_json`).
1326    ///
1327    /// For a `serde_json::Value` result this is one conversion, where
1328    /// `eval_into::<serde_json::Value>` (with `serde_json`) builds the value and then
1329    /// deserializes it into a second one.
1330    ///
1331    /// ```rust
1332    /// # #[cfg(feature = "serde_json")] {
1333    /// use datalogic_rs::Engine;
1334    ///
1335    /// let engine = Engine::new();
1336    /// let user: serde_json::Value = engine
1337    ///     .eval_as(r#"{"var": "user"}"#, r#"{"user": {"id": 7}}"#)
1338    ///     .unwrap();
1339    /// assert_eq!(user["id"], 7);
1340    /// # }
1341    /// ```
1342    pub fn eval_as<O, R, D>(&self, rule: R, data: D) -> Result<O>
1343    where
1344        O: crate::FromDataValue,
1345        R: crate::IntoLogic,
1346        D: crate::OwnedInput,
1347    {
1348        self.eval_with(rule, data)
1349    }
1350
1351    /// Internal generic shared by `eval` / `eval_str` / `eval_into` /
1352    /// `eval_as`. Compiles, brings the data into a per-call arena (an
1353    /// owned or `serde_json` input projected onto what the rule reads, as
1354    /// [`Self::evaluate`] does), evaluates, and converts the result
1355    /// through [`crate::FromDataValue`].
1356    fn eval_with<O, R, D>(&self, rule: R, data: D) -> Result<O>
1357    where
1358        O: crate::FromDataValue,
1359        R: crate::IntoLogic,
1360        D: crate::OwnedInput,
1361    {
1362        let compiled = self.compile(rule)?;
1363        data.lend_arena_for(&compiled, self, |data, arena| {
1364            let result = self.evaluate(&compiled, data, arena)?;
1365            O::from_arena(result)
1366        })
1367    }
1368
1369    /// Bump the per-thread dispatch-boundary depth counter, bailing with
1370    /// `ConfigurationError` if the configured cap is reached. Returns a
1371    /// guard that decrements the counter on drop (covers `?` early returns
1372    /// and panics).
1373    ///
1374    /// Called from every public boundary entry point (`Engine::evaluate`,
1375    /// `TracedSession::evaluate`, …). The counter is thread-local rather
1376    /// than per-`ContextStack` so it survives across nested
1377    /// `engine.evaluate(...)` calls — the scenario a `CustomOperator`
1378    /// holding `Arc<Engine>` creates by re-entering the engine from inside
1379    /// its own `evaluate(...)`.
1380    ///
1381    /// Tokio safety: dispatch is sync, so a task cannot `.await` while the
1382    /// counter is raised; between dispatches the value is restored to its
1383    /// prior level (zero at the outermost call). Cross-thread task migration
1384    /// thus starts fresh on whatever thread the task lands on.
1385    #[inline(always)]
1386    pub(crate) fn enter_dispatch_boundary(&self) -> Result<DepthGuard> {
1387        // Built-in operators can't re-enter `Engine::evaluate` (only a
1388        // `CustomOperator` holding `Arc<Engine>` can); when the registry
1389        // is empty, cross-evaluate recursion is impossible and we skip
1390        // the TLS bookkeeping. The pure-built-in benchmarks pay zero.
1391        if self.custom_operators.is_empty() {
1392            return Ok(DepthGuard(DepthGuard::NOOP));
1393        }
1394        self.enter_dispatch_boundary_checked()
1395    }
1396
1397    /// Slow path of [`Self::enter_dispatch_boundary`] — hit only when
1398    /// the engine has at least one custom operator registered. Marked
1399    /// `#[cold]` and `#[inline(never)]` so the hot fast-path stays
1400    /// inline-friendly.
1401    #[cold]
1402    #[inline(never)]
1403    fn enter_dispatch_boundary_checked(&self) -> Result<DepthGuard> {
1404        let prev_depth = DISPATCH_DEPTH.with(Cell::get);
1405        if prev_depth >= self.settings.config.max_recursion_depth {
1406            return Err(crate::Error::configuration_error(format!(
1407                "max recursion depth exceeded ({})",
1408                self.settings.config.max_recursion_depth
1409            )));
1410        }
1411        DISPATCH_DEPTH.with(|d| d.set(prev_depth + 1));
1412        Ok(DepthGuard(prev_depth))
1413    }
1414
1415    /// Arena-mode dispatch hub. Returns `&'a DataValue<'a>` for every
1416    /// `CompiledNode` shape — exhaustive match, no value-mode fallback.
1417    ///
1418    /// On error, accumulates the failing node's id onto the context stack's
1419    /// breadcrumb so [`Error`] consumers can surface the failing
1420    /// path. When a tracer is attached to `ctx`, records a step per
1421    /// non-literal node (entry context + result/error).
1422    #[inline(always)]
1423    pub(crate) fn dispatch_node<'a>(
1424        &self,
1425        node: &'a CompiledNode,
1426        ctx: &mut crate::arena::ContextStack<'a>,
1427        arena: &'a bumpalo::Bump,
1428    ) -> Result<&'a crate::arena::DataValue<'a>> {
1429        // Literal fast path — no breadcrumb push, no trace step.
1430        if let CompiledNode::Value { value, lit, .. } = node {
1431            // Every literal reachable from a `Logic` carries a pre-built
1432            // `PreLit` — trivial ones from `precompute_lit` at node
1433            // construction, composites from the `populate_lits` pass —
1434            // so the hot path is a borrow, not a conversion.
1435            if let Some(av) = lit {
1436                return Ok(av.as_ref());
1437            }
1438            // Synthetic composite wrappers built outside the compile
1439            // pipeline fall through to per-call arena conversion here.
1440            return Ok(literal_fallback(value, arena));
1441        }
1442
1443        // Snapshot context for trace BEFORE recursing — children will
1444        // mutate iteration frames. Cheap when no tracer is attached.
1445        #[cfg(feature = "trace")]
1446        let ctx_snapshot: Option<serde_json::Value> =
1447            ctx.has_tracer().then(|| ctx.current_data_as_value());
1448
1449        // One operation per dispatched node, charged before the work.
1450        // Literals returned above cost nothing — they are data the
1451        // compiler already resolved, not work the rule asked for. With
1452        // the `budget` feature off `charge` is an inlined `Ok(())`, so
1453        // this folds to the bare inner dispatch.
1454        let mut result = match ctx.charge(1) {
1455            Ok(()) => dispatch::dispatch_node_inner(self, node, ctx, arena),
1456            Err(e) => Err(e),
1457        };
1458
1459        // Accumulate the failing node's id on every Err, and name the
1460        // innermost failing operator. Errors are rare, so this is one cold
1461        // call and structured-error consumers get the breadcrumb.
1462        if let Err(e) = &mut result {
1463            record_failure(e, node, ctx);
1464        }
1465
1466        #[cfg(feature = "trace")]
1467        if let Some(ctx_data) = ctx_snapshot {
1468            ctx.record_node_result(node.id(), ctx_data, &result);
1469        }
1470
1471        result
1472    }
1473
1474    /// Evaluate an iteration body (map/filter/reduce/all/some/none) with the
1475    /// trace collector's iteration index/total markers set around it. The
1476    /// markers are no-ops when no tracer is attached, so plain-mode callers
1477    /// pay only one branch per iteration.
1478    #[inline]
1479    pub(crate) fn run_iter_body<'a>(
1480        &self,
1481        body: &'a CompiledNode,
1482        ctx: &mut crate::arena::ContextStack<'a>,
1483        arena: &'a bumpalo::Bump,
1484        _index: u32,
1485        _total: u32,
1486    ) -> Result<&'a crate::arena::DataValue<'a>> {
1487        #[cfg(feature = "trace")]
1488        ctx.trace_push_iteration(_index, _total);
1489        let res = self.dispatch_node(body, ctx, arena);
1490        #[cfg(feature = "trace")]
1491        ctx.trace_pop_iteration();
1492        res
1493    }
1494}