#cursor #tree #node-tree #mutation #reference #non-intrusive #cell-ref-cell

tree-cursor

A simple, non-intrusive tree cursor that supports node mutation without Cell/RefCell

7 unstable releases (3 breaking)

Uses old Rust 2015

0.3.0 Jul 6, 2018
0.2.2 Jul 6, 2018
0.2.1 Jun 11, 2018
0.1.0 May 31, 2018
0.0.2 May 30, 2018

#2071 in Data structures

MIT license

39KB
951 lines

tree-cursor

status

As far as I know, it's feature-complete, but there are potential soundness issues that I need to look at more closely. Feel free to use it, but be prepared for, well, potential unsoundness. The docs also need some work.


lib.rs:

Summary

This crate provides a simple, non-intrusive tree cursor that supports node mutation without Cell/RefCell.

Soundness

In a nutshell: I don't know if this crate is sound. I think it is, but you should still check it yourself before using it.

The current implementation uses unsafe code to work around the borrow checker's strictness about lifetimes. So far, I haven't formally proven that it's memory-safe, especially in the presence of malicious code. I've done my best to write adversarial tests, and I'll continue poking at corner cases until I'm personally satisfied that it's sound, but it's not likely I'll get around to formally proving it until I better understand the borrow checker.

Concepts

For the purposes of this crate...

  • a tree has exactly one root node (no tree is empty)
  • each node has zero or more children
  • "up" is toward the root
  • "down" is away from the root
  • at any point in time, a cursor has exactly one active node, also called the cursor's position

Guided tour

Let's look at a simple tree and how you might use a cursor to traverse it.

use tree_cursor::cursor::TreeCursor;
use tree_cursor::prelude::*;

struct Node(&'static str, Vec<Node>);

// This trait impl is used by TreeCursor::down to determine the next child
// to visit. You can create a TreeCursor for something that doesn't
// implement Down; you just won't be able to call TreeCursor::down.
impl Down for Node {
    fn down(&self, idx: usize) -> Option<&Self> {
        // idx starts at 0 when we visit this node going downward and
        // increments every time we visit it going upward. This allows us
        // to visit each child in order by going back up to this node after
        // each one.
        self.1.get(idx)
    }
}

let foobar = Node("foo", vec![
    Node("bar", vec![]),
    Node("zup", vec![]),
]);
// foobar is a tree; its root, "foo", has two children, named "bar" and
// "zup".

let mut cur = TreeCursor::new(&foobar);
assert_eq!(cur.get().0, "foo"); // The cursor starts at the root, "foo".
// TreeCursor::get() returns a reference to the active node.

assert!(cur.down());
// TreeCursor::down returns true if the position actually moved.
assert_eq!(cur.get().0, "bar");

assert!(!cur.down());
// "bar" has no children so we can't move the cursor down.

assert!(cur.up());
assert_eq!(cur.get().0, "foo"); // The cursor is at the root again.

assert!(cur.down());
assert_eq!(cur.get().0, "zup");

assert!(cur.up());
assert_eq!(cur.get().0, "foo"); // Back at the root.

assert!(!cur.down()); // No more children to visit.
assert!(!cur.up()); // The root has no parent.

When the cursor is created, its initial position is at the root of the tree. down and up are two methods for repositioning the cursor. There are several methods besides down for moving down a tree; typically down is used when fully traversing a tree, as we just saw. Here are two ways you might do a full traversal if you don't know the tree's exact shape ahead of time:

#
#
#
#
fn process_tree_pre_order(root: &Node) {
    let mut cur = TreeCursor::new(root);
    'outer: loop {
        process_node(cur.get());
        while !cur.down() {
            if !cur.up() { break 'outer; }
        }
    }
}

fn process_tree_post_order(root: &Node) {
    let mut cur = TreeCursor::new(root);
    loop {
        while cur.down() { }
        process_node(cur.get());
        if !cur.up() { break; }
    }
}

When you need more complex behavior or when there's no particular order to a node's children, you can use the down_map method instead, passing it a closure that determines the next child to visit.

Mutability and node references

TreeCursor is the immutable version of the tree cursor, meaning it holds a shared reference to the tree, preventing you from modifying the tree until the cursor goes out of scope. If you need to modify the tree, use TreeCursorMut instead, which gives you access to a mutable reference to the active node.

No runtime deps