use crate::OpCode;
use crate::arena::DataValue;
use crate::node::{MetadataHint, ReduceHint};
use crate::operators::meta::{Algebra, EqOp, Logic, OrdOp, Truth};
use crate::{CompiledNode, Engine};
use std::ops::ControlFlow;
use super::input::{IterSrc, plain_var_segments};
#[inline]
pub(super) fn is_filter_invariant(node: &CompiledNode) -> bool {
match node {
CompiledNode::Value { .. } => true,
CompiledNode::Var {
binding,
reduce_hint,
metadata_hint,
default_value,
..
} => {
*binding == crate::node::ScopeBinding::Root
&& *reduce_hint == ReduceHint::None
&& *metadata_hint == MetadataHint::None
&& default_value.is_none()
}
_ => false,
}
}
#[inline]
pub(super) fn try_extract_filter_field_cmp<'a>(
a: &'a CompiledNode,
b: &'a CompiledNode,
) -> Option<(&'a [crate::node::PathSegment], &'a CompiledNode)> {
let segments = plain_var_segments(a)?;
(!segments.is_empty() && is_filter_invariant(b)).then_some((segments, b))
}
#[derive(Debug, Clone)]
pub(crate) enum FastPredicate {
StrictEq {
var_path: Box<[crate::node::PathSegment]>,
literal: datavalue::OwnedDataValue,
negate: bool,
},
NumericCmp {
var_path: Box<[crate::node::PathSegment]>,
literal_f: f64,
op: OrdOp,
var_is_lhs: bool,
},
LooseNumericEq {
var_path: Box<[crate::node::PathSegment]>,
literal_f: f64,
negate: bool,
},
Truthy {
var_path: Box<[crate::node::PathSegment]>,
},
LooseStrEq {
var_path: Box<[crate::node::PathSegment]>,
literal: Box<str>,
negate: bool,
},
InStrLits {
var_path: Box<[crate::node::PathSegment]>,
items: Box<[Box<str>]>,
},
AllOf(Box<[FastPredicate]>),
AnyOf(Box<[FastPredicate]>),
Not {
inner: Box<FastPredicate>,
negate: bool,
},
}
fn f64_exact_literal(literal: &datavalue::OwnedDataValue) -> Option<f64> {
const EXACT: i64 = 1 << 53;
match literal {
datavalue::OwnedDataValue::Number(datavalue::NumberValue::Integer(i))
if !(-EXACT < *i && *i < EXACT) =>
{
None
}
_ => literal.as_f64(),
}
}
const MAX_PREDICATE_DEPTH: u32 = 4;
impl FastPredicate {
pub(crate) fn try_detect_owned(opcode: OpCode, pred_args: &[CompiledNode]) -> Option<Self> {
Self::detect_op(opcode, pred_args, 0)
}
fn detect_op(opcode: OpCode, args: &[CompiledNode], depth: u32) -> Option<Self> {
if depth >= MAX_PREDICATE_DEPTH {
return None;
}
match (opcode, opcode.algebra()) {
(_, Some(Algebra::Logic(logic))) if !args.is_empty() => {
let preds: Option<Box<[FastPredicate]>> = args
.iter()
.map(|a| Self::detect_operand(a, depth + 1))
.collect();
let preds = preds?;
Some(match logic {
Logic::And => FastPredicate::AllOf(preds),
Logic::Or => FastPredicate::AnyOf(preds),
})
}
(_, Some(Algebra::Truth(truth))) if args.len() == 1 => Some(FastPredicate::Not {
inner: Box::new(Self::detect_operand(&args[0], depth + 1)?),
negate: truth == Truth::Not,
}),
(OpCode::In, _) if args.len() == 2 => {
let segments = plain_var_segments(&args[0])?;
let CompiledNode::Value {
value: datavalue::OwnedDataValue::Array(items),
..
} = &args[1]
else {
return None;
};
let strs: Option<Box<[Box<str>]>> = items
.iter()
.map(|it| match it {
datavalue::OwnedDataValue::String(s) => Some(s.as_str().into()),
_ => None,
})
.collect();
Some(FastPredicate::InStrLits {
var_path: segments.into(),
items: strs?,
})
}
_ => Self::detect_cmp_leaf(opcode, args),
}
}
fn detect_operand(node: &CompiledNode, depth: u32) -> Option<Self> {
if let Some(segments) = plain_var_segments(node) {
return Some(FastPredicate::Truthy {
var_path: segments.into(),
});
}
match node {
CompiledNode::BuiltinOperator { opcode, args, .. } => {
Self::detect_op(*opcode, args, depth)
}
_ => None,
}
}
fn detect_cmp_leaf(opcode: OpCode, pred_args: &[CompiledNode]) -> Option<Self> {
if pred_args.len() != 2 {
return None;
}
for (var_idx, lit_idx, var_is_lhs) in [(0, 1, true), (1, 0, false)] {
if let Some(segments) = plain_var_segments(&pred_args[var_idx])
&& let CompiledNode::Value { value: literal, .. } = &pred_args[lit_idx]
{
let var_path: Box<[crate::node::PathSegment]> = segments.into();
match opcode.algebra() {
Some(Algebra::Eq(EqOp {
strict: true,
negate,
})) if !matches!(
literal,
datavalue::OwnedDataValue::Array(_) | datavalue::OwnedDataValue::Object(_)
) =>
{
return Some(FastPredicate::StrictEq {
var_path,
literal: literal.clone(),
negate,
});
}
Some(Algebra::Eq(EqOp {
strict: false,
negate,
})) => {
if let Some(lit_f) = f64_exact_literal(literal) {
return Some(FastPredicate::LooseNumericEq {
var_path,
literal_f: lit_f,
negate,
});
}
if let datavalue::OwnedDataValue::String(s) = literal {
return Some(FastPredicate::LooseStrEq {
var_path,
literal: s.as_str().into(),
negate,
});
}
}
Some(Algebra::Ord(op)) => {
if let Some(lit_f) = f64_exact_literal(literal) {
return Some(FastPredicate::NumericCmp {
var_path,
literal_f: lit_f,
op,
var_is_lhs,
});
}
}
_ => {}
}
}
}
None
}
#[inline]
pub(super) fn from_node(predicate: &CompiledNode) -> Option<&FastPredicate> {
if let CompiledNode::BuiltinOperator { predicate_hint, .. } = predicate {
return predicate_hint.as_deref();
}
None
}
#[inline(always)]
fn resolve_value<'b>(
segments: &[crate::node::PathSegment],
item: &'b DataValue<'b>,
) -> Option<&'b DataValue<'b>> {
if segments.is_empty() {
Some(item)
} else {
crate::arena::value::traverse_segments(item, segments)
}
}
#[inline(always)]
fn leaf_cost(&self) -> u64 {
match self {
FastPredicate::Truthy { .. } => 1,
FastPredicate::InStrLits { items, .. } => 2 + items.len() as u64,
_ => 2,
}
}
#[inline]
pub(super) fn evaluate_opt<'b>(
&self,
item: &'b DataValue<'b>,
engine: &crate::Engine,
) -> Option<(bool, u32)> {
match self {
FastPredicate::StrictEq {
var_path,
literal,
negate,
} => {
let av = Self::resolve_value(var_path, item).unwrap_or(&DataValue::Null);
strict_eq_literal(av, literal).map(|eq| (eq != *negate, 2))
}
FastPredicate::NumericCmp {
var_path,
literal_f,
op,
var_is_lhs,
} => {
let verdict = match Self::resolve_value(var_path, item) {
Some(DataValue::Number(n)) => {
let val_f = n.as_f64();
let (lhs, rhs) = if *var_is_lhs {
(val_f, *literal_f)
} else {
(*literal_f, val_f)
};
Some(op.cmp_f64(lhs, rhs))
}
_ => None,
};
verdict.map(|v| (v, 2))
}
FastPredicate::LooseNumericEq {
var_path,
literal_f,
negate,
} => match Self::resolve_value(var_path, item) {
Some(DataValue::Number(n)) => Some(((n.as_f64() == *literal_f) != *negate, 2)),
_ => None,
},
FastPredicate::Truthy { var_path } => Some((
match Self::resolve_value(var_path, item) {
Some(av) => crate::arena::truthy_arena(av, engine),
None => false,
},
1,
)),
FastPredicate::LooseStrEq {
var_path,
literal,
negate,
} => match Self::resolve_value(var_path, item) {
Some(DataValue::String(s)) => str_eq(s, literal).map(|eq| (eq != *negate, 2)),
_ => None,
},
FastPredicate::InStrLits { var_path, items } => {
let verdict = match Self::resolve_value(var_path, item) {
Some(DataValue::String(s)) => str_in(s, items),
_ => Some(false),
};
verdict.map(|v| {
(
v,
2u32.saturating_add(items.len().try_into().unwrap_or(u32::MAX)),
)
})
}
FastPredicate::AllOf(preds) => {
let mut cost: u32 = 1;
for p in preds.iter() {
let (v, c) = p.evaluate_opt(item, engine)?;
cost = cost.saturating_add(c);
if !v {
return Some((false, cost));
}
}
Some((true, cost))
}
FastPredicate::AnyOf(preds) => {
let mut cost: u32 = 1;
for p in preds.iter() {
let (v, c) = p.evaluate_opt(item, engine)?;
cost = cost.saturating_add(c);
if v {
return Some((true, cost));
}
}
Some((false, cost))
}
FastPredicate::Not { inner, negate } => inner
.evaluate_opt(item, engine)
.map(|(v, c)| (v != *negate, c.saturating_add(1))),
}
}
}
impl FastPredicate {
#[inline]
pub(super) fn scan<'b, F>(&self, src: &IterSrc<'b>, engine: &Engine, on_item: F) -> Option<u64>
where
F: FnMut(&'b DataValue<'b>, bool) -> ControlFlow<()>,
{
let examined = match self {
FastPredicate::NumericCmp {
var_path,
literal_f,
op,
var_is_lhs,
} => {
let lit = *literal_f;
let op = if *var_is_lhs { *op } else { op.flip() };
match op {
OrdOp::Gt => scan_leaf(src, var_path, on_item, |v| number(v).map(|x| x > lit)),
OrdOp::Ge => scan_leaf(src, var_path, on_item, |v| number(v).map(|x| x >= lit)),
OrdOp::Lt => scan_leaf(src, var_path, on_item, |v| number(v).map(|x| x < lit)),
OrdOp::Le => scan_leaf(src, var_path, on_item, |v| number(v).map(|x| x <= lit)),
}
}
FastPredicate::LooseNumericEq {
var_path,
literal_f,
negate,
} => {
let (lit, negate) = (*literal_f, *negate);
scan_leaf(src, var_path, on_item, |v| {
number(v).map(|x| (x == lit) != negate)
})
}
FastPredicate::StrictEq {
var_path,
literal,
negate,
} => scan_leaf(src, var_path, on_item, |v| {
let av = v.unwrap_or(&DataValue::Null);
strict_eq_literal(av, literal).map(|eq| eq != *negate)
}),
FastPredicate::LooseStrEq {
var_path,
literal,
negate,
} => scan_leaf(src, var_path, on_item, |v| match v {
Some(DataValue::String(s)) => str_eq(s, literal).map(|eq| eq != *negate),
_ => None,
}),
FastPredicate::InStrLits { var_path, items } => {
scan_leaf(src, var_path, on_item, |v| match v {
Some(DataValue::String(s)) => str_in(s, items),
_ => Some(false),
})
}
FastPredicate::Truthy { .. }
| FastPredicate::AllOf(_)
| FastPredicate::AnyOf(_)
| FastPredicate::Not { .. } => {
let mut on_item = on_item;
let mut cost = 0u64;
for item in src.0 {
let (verdict, c) = self.evaluate_opt(item, engine)?;
cost += u64::from(c);
if on_item(item, verdict).is_break() {
break;
}
}
return Some(cost);
}
}?;
Some(examined as u64 * self.leaf_cost())
}
}
#[inline(always)]
fn scan_leaf<'b>(
src: &IterSrc<'b>,
var_path: &[crate::node::PathSegment],
mut on_item: impl FnMut(&'b DataValue<'b>, bool) -> ControlFlow<()>,
test: impl Fn(Option<&'b DataValue<'b>>) -> Option<bool>,
) -> Option<usize> {
if var_path.is_empty() {
for (i, item) in src.0.iter().enumerate() {
if on_item(item, test(Some(item))?).is_break() {
return Some(i + 1);
}
}
} else {
for (i, item) in src.0.iter().enumerate() {
let value = crate::arena::value::traverse_segments(item, var_path);
if on_item(item, test(value)?).is_break() {
return Some(i + 1);
}
}
}
Some(src.len())
}
#[inline(always)]
fn number(value: Option<&DataValue<'_>>) -> Option<f64> {
match value {
Some(DataValue::Number(n)) => Some(n.as_f64()),
_ => None,
}
}
#[inline(always)]
fn strict_eq_literal(av: &DataValue<'_>, literal: &datavalue::OwnedDataValue) -> Option<bool> {
use crate::operators::comparison::{ProbeSide, datetime_probe};
if datetime_probe(ProbeSide::of(av), ProbeSide::of_owned(literal)) {
return None;
}
Some(value_equals_serde(av, literal))
}
#[inline(always)]
fn str_eq(s: &str, literal: &str) -> Option<bool> {
use crate::operators::comparison::{ProbeSide, datetime_probe};
if datetime_probe(ProbeSide::Str(s), ProbeSide::Str(literal)) {
return None;
}
Some(s == literal)
}
#[inline]
fn str_in(s: &str, items: &[Box<str>]) -> Option<bool> {
for lit in items {
if str_eq(s, lit)? {
return Some(true);
}
}
Some(false)
}
#[inline(always)]
fn value_equals_serde(av: &DataValue<'_>, v: &datavalue::OwnedDataValue) -> bool {
use datavalue::OwnedDataValue;
match (av, v) {
(DataValue::Null, OwnedDataValue::Null) => true,
(DataValue::Bool(a), OwnedDataValue::Bool(b)) => a == b,
(DataValue::Number(a), OwnedDataValue::Number(b)) => a == b,
(DataValue::String(s), OwnedDataValue::String(b)) => *s == b.as_str(),
(DataValue::Array(_), OwnedDataValue::Array(_))
| (DataValue::Object(_), OwnedDataValue::Object(_)) => value_equals_serde_compound(av, v),
_ => false,
}
}
#[inline(never)]
fn value_equals_serde_compound(av: &DataValue<'_>, v: &datavalue::OwnedDataValue) -> bool {
use datavalue::OwnedDataValue;
match (av, v) {
(DataValue::Array(items), OwnedDataValue::Array(b)) => {
items.len() == b.len()
&& items
.iter()
.zip(b.iter())
.all(|(x, y)| value_equals_serde(x, y))
}
(DataValue::Object(pairs), OwnedDataValue::Object(b)) => {
if pairs.len() != b.len() {
return false;
}
for (k, av) in *pairs {
match b.iter().find(|(bk, _)| bk == *k) {
Some((_, bv)) => {
if !value_equals_serde(av, bv) {
return false;
}
}
None => return false,
}
}
true
}
_ => false,
}
}
#[cfg(all(test, feature = "serde_json"))]
mod invariant_tests {
use super::is_filter_invariant;
use crate::node::CompiledNode;
use crate::{Engine, Logic};
fn filter_rhs(logic: &Logic) -> &CompiledNode {
let CompiledNode::BuiltinOperator { args, .. } = &logic.root else {
panic!("expected filter at the root");
};
let CompiledNode::BuiltinOperator { args: pred, .. } = &args[1] else {
panic!("expected a comparison predicate");
};
&pred[1]
}
fn nested_filter_rhs(logic: &Logic) -> &CompiledNode {
let CompiledNode::BuiltinOperator { args, .. } = &logic.root else {
panic!("expected map at the root");
};
let CompiledNode::BuiltinOperator { args: inner, .. } = &args[1] else {
panic!("expected a nested filter");
};
let CompiledNode::BuiltinOperator { args: pred, .. } = &inner[1] else {
panic!("expected a comparison predicate");
};
&pred[1]
}
#[test]
fn only_frame_independent_operands_are_hoistable() {
let engine = Engine::new();
for rule in [
r#"{"filter": [{"val": "xs"}, {"===": [{"var": "a"}, 1]}]}"#,
r#"{"filter": [{"val": "xs"}, {"===": [{"var": "a"}, {"val": [[2], "d"]}]}]}"#,
r#"{"filter": [{"val": "xs"}, {"===": [{"var": "a"}, {"val": [[1], "d"]}]}]}"#,
] {
let logic = engine.compile(rule).unwrap();
assert!(
is_filter_invariant(filter_rhs(&logic)),
"should be hoistable, losing this loses the fast path: {rule}"
);
}
for rule in [
r#"{"filter": [{"val": "xs"}, {"===": [{"var": "a"}, {"val": [[1], "index"]}]}]}"#,
r#"{"filter": [{"val": "xs"}, {"===": [{"var": "a"}, {"val": [[1], "key"]}]}]}"#,
] {
let logic = engine.compile(rule).unwrap();
assert!(
!is_filter_invariant(filter_rhs(&logic)),
"metadata hint must not be hoisted: {rule}"
);
}
for rule in [
r#"{"map": [{"val": "g"}, {"filter": [{"val": "i"}, {"===": [{"var": "a"}, {"val": [[1], "a"]}]}]}]}"#,
r#"{"map": [{"val": "g"}, {"filter": [{"val": "i"}, {"===": [{"var": "a"}, {"val": [[2], "a"]}]}]}]}"#,
] {
let logic = engine.compile(rule).unwrap();
assert!(
!is_filter_invariant(nested_filter_rhs(&logic)),
"an enclosing-frame reference takes the general path: {rule}"
);
}
let nested_root = r#"{"map": [{"val": "g"}, {"filter": [{"val": "i"}, {"===": [{"var": "a"}, {"val": [[4], "a"]}]}]}]}"#;
let logic = engine.compile(nested_root).unwrap();
assert!(
is_filter_invariant(nested_filter_rhs(&logic)),
"a level that clamps to root stays hoistable"
);
let ancestor = r#"{"map": [{"val": "g"}, {"map": [{"val": "h"}, {"filter": [{"val": "i"}, {"===": [{"var": "a"}, {"val": [[2], "a"]}]}]}]}]}"#;
let logic = engine.compile(ancestor).unwrap();
let CompiledNode::BuiltinOperator { args, .. } = &logic.root else {
panic!("expected map at the root");
};
let inner = nested_filter_rhs_of(&args[1]);
assert!(
!is_filter_invariant(inner),
"an ancestor reference takes the general path"
);
}
fn nested_filter_rhs_of(map: &CompiledNode) -> &CompiledNode {
let CompiledNode::BuiltinOperator { args, .. } = map else {
panic!("expected map");
};
let CompiledNode::BuiltinOperator { args: inner, .. } = &args[1] else {
panic!("expected a nested filter");
};
let CompiledNode::BuiltinOperator { args: pred, .. } = &inner[1] else {
panic!("expected a comparison predicate");
};
&pred[1]
}
}