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}