use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use datavalue::{NumberValue, OwnedDataValue};
use crate::OpCode;
use crate::node::{
CompiledMissingArg, CompiledMissingMin, CompiledMissingPaths, CompiledNode, CseData,
SYNTHETIC_ID,
};
const MIN_NODE_COUNT: usize = 8;
pub(crate) fn apply(root: &mut CompiledNode) -> u16 {
let mut walk = Walk::default();
let mut choices = Vec::new();
walk.visit(root, true, &mut choices);
walk.candidates.sort_unstable_by_key(|c| c.preorder);
let mut table = ClassTable::default();
for candidate in walk.candidates {
table.record(candidate.node, candidate.hash, candidate.choices);
}
table.assign_slots();
if table.slot_count == 0 {
return 0;
}
wrap(root, &table);
table.slot_count
}
type ChoicePath = Box<[(usize, u32)]>;
struct Class {
exemplar: CompiledNode,
occurrences: Vec<ChoicePath>,
slot: Option<u16>,
}
#[derive(Default)]
struct ClassTable {
buckets: HashMap<u64, Vec<Class>>,
order: Vec<(u64, usize)>,
by_node: HashMap<usize, (u64, usize)>,
slot_count: u16,
}
impl ClassTable {
fn record(&mut self, node: &CompiledNode, hash: u64, choices: ChoicePath) {
let address = node as *const CompiledNode as usize;
let bucket = self.buckets.entry(hash).or_default();
let index = match bucket
.iter()
.position(|class| structural_eq(&class.exemplar, node))
{
Some(index) => {
bucket[index].occurrences.push(choices);
index
}
None => {
bucket.push(Class {
exemplar: node.clone(),
occurrences: vec![choices],
slot: None,
});
self.order.push((hash, bucket.len() - 1));
bucket.len() - 1
}
};
self.by_node.insert(address, (hash, index));
}
fn assign_slots(&mut self) {
for (hash, index) in &self.order {
if self.slot_count == u16::MAX {
break;
}
let class = &mut self.buckets.get_mut(hash).expect("recorded bucket")[*index];
if class.occurrences.len() >= 2 && has_co_occurring_pair(&class.occurrences) {
class.slot = Some(self.slot_count);
self.slot_count += 1;
}
}
}
fn match_slot(&self, node: &CompiledNode) -> Option<u16> {
let (hash, index) = self.by_node.get(&(node as *const CompiledNode as usize))?;
self.buckets.get(hash)?.get(*index)?.slot
}
}
#[derive(Clone, Copy)]
struct Meta {
hash: u64,
size: usize,
pure: bool,
has_iter: bool,
}
struct Candidate<'t> {
node: &'t CompiledNode,
hash: u64,
choices: ChoicePath,
preorder: usize,
}
#[derive(Default)]
struct Walk<'t> {
candidates: Vec<Candidate<'t>>,
preorder: usize,
hashes: Vec<u64>,
}
impl<'t> Walk<'t> {
fn visit(
&mut self,
node: &'t CompiledNode,
record: bool,
choices: &mut Vec<(usize, u32)>,
) -> Meta {
if let CompiledNode::Cse(data) = node {
return self.visit(&data.inner, record, choices);
}
let preorder = self.preorder;
self.preorder += 1;
let base = self.hashes.len();
let mut meta = Meta {
hash: 0,
size: 1,
pure: is_cse_pure_local(node),
has_iter: matches!(
node,
CompiledNode::BuiltinOperator { opcode, .. } if opcode.meta().is_iterator()
),
};
match node {
CompiledNode::BuiltinOperator { opcode, args, .. } => {
let identity = node as *const CompiledNode as usize;
for (i, child) in args.iter().enumerate() {
let child_record = record && !child_never_cacheable(*opcode, i, args.len());
let is_choice_arm = if_value_arm(*opcode, i, args.len());
if is_choice_arm {
choices.push((identity, i as u32));
}
let child_meta = self.visit(child, child_record, choices);
if is_choice_arm {
choices.pop();
}
self.take(&mut meta, child_meta);
}
}
_ => node.visit_indexed_children(&mut |_, child| {
let child_meta = self.visit(child, record, choices);
self.take(&mut meta, child_meta);
}),
}
let mut hasher = NodeHasher::default();
hash_local(node, &mut self.hashes[base..].iter(), &mut hasher);
self.hashes.truncate(base);
meta.hash = hasher.finish();
if record && is_candidate(node, meta) {
self.candidates.push(Candidate {
node,
hash: meta.hash,
choices: choices.as_slice().into(),
preorder,
});
}
meta
}
#[inline]
fn take(&mut self, parent: &mut Meta, child: Meta) {
self.hashes.push(child.hash);
parent.size += child.size;
parent.pure &= child.pure;
parent.has_iter |= child.has_iter;
}
}
fn co_occurring(a: &[(usize, u32)], b: &[(usize, u32)]) -> bool {
for (node_a, arm_a) in a {
for (node_b, arm_b) in b {
if node_a == node_b && arm_a != arm_b {
return false;
}
}
}
true
}
fn has_co_occurring_pair(occurrences: &[ChoicePath]) -> bool {
for (i, a) in occurrences.iter().enumerate() {
for b in occurrences.iter().skip(i + 1) {
if co_occurring(a, b) {
return true;
}
}
}
false
}
fn if_value_arm(opcode: OpCode, index: usize, len: usize) -> bool {
if !matches!(opcode, OpCode::If) {
return false;
}
len == 1 || index % 2 == 1 || (len % 2 == 1 && index == len - 1)
}
fn is_candidate(node: &CompiledNode, meta: Meta) -> bool {
matches!(node, CompiledNode::BuiltinOperator { .. })
&& meta.pure
&& (meta.has_iter || meta.size >= MIN_NODE_COUNT)
}
fn wrap(node: &mut CompiledNode, table: &ClassTable) {
if let Some(slot) = table.match_slot(node) {
let placeholder = CompiledNode::Value {
id: SYNTHETIC_ID,
value: OwnedDataValue::Null,
lit: None,
};
let inner = std::mem::replace(node, placeholder);
*node = CompiledNode::Cse(Box::new(CseData { slot, inner }));
if let CompiledNode::Cse(data) = node {
wrap_children(&mut data.inner, table);
}
return;
}
wrap_children(node, table);
}
fn wrap_children(node: &mut CompiledNode, table: &ClassTable) {
match node {
CompiledNode::BuiltinOperator { opcode, args, .. } => {
let len = args.len();
for (i, child) in args.iter_mut().enumerate() {
if !child_never_cacheable(*opcode, i, len) {
wrap(child, table);
}
}
}
_ => node.visit_children_mut(&mut |child| wrap(child, table)),
}
}
fn child_never_cacheable(opcode: OpCode, index: usize, len: usize) -> bool {
crate::compile::scope::frames_pushed_for_child(opcode, index, len) > 0
}
fn is_cse_pure_local(node: &CompiledNode) -> bool {
match node {
CompiledNode::BuiltinOperator { opcode, .. } => opcode.meta().cse_pure(),
CompiledNode::CustomOperator(_) => false,
#[cfg(feature = "templating")]
CompiledNode::StructuredObject(_) => false,
#[cfg(feature = "error-handling")]
CompiledNode::Throw(_) => false,
CompiledNode::InvalidArgs { .. } => false,
_ => true,
}
}
#[derive(Default)]
struct NodeHasher(u64);
impl NodeHasher {
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
#[inline]
fn mix(&mut self, word: u64) {
self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(Self::SEED);
}
}
impl Hasher for NodeHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let (chunks, rest) = bytes.as_chunks::<8>();
for chunk in chunks {
self.mix(u64::from_le_bytes(*chunk));
}
let mut tail = [0u8; 8];
tail[..rest.len()].copy_from_slice(rest);
self.mix(u64::from_le_bytes(tail));
}
#[inline]
fn write_u8(&mut self, n: u8) {
self.mix(u64::from(n));
}
#[inline]
fn write_u32(&mut self, n: u32) {
self.mix(u64::from(n));
}
#[inline]
fn write_u64(&mut self, n: u64) {
self.mix(n);
}
#[inline]
fn write_usize(&mut self, n: usize) {
self.mix(n as u64);
}
#[inline]
fn finish(&self) -> u64 {
self.0
}
}
fn hash_local<H: Hasher>(node: &CompiledNode, kids: &mut std::slice::Iter<'_, u64>, h: &mut H) {
let mut child = |h: &mut H| h.write_u64(*kids.next().expect("one hash per child"));
match node {
CompiledNode::Value { value, .. } => {
h.write_u8(0);
hash_owned(value, h);
}
CompiledNode::Array { nodes, .. } => {
h.write_u8(1);
h.write_usize(nodes.len());
for _ in nodes.iter() {
child(h);
}
}
CompiledNode::BuiltinOperator { opcode, args, .. } => {
h.write_u8(2);
h.write_u8(*opcode as u8);
h.write_usize(args.len());
for _ in args.iter() {
child(h);
}
}
CompiledNode::CustomOperator(data) => {
h.write_u8(3);
data.name.hash(h);
h.write_usize(data.args.len());
for _ in data.args.iter() {
child(h);
}
}
CompiledNode::Cse(_) => child(h),
#[cfg(feature = "templating")]
CompiledNode::StructuredObject(data) => {
h.write_u8(4);
h.write_usize(data.fields.len());
data.escape.hash(h);
for (key, _) in data.fields.iter() {
key.hash(h);
child(h);
}
}
CompiledNode::Var {
scope_level,
segments,
reduce_hint,
metadata_hint,
default_value,
..
} => {
h.write_u8(5);
h.write_u32(*scope_level);
segments.hash(h);
reduce_hint.hash(h);
metadata_hint.hash(h);
match default_value {
Option::Some(_) => {
h.write_u8(1);
child(h);
}
Option::None => h.write_u8(0),
}
}
#[cfg(feature = "ext-control")]
CompiledNode::Exists(data) => {
h.write_u8(6);
h.write_u32(data.scope_level);
data.segments.hash(h);
}
#[cfg(feature = "error-handling")]
CompiledNode::Throw(data) => {
h.write_u8(7);
hash_owned(&data.error, h);
}
CompiledNode::Missing(data) => {
h.write_u8(8);
h.write_usize(data.args.len());
for arg in data.args.iter() {
match arg {
CompiledMissingArg::Now((path, _)) => {
h.write_u8(0);
path.hash(h);
}
CompiledMissingArg::Later(_) => {
h.write_u8(1);
child(h);
}
}
}
}
CompiledNode::MissingSome(data) => {
h.write_u8(9);
match &data.min_present {
CompiledMissingMin::Now(n) => {
h.write_u8(0);
h.write_usize(*n);
}
CompiledMissingMin::Later(_) => {
h.write_u8(1);
child(h);
}
}
match &data.paths {
CompiledMissingPaths::Now(paths) => {
h.write_u8(0);
h.write_usize(paths.len());
for (path, _) in paths.iter() {
path.hash(h);
}
}
CompiledMissingPaths::Later(_) => {
h.write_u8(1);
child(h);
}
}
}
CompiledNode::InvalidArgs { op_name, args, .. } => {
h.write_u8(10);
op_name.hash(h);
hash_owned(args, h);
}
}
}
fn hash_owned<H: Hasher>(value: &OwnedDataValue, h: &mut H) {
match value {
OwnedDataValue::Null => h.write_u8(0),
OwnedDataValue::Bool(b) => {
h.write_u8(1);
h.write_u8(u8::from(*b));
}
OwnedDataValue::Number(NumberValue::Integer(i)) => {
h.write_u8(2);
h.write_i64(*i);
}
OwnedDataValue::Number(NumberValue::Float(f)) => {
h.write_u8(3);
h.write_u64(f.to_bits());
}
OwnedDataValue::String(s) => {
h.write_u8(4);
s.hash(h);
}
OwnedDataValue::Array(items) => {
h.write_u8(5);
h.write_usize(items.len());
for item in items {
hash_owned(item, h);
}
}
OwnedDataValue::Object(fields) => {
h.write_u8(6);
h.write_usize(fields.len());
for (key, item) in fields {
key.hash(h);
hash_owned(item, h);
}
}
#[cfg(feature = "datetime")]
OwnedDataValue::DateTime(d) => {
h.write_u8(7);
format!("{d:?}").hash(h);
}
#[cfg(feature = "datetime")]
OwnedDataValue::Duration(d) => {
h.write_u8(8);
format!("{d:?}").hash(h);
}
#[cfg(feature = "tensor")]
OwnedDataValue::Tensor(t) => {
h.write_u8(9);
t.dtype().name().hash(h);
t.shape().hash(h);
t.data().hash(h);
}
}
}
fn structural_eq(a: &CompiledNode, b: &CompiledNode) -> bool {
match (a, b) {
(CompiledNode::Value { value: va, .. }, CompiledNode::Value { value: vb, .. }) => {
owned_eq(va, vb)
}
(CompiledNode::Array { nodes: na, .. }, CompiledNode::Array { nodes: nb, .. }) => {
na.len() == nb.len() && na.iter().zip(nb.iter()).all(|(x, y)| structural_eq(x, y))
}
(
CompiledNode::BuiltinOperator {
opcode: oa,
args: aa,
..
},
CompiledNode::BuiltinOperator {
opcode: ob,
args: ab,
..
},
) => {
oa == ob
&& aa.len() == ab.len()
&& aa.iter().zip(ab.iter()).all(|(x, y)| structural_eq(x, y))
}
(CompiledNode::CustomOperator(da), CompiledNode::CustomOperator(db)) => {
da.name == db.name
&& da.args.len() == db.args.len()
&& da
.args
.iter()
.zip(db.args.iter())
.all(|(x, y)| structural_eq(x, y))
}
(CompiledNode::Cse(da), CompiledNode::Cse(db)) => {
da.slot == db.slot && structural_eq(&da.inner, &db.inner)
}
#[cfg(feature = "templating")]
(CompiledNode::StructuredObject(da), CompiledNode::StructuredObject(db)) => {
da.escape == db.escape
&& da.fields.len() == db.fields.len()
&& da
.fields
.iter()
.zip(db.fields.iter())
.all(|((ka, na), (kb, nb))| ka == kb && structural_eq(na, nb))
}
(
CompiledNode::Var {
scope_level: sa,
segments: ga,
reduce_hint: ra,
metadata_hint: ma,
default_value: da,
..
},
CompiledNode::Var {
scope_level: sb,
segments: gb,
reduce_hint: rb,
metadata_hint: mb,
default_value: db,
..
},
) => {
sa == sb
&& ga == gb
&& ra == rb
&& ma == mb
&& match (da, db) {
(Option::None, Option::None) => true,
(Option::Some(x), Option::Some(y)) => structural_eq(x, y),
_ => false,
}
}
#[cfg(feature = "ext-control")]
(CompiledNode::Exists(da), CompiledNode::Exists(db)) => {
da.scope_level == db.scope_level && da.segments == db.segments
}
#[cfg(feature = "error-handling")]
(CompiledNode::Throw(da), CompiledNode::Throw(db)) => owned_eq(&da.error, &db.error),
(CompiledNode::Missing(da), CompiledNode::Missing(db)) => {
da.args.len() == db.args.len()
&& da
.args
.iter()
.zip(db.args.iter())
.all(|(x, y)| missing_arg_eq(x, y))
}
(CompiledNode::MissingSome(da), CompiledNode::MissingSome(db)) => {
let min_eq = match (&da.min_present, &db.min_present) {
(CompiledMissingMin::Now(x), CompiledMissingMin::Now(y)) => x == y,
(CompiledMissingMin::Later(x), CompiledMissingMin::Later(y)) => structural_eq(x, y),
_ => false,
};
let paths_eq = match (&da.paths, &db.paths) {
(CompiledMissingPaths::Now(x), CompiledMissingPaths::Now(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|((pa, _), (pb, _))| pa == pb)
}
(CompiledMissingPaths::Later(x), CompiledMissingPaths::Later(y)) => {
structural_eq(x, y)
}
_ => false,
};
min_eq && paths_eq
}
(
CompiledNode::InvalidArgs {
op_name: na,
args: aa,
..
},
CompiledNode::InvalidArgs {
op_name: nb,
args: ab,
..
},
) => na == nb && aa == ab,
_ => false,
}
}
fn missing_arg_eq(a: &CompiledMissingArg, b: &CompiledMissingArg) -> bool {
match (a, b) {
(CompiledMissingArg::Now((pa, _)), CompiledMissingArg::Now((pb, _))) => pa == pb,
(CompiledMissingArg::Later(x), CompiledMissingArg::Later(y)) => structural_eq(x, y),
_ => false,
}
}
fn owned_eq(a: &OwnedDataValue, b: &OwnedDataValue) -> bool {
match (a, b) {
(OwnedDataValue::Null, OwnedDataValue::Null) => true,
(OwnedDataValue::Bool(x), OwnedDataValue::Bool(y)) => x == y,
(
OwnedDataValue::Number(NumberValue::Integer(x)),
OwnedDataValue::Number(NumberValue::Integer(y)),
) => x == y,
(
OwnedDataValue::Number(NumberValue::Float(x)),
OwnedDataValue::Number(NumberValue::Float(y)),
) => x.to_bits() == y.to_bits(),
(OwnedDataValue::String(x), OwnedDataValue::String(y)) => x == y,
(OwnedDataValue::Array(x), OwnedDataValue::Array(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| owned_eq(a, b))
}
(OwnedDataValue::Object(x), OwnedDataValue::Object(y)) => {
x.len() == y.len()
&& x.iter()
.zip(y.iter())
.all(|((ka, va), (kb, vb))| ka == kb && owned_eq(va, vb))
}
#[cfg(feature = "datetime")]
(OwnedDataValue::DateTime(x), OwnedDataValue::DateTime(y)) => {
format!("{x:?}") == format!("{y:?}")
}
#[cfg(feature = "datetime")]
(OwnedDataValue::Duration(x), OwnedDataValue::Duration(y)) => {
format!("{x:?}") == format!("{y:?}")
}
#[cfg(feature = "tensor")]
(OwnedDataValue::Tensor(x), OwnedDataValue::Tensor(y)) => x == y,
_ => false,
}
}
#[cfg(test)]
mod tests {
use crate::Engine;
const AGG: &str =
r#"{"reduce": [{"var": "items"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}"#;
fn slot_count(rule: &str) -> u16 {
Engine::new().compile(rule).unwrap().cse_slot_count
}
#[test]
fn repeated_aggregate_shares_one_slot() {
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
assert_eq!(slot_count(&rule), 1);
}
#[test]
fn single_occurrence_gets_no_slot() {
assert_eq!(slot_count(AGG), 0);
}
#[test]
fn small_scalar_repeats_get_no_slot() {
let rule = r#"{"+": [{"*": [{"var": "x"}, 2]}, {"*": [{"var": "x"}, 2]}]}"#;
assert_eq!(slot_count(rule), 0);
}
#[test]
fn integer_and_float_near_twins_do_not_share() {
let agg_float = AGG.replace(", 0]", ", 0.0]");
let rule = format!(r#"{{"+": [{AGG}, {AGG}, {agg_float}, {agg_float}]}}"#);
assert_eq!(slot_count(&rule), 2);
}
#[test]
fn iterator_body_occurrences_neither_count_nor_wrap() {
let rule = format!(
r#"{{"+": [{AGG}, {{"reduce": [{{"map": [{{"var": "xs"}}, {AGG}]}}, {{"+": [{{"var": "accumulator"}}, {{"var": "current"}}]}}, 0]}}]}}"#
);
assert_eq!(slot_count(&rule), 0);
}
#[cfg(feature = "ext-array")]
#[test]
fn key_expr_occurrences_neither_count_nor_wrap() {
for op in ["group_by", "distinct"] {
let rule = format!(
r#"{{"+": [{AGG}, {{"reduce": [{{"{op}": [{{"var": "xs"}}, {AGG}]}}, {{"+": [{{"var": "accumulator"}}, 1]}}, 0]}}]}}"#
);
assert_eq!(slot_count(&rule), 0, "key expr of {op} must be ineligible");
}
}
#[cfg(feature = "error-handling")]
#[test]
fn try_catch_arm_occurrences_neither_count_nor_wrap() {
let rule = format!(r#"{{"try": [{AGG}, {{"+": [{AGG}, 1]}}]}}"#);
assert_eq!(slot_count(&rule), 0);
}
#[cfg(feature = "error-handling")]
#[test]
fn try_protected_arms_share() {
let rule = format!(r#"{{"try": [{{"+": [{AGG}, {AGG}]}}, 0]}}"#);
assert_eq!(slot_count(&rule), 1);
}
#[test]
fn mutually_exclusive_if_arms_get_no_slot() {
let rule = format!(r#"{{"if": [{{"var": "c"}}, {AGG}, {AGG}]}}"#);
assert_eq!(slot_count(&rule), 0);
}
#[test]
fn if_condition_co_occurs_with_taken_arm() {
let rule = format!(r#"{{"if": [{{">": [{AGG}, 10]}}, {AGG}, 0]}}"#);
assert_eq!(slot_count(&rule), 1);
}
#[test]
fn exclusive_arms_still_share_with_a_root_occurrence() {
let rule = format!(r#"{{"+": [{AGG}, {{"if": [{{"var": "c"}}, {AGG}, {AGG}]}}]}}"#);
assert_eq!(slot_count(&rule), 1);
}
#[test]
fn no_fold_compile_produces_no_slots() {
let engine = Engine::builder().with_constant_folding(false).build();
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
assert_eq!(engine.compile(rule.as_str()).unwrap().cse_slot_count, 0);
}
#[test]
fn custom_truthy_evaluator_disables_the_pass() {
let config = crate::EvaluationConfig::default().with_truthy_evaluator(
crate::TruthyEvaluator::Custom(std::sync::Arc::new(|_| true)),
);
let engine = Engine::builder().with_config(config).build();
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
assert_eq!(engine.compile(rule.as_str()).unwrap().cse_slot_count, 0);
}
#[test]
fn impure_subtrees_are_not_candidates() {
let agg = r#"{"reduce": [{"var": "items"}, {"my_op": [{"var": "current"}]}, 0]}"#;
let rule = format!(r#"{{"+": [{agg}, {agg}]}}"#);
assert_eq!(slot_count(&rule), 0);
}
#[test]
fn nested_classes_get_their_own_slots() {
let rule = format!(r#"{{"+": [{AGG}, {{"*": [{AGG}, 2]}}, {{"*": [{AGG}, 2]}}]}}"#);
assert_eq!(slot_count(&rule), 2);
}
#[test]
fn serialization_is_wrapper_transparent() {
let engine = Engine::new();
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
let with_cse = engine.compile(rule.as_str()).unwrap();
assert_eq!(with_cse.cse_slot_count, 1);
let no_cse_engine = Engine::builder().with_constant_folding(false).build();
let without_cse = no_cse_engine.compile(rule.as_str()).unwrap();
assert_eq!(with_cse.to_json(), without_cse.to_json());
}
#[test]
fn memo_does_not_leak_across_evaluations() {
let engine = Engine::new();
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
let compiled = engine.compile(rule.as_str()).unwrap();
assert_eq!(compiled.cse_slot_count, 1);
let mut session = engine.session();
assert_eq!(
session
.eval_str(&compiled, r#"{"items": [1, 2, 3]}"#)
.unwrap(),
"12"
);
session.reset();
assert_eq!(
session.eval_str(&compiled, r#"{"items": [10]}"#).unwrap(),
"20"
);
}
#[cfg(feature = "trace")]
#[test]
fn traced_eval_of_cse_compiled_logic_matches_plain_eval() {
let engine = Engine::new();
let rule = format!(r#"{{"+": [{AGG}, {AGG}]}}"#);
let compiled = engine.compile(rule.as_str()).unwrap();
assert_eq!(compiled.cse_slot_count, 1);
let plain = engine.eval_str(rule.as_str(), r#"{"items": [1, 2, 3]}"#);
let traced = engine.trace().eval(&compiled, r#"{"items": [1, 2, 3]}"#);
assert_eq!(
plain.unwrap(),
traced.result.unwrap().to_json_string(),
"traced and plain evaluation of a CSE'd tree must agree"
);
}
#[test]
fn object_literals_compare_order_sensitively() {
use datavalue::OwnedDataValue as V;
let ab = V::Object(vec![
("a".into(), V::Bool(true)),
("b".into(), V::Bool(false)),
]);
let ba = V::Object(vec![
("b".into(), V::Bool(false)),
("a".into(), V::Bool(true)),
]);
assert!(!super::owned_eq(&ab, &ba));
assert!(super::owned_eq(&ab, &ab.clone()));
}
#[test]
fn integer_and_float_values_are_distinct() {
use datavalue::{NumberValue, OwnedDataValue as V};
let int_one = V::Number(NumberValue::Integer(1));
let float_one = V::Number(NumberValue::Float(1.0));
assert!(!super::owned_eq(&int_one, &float_one));
}
#[test]
fn no_cse_node_sits_under_a_pushed_frame() {
use crate::compile::scope::frames_pushed_for_child;
use crate::node::CompiledNode;
fn walk(node: &CompiledNode, depth: u32) {
if let CompiledNode::Cse(data) = node {
assert_eq!(depth, 0, "Cse wrapper found at static frame depth {depth}");
walk(&data.inner, depth);
return;
}
if let CompiledNode::BuiltinOperator { opcode, args, .. } = node {
let len = args.len();
for (i, child) in args.iter().enumerate() {
walk(child, depth + frames_pushed_for_child(*opcode, i, len));
}
return;
}
node.visit_indexed_children(&mut |_, child| walk(child, depth));
}
let engine = Engine::new();
for rule in [
AGG,
r#"{"map": [{"val": "xs"}, {"reduce": [{"val": "ys"}, {"+": [{"val": "current"}, {"val": "accumulator"}]}, 0]}]}"#,
r#"{"if": [{"val": "c"}, {"reduce": [{"val": "xs"}, {"+": [{"val": "current"}, {"val": "accumulator"}]}, 0]}, {"reduce": [{"val": "xs"}, {"+": [{"val": "current"}, {"val": "accumulator"}]}, 0]}]}"#,
] {
let logic = engine.compile(rule).unwrap();
walk(&logic.root, 0);
}
}
}