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}