use std::collections::{BTreeMap, BTreeSet};
use proptest::prelude::*;
use super::harness::{
Program, Transaction, ZSet, any_config, check, configs, fixpoint, map_steps, read_zset,
set_after, set_zset, workloads,
};
use crate::{
OutputHandle, RootCircuit, Stream, ZSetHandle, ZWeight,
typed_batch::{OrdZSet, SpineSnapshot},
utils::{Tup2, Tup3, Tup5, test::CIRCUIT_CASES},
};
type Leaf = Tup3<u64, u64, Option<i64>>;
type Inner = Tup5<u64, u64, bool, u64, u64>;
type Ref = Tup3<u64, u64, u64>;
type Root = Tup2<u64, u64>;
type Value = Tup3<u64, u64, Option<i64>>;
type Total = Tup2<u64, i64>;
fn apply(is_add: bool, left: Option<i64>, right: Option<i64>) -> Option<i64> {
let (left, right) = (left?, right?);
Some(if is_add {
left.wrapping_add(right)
} else {
left.wrapping_mul(right)
})
}
#[derive(Clone, Debug, Default)]
struct TreeInput {
leaves: Vec<(Leaf, ZWeight)>,
inner: Vec<(Inner, ZWeight)>,
refs: Vec<(Ref, ZWeight)>,
roots: Vec<(Root, ZWeight)>,
}
#[derive(Clone, Copy, Debug)]
enum Plan {
ByChild,
ByTree,
}
#[derive(Clone)]
struct TreeEvaluation(Plan);
impl Program for TreeEvaluation {
type Input = TreeInput;
type Handles = (
ZSetHandle<Leaf>,
ZSetHandle<Inner>,
ZSetHandle<Ref>,
ZSetHandle<Root>,
OutputHandle<SpineSnapshot<OrdZSet<Value>>>,
OutputHandle<SpineSnapshot<OrdZSet<Total>>>,
);
type Output = (ZSet<Value>, ZSet<Total>);
fn build(&self, circuit: &mut RootCircuit) -> Self::Handles {
let (leaves, leaves_handle) = circuit.add_input_zset::<Leaf>();
let (inner, inner_handle) = circuit.add_input_zset::<Inner>();
let (refs, refs_handle) = circuit.add_input_zset::<Ref>();
let (roots, roots_handle) = circuit.add_input_zset::<Root>();
let (values, totals) = circuit
.recursive(
|child, (values, totals): (Stream<_, OrdZSet<Value>>, Stream<_, OrdZSet<Total>>)| {
let values_by_node =
values.map_index(|Tup3(tree, node, value)| (Tup2(*tree, *node), *value));
let computed = match self.0 {
Plan::ByChild => {
let inner_by_left = inner.delta0(child).map_index(
|Tup5(tree, node, is_add, left, right)| {
(Tup2(*tree, *left), Tup3(*node, *is_add, *right))
},
);
let with_left = values_by_node.join_index(
&inner_by_left,
|Tup2(tree, _left), left, Tup3(node, is_add, right)| {
Some((Tup2(*tree, *right), Tup3(*node, *is_add, *left)))
},
);
with_left.join(
&values_by_node,
|Tup2(tree, _right), Tup3(node, is_add, left), right| {
Tup3(*tree, *node, apply(*is_add, *left, *right))
},
)
}
Plan::ByTree => {
let values_by_tree = values
.map_index(|Tup3(tree, node, value)| (*tree, Tup2(*node, *value)));
let pairs = values_by_tree.join_index(
&values_by_tree,
|tree, Tup2(left, left_value), Tup2(right, right_value)| {
Some((
Tup3(*tree, *left, *right),
Tup2(*left_value, *right_value),
))
},
);
pairs.join(
&inner.delta0(child).map_index(
|Tup5(tree, node, is_add, left, right)| {
(Tup3(*tree, *left, *right), Tup2(*node, *is_add))
},
),
|Tup3(tree, _, _), Tup2(left, right), Tup2(node, is_add)| {
Tup3(*tree, *node, apply(*is_add, *left, *right))
},
)
}
};
let referenced = refs
.delta0(child)
.map_index(|Tup3(tree, node, target)| (*target, Tup2(*tree, *node)))
.join(
&totals.map_index(|Tup2(tree, total)| (*tree, *total)),
|_target, Tup2(tree, node), total| Tup3(*tree, *node, Some(*total)),
);
let values_next = leaves.delta0(child).plus(&computed).plus(&referenced);
let totals_next = values_by_node.join(
&roots
.delta0(child)
.map_index(|Tup2(tree, root)| (Tup2(*tree, *root), ())),
|Tup2(tree, _root), value, _| Tup2(*tree, value.unwrap_or(0)),
);
Ok((values_next, totals_next))
},
)
.unwrap();
(
leaves_handle,
inner_handle,
refs_handle,
roots_handle,
values.accumulate_integrate().accumulate_output(),
totals.accumulate_integrate().accumulate_output(),
)
}
fn push(&self, (leaves, inner, refs, roots, _, _): &Self::Handles, input: &TreeInput) {
for (row, weight) in &input.leaves {
leaves.push(*row, *weight);
}
for (row, weight) in &input.inner {
inner.push(*row, *weight);
}
for (row, weight) in &input.refs {
refs.push(*row, *weight);
}
for (row, weight) in &input.roots {
roots.push(*row, *weight);
}
}
fn read(&self, (_, _, _, _, values, totals): &Self::Handles) -> Self::Output {
(read_zset(values), read_zset(totals))
}
fn model(&self, inputs: &[TreeInput]) -> Self::Output {
let leaves = set_after(inputs.iter().map(|input| input.leaves.as_slice()));
let inner = set_after(inputs.iter().map(|input| input.inner.as_slice()));
let refs = set_after(inputs.iter().map(|input| input.refs.as_slice()));
let roots = set_after(inputs.iter().map(|input| input.roots.as_slice()));
let value_of = |values: &BTreeSet<Value>, tree: u64, node: u64| {
values
.range(Tup3(tree, node, None)..=Tup3(tree, node, Some(i64::MAX)))
.map(|Tup3(_, _, value)| *value)
.collect::<Vec<_>>()
};
let (values, totals) = fixpoint(
(BTreeSet::new(), BTreeSet::new()),
|(values, totals): &(BTreeSet<Value>, BTreeSet<Total>)| {
let mut values_next = leaves.clone();
for Tup5(tree, node, is_add, left, right) in &inner {
for left in value_of(values, *tree, *left) {
for right in value_of(values, *tree, *right) {
values_next.insert(Tup3(*tree, *node, apply(*is_add, left, right)));
}
}
}
for Tup3(tree, node, target) in &refs {
for Tup2(_, total) in
totals.range(Tup2(*target, i64::MIN)..=Tup2(*target, i64::MAX))
{
values_next.insert(Tup3(*tree, *node, Some(*total)));
}
}
let mut totals_next = BTreeSet::new();
for Tup2(tree, root) in &roots {
for value in value_of(values, *tree, *root) {
totals_next.insert(Tup2(*tree, value.unwrap_or(0)));
}
}
(values_next, totals_next)
},
);
(set_zset(&values), set_zset(&totals))
}
}
fn changes<T: Clone>(rows: &[T], weight: ZWeight) -> Vec<(T, ZWeight)> {
rows.iter().map(|row| (row.clone(), weight)).collect()
}
fn uneven_tree(x: Option<i64>) -> TreeInput {
TreeInput {
leaves: changes(
&[
Tup3(3, 0, x),
Tup3(3, 1, Some(5)),
Tup3(3, 10, Some(0)),
Tup3(3, 11, Some(1)),
Tup3(3, 13, Some(2)),
],
1,
),
inner: changes(
&[
Tup5(3, 2, false, 0, 1),
Tup5(3, 12, false, 10, 11),
Tup5(3, 14, false, 12, 13),
Tup5(3, 99, true, 2, 14),
],
1,
),
refs: vec![],
roots: changes(&[Tup2(3, 99)], 1),
}
}
fn set_leaf(tree: u64, node: u64, old: Option<i64>, new: Option<i64>) -> TreeInput {
TreeInput {
leaves: vec![(Tup3(tree, node, old), -1), (Tup3(tree, node, new), 1)],
..TreeInput::default()
}
}
fn shared_deep_child() -> TreeInput {
let mut inner = vec![
Tup5(0, 4, false, 0, 3),
Tup5(0, 5, false, 1, 3),
Tup5(0, 6, false, 5, 3),
];
let mut below = 2;
for node in 7..=11 {
inner.push(Tup5(0, node, false, below, 3));
below = node;
}
inner.extend([
Tup5(0, 12, true, 4, 11),
Tup5(0, 13, true, 6, 11),
Tup5(0, 14, true, 12, 13),
]);
TreeInput {
leaves: changes(
&[
Tup3(0, 0, None),
Tup3(0, 1, None),
Tup3(0, 2, Some(7)),
Tup3(0, 3, Some(1)),
],
1,
),
inner: changes(&inner, 1),
refs: vec![],
roots: changes(&[Tup2(0, 14)], 1),
}
}
fn three_trees() -> Vec<TreeInput> {
let tree_1 = TreeInput {
leaves: changes(&[Tup3(1, 0, None), Tup3(1, 1, Some(1))], 1),
inner: changes(&[Tup5(1, 2, false, 0, 1)], 1),
refs: vec![],
roots: changes(&[Tup2(1, 2)], 1),
};
let tree_2 = TreeInput {
leaves: changes(&[Tup3(2, 1, Some(0))], 1),
inner: changes(&[Tup5(2, 2, true, 0, 1)], 1),
refs: changes(&[Tup3(2, 0, 1)], 1),
roots: changes(&[Tup2(2, 2)], 1),
};
vec![tree_1, tree_2, uneven_tree(None)]
}
fn tree_triggers() -> Vec<Vec<Transaction<TreeInput>>> {
let set_x = |tree| set_leaf(tree, 0, None, Some(10));
vec![
vec![vec![uneven_tree(None)], vec![set_x(3)]],
vec![vec![uneven_tree(Some(10))]],
vec![three_trees(), vec![set_x(1), set_x(3)]],
vec![
vec![shared_deep_child()],
vec![TreeInput {
leaves: [
set_leaf(0, 0, None, Some(2)).leaves,
set_leaf(0, 1, None, Some(3)).leaves,
]
.concat(),
..TreeInput::default()
}],
vec![
set_leaf(0, 0, Some(2), Some(4)),
set_leaf(0, 1, Some(3), Some(5)),
],
vec![set_leaf(0, 0, Some(4), None), set_leaf(0, 1, Some(5), None)],
],
]
}
#[derive(Clone, Debug)]
enum Definition {
Leaf(Option<i64>),
Inner(bool, u64, u64),
Ref(u64),
}
impl Definition {
fn push(&self, input: &mut TreeInput, tree: u64, node: u64, weight: ZWeight) {
match *self {
Definition::Leaf(value) => input.leaves.push((Tup3(tree, node, value), weight)),
Definition::Inner(is_add, left, right) => input
.inner
.push((Tup5(tree, node, is_add, left, right), weight)),
Definition::Ref(target) => input.refs.push((Tup3(tree, node, target), weight)),
}
}
}
fn definition(tree: u64, node: u64) -> BoxedStrategy<Option<Definition>> {
let mut options = vec![
Just(None).boxed(),
prop::option::of(0..3i64)
.prop_map(|value| Some(Definition::Leaf(value)))
.boxed(),
];
if node > 0 {
options.push(
(any::<bool>(), 0..node, 0..node)
.prop_map(|(is_add, left, right)| Some(Definition::Inner(is_add, left, right)))
.boxed(),
);
}
if tree > 0 {
options.push(
(0..tree)
.prop_map(|target| Some(Definition::Ref(target)))
.boxed(),
);
}
prop::strategy::Union::new(options).boxed()
}
fn well_formed_workloads() -> impl Strategy<Value = Vec<Transaction<TreeInput>>> {
let node_change = (0..3u64, 0..6u64)
.prop_flat_map(|(tree, node)| (Just(tree), Just(node), definition(tree, node)));
let root_change = (0..3u64, prop::option::of(0..6u64));
let step = (
prop::collection::vec(node_change, 0..4),
prop::collection::vec(root_change, 0..2),
);
workloads(step, 5, 3).prop_map(|raw| {
let mut definitions: BTreeMap<(u64, u64), Definition> = BTreeMap::new();
let mut roots = BTreeMap::new();
map_steps(raw, |(node_changes, root_changes)| {
let mut input = TreeInput::default();
for (tree, node, definition) in node_changes {
if let Some(old) = definitions.remove(&(tree, node)) {
old.push(&mut input, tree, node, -1);
}
if let Some(new) = definition {
new.push(&mut input, tree, node, 1);
definitions.insert((tree, node), new);
}
}
for (tree, root) in root_changes {
if let Some(old) = roots.remove(&tree) {
input.roots.push((Tup2(tree, old), -1));
}
if let Some(new) = root {
input.roots.push((Tup2(tree, new), 1));
roots.insert(tree, new);
}
}
input
})
})
}
#[test]
fn tree_evaluation_triggers() {
for plan in [Plan::ByChild, Plan::ByTree] {
for workload in tree_triggers() {
for config in configs() {
check(&TreeEvaluation(plan), &workload, config);
}
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(CIRCUIT_CASES))]
#[test]
fn tree_evaluation_by_child_random(
workload in well_formed_workloads(),
config in any_config()
) {
check(&TreeEvaluation(Plan::ByChild), &workload, config);
}
#[test]
fn tree_evaluation_by_tree_random(workload in well_formed_workloads(), config in any_config()) {
check(&TreeEvaluation(Plan::ByTree), &workload, config);
}
}