use crate::Engine;
use crate::arena::ContextStack;
#[inline(always)]
pub(super) fn allowed(ctx: &ContextStack<'_>, engine: &Engine) -> bool {
!ctx.is_tracing() && engine.reads_fields_inline()
}
#[cfg(all(test, feature = "serde_json"))]
use super::fused::FusedMapBody;
#[cfg(all(test, feature = "serde_json"))]
mod tests {
use super::super::filter::strict_eq_field_shape;
use super::super::reduce::detect_fold_shape;
use super::FusedMapBody;
use crate::{CompiledNode, Engine, Logic};
fn compile(rule: &str) -> Logic {
Engine::new().compile(rule).unwrap()
}
fn arg(logic: &Logic, i: usize) -> &CompiledNode {
let CompiledNode::BuiltinOperator { args, .. } = &logic.root else {
panic!("root is not a call: {:?}", logic.root)
};
&args[i]
}
fn predicate_hinted(logic: &Logic) -> bool {
matches!(
arg(logic, 1),
CompiledNode::BuiltinOperator {
predicate_hint: Some(_),
..
}
)
}
#[test]
fn fast_predicate_fires() {
for rule in [
r#"{"filter": [{"var": "rows"}, {">": [{"var": "k"}, 1]}]}"#,
r#"{"filter": [{"var": "rows"}, {"<=": [2, {"var": "k"}]}]}"#,
r#"{"some": [{"var": "rows"}, {"==": [{"var": "k"}, "a"]}]}"#,
r#"{"all": [{"var": "xs"}, {">=": [{"var": ""}, 0]}]}"#,
r#"{"none": [{"var": "rows"}, {"and": [{"var": "k"}, {"!": {"var": "n"}}]}]}"#,
r#"{"filter": [{"var": "rows"}, {"in": [{"var": "k"}, ["a", "b"]]}]}"#,
] {
assert!(predicate_hinted(&compile(rule)), "{rule}");
}
}
#[test]
fn strict_eq_field_fires() {
for rule in [
r#"{"filter": [{"var": "rows"}, {"===": [{"var": "k"}, 1]}]}"#,
r#"{"filter": [{"var": "rows"}, {"!==": [null, {"var": "k"}]}]}"#,
r#"{"filter": [{"var": "rows"}, {"===": [{"var": "k"}, {"val": [[1], "target"]}]}]}"#,
] {
assert!(
strict_eq_field_shape(arg(&compile(rule), 1)).is_some(),
"{rule}"
);
}
let whole = compile(r#"{"filter": [{"var": "xs"}, {"===": [{"var": ""}, 1]}]}"#);
assert!(strict_eq_field_shape(arg(&whole, 1)).is_none());
}
#[test]
fn map_fusions_fire() {
for rule in [
r#"{"map": [{"var": "rows"}, {"var": "k"}]}"#,
r#"{"map": [{"var": "xs"}, {"var": ""}]}"#,
r#"{"map": [{"var": "rows"}, {"+": [{"var": "k"}, 1]}]}"#,
r#"{"map": [{"var": "rows"}, {"-": [10, {"var": "k"}]}]}"#,
r#"{"map": [{"var": "rows"}, {"*": [{"var": "k"}, {"var": "n"}]}]}"#,
] {
assert!(
FusedMapBody::detect(arg(&compile(rule), 1)).is_some(),
"{rule}"
);
}
let indexed = compile(r#"{"map": [{"var": "xs"}, {"val": [[1], "index"]}]}"#);
assert!(FusedMapBody::detect(arg(&indexed, 1)).is_none());
}
#[test]
fn reduce_folds_fire() {
for rule in [
r#"{"reduce": [{"var": "xs"}, {"+": [{"var": "current"}, {"var": "accumulator"}]}, 0]}"#,
r#"{"reduce": [{"var": "xs"}, {"-": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#,
r#"{"reduce": [{"var": "rows"}, {"*": [{"var": "current.k"}, {"var": "accumulator"}]}, 1]}"#,
] {
assert!(
detect_fold_shape(arg(&compile(rule), 1)).is_some(),
"{rule}"
);
}
let fused = compile(
r#"{"reduce": [{"map": [{"var": "rows"}, {"var": "k"}]}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#,
);
let CompiledNode::BuiltinOperator { args: map_args, .. } = arg(&fused, 0) else {
panic!("source is not a map")
};
assert!(FusedMapBody::detect(&map_args[1]).is_some());
assert!(detect_fold_shape(arg(&fused, 1)).is_some());
}
#[cfg(feature = "ext-array")]
#[test]
fn sort_by_field_fires() {
use super::super::sort::sort_key_field;
let rule = compile(r#"{"sort": [{"var": "rows"}, true, {"var": "k"}]}"#);
assert!(sort_key_field(arg(&rule, 2)).is_some());
let computed = compile(r#"{"sort": [{"var": "rows"}, true, {"length": {"var": "k"}}]}"#);
assert!(sort_key_field(arg(&computed, 2)).is_none());
}
#[cfg(feature = "budget")]
#[test]
fn fast_and_general_paths_charge_the_same() {
use crate::{EvaluationConfig, MissingVar};
let fast = Engine::new();
let general = Engine::builder()
.with_config(EvaluationConfig::default().with_missing_var(MissingVar::Error))
.build();
let ops = |engine: &Engine, rule: &str, data: &str| -> u64 {
let compiled = engine.compile(rule).unwrap();
let arena = bumpalo::Bump::new();
engine
.evaluate_metered(&compiled, data, &arena, u64::MAX)
.unwrap_or_else(|e| panic!("{rule} over {data}: {e}"))
.ops
};
let nums = r#"{"xs": [1, 5, 2, 8, 3], "t": 2}"#;
let mixed = r#"{"xs": [1, 5, "2", 8, 3], "t": 2}"#;
let rows = r#"{"rows": [{"k": 1, "n": 2, "s": "a"}, {"k": 4, "n": 3, "s": "b"}, {"k": 2, "n": 5, "s": "c"}], "t": 4}"#;
let mixed_rows = r#"{"rows": [{"k": 1, "n": 2, "s": "a"}, {"k": "4", "n": 3, "s": 7}, {"k": 2, "n": 5, "s": "c"}], "t": 4}"#;
let obj = r#"{"rows": {"a": {"k": 1}, "b": {"k": 2}}}"#;
let empty = r#"{"rows": [], "xs": []}"#;
let cases: &[&str] = &[
r#"{"filter": [{"var": "xs"}, {">": [{"var": ""}, 2]}]}"#,
r#"{"filter": [{"var": "xs"}, {"<=": [3, {"var": ""}]}]}"#,
r#"{"filter": [{"var": "xs"}, {"==": [{"var": ""}, 5]}]}"#,
r#"{"filter": [{"var": "xs"}, {"===": [{"var": ""}, 5]}]}"#,
r#"{"filter": [{"var": "xs"}, {"!==": [{"var": ""}, [1]]}]}"#,
r#"{"filter": [{"var": "rows"}, {"==": [{"var": "s"}, "b"]}]}"#,
r#"{"filter": [{"var": "rows"}, {"in": [{"var": "s"}, ["a", "c", "z"]]}]}"#,
r#"{"some": [{"var": "xs"}, {">": [{"var": ""}, 4]}]}"#,
r#"{"all": [{"var": "xs"}, {"<": [{"var": ""}, 4]}]}"#,
r#"{"none": [{"var": "rows"}, {"===": [{"var": "k"}, 2]}]}"#,
r#"{"filter": [{"var": "rows"}, {"and": [{">": [{"var": "k"}, 1]}, {"var": "n"}, {"!": {"var": "s"}}]}]}"#,
r#"{"filter": [{"var": "rows"}, {"or": [{"==": [{"var": "k"}, 1]}, {"!!": {"var": "n"}}]}]}"#,
r#"{"some": [{"var": "rows"}, {"and": [{"in": [{"var": "s"}, ["b"]]}, {">=": [{"var": "n"}, 3]}]}]}"#,
r#"{"filter": [{"var": "rows"}, {"!!": [{"==": [{"var": "s"}, "c"]}]}]}"#,
r#"{"filter": [{"var": "rows"}, {"===": [{"var": "k"}, 4]}]}"#,
r#"{"filter": [{"var": "rows"}, {"!==": [{"val": [[1], "t"]}, {"var": "k"}]}]}"#,
r#"{"map": [{"var": "rows"}, {"var": "k"}]}"#,
r#"{"map": [{"var": "xs"}, {"var": ""}]}"#,
r#"{"map": [{"var": "rows"}, {"*": [{"var": "k"}, 2]}]}"#,
r#"{"map": [{"var": "rows"}, {"-": [10, {"var": "k"}]}]}"#,
r#"{"map": [{"var": "rows"}, {"+": [{"var": "k"}, {"var": "n"}]}]}"#,
r#"{"reduce": [{"var": "xs"}, {"+": [{"var": "current"}, {"var": "accumulator"}]}, 0]}"#,
r#"{"reduce": [{"var": "rows"}, {"*": [{"var": "accumulator"}, {"var": "current.n"}]}, 1]}"#,
r#"{"reduce": [{"map": [{"var": "rows"}, {"var": "k"}]}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#,
r#"{"reduce": [{"map": [{"var": "rows"}, {"*": [{"var": "k"}, {"var": "n"}]}]}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#,
r#"{"reduce": [{"map": [{"var": "rows"}, {"+": [{"var": "k"}, 1]}]}, {"-": [{"var": "current"}, {"var": "accumulator"}]}, 0]}"#,
r#"{"reduce": [{"map": [{"var": "rows"}, {"var": "k"}]}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, "0"]}"#,
r#"{"reduce": [{"map": [{"filter": [{"var": "rows"}, {">": [{"var": "k"}, 1]}]}, {"var": "k"}]}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#,
];
let mut wrong = Vec::new();
let mut compared = 0;
for &rule in cases {
for data in [nums, mixed, rows, mixed_rows, obj, empty] {
let Ok(expected) = general.compile(rule).map_err(|_| ()).and_then(|c| {
let arena = bumpalo::Bump::new();
general
.evaluate_metered(&c, data, &arena, u64::MAX)
.map(|m| m.ops)
.map_err(|_| ())
}) else {
continue;
};
compared += 1;
let got = ops(&fast, rule, data);
if got != expected {
wrong.push(format!(
"{rule} over {data}: fast {got}, general {expected}"
));
}
}
}
#[cfg(feature = "ext-array")]
for (rule, data) in [
(r#"{"sort": [{"var": "rows"}, true, {"var": "k"}]}"#, rows),
(
r#"{"sort": [{"var": "rows"}, false, {"var": "s"}]}"#,
mixed_rows,
),
] {
let (got, expected) = (ops(&fast, rule, data), ops(&general, rule, data));
if got != expected {
wrong.push(format!(
"{rule} over {data}: fast {got}, general {expected}"
));
}
}
assert!(compared >= 50, "only {compared} pairings ran");
assert!(wrong.is_empty(), "\n{}", wrong.join("\n"));
}
}