diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index 0b7602c5..66ba30f0 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -606,17 +606,6 @@ impl> ByteNode } impl CellByteNode { - - /// Clones this node into its ordinary representation without cloning its pinned CoFrees. - pub(crate) fn clone_as_dense(&self) -> DenseByteNode { - let mut dense_node = DenseByteNode::with_capacity_in(self.values.len(), self.alloc.clone()); - dense_node.mask = self.mask; - dense_node.values.extend(self.values.iter().map(|cf| { - OrdinaryCoFree::new(cf.rec().cloned(), cf.val().cloned()) - })); - dense_node - } - /// Ensures that a CoFree exists for the specified key, and returns a reference to the node and /// value option /// @@ -1454,8 +1443,10 @@ impl> TrieNode self.pmeet(other_dense_node).map(|new_node| TrieNodeODRc::new_in(new_node, self.alloc.clone())) }, LINE_LIST_NODE_TAG => { + //The list node enumerates its payloads, but `self` is the left operand of every + // collision, hence `swapped` let other_list_node = unsafe { other.as_list_unchecked() }; - other_list_node.pmeet_dyn(self.as_tagged()).invert_identity() + other_list_node.pmeet_dyn_oriented(self.as_tagged(), true).invert_identity() }, #[cfg(feature = "bridge_nodes")] TaggedNodeRef::BridgeNode(other_bridge_node) => { @@ -1465,9 +1456,9 @@ impl> TrieNode let other_byte_node = unsafe { other.as_cell_unchecked() }; self.pmeet(other_byte_node).map(|new_node| TrieNodeODRc::new_in(new_node, self.alloc.clone())) }, - TINY_REF_NODE_TAG => { - let tiny_node = unsafe { other.as_tiny_unchecked() }; - tiny_node.pmeet_dyn(self.as_tagged()).invert_identity() + TINY_REF_NODE_TAG => match unsafe { other.as_tiny_unchecked() }.into_full() { + Some(full_node) => self.pmeet_dyn(full_node.as_tagged()), + None => AlgebraicResult::None, }, EMPTY_NODE_TAG => AlgebraicResult::None, _ => unsafe{ unreachable_unchecked() } diff --git a/src/line_list_node.rs b/src/line_list_node.rs index aa85da28..bb3cc76c 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -1386,10 +1386,11 @@ fn merge_guts<'a, V: Clone + Lattice + Send + Sync, A: Allocator, const ASLOT: u unsafe{ intermediate_node.set_payload_owned::<0>(&a_key[overlap..], a_payload); } debug_assert!(validate_node(&intermediate_node)); let intermediate_node = TrieNodeODRc::new_in(intermediate_node, a.alloc.clone()); - return match b_child.pjoin(&intermediate_node) { + //`a` is the left operand, so its payload goes on the left of the join + return match intermediate_node.pjoin(b_child) { AlgebraicResult::Element(joined) => AlgebraicResult::Element((&a_key[0..overlap], ValOrChild::Child(joined))), //`b`'s child already held `a`'s payload, so `b`'s slot is the result - AlgebraicResult::Identity(mask) if mask & SELF_IDENT > 0 => AlgebraicResult::Identity(COUNTER_IDENT), + AlgebraicResult::Identity(mask) if mask & COUNTER_IDENT > 0 => AlgebraicResult::Identity(COUNTER_IDENT), AlgebraicResult::Identity(_) if a.other_slot_has_value_at::(b_key) && b_child.is_empty() => AlgebraicResult::None, AlgebraicResult::Identity(_) => AlgebraicResult::Element((&a_key[0..overlap], ValOrChild::Child(intermediate_node))), AlgebraicResult::None => unreachable!(), //`intermediate_node` is never empty @@ -2723,36 +2724,13 @@ impl TrieNode for LineListNode Err(joined_dense_node) => joined_dense_node.map(|node| TrieNodeODRc::new_in(node, self.alloc.clone())), } }, - DENSE_BYTE_NODE_TAG => { - let other_dense_node = unsafe{ other.as_dense_unchecked() }; - let mut new_node = other_dense_node.clone(); - match new_node.merge_from_list_node(self) { - //Both nodes were empty so the join is empty too - AlgebraicStatus::None => { - debug_assert!(self.node_is_empty() && other_dense_node.node_is_empty()); - AlgebraicResult::None - }, - AlgebraicStatus::Identity => AlgebraicResult::Identity(COUNTER_IDENT), - AlgebraicStatus::Element => AlgebraicResult::Element(TrieNodeODRc::new_in(new_node, self.alloc.clone())) - } - }, + //Joined through a byte-node copy of `self`, so `self` stays the left operand and keeps its + // values on a collision. Merging `self` into a clone of `other` would keep `other`'s + DENSE_BYTE_NODE_TAG | CELL_BYTE_NODE_TAG => self.to_dense().as_tagged().pjoin_dyn(other), #[cfg(feature = "bridge_nodes")] TaggedNodeRef::BridgeNode(_other_bridge_node) => { unimplemented!() }, - CELL_BYTE_NODE_TAG => { - let other_cell_node = unsafe{ other.as_cell_unchecked() }; - let mut new_node = other_cell_node.clone_as_dense(); - match new_node.merge_from_list_node(self) { - //See the DENSE_BYTE_NODE_TAG arm: two empty nodes join to an empty result - AlgebraicStatus::None => { - debug_assert!(self.node_is_empty() && other_cell_node.node_is_empty()); - AlgebraicResult::None - }, - AlgebraicStatus::Identity => AlgebraicResult::Identity(COUNTER_IDENT), - AlgebraicStatus::Element => AlgebraicResult::Element(TrieNodeODRc::new_in(new_node, self.alloc.clone())) - } - }, TINY_REF_NODE_TAG => { //Expand the tiny node and keep `self` on the left (see DenseByteNode::pjoin_dyn) let tiny_node = unsafe{ other.as_tiny_unchecked() }; @@ -2776,23 +2754,19 @@ impl TrieNode for LineListNode let other_list_node = unsafe{ other_node.as_list_unchecked() }; merge_into_list_nodes(self, other_list_node) }, - DENSE_BYTE_NODE_TAG => { - let other_dense_node = unsafe{ other_node.as_dense_unchecked() }; - let mut new_node = other_dense_node.clone(); - let status = new_node.merge_from_list_node(self); - debug_assert!(!status.is_none()); - (AlgebraicStatus::Element, Err(TrieNodeODRc::new_in(new_node, self.alloc.clone()))) + //As in `pjoin_dyn`: through a byte-node copy of `self`, which then replaces `self` + DENSE_BYTE_NODE_TAG | CELL_BYTE_NODE_TAG => { + let mut new_node = self.to_dense(); + let (_, result) = new_node.make_mut().join_into_dyn(other); + if let Err(replacement) = result { + new_node = replacement; + } + (AlgebraicStatus::Element, Err(new_node)) }, #[cfg(feature = "bridge_nodes")] TaggedNodeRef::BridgeNode(_other_bridge_node) => { unimplemented!() }, - CELL_BYTE_NODE_TAG => { - let mut new_node = unsafe{ other_node.as_cell_unchecked() }.clone_as_dense(); - let status = new_node.merge_from_list_node(self); - debug_assert!(!status.is_none()); - (AlgebraicStatus::Element, Err(TrieNodeODRc::new_in(new_node, self.alloc.clone()))) - }, EMPTY_NODE_TAG => (AlgebraicStatus::Identity, Ok(())), _ => unsafe{ unreachable_unchecked() } } @@ -2988,6 +2962,29 @@ impl TrieNode for LineListNode } fn pmeet_dyn(&self, other: TaggedNodeRef) -> AlgebraicResult> where V: Lattice { + self.pmeet_dyn_oriented(other, false) + } + fn psubtract_dyn(&self, other: TaggedNodeRef) -> AlgebraicResult> where V: DistributiveLattice { + debug_assert!(validate_node(self)); + let slot0_result = self.subtract_from_slot_contents::<0>(other); + let slot1_result = self.subtract_from_slot_contents::<1>(other); + self.combine_slot_results_into_node_result(slot0_result, slot1_result) + } + fn prestrict_dyn(&self, other: TaggedNodeRef) -> AlgebraicResult> { + debug_assert!(validate_node(self)); + let slot0_result = self.restrict_slot_contents::<0>(other); + let slot1_result = self.restrict_slot_contents::<1>(other); + self.combine_slot_results_into_node_result(slot0_result, slot1_result) + } + fn clone_self(&self) -> TrieNodeODRc { + TrieNodeODRc::new_in(self.clone(), self.alloc.clone()) + } +} + +impl LineListNode { + /// [TrieNode::pmeet_dyn], with `swapped` meaning `self` is really the *right* operand and + /// `other` the left one (see [pmeet_generic]). A byte node meets a list node this way + pub(crate) fn pmeet_dyn_oriented(&self, other: TaggedNodeRef, swapped: bool) -> AlgebraicResult> where V: Lattice { debug_assert!(validate_node(self)); let mut self_payloads_buf: [(&[u8], PayloadRef); 2] = [(&[], PayloadRef::None); 2]; @@ -3012,32 +3009,33 @@ impl TrieNode for LineListNode _ => unsafe{ unreachable_unchecked() } }; - pmeet_generic::<2, V, A, _>(self_payloads, other, |payloads| { + pmeet_generic::<2, V, A, _>(self_payloads, other, swapped, |payloads| { debug_assert_eq!(payloads.len(), self_payloads.len()); - let slot0_payload = payloads.get_mut(0).and_then(|p| core::mem::take(p)).map(|p| p.into()); - let slot1_payload = payloads.get_mut(1).and_then(|p| core::mem::take(p)).map(|p| p.into()); - let new_node = self.clone_with_updated_payloads(slot0_payload, slot1_payload).unwrap(); + let mut slot0_payload = payloads.get_mut(0).and_then(|p| core::mem::take(p)); + let mut slot1_payload = payloads.get_mut(1).and_then(|p| core::mem::take(p)); + //A child that met down to an empty node is a dangling sentinel. Beside a value under the + // same key it is redundant, since the value keeps the path, and a valid node may not hold it + if self_slot_count == 2 && self_payloads[0].0 == self_payloads[1].0 { + let is_empty_child = |p: &Option>| matches!(p, Some(ValOrChild::Child(child)) if child.as_tagged().node_is_empty()); + let is_val = |p: &Option>| matches!(p, Some(ValOrChild::Val(_))); + if is_empty_child(&slot0_payload) && is_val(&slot1_payload) { + slot0_payload = None; + } + if is_empty_child(&slot1_payload) && is_val(&slot0_payload) { + slot1_payload = None; + } + } + let new_node = self.clone_with_updated_payloads(slot0_payload.map(|p| p.into()), slot1_payload.map(|p| p.into())).unwrap(); TrieNodeODRc::new_in(new_node, self.alloc.clone()) }) } - fn psubtract_dyn(&self, other: TaggedNodeRef) -> AlgebraicResult> where V: DistributiveLattice { - debug_assert!(validate_node(self)); - let slot0_result = self.subtract_from_slot_contents::<0>(other); - let slot1_result = self.subtract_from_slot_contents::<1>(other); - self.combine_slot_results_into_node_result(slot0_result, slot1_result) - } - fn prestrict_dyn(&self, other: TaggedNodeRef) -> AlgebraicResult> { - debug_assert!(validate_node(self)); - let slot0_result = self.restrict_slot_contents::<0>(other); - let slot1_result = self.restrict_slot_contents::<1>(other); - self.combine_slot_results_into_node_result(slot0_result, slot1_result) - } - fn clone_self(&self) -> TrieNodeODRc { - TrieNodeODRc::new_in(self.clone(), self.alloc.clone()) + + /// A byte node holding the same contents, for an operation against a byte node that has to + /// keep `self` on the left + pub(crate) fn to_dense(&self) -> TrieNodeODRc { + self.clone().convert_to_dense::>(2) } -} -impl LineListNode { #[inline] fn preserve_prune_limit(&mut self, key: &[u8], prune_limit: usize) { if prune_limit > 0 && prune_limit < key.len() { diff --git a/src/trie_map.rs b/src/trie_map.rs index 5c0b1dfd..e88665d4 100644 --- a/src/trie_map.rs +++ b/src/trie_map.rs @@ -1592,6 +1592,41 @@ mod tests { assert_eq!(map.val_at(b"foo/bar"), Some(&300)); assert_eq!(map.val_at(b"missing"), None); } + /// `b` is a clone of `a` plus one value, so the two share nodes. A join may not lose that + /// value in either operand order. Found by the algebraic fuzzer's `join-commutative` law. + #[test] + fn map_join_keeps_value_across_shared_structure() { + let mut a = PathMap::::new(); + for p in [&[0u8, 0, 0, 0, 0][..], &[1, 0, 1], &[1, 0, 3]] { + a.create_path(p); + } + let mut b = a.clone(); + b.set_val_at([1u8, 0, 0, 0], 1); + + assert_eq!(a.join(&b).val_at([1u8, 0, 0, 0]), Some(&1)); + assert_eq!(b.join(&a).val_at([1u8, 0, 0, 0]), Some(&1)); + } + + /// A meet where a list node holds a value and a child under one key, and the child meets an + /// empty node: the result must not keep an empty child beside the value + #[test] + fn map_meet_value_beside_emptied_child() { + let mut x = PathMap::::new(); + x.create_path([2u8]); + let mut big = PathMap::::new(); + big.set_val_at([1u8], 2); + big.create_path([1u8, 2, 1, 2, 1]); + big.set_val_at([2u8], 1); + let mut c = PathMap::::new(); + c.set_val_at([2u8], 1); + c.set_val_at([2u8, 0], 2); + c.set_val_at([2u8, 2], 2); + + let met = x.join(&big).meet(&x.join(&c)); + assert_eq!(met.val_at([2u8]), Some(&1)); + assert_eq!(met.val_count(), 1); + } + } //GOAT, Consider refactor of zipper traits. `WriteZipper` -> `PathWriter`. Zipper is split into the zipper diff --git a/src/trie_node.rs b/src/trie_node.rs index 19b7031b..59f5cc7e 100644 --- a/src/trie_node.rs +++ b/src/trie_node.rs @@ -646,7 +646,16 @@ impl ValOrChildUnion { // was observed. Therefore the the ~20% slowdown is simply the higher overheads of this generic function. // //The next port of call for optimization is probably to remove the recursion -pub(crate) fn pmeet_generic(self_payloads: &[(&[u8], PayloadRef)], other: TaggedNodeRef, merge_f: MergeF) -> AlgebraicResult> +// +/// `swapped` says which operand `self_payloads` came from. The meet is left-biased (a `Lattice` +/// impl resolves a collision as `left.pmeet(right)`), so when a caller enumerates the *right* +/// operand's payloads because that node type is the easier one to iterate, it passes `swapped = +/// true`: every value and every recursive node meet is then computed as `other op self` and the +/// identity masks are re-expressed relative to `self_payloads`. The caller still applies +/// `invert_identity()` to the final result to get back to its own orientation. Without this, a +/// dense-node-versus-list-node meet returned the list node's values regardless of which side it +/// was on. +pub(crate) fn pmeet_generic(self_payloads: &[(&[u8], PayloadRef)], other: TaggedNodeRef, swapped: bool, merge_f: MergeF) -> AlgebraicResult> where MergeF: FnOnce(&mut [Option>]) -> TrieNodeODRc, V: Clone + Send + Sync + Lattice @@ -661,7 +670,7 @@ pub(crate) fn pmeet_generic(self_payloads, &mut request_keys[..], &mut request_results[..], &mut element_results[..], other); + let is_exhaustive = pmeet_generic_internal::(self_payloads, &mut request_keys[..], &mut request_results[..], &mut element_results[..], other, swapped); let mut is_none = true; let mut combined_mask = SELF_IDENT | COUNTER_IDENT; let mut result_payloads = ArrayVec::>, MAX_PAYLOAD_CNT>::new(); @@ -701,7 +710,7 @@ pub(crate) fn node_count_branches_recursive(self_payloads: &[(&[u8], PayloadRef)], keys: &mut [(&[u8], bool)], request_results: &mut [(usize, PayloadRef<'trie, V, A>)], results: &mut [FatAlgebraicResult>], other_node: TaggedNodeRef<'trie, V, A>) -> bool +pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: Allocator>(self_payloads: &[(&[u8], PayloadRef)], keys: &mut [(&[u8], bool)], request_results: &mut [(usize, PayloadRef<'trie, V, A>)], results: &mut [FatAlgebraicResult>], other_node: TaggedNodeRef<'trie, V, A>, swapped: bool) -> bool where V: Clone + Send + Sync + Lattice { //If is_exhaustive gets set to `false`, then the pmeet method cannot return a `COUNTER_IDENTITY` result @@ -733,14 +742,14 @@ pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: // we have the same node as the previous time through the loop if cur_group.is_some() { if (cur_group.as_ref().unwrap().1 as *const TrieNodeODRc) != (child as *const TrieNodeODRc) { - pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results); + pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results, swapped); cur_group = Some((idx, child)); } } else { cur_group = Some((idx, child)); } } else { - pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results); + pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results, swapped); //We've arrived at a contained value or onward link that has a correspondence // to one of the values or links in `self` @@ -749,13 +758,13 @@ pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: let result = match &self_payloads[idx].1 { PayloadRef::Child(self_link) => { let other_link = payload.child(); - let result = self_link.pmeet(other_link); + let result = if swapped { other_link.pmeet(self_link).invert_identity() } else { self_link.pmeet(other_link) }; FatAlgebraicResult::from_binary_op_result(result, self_link, other_link) .map(|child| ValOrChild::Child(child)) }, PayloadRef::Val(self_val) => { let other_val = payload.val(); - let result = (*self_val).pmeet(other_val); + let result = if swapped { other_val.pmeet(*self_val).invert_identity() } else { (*self_val).pmeet(other_val) }; FatAlgebraicResult::from_binary_op_result(result, *self_val, other_val) .map(|val| ValOrChild::Val(val)) }, @@ -766,13 +775,17 @@ pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: results[idx] = result; } } else { - pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results); + pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, idx, self_payloads, keys, request_results, results, swapped); let result = match &self_payloads[idx].1 { PayloadRef::Child(self_link) => { match other_node.get_node_at_key(keys[idx].0).into_option() { Some(other_onward_node) => { - let result = self_link.as_tagged().pmeet_dyn(other_onward_node.as_tagged()); + let result = if swapped { + other_onward_node.as_tagged().pmeet_dyn(self_link.as_tagged()).invert_identity() + } else { + self_link.as_tagged().pmeet_dyn(other_onward_node.as_tagged()) + }; FatAlgebraicResult::from_binary_op_result(result, self_link, &other_onward_node) .map(|child| ValOrChild::Child(child)) }, @@ -795,7 +808,7 @@ pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: results[idx] = result; } } - pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, keys.len(), self_payloads, keys, request_results, results); + pmeet_generic_recursive_reset::(&mut cur_group, &mut is_exhaustive, keys.len(), self_payloads, keys, request_results, results, swapped); is_exhaustive } @@ -803,7 +816,7 @@ pub(crate) fn pmeet_generic_internal<'trie, const MAX_PAYLOAD_CNT: usize, V, A: /// Effectively part of `pmeet_generic_internal`, but factored out separately because it's called in /// several different places. Resets the `cur_group` state and does a recursive call of `pmeet_generic_internal` #[inline] -fn pmeet_generic_recursive_reset<'trie, const MAX_PAYLOAD_CNT: usize, V, A: Allocator>(cur_group: &mut Option<(usize, &'trie TrieNodeODRc)>, is_exhaustive: &mut bool, idx: usize, self_payloads: &[(&[u8], PayloadRef)], keys: &mut [(&[u8], bool)], request_results: &mut [(usize, PayloadRef<'trie, V, A>)], results: &mut [FatAlgebraicResult>]) +fn pmeet_generic_recursive_reset<'trie, const MAX_PAYLOAD_CNT: usize, V, A: Allocator>(cur_group: &mut Option<(usize, &'trie TrieNodeODRc)>, is_exhaustive: &mut bool, idx: usize, self_payloads: &[(&[u8], PayloadRef)], keys: &mut [(&[u8], bool)], request_results: &mut [(usize, PayloadRef<'trie, V, A>)], results: &mut [FatAlgebraicResult>], swapped: bool) where V: Clone + Send + Sync + Lattice { match core::mem::take(cur_group) { @@ -811,7 +824,7 @@ fn pmeet_generic_recursive_reset<'trie, const MAX_PAYLOAD_CNT: usize, V, A: Allo let group_keys = &mut keys[group_start..idx]; let group_results = &mut results[group_start..idx]; let group_self_payloads = &self_payloads[group_start..idx]; - if !pmeet_generic_internal::(group_self_payloads, group_keys, request_results, group_results, next_node.as_tagged()) { + if !pmeet_generic_internal::(group_self_payloads, group_keys, request_results, group_results, next_node.as_tagged(), swapped) { *is_exhaustive = false; } }, @@ -3474,6 +3487,65 @@ mod tests { use crate::PathMap; use crate::zipper::*; + fn mk(ps: &[(&[u8], u64)]) -> PathMap { + let mut m = PathMap::::new(); + for (p, v) in ps { m.set_val_at(p, *v); } + m + } + fn vals(m: &PathMap) -> Vec<(Vec, u64)> { + m.iter().map(|(p, v)| (p.to_vec(), *v)).collect() + } + + /// `Lattice for u64` keeps `self` on a collision, so a meet carries the *left* operand's value. + /// That must not depend on which node type each operand happens to be stored in: a byte node + /// meeting a list node used to run the meet with the operands swapped and returned the list + /// node's values whichever side it was on. Found by lean/differential.py. + #[test] + fn meet_value_bias_is_left_regardless_of_node_layout() { + let two_payload_list = mk(&[(&[0], 0), (&[0, 0], 0), (&[3, 0], 1)]); + let single_line = mk(&[(&[0], 1)]); + let dense = mk(&[(&[0], 0), (&[1], 0), (&[2], 0), (&[3], 0), (&[4], 0)]); + for (a, b, expect) in [ + (&two_payload_list, &single_line, 0u64), + (&single_line, &two_payload_list, 1), + (&dense, &single_line, 0), + (&single_line, &dense, 1), + (&dense, &two_payload_list, 0), + (&two_payload_list, &dense, 0), + ] { + let mut out = PathMap::::new(); + { let mut wz = out.write_zipper(); wz.meet_2(&a.read_zipper(), &b.read_zipper()); } + assert_eq!(out.val_at([0u8]), Some(&expect), "meet_2 of {:?} and {:?}", vals(a), vals(b)); + + let mut into = a.clone(); + { let mut wz = into.write_zipper(); wz.meet_into(&b.read_zipper(), false); } + assert_eq!(into.val_at([0u8]), Some(&expect), "meet_into of {:?} and {:?}", vals(a), vals(b)); + } + } + + /// The same for joins: `PathMap::join` and `join_into` keep the left operand's value, whether + /// the left operand is a list node joining into a byte node or the other way round. + #[test] + fn join_value_bias_is_left_regardless_of_node_layout() { + let line = mk(&[(&[1], 0)]); + let dense = mk(&[(&[0], 0), (&[1], 1), (&[2], 0)]); + assert_eq!(line.join(&dense).val_at([1u8]), Some(&0)); + assert_eq!(dense.join(&line).val_at([1u8]), Some(&1)); + + let mut into = line.clone(); + { let mut wz = into.write_zipper(); wz.join_into(&dense.read_zipper()); } + assert_eq!(vals(&into), vec![(vec![0], 0), (vec![1], 0), (vec![2], 0)]); + let mut into = dense.clone(); + { let mut wz = into.write_zipper(); wz.join_into(&line.read_zipper()); } + assert_eq!(vals(&into), vec![(vec![0], 0), (vec![1], 1), (vec![2], 0)]); + + //A deeper collision, so the child-node join is exercised as well as the value join + let line = mk(&[(&[1, 5], 0), (&[1, 6], 0)]); + let dense = mk(&[(&[0], 0), (&[1, 5], 1), (&[2], 0)]); + assert_eq!(line.join(&dense).val_at([1u8, 5]), Some(&0)); + assert_eq!(dense.join(&line).val_at([1u8, 5]), Some(&1)); + } + #[test] fn slim_ptrs_test1() { let map = PathMap::<()>::new();