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) };
}
#[cfg(feature = "budget")]
#[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Metered<T> {
pub value: T,
pub ops: u64,
}
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) id: u64,
pub(super) custom_operators: CustomOperators,
settings: crate::builder::EngineSettings,
fold_fingerprint: u64,
}
#[inline]
pub(crate) fn builtin_in(families: u32, name: &str) -> Option<crate::OpCode> {
name.parse::<crate::OpCode>()
.ok()
.filter(|op| op.in_families(families))
}
mod dispatch;
static NEXT_ENGINE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
pub(super) struct CustomOperators {
ops: Vec<std::sync::Arc<dyn crate::CustomOperator>>,
slots: HashMap<String, u32>,
}
impl CustomOperators {
fn new(operators: HashMap<String, std::sync::Arc<dyn crate::CustomOperator>>) -> Self {
let mut ops = Vec::with_capacity(operators.len());
let mut slots = HashMap::with_capacity(operators.len());
for (name, op) in operators {
slots.insert(name, ops.len() as u32);
ops.push(op);
}
CustomOperators { ops, slots }
}
fn len(&self) -> usize {
self.ops.len()
}
fn is_empty(&self) -> bool {
self.ops.is_empty()
}
pub(super) fn slot(&self, name: &str) -> Option<u32> {
self.slots.get(name).copied()
}
pub(super) fn get(&self, name: &str) -> Option<&dyn crate::CustomOperator> {
self.slot(name).map(|slot| self.at(slot))
}
#[inline]
pub(super) fn at(&self, slot: u32) -> &dyn crate::CustomOperator {
&*self.ops[slot as usize]
}
fn names(&self) -> impl Iterator<Item = &str> {
self.slots.keys().map(String::as_str)
}
fn shared(&self) -> HashMap<String, std::sync::Arc<dyn crate::CustomOperator>> {
self.slots
.iter()
.map(|(name, &slot)| {
(
name.clone(),
std::sync::Arc::clone(&self.ops[slot as usize]),
)
})
.collect()
}
}
#[cold]
#[inline(never)]
fn record_failure(
err: &mut crate::Error,
node: &CompiledNode,
ctx: &mut crate::arena::ContextStack<'_>,
) {
ctx.push_error_step(node.id());
err.name_operator_from(node);
}
#[cold]
#[inline(never)]
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.view_in(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 families: Vec<&str> = crate::Family::ALL
.iter()
.filter(|f| f.is_compiled() && self.settings.families & f.bit() != 0)
.map(|f| f.name())
.collect();
let settings = &self.settings;
let mut s = f.debug_struct("Engine");
s.field("custom_operators", &self.custom_operators.len());
s.field("templating", &settings.templating);
s.field("template_key_escape", &settings.template_key_escape);
s.field("constant_folding", &settings.constant_folding);
s.field("families", &families);
s.field("config", &settings.config);
s.finish_non_exhaustive()
}
}
impl Engine {
#[inline]
pub fn builder() -> crate::EngineBuilder {
crate::EngineBuilder::new()
}
pub fn to_builder(&self) -> crate::EngineBuilder {
crate::EngineBuilder::from_engine_parts(
self.settings.clone(),
self.custom_operators.shared(),
)
}
#[inline]
pub fn session(&self) -> crate::Session<'_> {
crate::Session::borrowing(self)
}
#[inline]
pub(crate) fn from_builder_parts(
mut settings: crate::builder::EngineSettings,
operators: HashMap<String, std::sync::Arc<dyn crate::CustomOperator>>,
) -> Self {
settings.templating &= cfg!(feature = "templating");
Self {
id: NEXT_ENGINE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
custom_operators: CustomOperators::new(operators),
fold_fingerprint: settings.config.fold_fingerprint(),
settings,
}
}
pub fn new() -> Self {
Self::from_builder_parts(crate::builder::EngineSettings::default(), HashMap::new())
}
pub fn config(&self) -> &EvaluationConfig {
&self.settings.config
}
#[inline]
pub(crate) fn constant_folding_enabled(&self) -> bool {
self.settings.constant_folding
}
#[inline]
pub(crate) fn is_templating_enabled(&self) -> bool {
self.settings.templating
}
#[inline]
pub(crate) fn template_key_escape(&self) -> Option<char> {
self.settings.template_key_escape
}
pub fn has_custom_operator(&self, name: &str) -> bool {
self.custom_operators.slot(name).is_some()
}
pub fn custom_operator_info(&self, name: &str) -> Option<crate::CustomOperatorInfo> {
self.custom_operators.get(name).map(|op| op.info())
}
#[inline]
pub(crate) fn id(&self) -> u64 {
self.id
}
#[inline]
pub(crate) fn fold_fingerprint(&self) -> u64 {
self.fold_fingerprint
}
pub(crate) fn custom_operator(&self, name: &str) -> Option<&dyn crate::CustomOperator> {
self.custom_operators.get(name)
}
#[inline(always)]
pub(crate) fn reads_fields_inline(&self) -> bool {
self.settings.config.missing_var == crate::MissingVar::Null
}
pub(crate) fn custom_operator_slot(&self, name: &str) -> Option<(u64, u32)> {
self.custom_operators.slot(name).map(|slot| (self.id, slot))
}
pub fn custom_operator_names(&self) -> impl Iterator<Item = &str> {
self.custom_operators.names()
}
pub fn builtin_operator_names(&self) -> impl Iterator<Item = &'static str> + use<> {
crate::operators::table::builtin_operator_names_in(self.settings.families)
}
pub fn operators(&self) -> impl Iterator<Item = crate::OperatorInfo> + use<> {
crate::operators::info::operators_in(self.settings.families)
}
#[inline]
pub(crate) fn builtin(&self, name: &str) -> Option<crate::OpCode> {
builtin_in(self.settings.families, name)
}
pub fn compile<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
let owned = rule.logic_source()?;
Logic::compile_with(&owned, self)
}
pub fn check<R: crate::IntoLogic>(
&self,
rule: R,
mode: crate::CheckMode,
) -> Vec<crate::Diagnostic> {
let owned = match rule.logic_source() {
Ok(owned) => owned,
Err(err) => return crate::CompileError::from_error(err).diagnostics,
};
let templating = match mode {
crate::CheckMode::Engine => self.is_templating_enabled(),
crate::CheckMode::Strict => false,
crate::CheckMode::Template => true,
};
crate::check::check(self, &owned, templating)
}
pub fn compile_checked<R: crate::IntoLogic>(
&self,
rule: R,
) -> std::result::Result<Logic, crate::CompileError> {
let owned = rule
.logic_source()
.map_err(crate::CompileError::from_error)?;
let diagnostics = crate::check::check(self, &owned, self.is_templating_enabled());
if diagnostics
.iter()
.any(|d| d.severity == crate::Severity::Error)
{
return Err(crate::CompileError { diagnostics });
}
Logic::compile_with(&owned, self).map_err(crate::CompileError::from_error)
}
#[cfg(feature = "templating")]
#[cfg_attr(docsrs, doc(cfg(feature = "templating")))]
pub fn compile_template<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
let owned = rule.logic_source()?;
Logic::compile_in_mode(&owned, self, true)
}
pub fn compile_strict<R: crate::IntoLogic>(&self, rule: R) -> Result<Logic> {
let owned = rule.logic_source()?;
Logic::compile_in_mode(&owned, self, false)
}
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 compiled = compiled.for_engine(self);
let _depth_guard = self.enter_dispatch_boundary()?;
let data_ref = data.into_arena_for(compiled, self, arena)?;
let mut ctx = self.new_context(compiled, 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)),
}
}
#[cfg(feature = "trace")]
#[inline(always)]
pub(crate) fn run<'a, R>(
&self,
compiled: &'a Logic,
arena: &'a bumpalo::Bump,
input: impl FnOnce(&'a Logic) -> Result<&'a crate::arena::DataValue<'a>>,
prepare: impl FnOnce(&mut crate::arena::ContextStack<'a>),
finish: impl FnOnce(
Result<&'a crate::arena::DataValue<'a>>,
&mut crate::arena::ContextStack<'a>,
) -> Result<R>,
) -> Result<R> {
let compiled = compiled.for_engine(self);
let _depth_guard = self.enter_dispatch_boundary()?;
let data_ref = input(compiled)?;
let mut ctx = self.new_context(compiled, data_ref);
prepare(&mut ctx);
let result = match self.dispatch_node(&compiled.root, &mut ctx, arena) {
Ok(av) => Ok(av),
Err(e) => Err(e.decorated(ctx.take_error_path(), compiled)),
};
finish(result, &mut ctx)
}
#[inline(always)]
pub(crate) fn new_context<'a>(
&self,
compiled: &'a Logic,
data_ref: &'a crate::arena::DataValue<'a>,
) -> crate::arena::ContextStack<'a> {
#[cfg_attr(not(feature = "budget"), allow(unused_mut))]
let mut ctx = crate::arena::ContextStack::new(data_ref, compiled.needs_ancestor_frames);
#[cfg(feature = "budget")]
if let Some(budget) = self.settings.config.ops_budget {
ctx.set_budget(budget);
}
ctx
}
#[cfg(feature = "budget")]
#[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
#[inline]
pub fn resolve_ops_budget(&self, explicit: Option<u64>) -> u64 {
explicit
.or(self.settings.config.ops_budget)
.unwrap_or(u64::MAX)
}
#[cfg(feature = "budget")]
#[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
pub fn evaluate_metered<'a, D: crate::EvalInput<'a>>(
&self,
compiled: &'a Logic,
data: D,
arena: &'a bumpalo::Bump,
budget: u64,
) -> Result<Metered<&'a crate::arena::DataValue<'a>>> {
let compiled = compiled.for_engine(self);
let _depth_guard = self.enter_dispatch_boundary()?;
let data_ref = data.into_arena_for(compiled, self, arena)?;
let mut ctx = self.new_context(compiled, data_ref);
ctx.set_budget(budget);
match self.dispatch_node(&compiled.root, &mut ctx, arena) {
Ok(value) => Ok(Metered {
value,
ops: ctx.ops_spent(),
}),
Err(e) => Err(e.decorated(ctx.take_error_path(), compiled)),
}
}
#[inline]
pub fn truthy(&self, value: &crate::arena::DataValue<'_>) -> bool {
crate::arena::truthy_arena(value, self)
}
#[inline]
pub fn truthy_of<V: crate::TruthyInput>(&self, value: V) -> bool {
value.truthy_with(self)
}
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)
}
pub fn eval_as<O, R, D>(&self, rule: R, data: D) -> Result<O>
where
O: crate::FromDataValue,
R: crate::IntoLogic,
D: crate::OwnedInput,
{
self.eval_with(rule, data)
}
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)?;
data.lend_arena_for(&compiled, self, |data, arena| {
let result = self.evaluate(&compiled, 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.settings.config.max_recursion_depth {
return Err(crate::Error::configuration_error(format!(
"max recursion depth exceeded ({})",
self.settings.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.as_ref());
}
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 mut result = match ctx.charge(1) {
Ok(()) => dispatch::dispatch_node_inner(self, node, ctx, arena),
Err(e) => Err(e),
};
if let Err(e) = &mut result {
record_failure(e, node, ctx);
}
#[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
}
}