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
18use bumpalo::Bump;
19use datavalue::OwnedDataValue;
20
21use crate::arena::DataValue;
22use crate::{Engine, EvalInput, Logic, Result};
23
24/// Reusable evaluation handle. Construct via [`Engine::session`].
25///
26/// Owns a [`bumpalo::Bump`]; the caller controls reset via [`Self::reset`].
27/// Subsequent `evaluate*` calls append to the bump until the caller resets
28/// or the session is dropped — letting the caller amortise reset cost across
29/// logical batches and avoid resetting between calls that don't need it.
30///
31/// # Example
32///
33/// ```rust
34/// use datalogic_rs::Engine;
35///
36/// let engine = Engine::new();
37/// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
38/// let mut session = engine.session();
39///
40/// for x in 0..3 {
41///     let payload = format!(r#"{{"x": {}}}"#, x);
42///     let result = session.eval_str(&compiled, &payload).unwrap();
43///     assert_eq!(result, (x + 1).to_string());
44///     // Reset between iterations to keep peak memory bounded by the
45///     // largest single evaluation rather than the cumulative loop.
46///     session.reset();
47/// }
48/// ```
49pub struct Session<'engine> {
50    engine: &'engine Engine,
51    arena: Bump,
52}
53
54impl std::fmt::Debug for Session<'_> {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        // Print the engine handle plus the arena's currently-allocated byte
57        // count — useful for tracking high-water marks across resets without
58        // dumping every chunk's raw bytes.
59        f.debug_struct("Session")
60            .field("engine", &self.engine)
61            .field("arena_allocated_bytes", &self.arena.allocated_bytes())
62            .finish_non_exhaustive()
63    }
64}
65
66impl<'engine> Session<'engine> {
67    #[inline]
68    pub(crate) fn new(engine: &'engine Engine) -> Self {
69        Self {
70            engine,
71            arena: Bump::new(),
72        }
73    }
74
75    /// Reset the session's arena, returning every allocated chunk to the
76    /// free list's start-of-chunk position without freeing OS memory.
77    ///
78    /// Call this between logical batches to bound peak memory. After reset,
79    /// any borrowed reference previously returned by [`Self::eval_borrowed`]
80    /// is invalidated — the borrow checker enforces this for the common case
81    /// (the result borrow ends with the previous `&mut self` borrow).
82    ///
83    /// `Bump::reset` is constant-time (resets a few pointers); the freed
84    /// chunks remain allocated and serve subsequent calls without re-asking
85    /// the OS for memory.
86    #[inline]
87    pub fn reset(&mut self) {
88        self.arena.reset();
89    }
90
91    /// Drop the session's arena and replace it with a fresh one whose
92    /// initial chunk holds at least `capacity` bytes.
93    ///
94    /// Use this when you know the steady-state high-water mark of your
95    /// workload (e.g. captured via [`Self::allocated_bytes`] after a
96    /// warm-up pass) and want subsequent calls to run on a single
97    /// pre-sized chunk — no chunk-growth events during the timed window.
98    ///
99    /// Unlike [`Self::reset`], which keeps the existing chunks and only
100    /// rewinds the bump pointer, this drops the chunks entirely and
101    /// allocates one new chunk of the requested capacity. Any reference
102    /// previously returned by [`Self::eval_borrowed`] is invalidated; the
103    /// `&mut self` signature lets the borrow checker enforce this.
104    pub fn reset_with_capacity(&mut self, capacity: usize) {
105        self.arena = Bump::with_capacity(capacity);
106    }
107
108    /// Total bytes currently occupied by the session's arena chunks.
109    ///
110    /// Useful for capturing a workload's steady-state high-water mark
111    /// after a warm-up pass — feed the returned value into
112    /// [`Self::reset_with_capacity`] to pre-size the arena before a timed
113    /// loop. Stable across [`Self::reset`] calls (chunks aren't freed);
114    /// drops to the new chunk size after [`Self::reset_with_capacity`].
115    ///
116    /// Forwards to [`bumpalo::Bump::allocated_bytes`].
117    #[inline]
118    pub fn allocated_bytes(&self) -> usize {
119        self.arena.allocated_bytes()
120    }
121
122    /// Evaluate `compiled` against `data` and deep-clone the result into
123    /// an [`OwnedDataValue`] that survives subsequent calls and resets.
124    ///
125    /// The intermediate arena allocations stay in the session's bump
126    /// until the caller invokes [`Self::reset`]. For long-running loops,
127    /// call `reset` between iterations to keep peak memory bounded.
128    ///
129    /// # Example
130    ///
131    /// ```rust
132    /// use datalogic_rs::Engine;
133    ///
134    /// let engine = Engine::new();
135    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
136    /// let mut session = engine.session();
137    /// let result = session.eval(&compiled, r#"{"x": 1}"#).unwrap();
138    /// assert_eq!(result.as_bool(), Some(true));
139    /// ```
140    pub fn eval<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<OwnedDataValue>
141    where
142        D: EvalInput<'a>,
143    {
144        let arena: &'a Bump = &self.arena;
145        let av = data.into_arena_value(arena)?;
146        let result = self.engine.evaluate(compiled, av, arena)?;
147        crate::FromDataValue::from_arena(result)
148    }
149
150    /// JSON-string convenience: evaluate against `data` and serialise
151    /// the result back to a JSON [`String`]. Reuses the arena across
152    /// calls; does not reset — see [`Self::reset`].
153    pub fn eval_str<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<String>
154    where
155        D: EvalInput<'a>,
156    {
157        let arena: &'a Bump = &self.arena;
158        let av = data.into_arena_value(arena)?;
159        let result = self.engine.evaluate(compiled, av, arena)?;
160        crate::FromDataValue::from_arena(result)
161    }
162
163    /// Typed convenience: evaluate and deserialise the result into
164    /// `T: DeserializeOwned`. Use `T = serde_json::Value` for a JSON
165    /// `Value` result; use a typed struct for direct mapping. Internally
166    /// routes through `serde_json`.
167    #[cfg(feature = "serde_json")]
168    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
169    pub fn eval_into<'a, T, D>(&'a mut self, compiled: &Logic, data: D) -> Result<T>
170    where
171        T: serde::de::DeserializeOwned,
172        D: EvalInput<'a>,
173    {
174        let value: serde_json::Value = {
175            let arena: &'a Bump = &self.arena;
176            let av = data.into_arena_value(arena)?;
177            let result = self.engine.evaluate(compiled, av, arena)?;
178            crate::FromDataValue::from_arena(result)?
179        };
180        serde_json::from_value(value).map_err(crate::Error::from)
181    }
182
183    /// Evaluate and return a borrowed result tied to this session's
184    /// arena. Same semantics as [`Self::eval`] but skips the deep-clone
185    /// — the returned reference is invalidated by the next `&mut self`
186    /// call (the borrow checker enforces). Use this when the result is
187    /// consumed before the next session call; for cross-call retention
188    /// use [`Self::eval`].
189    ///
190    /// Symmetric with [`Engine::evaluate`] (caller-managed bump,
191    /// borrowed result) but with the bump owned by the session. Does
192    /// not reset the arena — call [`Self::reset`] explicitly.
193    ///
194    /// # Example
195    ///
196    /// ```rust
197    /// use datalogic_rs::Engine;
198    ///
199    /// let engine = Engine::new();
200    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
201    /// let mut session = engine.session();
202    /// let result = session.eval_borrowed(&compiled, r#"{"x": 5}"#).unwrap();
203    /// assert_eq!(result.as_i64(), Some(6));
204    /// ```
205    pub fn eval_borrowed<'a, D>(
206        &'a mut self,
207        compiled: &'a Logic,
208        data: D,
209    ) -> Result<&'a DataValue<'a>>
210    where
211        D: EvalInput<'a>,
212    {
213        let arena: &'a Bump = &self.arena;
214        let av = data.into_arena_value(arena)?;
215        self.engine.evaluate(compiled, av, arena)
216    }
217}