indextree Documentation

repository·main·Indexed 21 days ago

https://github.com/saschagrunert/indextree

A high-performance, arena-based tree data structure for Rust (version 4.8.1). It utilizes numerical indices instead of reference-counted pointers to enable efficient, thread-safe tree manipulation and parallel traversal. Key features include a declarative `tree!` macro, Serde support via the `deser` feature, and parallel iteration via `rayon` through the `par_iter` feature. The library provides an `Arena` for node storage and `NodeId` handles for navigation and mutation.

Tokens
7.2K
Snippets
24
Records
29
Agent score
73%

What's inside indextree

  1. What is indextree and how does it work?

    main

    indextree is an arena-based tree structure implementation. Instead of using reference-counted pointers (like Rc or Arc), it uses a single Vec and numerical identifiers (indices in the vector) to represent nodes.

    Key Benefits:

    • Idiomatic Mutability: It avoids RefCell by handling mutability through unique (&mut) access to the Arena.
    • Thread Safety: Because the tree is backed by a Vec, it can be sent or shared across threads, enabling parallel tree traversals.
    • Performance: Using indices instead of pointers reduces overhead associated with reference counting.
  2. Handle node removal and stale references

    main

    When a node is removed from an Arena, its slot may eventually be reused for a new node. To prevent logic errors where a NodeId from a previous lifecycle is used to access a new node, use is_removed.

    NodeId::is_removed(arena) returns true if the NodeId points to a node that has been removed or if the underlying slot has been reused for a different node.

    use indextree::Arena;
    
    let mut arena = Arena::new();
    let n1 = arena.new_node("1");
    
    // Assume n1 is removed via some arena method
    // (Note: actual removal logic is in Arena, not NodeId)
    // If n1 is removed:
    if n1.is_removed(&arena) {
        println!("This reference is no longer valid!");
    }
  3. How the Arena manages node storage

    main

    The Arena<T> is the central owner of all tree nodes in indextree. Nodes are stored contiguously in a single Vec and are accessed via NodeId. This design allows for efficient memory usage and cache locality.

    Key behaviors:

    • Node Lifecycle: When a node is removed, it is not immediately deleted from the underlying vector but is marked as removed. The arena maintains an internal free list to recycle these slots for new nodes via new_node.
    • Indexing: The arena uses 1-based indexing for NodeId internally, but provides standard Index and IndexMut implementations for NodeId to access nodes.
    • Capacity: You can pre-allocate space using with_capacity or reserve to minimize reallocations.
    • Clearing: clear() removes all nodes from storage and invalidates all existing NodeIds, but retains the allocated capacity for reuse.
    use indextree::Arena;
    
    let mut arena = Arena::new();
    let id = arena.new_node("data");
    let node = arena.get(id).unwrap();
    assert_eq!(*node.get(), "data");
  4. Use NodeId to reference and manipulate nodes

    main

    A NodeId is a lightweight handle used to reference a Node within an Arena. Most tree operations (appending, removing, traversing) are implemented as methods on NodeId and require a reference to the Arena they belong to.

    Key capabilities include:

    • Navigation: Access parents, ancestors, descendants, and siblings.
    • Tree Modification: Append children, prepend children, insert siblings, or detach nodes.
    • Safety: NodeId includes a NodeStamp to detect if a node has been removed and its slot reused, preventing stale reference bugs.
    use indextree::Arena;
    
    let mut arena = Arena::new();
    let n1 = arena.new_node("root");
    let n1_1 = n1.append_value("child", &mut arena);
    
    // Use NodeId to navigate
    if let Some(parent) = n1_1.parent(&arena) {
        println!("Parent ID: {}", parent);
    }
  5. How the Arena-based tree structure works

    main

    The indextree crate implements a tree structure using an Arena. Instead of using reference-counted pointers (like Rc or Arc), the tree is stored in a single Vec. Nodes are referenced using NodeId, which are numerical indices into that vector.

    Key Benefits

    • Idiomatic Mutability: Because the tree is managed by the Arena, mutability is handled via unique (&mut) access to the arena rather than through RefCell or other interior mutability patterns.
    • Thread Safety: The Arena and NodeId can be sent or shared across threads (implementing Send and Sync), enabling parallel tree traversals.
    • Efficient Node Reuse: When a node is removed via NodeId::remove, the slot is not immediately deallocated. Instead, it is marked for reuse using an internal generation counter (stamp). Future calls to Arena::new_node may recycle these slots. Stale NodeId references can be detected using NodeId::is_removed by comparing the ID's stamp against the current slot stamp.
    use indextree::Arena;
    
    // Create a new arena
    let arena = &mut Arena::new();
    
    // Add some new nodes to the arena
    let a = arena.new_node(1);
    let b = arena.new_node(2);
    
    // Append b to a
    a.append(b, arena);
    assert_eq!(b.ancestors(arena).count(), 2);
  6. Handle tree modification errors

    main

    Methods that modify the tree structure provide two variants:

    1. Panicking variants: (e.g., NodeId::append) These call .expect() internally and will panic on failure.
    2. Checked variants: (e.g., NodeId::checked_append) These return a Result<T, NodeError>, allowing you to handle errors gracefully.

    Common errors include NodeError::AppendSelf when attempting to append a node to itself.

    use indextree::{Arena, NodeError};
    
    let mut arena = Arena::new();
    let root = arena.new_node("root");
    
    // Cannot append a node to itself
    assert!(matches!(
        root.checked_append(root, &mut arena),
        Err(NodeError::AppendSelf)
    ));
  7. Build and traverse a file system tree

    main

    You can build hierarchical structures like file systems using append_value. To retrieve data from the nodes during traversal, use the descendants iterator and access the node's value via the arena index.

    use indextree::Arena;
    
    let arena = &mut Arena::new();
    let root = arena.new_node("/");
    let etc = root.append_value("etc/", arena);
    let usr = root.append_value("usr/", arena);
    
    etc.append_value("hosts", arena);
    etc.append_value("resolv.conf", arena);
    
    let bin = usr.append_value("bin/", arena);
    bin.append_value("rustc", arena);
    
    // Traverse and collect paths
    let descendants: Vec<_> = root
        .descendants(arena)
        .map(|id| *arena[id].get())
        .collect();
    assert_eq!(
        descendants,
        vec!["/", "etc/", "hosts", "resolv.conf", "usr/", "bin/", "rustc"]
    );
  8. Basic usage of Arena and Node manipulation

    main

    To use indextree, you primarily interact with the Arena to create nodes and manage their relationships. Nodes are manipulated by passing a mutable reference to the Arena to methods like append or ancestors.

    use indextree::Arena;
    
    // Create a new arena
    let arena = &mut Arena::new();
    
    // Add some new nodes to the arena
    let a = arena.new_node(1);
    let b = arena.new_node(2);
    
    // Append b to a
    a.append(b, arena);
    assert_eq!(b.ancestors(arena).count(), 2);
  9. Build trees declaratively with the `tree!` macro

    main

    If the macros feature is enabled, you can use the tree! macro to construct complex tree structures using a declarative syntax. The macro takes the arena as its first argument, followed by key-value pairs representing node values and their children.

    use indextree::{Arena, macros::tree};
    
    let arena = &mut Arena::new();
    let root = tree!(arena, "root" => {
        "child_1" => {
            "grandchild_1",
            "grandchild_2",
        },
        "child_2",
        "child_3",
    });
    
    assert_eq!(root.child_count(arena), 3);
    assert_eq!(root.descendants(arena).count(), 6);
  10. Perform parallel iteration with `par_iter`

    main

    By enabling the par_iter feature, you can use rayon to perform parallel operations across the entire arena. The arena.par_iter() method provides a parallel iterator over all nodes in the arena.

    use indextree::Arena;
    use rayon::prelude::*;
    
    let arena = &mut Arena::new();
    let root = arena.new_node(0);
    for i in 1..=1000 {
        root.append_value(i, arena);
    }
    
    let sum: i64 = arena.par_iter().map(|node| *node.get()).sum();
    assert_eq!(sum, 500500);
  11. Configure indextree features

    main

    indextree provides several optional features that can be enabled in your Cargo.toml:

    FeatureDefaultDescription
    stdyesStandard library support. Disable for no_std (requires alloc).
    macrosyestree! macro for declarative tree construction.
    desernoSerde serialization and deserialization.
    par_iternoParallel iteration via rayon.
  12. Debug pretty-print a node and its descendants

    main

    The debug_pretty_print method returns a DebugPrettyPrint proxy object used for visualizing the tree structure in a human-readable format. This is primarily intended for debugging.

    Supported Formats:

    • {:?}: Produces a tree-like string representation using characters like |-- and `--.
    • {:#?}: Produces an alternate, more expanded debug format.
    • {:#}: Produces a plain string representation without the debug symbols.
    # use indextree::Arena;
    let mut arena = Arena::new();
    let root = arena.new_node("root");
    let child = arena.new_node("child");
    root.append(child, &mut arena);
    
    let printable = root.debug_pretty_print(&arena);
    println!("{:?}", printable);
    // Output:
    // "root"
    // |-- "child"