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}