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