use super::{CompiledNode, populate_lits};
#[derive(Clone)]
pub struct Logic {
pub(crate) root: CompiledNode,
pub(crate) root_op_name: Option<std::borrow::Cow<'static, str>>,
pub(crate) cse_slot_count: u16,
pub(crate) needs_ancestor_frames: bool,
pub(crate) engine_id: u64,
pub(crate) pointers: Option<super::compile_ctx::NodePointers>,
pub(crate) projection: std::sync::OnceLock<Option<Box<crate::projection::Projection>>>,
pub(crate) refold: Option<Box<Refold>>,
}
const REFOLD_SLOTS: usize = 4;
#[derive(Clone)]
pub(crate) struct Refold {
source: datavalue::OwnedDataValue,
templating: bool,
fingerprint: u64,
others: [std::sync::OnceLock<(u64, Box<Logic>)>; REFOLD_SLOTS],
}
impl Refold {
pub(crate) fn new(
source: datavalue::OwnedDataValue,
templating: bool,
fingerprint: u64,
) -> Self {
Self {
source,
templating,
fingerprint,
others: Default::default(),
}
}
#[cold]
fn on(&self, engine: &crate::Engine) -> Option<&Logic> {
let fingerprint = engine.fold_fingerprint();
for slot in &self.others {
if slot.get().is_none() {
let mut logic =
Logic::compile_in_mode(&self.source, engine, self.templating).ok()?;
logic.refold = None;
let _ = slot.set((fingerprint, Box::new(logic)));
}
if let Some((held, logic)) = slot.get()
&& *held == fingerprint
{
return Some(logic);
}
}
None
}
}
impl std::fmt::Debug for Logic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Logic")
.field("root", &self.root)
.field("root_op_name", &self.root_op_name)
.finish_non_exhaustive()
}
}
impl Logic {
pub(crate) fn new(
mut root: CompiledNode,
cse_slot_count: u16,
needs_ancestor_frames: bool,
) -> Self {
populate_lits(&mut root);
let root_op_name = root.operator_name();
Self {
root,
root_op_name,
cse_slot_count,
needs_ancestor_frames,
engine_id: 0,
pointers: None,
projection: std::sync::OnceLock::new(),
refold: None,
}
}
#[inline]
pub(crate) fn for_engine(&self, engine: &crate::Engine) -> &Logic {
match &self.refold {
Some(refold)
if self.engine_id != engine.id()
&& refold.fingerprint != engine.fold_fingerprint() =>
{
refold.on(engine).unwrap_or(self)
}
_ => self,
}
}
pub fn compiled_on(&self, engine: &crate::Engine) -> bool {
self.engine_id == engine.id()
}
pub fn pointer(&self, id: u32) -> Option<&str> {
let pointers = self.pointers.as_deref()?;
pointers
.binary_search_by_key(&id, |(node, _)| *node)
.ok()
.map(|at| &*pointers[at].1)
}
#[inline]
pub(crate) fn projection_for(
&self,
engine: &crate::Engine,
) -> Option<&crate::projection::Projection> {
if !self.compiled_on(engine) {
return None;
}
self.projection
.get_or_init(|| crate::projection::Projection::of(&self.facts()).map(Box::new))
.as_deref()
}
pub fn pointers(&self) -> impl Iterator<Item = (u32, &str)> {
self.pointers
.as_deref()
.unwrap_or_default()
.iter()
.map(|(id, pointer)| (*id, &**pointer))
}
pub fn is_static(&self) -> bool {
node_is_static(&self.root)
}
pub fn is_constant(&self) -> bool {
matches!(self.root, CompiledNode::Value { .. })
}
pub fn facts(&self) -> crate::Facts {
crate::facts::collect(&self.root)
}
pub fn cse_slot_count(&self) -> u16 {
self.cse_slot_count
}
pub fn to_json(&self) -> String {
crate::node_serialize::node_to_json_string(&self.root)
}
}
impl std::fmt::Display for Logic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_json())
}
}
pub(crate) fn node_is_static(node: &CompiledNode) -> bool {
match node {
CompiledNode::Value { .. } => true,
CompiledNode::Array { nodes, .. } => nodes.iter().all(node_is_static),
CompiledNode::BuiltinOperator { opcode, args, .. } => opcode.meta().can_fold(args),
CompiledNode::CustomOperator(data) => {
data.info.deterministic
&& !data.info.reads_context
&& data.args.iter().all(node_is_static)
}
CompiledNode::Cse(data) => node_is_static(&data.inner),
CompiledNode::Var { .. } => false,
#[cfg(feature = "ext-control")]
CompiledNode::Exists(_) => false,
#[cfg(feature = "error-handling")]
CompiledNode::Throw(_) => false,
#[cfg(feature = "templating")]
CompiledNode::StructuredObject(data) => {
data.escape.is_none() && data.fields.iter().all(|(_, node)| node_is_static(node))
}
CompiledNode::Missing(_) | CompiledNode::MissingSome(_) => false,
CompiledNode::InvalidArgs { .. } => false,
}
}
#[cfg(test)]
mod tests {
use crate::Engine;
#[test]
fn is_constant_tracks_folding() {
let engine = Engine::new();
let folded = engine.compile(r#"{"+": [1, {"*": [2, 3]}]}"#).unwrap();
assert!(folded.is_constant());
assert!(folded.is_static());
assert!(engine.compile("42").unwrap().is_constant());
assert!(engine.compile("[1, 2, 3]").unwrap().is_constant());
let dynamic = engine.compile(r#"{"var": "x"}"#).unwrap();
assert!(!dynamic.is_constant());
assert!(!dynamic.is_static());
assert!(
!engine
.compile(r#"{"merge": [[1], [2]]}"#)
.unwrap()
.is_constant()
);
let div = engine.compile(r#"{"/": [1, 0]}"#).unwrap();
assert!(div.is_static());
assert!(!div.is_constant());
}
#[cfg(feature = "ext-array")]
#[test]
fn group_by_distinct_folding_classification() {
let engine = Engine::new();
let grouped = engine.compile(r#"{"group_by": [[1, 2, 1], 7]}"#).unwrap();
assert!(!grouped.is_constant());
assert_eq!(
engine
.eval_str(r#"{"group_by": [[1, 2, 1], 7]}"#, "null")
.unwrap(),
r#"[{"key":7,"items":[1,2,1]}]"#
);
let unkeyed = engine.compile(r#"{"distinct": [[1, 1, 2]]}"#).unwrap();
assert!(unkeyed.is_constant());
assert_eq!(
engine
.eval_str(r#"{"distinct": [[1, 1, 2]]}"#, "null")
.unwrap(),
"[1,2]"
);
assert!(
!engine
.compile(r#"{"distinct": [[1, 1, 2], {"var": ""}]}"#)
.unwrap()
.is_constant()
);
assert!(
!engine
.compile(r#"{"distinct": [{"var": "xs"}]}"#)
.unwrap()
.is_constant()
);
}
#[cfg(feature = "ext-object")]
#[test]
fn object_ops_fold_when_static() {
let engine = Engine::new();
let keys = engine.compile(r#"{"keys": [null]}"#).unwrap();
assert!(keys.is_constant());
assert_eq!(
engine.eval_str(r#"{"keys": [null]}"#, "null").unwrap(),
"[]"
);
assert!(
!engine
.compile(r#"{"keys": [{"var": "o"}]}"#)
.unwrap()
.is_constant()
);
}
#[test]
fn cloned_logic_keeps_prebuilt_composite_literals() {
let engine = Engine::new();
let rule = r#"{"in": [{"var": "x"}, ["a", "b", "c"]]}"#;
let original = engine.compile(rule).unwrap();
let cloned = original.clone();
drop(original);
assert_eq!(
engine.eval_str(rule, r#"{"x": "b"}"#).unwrap(),
"true",
"sanity: rule matches via one-shot path"
);
let mut session = engine.session();
assert_eq!(session.eval_str(&cloned, r#"{"x": "b"}"#).unwrap(), "true");
session.reset();
assert_eq!(session.eval_str(&cloned, r#"{"x": "z"}"#).unwrap(), "false");
}
#[cfg(feature = "ext-control")]
#[test]
fn folded_switch_case_tables_match() {
let engine = Engine::new();
let rule = r#"{"switch": [{"var": "x"}, [[1, "one"], [2, "two"]], "dflt"]}"#;
assert_eq!(engine.eval_str(rule, r#"{"x": 1}"#).unwrap(), "\"one\"");
assert_eq!(engine.eval_str(rule, r#"{"x": 2}"#).unwrap(), "\"two\"");
assert_eq!(engine.eval_str(rule, r#"{"x": 3}"#).unwrap(), "\"dflt\"");
let folded = engine
.compile(r#"{"switch": ["b", [["a", 1], ["b", 2]], 0]}"#)
.unwrap();
assert!(folded.is_constant());
assert_eq!(
engine.eval_str(folded.to_json().as_str(), "null").unwrap(),
"2"
);
let mixed = r#"{"switch": [{"var": "x"}, [["s", "static-hit"], [{"var": "k"}, "dyn-hit"]], "none"]}"#;
assert_eq!(
engine.eval_str(mixed, r#"{"x": "s", "k": "?"}"#).unwrap(),
"\"static-hit\""
);
assert_eq!(
engine.eval_str(mixed, r#"{"x": "d", "k": "d"}"#).unwrap(),
"\"dyn-hit\""
);
}
}