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