use std::cell::Cell;
use std::collections::HashMap;
use crate::config::EvaluationConfig;
use crate::{CompiledNode, Logic, Result};
thread_local! {
static DISPATCH_DEPTH: Cell<u32> = const { Cell::new(0) };
}
pub(crate) struct DepthGuard(u32);
impl DepthGuard {
const NOOP: u32 = u32::MAX;
}
impl Drop for DepthGuard {
#[inline]
fn drop(&mut self) {
if self.0 != Self::NOOP {
DISPATCH_DEPTH.with(|d| d.set(self.0));
}
}
}
#[cfg_attr(
feature = "serde_json",
doc = "[`Self::eval_str`], [`Self::eval_into`]), and opening hot-loop"
)]
#[cfg_attr(
not(feature = "serde_json"),
doc = "[`Self::eval_str`], `Self::eval_into`), and opening hot-loop"
)]
#[cfg_attr(
feature = "trace",
doc = "Enabling the `trace` feature also exposes [`Self::trace`] for traced sessions."
)]
#[cfg_attr(
feature = "serde_json",
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`. |"
)]
#[cfg_attr(
not(feature = "serde_json"),
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`. |"
)]
#[cfg_attr(
feature = "serde_json",
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. |"
)]
#[cfg_attr(
not(feature = "serde_json"),
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. |"
)]
pub struct Engine {
pub(super) custom_operators: HashMap<String, Box<dyn crate::CustomOperator>>,
#[cfg(feature = "templating")]
templating: bool,
constant_folding: bool,
config: EvaluationConfig,
}
mod dispatch;
#[inline]
fn literal_fallback<'a>(
value: &'a datavalue::OwnedDataValue,
arena: &'a bumpalo::Bump,
) -> &'a crate::arena::DataValue<'a> {
use datavalue::OwnedDataValue;
match value {
OwnedDataValue::Null => crate::arena::singletons::singleton_null(),
OwnedDataValue::Bool(b) => crate::arena::singletons::singleton_bool(*b),
OwnedDataValue::String(s) if s.is_empty() => {
crate::arena::singletons::singleton_empty_string()
}
OwnedDataValue::Array(a) if a.is_empty() => {
crate::arena::singletons::singleton_empty_array()
}
OwnedDataValue::Object(o) if o.is_empty() => {
crate::arena::singletons::singleton_empty_object()
}
OwnedDataValue::String(s) => arena.alloc(crate::arena::DataValue::String(s.as_str())),
_ => arena.alloc(value.to_arena(arena)),
}
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Engine");
s.field("custom_operators", &self.custom_operators.len());
#[cfg(feature = "templating")]
s.field("templating", &self.templating);
s.field("config", &self.config);
s.finish_non_exhaustive()
}
}
impl Engine {
#[inline]
pub fn builder() -> crate::EngineBuilder {
crate::EngineBuilder::new()
}
#[inline]
pub fn session(&self) -> crate::Session<'_> {
crate::Session::new(self)
}
#[inline]
pub(crate) fn from_builder_parts(
config: EvaluationConfig,
_templating: bool,
constant_folding: bool,
operators: HashMap<String, Box<dyn crate::CustomOperator>>,
) -> Self {
Self {
custom_operators: operators,
#[cfg(feature = "templating")]
templating: _templating,
constant_folding,
config,
}
}
pub fn new() -> Self {
Self::from_builder_parts(EvaluationConfig::default(), false, true, HashMap::new())
}
pub fn config(&self) -> &EvaluationConfig {
&self.config
}
#[inline]
pub(crate) fn constant_folding_enabled(&self) -> bool {
self.constant_folding
}
#[inline]
pub(crate) fn is_templating_enabled(&self) -> bool {
#[cfg(feature = "templating")]
{
self.templating
}
#[cfg(not(feature = "templating"))]
{
false
}
}
pub fn has_custom_operator(&self, name: &str) -> bool {
self.custom_operators.contains_key(name)
}
pub fn custom_operator_names(&self) -> impl Iterator<Item = &str> {
self.custom_operators.keys().map(String::as_str)
}
pub fn compile<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
let owned = rule.into_owned_logic()?;
Logic::compile_with(&owned, self)
}
pub fn compile_arc<R: crate::IntoLogic>(&self, rule: R) -> Result<std::sync::Arc<Logic>> {
Ok(std::sync::Arc::new(self.compile(rule)?))
}
#[cfg(feature = "trace")]
#[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
#[inline]
pub fn trace(&self) -> crate::trace::TracedSession<'_> {
crate::trace::TracedSession::new(self)
}
#[cfg_attr(feature = "serde_json", doc = "/ [`Self::eval_into`].")]
#[cfg_attr(
not(feature = "serde_json"),
doc = "(plus `Self::eval_into` with the `serde_json` feature)."
)]
#[inline(always)]
pub fn evaluate<'a, D: crate::EvalInput<'a>>(
&self,
compiled: &'a Logic,
data: D,
arena: &'a bumpalo::Bump,
) -> Result<&'a crate::arena::DataValue<'a>> {
let _depth_guard = self.enter_dispatch_boundary()?;
let data_ref = data.into_arena_value(arena)?;
let mut ctx = crate::arena::ContextStack::new(data_ref);
match self.dispatch_node(&compiled.root, &mut ctx, arena) {
Ok(av) => Ok(av),
Err(e) => Err(e.decorated(ctx.take_error_path(), compiled, true)),
}
}
pub fn eval<R, D>(&self, rule: R, data: D) -> Result<datavalue::OwnedDataValue>
where
R: crate::IntoLogic,
D: crate::OwnedInput,
{
self.eval_with::<datavalue::OwnedDataValue, _, _>(rule, data)
}
pub fn eval_str<R, D>(&self, rule: R, data: D) -> Result<String>
where
R: crate::IntoLogic,
D: crate::OwnedInput,
{
self.eval_with::<String, _, _>(rule, data)
}
#[cfg(feature = "serde_json")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
pub fn eval_into<T, R, D>(&self, rule: R, data: D) -> Result<T>
where
T: serde::de::DeserializeOwned,
R: crate::IntoLogic,
D: crate::OwnedInput,
{
let value: serde_json::Value = self.eval_with(rule, data)?;
serde_json::from_value(value).map_err(crate::Error::from)
}
fn eval_with<O, R, D>(&self, rule: R, data: D) -> Result<O>
where
O: crate::FromDataValue,
R: crate::IntoLogic,
D: crate::OwnedInput,
{
let compiled = self.compile(rule)?;
let arena = bumpalo::Bump::with_capacity(4096);
let owned_data = data.into_owned_input()?;
let result = self.evaluate(&compiled, &owned_data, &arena)?;
O::from_arena(result)
}
#[inline(always)]
pub(crate) fn enter_dispatch_boundary(&self) -> Result<DepthGuard> {
if self.custom_operators.is_empty() {
return Ok(DepthGuard(DepthGuard::NOOP));
}
self.enter_dispatch_boundary_checked()
}
#[cold]
#[inline(never)]
fn enter_dispatch_boundary_checked(&self) -> Result<DepthGuard> {
let prev_depth = DISPATCH_DEPTH.with(Cell::get);
if prev_depth >= self.config.max_recursion_depth {
return Err(crate::Error::configuration_error(format!(
"max recursion depth exceeded ({})",
self.config.max_recursion_depth
)));
}
DISPATCH_DEPTH.with(|d| d.set(prev_depth + 1));
Ok(DepthGuard(prev_depth))
}
#[inline(always)]
pub(crate) fn dispatch_node<'a>(
&self,
node: &'a CompiledNode,
ctx: &mut crate::arena::ContextStack<'a>,
arena: &'a bumpalo::Bump,
) -> Result<&'a crate::arena::DataValue<'a>> {
if let CompiledNode::Value { value, lit, .. } = node {
if let Some(av) = lit {
return Ok(av);
}
return Ok(literal_fallback(value, arena));
}
#[cfg(feature = "trace")]
let ctx_snapshot: Option<serde_json::Value> =
ctx.has_tracer().then(|| ctx.current_data_as_value());
let result = dispatch::dispatch_node_inner(self, node, ctx, arena);
if result.is_err() {
ctx.push_error_step(node.id());
}
#[cfg(feature = "trace")]
if let Some(ctx_data) = ctx_snapshot {
ctx.record_node_result(node.id(), ctx_data, &result);
}
result
}
#[inline]
pub(crate) fn run_iter_body<'a>(
&self,
body: &'a CompiledNode,
ctx: &mut crate::arena::ContextStack<'a>,
arena: &'a bumpalo::Bump,
_index: u32,
_total: u32,
) -> Result<&'a crate::arena::DataValue<'a>> {
#[cfg(feature = "trace")]
ctx.trace_push_iteration(_index, _total);
let res = self.dispatch_node(body, ctx, arena);
#[cfg(feature = "trace")]
ctx.trace_pop_iteration();
res
}
}