Skip to main content

datalogic_rs/
session.rs

1//! Reusable evaluation handle that owns its arena.
2//!
3//! [`Session`] owns a [`bumpalo::Bump`] and exposes [`Session::reset`] for the
4//! caller to bound peak memory between calls. The session itself never resets
5//! the arena — every `evaluate*` method appends to the bump and the caller
6//! decides when to release that memory back to the start-of-chunk position.
7//! Inputs go through [`crate::EvalInput`] so callers pass whatever they have
8//! on hand (`&str`, `&OwnedDataValue`, `&serde_json::Value`, …); outputs are
9//! either owned ([`OwnedDataValue`] / `String` / `serde_json::Value`) or
10//! borrowed from the arena ([`Self::eval_borrowed`]) — borrowed results are
11//! invalidated by the next `&mut self` call (Rust's borrow checker enforces).
12//!
13//! For a one-shot evaluation that owns and drops its arena per call, use
14//! [`crate::Engine::eval_str`] (convenience). For full caller control of
15//! the arena lifecycle, use [`crate::Engine::evaluate`] directly with a
16//! caller-passed `&Bump`.
17//!
18//! # What each input adds to the arena
19//!
20//! Every call brings its data into the arena before evaluating, and that
21//! stays until [`Session::reset`]. What it costs depends on the input:
22//!
23//! | Input | Arena cost per call |
24//! |-------|---------------------|
25//! | `&ParsedData` | nothing: the tree already lives in the handle |
26//! | `&DataValue` | nothing |
27//! | `&OwnedDataValue` | the array and object spines; strings, keys and tensor bytes are borrowed |
28//! | `&serde_json::Value` | the array and object spines; strings and keys are borrowed |
29//! | `&str` | a parse tree; unescaped strings are borrowed from the text |
30//!
31//! For a large context evaluated many times, build a
32//! [`crate::ParsedData`] once (`from_json`, `from_value` or `from_owned`)
33//! and pass the handle: each call then costs only what the rule does.
34
35use std::marker::PhantomData;
36use std::ops::Deref;
37use std::sync::Arc;
38
39use bumpalo::Bump;
40use datavalue::OwnedDataValue;
41
42use crate::arena::DataValue;
43use crate::{Engine, EvalInput, Logic, Result};
44
45/// Reusable evaluation handle. Construct via [`Engine::session`].
46///
47/// Owns a [`bumpalo::Bump`]; the caller controls reset via [`Self::reset`].
48/// Subsequent `evaluate*` calls append to the bump until the caller resets
49/// or the session is dropped — letting the caller amortise reset cost across
50/// logical batches and avoid resetting between calls that don't need it.
51///
52/// # Example
53///
54/// ```rust
55/// use datalogic_rs::Engine;
56///
57/// let engine = Engine::new();
58/// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
59/// let mut session = engine.session();
60///
61/// for x in 0..3 {
62///     let payload = format!(r#"{{"x": {}}}"#, x);
63///     let result = session.eval_str(&compiled, &payload).unwrap();
64///     assert_eq!(result, (x + 1).to_string());
65///     // Reset between iterations to keep peak memory bounded by the
66///     // largest single evaluation rather than the cumulative loop.
67///     session.reset();
68/// }
69/// ```
70///
71/// # Holding the engine
72///
73/// `E` is how the session holds its engine. [`Engine::session`] borrows it
74/// (`Session<'_>`, the default). A [`SharedSession`] holds an
75/// `Arc<Engine>` instead, so it is `'static + Send`: it can be stored in a
76/// struct, moved to another thread or held across an `.await` without
77/// tying it to the engine's lifetime. Both have the same methods.
78pub struct Session<'engine, E = &'engine Engine> {
79    engine: E,
80    arena: Bump,
81    _engine: PhantomData<&'engine Engine>,
82}
83
84/// A [`Session`] that holds its engine by `Arc`: `'static + Send`, with no
85/// lifetime to thread through the caller's types. Build one with
86/// [`SharedSession::new`] or `SharedSession::from(arc)`.
87///
88/// ```rust
89/// use std::sync::Arc;
90/// use datalogic_rs::{Engine, SharedSession};
91///
92/// let engine = Arc::new(Engine::new());
93/// let logic = engine.compile(r#"{"var": "x"}"#).unwrap();
94/// let mut session = SharedSession::new(engine);
95/// let handle = std::thread::spawn(move || session.eval_str(&logic, r#"{"x": 1}"#).unwrap());
96/// assert_eq!(handle.join().unwrap(), "1");
97/// ```
98pub type SharedSession = Session<'static, Arc<Engine>>;
99
100impl SharedSession {
101    /// A session over a shared engine. The session keeps the engine alive.
102    pub fn new(engine: Arc<Engine>) -> Self {
103        Session::with_engine(engine)
104    }
105}
106
107impl From<Arc<Engine>> for SharedSession {
108    fn from(engine: Arc<Engine>) -> Self {
109        SharedSession::new(engine)
110    }
111}
112
113impl<E: Deref<Target = Engine>> std::fmt::Debug for Session<'_, E> {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        // Print the engine handle plus the arena's currently-allocated byte
116        // count — useful for tracking high-water marks across resets without
117        // dumping every chunk's raw bytes.
118        f.debug_struct("Session")
119            .field("engine", &*self.engine)
120            .field("arena_allocated_bytes", &self.arena.allocated_bytes())
121            .finish_non_exhaustive()
122    }
123}
124
125impl<'engine> Session<'engine> {
126    #[inline]
127    pub(crate) fn borrowing(engine: &'engine Engine) -> Self {
128        Session::with_engine(engine)
129    }
130}
131
132impl<'engine, E: Deref<Target = Engine>> Session<'engine, E> {
133    #[inline]
134    fn with_engine(engine: E) -> Self {
135        Self {
136            engine,
137            arena: Bump::new(),
138            _engine: PhantomData,
139        }
140    }
141
142    /// The engine this session evaluates with.
143    #[inline]
144    pub fn engine(&self) -> &Engine {
145        &self.engine
146    }
147
148    /// Reset the session's arena, returning every allocated chunk to the
149    /// free list's start-of-chunk position without freeing OS memory.
150    ///
151    /// Call this between logical batches to bound peak memory. After reset,
152    /// any borrowed reference previously returned by [`Self::eval_borrowed`]
153    /// is invalidated — the borrow checker enforces this for the common case
154    /// (the result borrow ends with the previous `&mut self` borrow).
155    ///
156    /// `Bump::reset` is constant-time (resets a few pointers); the freed
157    /// chunks remain allocated and serve subsequent calls without re-asking
158    /// the OS for memory.
159    #[inline]
160    pub fn reset(&mut self) {
161        self.arena.reset();
162    }
163
164    /// Drop the session's arena and replace it with a fresh one whose
165    /// initial chunk holds at least `capacity` bytes.
166    ///
167    /// Use this when you know the steady-state high-water mark of your
168    /// workload (e.g. captured via [`Self::allocated_bytes`] after a
169    /// warm-up pass) and want subsequent calls to run on a single
170    /// pre-sized chunk — no chunk-growth events during the timed window.
171    ///
172    /// Unlike [`Self::reset`], which keeps the existing chunks and only
173    /// rewinds the bump pointer, this drops the chunks entirely and
174    /// allocates one new chunk of the requested capacity. Any reference
175    /// previously returned by [`Self::eval_borrowed`] is invalidated; the
176    /// `&mut self` signature lets the borrow checker enforce this.
177    pub fn reset_with_capacity(&mut self, capacity: usize) {
178        self.arena = Bump::with_capacity(capacity);
179    }
180
181    /// Total bytes currently occupied by the session's arena chunks.
182    ///
183    /// Useful for capturing a workload's steady-state high-water mark
184    /// after a warm-up pass — feed the returned value into
185    /// [`Self::reset_with_capacity`] to pre-size the arena before a timed
186    /// loop. Stable across [`Self::reset`] calls (chunks aren't freed);
187    /// drops to the new chunk size after [`Self::reset_with_capacity`].
188    ///
189    /// Forwards to [`bumpalo::Bump::allocated_bytes`].
190    #[inline]
191    pub fn allocated_bytes(&self) -> usize {
192        self.arena.allocated_bytes()
193    }
194
195    /// Evaluate `compiled` against `data` and deep-clone the result into
196    /// an [`OwnedDataValue`] that survives subsequent calls and resets.
197    ///
198    /// The intermediate arena allocations stay in the session's bump
199    /// until the caller invokes [`Self::reset`]. For long-running loops,
200    /// call `reset` between iterations to keep peak memory bounded.
201    ///
202    /// # Example
203    ///
204    /// ```rust
205    /// use datalogic_rs::Engine;
206    ///
207    /// let engine = Engine::new();
208    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
209    /// let mut session = engine.session();
210    /// let result = session.eval(&compiled, r#"{"x": 1}"#).unwrap();
211    /// assert_eq!(result.as_bool(), Some(true));
212    /// ```
213    pub fn eval<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<OwnedDataValue>
214    where
215        D: EvalInput<'a>,
216    {
217        self.eval_as(compiled, data)
218    }
219
220    /// Evaluate and convert the result straight into `R`, any
221    /// [`crate::FromDataValue`] output: [`OwnedDataValue`], a JSON
222    /// [`String`], or a `serde_json::Value` (with `serde_json`). The shared
223    /// body of [`Self::eval`] / [`Self::eval_str`] / `eval_into`.
224    ///
225    /// For a `serde_json::Value` result this is one conversion, where
226    /// `eval_into::<serde_json::Value>` builds the value and then
227    /// deserializes it into a second one.
228    ///
229    /// ```rust
230    /// # #[cfg(feature = "serde_json")] {
231    /// use datalogic_rs::Engine;
232    ///
233    /// let engine = Engine::new();
234    /// let compiled = engine.compile(r#"{"var": "user"}"#).unwrap();
235    /// let mut session = engine.session();
236    /// let user: serde_json::Value = session
237    ///     .eval_as(&compiled, r#"{"user": {"id": 7}}"#)
238    ///     .unwrap();
239    /// assert_eq!(user["id"], 7);
240    /// # }
241    /// ```
242    pub fn eval_as<'a, R, D>(&'a mut self, compiled: &Logic, data: D) -> Result<R>
243    where
244        R: crate::FromDataValue,
245        D: EvalInput<'a>,
246    {
247        let arena: &'a Bump = &self.arena;
248        // Bring the input in against the rule as this engine runs it, so
249        // a projection matches the tree `evaluate` dispatches.
250        let compiled = compiled.for_engine(&self.engine);
251        let av = data.into_arena_for(compiled, &self.engine, arena)?;
252        R::from_arena(self.engine.evaluate(compiled, av, arena)?)
253    }
254
255    /// JSON-string convenience: evaluate against `data` and serialise
256    /// the result back to a JSON [`String`]. Reuses the arena across
257    /// calls; does not reset — see [`Self::reset`].
258    pub fn eval_str<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<String>
259    where
260        D: EvalInput<'a>,
261    {
262        self.eval_as(compiled, data)
263    }
264
265    /// Typed convenience: evaluate and deserialise the result into
266    /// `T: DeserializeOwned`. Use `T = serde_json::Value` for a JSON
267    /// `Value` result; use a typed struct for direct mapping. Internally
268    /// routes through `serde_json`.
269    #[cfg(feature = "serde_json")]
270    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
271    pub fn eval_into<'a, T, D>(&'a mut self, compiled: &Logic, data: D) -> Result<T>
272    where
273        T: serde::de::DeserializeOwned,
274        D: EvalInput<'a>,
275    {
276        let value: serde_json::Value = self.eval_as(compiled, data)?;
277        serde_json::from_value(value).map_err(crate::Error::from)
278    }
279
280    /// Evaluate under an explicit operation budget, reporting what the
281    /// evaluation spent.
282    ///
283    /// Session-arena counterpart to [`Engine::evaluate_metered`], which
284    /// documents what one operation is. The value is deep-cloned out like
285    /// [`Self::eval`]'s, so the count survives the next `reset`.
286    ///
287    /// # Example
288    ///
289    /// ```rust
290    /// # #[cfg(feature = "budget")] {
291    /// use datalogic_rs::{Engine, Metered};
292    ///
293    /// let engine = Engine::new();
294    /// let compiled = engine.compile(r#"{"filter": [{"var": "xs"}, {">": [{"var": ""}, 1]}]}"#).unwrap();
295    /// let mut session = engine.session();
296    ///
297    /// let Metered { value, ops } = session
298    ///     .eval_metered(&compiled, r#"{"xs": [1, 2, 3]}"#, u64::MAX)
299    ///     .unwrap();
300    /// assert_eq!(value.to_json_string(), "[2,3]");
301    /// assert!(ops >= 3, "at least one operation per item filtered");
302    /// # }
303    /// ```
304    ///
305    /// # Errors
306    ///
307    /// [`ErrorKind::BudgetExceeded`](crate::ErrorKind::BudgetExceeded)
308    /// when the rule charges past `budget`, plus every error
309    /// [`Self::eval`] can return. To observe the operations spent even
310    /// when evaluation fails, call [`Self::eval_metered_report`].
311    #[cfg(feature = "budget")]
312    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
313    pub fn eval_metered<'a, D>(
314        &'a mut self,
315        compiled: &Logic,
316        data: D,
317        budget: u64,
318    ) -> Result<crate::Metered<OwnedDataValue>>
319    where
320        D: EvalInput<'a>,
321    {
322        self.eval_metered_report(compiled, data, budget).transpose()
323    }
324
325    /// Failure-inclusive counterpart to [`Self::eval_metered`]: evaluates
326    /// under `budget` and returns a [`crate::Metered`] report whose `ops`
327    /// is populated whether evaluation succeeds (`value: Ok(...)`) or
328    /// fails (`value: Err(...)`).
329    ///
330    /// Session-arena counterpart to [`Engine::evaluate_metered_report`],
331    /// which documents the failure charge semantics (ordinary runtime
332    /// errors, `BudgetExceeded`, and pre-dispatch input adaptation errors).
333    ///
334    /// # Example
335    ///
336    /// ```rust
337    /// # #[cfg(feature = "budget")] {
338    /// use datalogic_rs::{Engine, ErrorCode, Metered};
339    ///
340    /// let engine = Engine::new();
341    /// let compiled = engine.compile(r#"{"/": [1, {"var": "zero"}]}"#).unwrap();
342    /// let mut session = engine.session();
343    ///
344    /// let Metered { value, ops } = session
345    ///     .eval_metered_report(&compiled, r#"{"zero": 0}"#, 100);
346    /// let err = value.unwrap_err();
347    /// assert_eq!(err.code(), ErrorCode::Thrown);
348    /// assert_eq!(err.operator(), Some("/"));
349    /// assert_eq!(ops, 2);
350    /// # }
351    /// ```
352    #[cfg(feature = "budget")]
353    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
354    pub fn eval_metered_report<'a, D>(
355        &'a mut self,
356        compiled: &Logic,
357        data: D,
358        budget: u64,
359    ) -> crate::Metered<Result<OwnedDataValue>>
360    where
361        D: EvalInput<'a>,
362    {
363        let arena: &'a Bump = &self.arena;
364        let compiled = compiled.for_engine(&self.engine);
365        let av = match data.into_arena_for(compiled, &self.engine, arena) {
366            Ok(av) => av,
367            Err(e) => {
368                return crate::Metered {
369                    value: Err(e),
370                    ops: 0,
371                };
372            }
373        };
374        let metered = self
375            .engine
376            .evaluate_metered_report(compiled, av, arena, budget);
377        crate::Metered {
378            value: metered.value.and_then(crate::FromDataValue::from_arena),
379            ops: metered.ops,
380        }
381    }
382
383    /// Evaluate and return a borrowed result tied to this session's
384    /// arena. Same semantics as [`Self::eval`] but skips the deep-clone
385    /// — the returned reference is invalidated by the next `&mut self`
386    /// call (the borrow checker enforces). Use this when the result is
387    /// consumed before the next session call; for cross-call retention
388    /// use [`Self::eval`].
389    ///
390    /// Symmetric with [`Engine::evaluate`] (caller-managed bump,
391    /// borrowed result) but with the bump owned by the session. Does
392    /// not reset the arena — call [`Self::reset`] explicitly.
393    ///
394    /// # Example
395    ///
396    /// ```rust
397    /// use datalogic_rs::Engine;
398    ///
399    /// let engine = Engine::new();
400    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
401    /// let mut session = engine.session();
402    /// let result = session.eval_borrowed(&compiled, r#"{"x": 5}"#).unwrap();
403    /// assert_eq!(result.as_i64(), Some(6));
404    /// ```
405    pub fn eval_borrowed<'a, D>(
406        &'a mut self,
407        compiled: &'a Logic,
408        data: D,
409    ) -> Result<&'a DataValue<'a>>
410    where
411        D: EvalInput<'a>,
412    {
413        let arena: &'a Bump = &self.arena;
414        self.engine.evaluate(compiled, data, arena)
415    }
416}