use crate::OpCode;
use crate::node::{CompiledNode, MetadataHint, ScopeBinding, metadata_reads_ancestor};
pub(crate) fn frames_pushed_for_child(opcode: OpCode, index: usize, len: usize) -> u32 {
opcode.meta().frames_for(index, len)
}
pub(crate) fn resolve(root: &mut CompiledNode) -> bool {
let mut needs_ancestors = false;
resolve_at(root, 0, &mut needs_ancestors);
needs_ancestors
}
fn resolve_at(node: &mut CompiledNode, depth: u32, needs_ancestors: &mut bool) {
match node {
CompiledNode::Var {
scope_level,
binding,
metadata_hint,
..
} => {
*binding = ScopeBinding::resolve(depth, *scope_level);
*needs_ancestors |= if *metadata_hint != MetadataHint::None {
metadata_reads_ancestor(depth, *scope_level)
} else {
*binding == ScopeBinding::Ancestor
};
}
#[cfg(feature = "ext-control")]
CompiledNode::Exists(data) => {
data.binding = ScopeBinding::resolve(depth, data.scope_level);
*needs_ancestors |= data.binding == ScopeBinding::Ancestor;
}
CompiledNode::BuiltinOperator {
opcode: OpCode::Val | OpCode::VarDefault,
..
} => *needs_ancestors = true,
_ => {}
}
if let CompiledNode::BuiltinOperator { opcode, args, .. } = node {
let opcode = *opcode;
let len = args.len();
for (index, child) in args.iter_mut().enumerate() {
resolve_at(
child,
depth + frames_pushed_for_child(opcode, index, len),
needs_ancestors,
);
}
return;
}
node.visit_children_mut(&mut |child| resolve_at(child, depth, needs_ancestors));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::{ScopeBinding, metadata_reads_ancestor};
#[test]
fn iterator_bodies_push_one_frame() {
for op in [
OpCode::Filter,
OpCode::Map,
OpCode::All,
OpCode::Some,
OpCode::None,
OpCode::Reduce,
] {
assert_eq!(frames_pushed_for_child(op, 0, 3), 0, "{op:?} source");
assert_eq!(frames_pushed_for_child(op, 1, 3), 1, "{op:?} body");
}
}
#[test]
fn reduce_initial_accumulator_is_frameless() {
assert_eq!(frames_pushed_for_child(OpCode::Reduce, 2, 3), 0);
}
#[test]
fn bodyless_iterators_push_nothing() {
for op in [OpCode::Min, OpCode::Max, OpCode::Merge] {
for index in 0..3 {
assert_eq!(frames_pushed_for_child(op, index, 3), 0, "{op:?}[{index}]");
}
}
}
#[cfg(feature = "ext-array")]
#[test]
fn sort_and_key_extractors() {
assert_eq!(frames_pushed_for_child(OpCode::Sort, 1, 3), 0);
assert_eq!(frames_pushed_for_child(OpCode::Sort, 2, 3), 1);
for op in [OpCode::GroupBy, OpCode::Distinct] {
assert_eq!(frames_pushed_for_child(op, 0, 2), 0, "{op:?} source");
assert_eq!(frames_pushed_for_child(op, 1, 2), 1, "{op:?} key");
}
}
#[cfg(feature = "error-handling")]
#[test]
fn try_catch_arm_only_for_multi_arg() {
assert_eq!(frames_pushed_for_child(OpCode::Try, 0, 1), 0);
assert_eq!(frames_pushed_for_child(OpCode::Try, 0, 2), 0);
assert_eq!(frames_pushed_for_child(OpCode::Try, 1, 2), 1);
assert_eq!(frames_pushed_for_child(OpCode::Try, 1, 3), 0);
assert_eq!(frames_pushed_for_child(OpCode::Try, 2, 3), 1);
}
#[test]
fn resolution_table() {
use ScopeBinding::{Ancestor, Current, Root};
let expected = [
(0, 0, Root), (0, 1, Root), (0, 5, Root), (1, 0, Current), (1, 1, Root), (1, 2, Root),
(2, 0, Current),
(2, 1, Ancestor), (2, 2, Ancestor), (2, 3, Root), (2, 4, Root),
(3, 0, Current),
(3, 1, Ancestor), (3, 2, Ancestor),
(3, 3, Ancestor), (3, 4, Ancestor),
(3, 5, Root), (4, 4, Ancestor),
(4, 6, Ancestor),
(4, 7, Root),
];
for (depth, level, want) in expected {
assert_eq!(
ScopeBinding::resolve(depth, level),
want,
"resolve(depth={depth}, level={level})"
);
}
}
#[test]
fn every_frame_is_addressable() {
use crate::arena::{FrameTarget, frame_at_climb};
for depth in 2..8u32 {
assert_eq!(
frame_at_climb(depth as usize, depth as usize - 1),
FrameTarget::Ancestor(0),
"the outermost frame sits at parents[0] at depth={depth}"
);
for level in [2 * depth - 3, 2 * depth - 2] {
assert_eq!(
ScopeBinding::resolve(depth, level),
ScopeBinding::Ancestor,
"depth={depth} level={level} names the outermost frame"
);
}
assert_eq!(
ScopeBinding::resolve(depth, 2 * depth - 1),
ScopeBinding::Root,
"one level further clamps to the root"
);
}
}
#[cfg(feature = "serde_json")]
#[test]
fn ancestor_tracking_turns_on_only_where_a_level_can_reach_one() {
let engine = crate::Engine::new();
for (rule, want, why) in [
(
r#"{"map": [{"val": "a"}, {"val": [[2], "x"]}]}"#,
false,
"one frame: clamps to root",
),
(
r#"{"map": [{"val": "a"}, {"val": [[1], "index"]}]}"#,
false,
"metadata of the current frame",
),
(
r#"{"map": [{"val": "a"}, {"map": [{"val": "b"}, {"val": "x"}]}]}"#,
false,
"nested, but no level",
),
(
r#"{"map": [{"val": "a"}, {"map": [{"val": "b"}, {"val": [[1], "x"]}]}]}"#,
true,
"nested data level",
),
(
r#"{"map": [{"val": "a"}, {"map": [{"val": "b"}, {"val": [[3], "index"]}]}]}"#,
true,
"nested metadata level: the data climb clamps to root, the metadata climb does not",
),
(
r#"{"map": [{"val": "a"}, {"map": [{"val": "b"}, {"val": [[1], "index"]}]}]}"#,
false,
"nested metadata level naming the innermost frame: the data binding is Ancestor, but the node never walks it",
),
(
r#"{"map": [{"val": "a"}, {"map": [{"val": "b"}, {"val": [[4], "x"]}]}]}"#,
false,
"nested, but the level clamps past both frames",
),
] {
let logic = engine.compile(rule).expect("compiles");
assert_eq!(logic.needs_ancestor_frames, want, "{why}: {rule}");
}
}
#[test]
fn metadata_climb_can_need_ancestors_when_data_does_not() {
assert_eq!(ScopeBinding::resolve(2, 3), ScopeBinding::Root);
assert!(metadata_reads_ancestor(2, 3));
assert!(!metadata_reads_ancestor(4, 1));
assert!(!metadata_reads_ancestor(4, 2));
}
#[test]
fn plain_operators_push_nothing() {
for index in 0..4 {
assert_eq!(frames_pushed_for_child(OpCode::Add, index, 4), 0);
assert_eq!(frames_pushed_for_child(OpCode::If, index, 4), 0);
}
}
}