use super::{CompiledNode, populate_lits};
use crate::opcode::OpCode;
#[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,
}
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) -> Self {
populate_lits(&mut root);
let root_op_name = root.operator_name();
Self {
root,
root_op_name,
cse_slot_count,
}
}
pub fn is_static(&self) -> bool {
node_is_static(&self.root)
}
pub fn is_constant(&self) -> bool {
matches!(self.root, CompiledNode::Value { .. })
}
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_is_static(opcode, args),
CompiledNode::CustomOperator(_) => false,
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.fields.iter().all(|(_, node)| node_is_static(node))
}
CompiledNode::Missing(_) | CompiledNode::MissingSome(_) => false,
CompiledNode::InvalidArgs { .. } => false,
}
}
fn opcode_is_static(opcode: &OpCode, args: &[CompiledNode]) -> bool {
use OpCode::*;
let args_static = || args.iter().all(node_is_static);
match opcode {
Val | Missing | MissingSome => false,
#[cfg(feature = "ext-control")]
Exists => false,
Map | Filter | Reduce | All | Some | None => false,
#[cfg(feature = "error-handling")]
Try | Throw => false,
#[cfg(feature = "datetime")]
Now => false,
#[cfg(feature = "flagd")]
Fractional => false,
#[cfg(feature = "flagd")]
SemVer => args_static(),
Merge | Min | Max => false,
_ => args_static(),
}
}
#[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());
}
#[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\""
);
}
}