flat-tree
flat-tree
is one of the first modules that should be implemented for the Dat
protocol. See Key Concepts: Flat Tree for an overview
of how Flat Trees work.
Core API
The core flat-tree API consists of several methods that can calculate the nodes relative to each other.
Children
Returns both children of a node. It cannot return any children when querying a
leaf node, so the result must be a Null
, Maybe
, or Option
type, depending
on the language used.
# #![allow(unused_variables)] #fn main() { pub fn children_with_depth(i: usize, depth: usize) -> Option<(usize, usize)> #}
count
Returns how many nodes are under the tree that the node spans.
# #![allow(unused_variables)] #fn main() { pub fn count_with_depth(i: usize) -> usize #}
depth
Returns the depth of a node at a given index. is removed from the left-most node at its current depth
# #![allow(unused_variables)] #fn main() { pub fn depth(i: usize) -> usize #}
full_roots
Returns a list of all the full roots (subtrees where all nodes have either 2 or 0 children).
# #![allow(unused_variables)] #fn main() { pub fn full_roots(i: usize) -> Vec<usize>{ #}
index
Return the index for a node at a given depth and offset.
# #![allow(unused_variables)] #fn main() { pub fn index(depth: usize, offset: usize) -> usize #}
left_child
Return the left child of the node at index.
# #![allow(unused_variables)] #fn main() { pub fn left_child(i: usize) -> Option<usize> #}
left_span
Returns the left-most child of the node at index.
# #![allow(unused_variables)] #fn main() { pub fn left_span(i: usize) -> usize #}
offset
Returns the offset of a node at a given index. The offset of a node is how far it is removed from the left-most node at its current depth
# #![allow(unused_variables)] #fn main() { pub fn offset(i: usize) -> usize #}
parent
Returns the parent of a node.
# #![allow(unused_variables)] #fn main() { pub fn parent(i: usize) -> usize #}
right_child
Return the right child of the node at index.
# #![allow(unused_variables)] #fn main() { pub fn right_child(i: usize) -> Option<usize> #}
right_span
Returns the right-most child of the node at index.
# #![allow(unused_variables)] #fn main() { pub fn right_span(i: usize) -> usize #}
left_span
Returns the left-most child of the node at index.
# #![allow(unused_variables)] #fn main() { pub fn right_span(i: usize) -> usize #}
sibling
Returns the node that shares the same parent node.
# #![allow(unused_variables)] #fn main() { pub fn sibling(i: usize) -> usize #}
spans
Returns a pair of the left-most node in the tree, and the right-most node in the tree.
# #![allow(unused_variables)] #fn main() { pub fn spans(i: usize) -> (usize, usize) #}
uncle
Returns the parent's sibling node.
# #![allow(unused_variables)] #fn main() { pub fn uncle(i: usize) -> usize #}
Iterator API
Some upstream modules require stateful traversal of the tree. It can be
convenient to expose a stateful iterator module as part of flat-tree
for those
cases.
The iterator should be a constructor that takes an initial index, and exposes methods to move to child nodes, parent nodes, etc. The iterator should also expose the index so it can be stored or used for other computations.
Optimizations
Calculate Depth
The depth of a node can be calculated by counting the number of tailing zeros in
a number. Languages such as Rust expose <num>.trailing_zeros()
which counts
the amount of zeros at the end of a number (cttz
intrinsic). By combining a
bitwise negation (!
or NOT
operation) with the .trailing_zeros()
method,
the amount of trailing ones can be counted. On x86 machines this should be
around 3 instructions when inlined.
Re-use the depth parameter
Sometimes when calling multiple functions on the same index, depending on the
execution environment it can be efficient to reuse the depth
parameter.
However if the optimized depth method is used, there's no strict need to expose
the with_depth*
methods, as the compiler will detect the duplicate
computation, and remove it anyway. Mileage may vary, but this technique has been
tested on LLVM output for the Rust version. In the worst case there might be
penalty of up 3 instructions per call, which seems like a negligible penalty.
Calculate children
Calculating whether a node has children can be sped up by quickly checking if a number is even or uneven. If a number is uneven, it's already guaranteed to be a child node, so it can't have child nodes.
Spans
Similarly for spans. If an even number is targeted, it is at the bottom of the
tree, so it will only span itself. Therefore it's easy to implement an is_even
check, and return the index that was passed in if it's true.
Full Roots
To prevent allocations full-roots
could also write to either a pre-allocated
vector, or a stack-allocated collection instead.