// Copyright (c) 2026 Karl Gaissmaier // SPDX-License-Identifier: MIT package nodes import ( "iter" "github.com/gaissmai/bart/internal/lpm" "github.com/gaissmai/bart/internal/sparse" ) // FastNode is based on [BartNode], but it also uses a cache ([256]uint8) // per node to speed up traversal of the multi-bit trie. // Lookups become faster, but this requires more memory per prefix, // and updates (insertions/deletions) also become slower due to the // overhead of managing the cache. type FastNode[V any] struct { Prefixes sparse.Array256[V] Children sparse.Array256[any] // map addr to slice idx (aka rank) childRankCache [256]uint8 } // IsEmpty returns true if the node contains no routing entries (prefixes) // and no child nodes. Empty nodes are candidates for compression or removal // during trie optimization. func (n *FastNode[V]) IsEmpty() bool { if n == nil { return true } return n.Prefixes.Len() == 0 && n.Children.Len() == 0 } // PrefixCount returns the number of prefixes stored in this node. func (n *FastNode[V]) PrefixCount() int { return n.Prefixes.Len() } // ChildCount returns the number of slots used in this node. func (n *FastNode[V]) ChildCount() int { return n.Children.Len() } // InsertPrefix adds or updates a routing entry at the specified index with the given value. // It returns true if a prefix already existed at that index (indicating an update), // false if this is a new insertion. func (n *FastNode[V]) InsertPrefix(idx uint8, val V) (exists bool) { _, exists = n.Prefixes.InsertAt(idx, val) return } // GetPrefix retrieves the value associated with the prefix at the given index. // Returns the value and true if found, or zero value and false if not present. func (n *FastNode[V]) GetPrefix(idx uint8) (val V, exists bool) { return n.Prefixes.Get(idx) } // MustGetPrefix retrieves the value at the specified index, panicking if not found. // This method should only be used when the caller is certain the index exists. func (n *FastNode[V]) MustGetPrefix(idx uint8) (val V) { return n.Prefixes.MustGet(idx) } // AllIndices returns an iterator over all prefix entries. // Each iteration yields the prefix index (uint8) and its associated value (V). func (n *FastNode[V]) AllIndices() iter.Seq2[uint8, V] { return func(yield func(uint8, V) bool) { var buf [256]uint8 for i, idx := range n.Prefixes.AsSlice(&buf) { if !yield(idx, n.Prefixes.Items[i]) { return } } } } // DeletePrefix removes the prefix at the specified index. // Returns true if the prefix existed, otherwise false. func (n *FastNode[V]) DeletePrefix(idx uint8) (exists bool) { _, exists = n.Prefixes.DeleteAt(idx) return exists } // InsertChild adds a child node at the specified address (0-255). // The child can be a *FastNode[V], *LeafNode[V], or *FringeNode[V]. // Returns true if a child already existed at that address. func (n *FastNode[V]) InsertChild(addr uint8, child any) (exists bool) { var rank0 int rank0, exists = n.Children.InsertAt(addr, child) if exists { // Update only: the value at addr is overwritten in-place by InsertAt. // childRankCache[addr] is not refreshed because the rank is unchanged // the position of addr in the sparse slice does not shift on an update. return } // new child inserted? cache the rank value for this addr //nolint:gosec // G115: integer overflow conversion int -> uint8 n.childRankCache[addr] = uint8(rank0) // increment all cached ranks after addr for i := 255; i > int(addr); i-- { n.childRankCache[i]++ } return } // DeleteChild removes the child node at the specified address. // This operation is idempotent - removing a non-existent child is safe. func (n *FastNode[V]) DeleteChild(addr uint8) (exists bool) { _, exists = n.Children.DeleteAt(addr) // nothing deleted if !exists { return exists } // decrement all cached ranks after addr for i := 255; i > int(addr); i-- { n.childRankCache[i]-- } return exists } // GetChild retrieves the child node at the specified address. // Returns the child and true if found, or nil and false if not present. func (n *FastNode[V]) GetChild(addr uint8) (any, bool) { if n.Children.Test(addr) { rank0 := n.childRankCache[addr] return n.Children.Items[rank0], true } return nil, false } // MustGetChild retrieves the child at addr using the pre-cached rank stored in // childRankCache[addr] for a direct O(1) array access without an existence check. // // The caller must guarantee that addr is present (Children.Test(addr) == true). // If addr is absent, childRankCache[addr] contains the number of occupied // addresses less than addr (maintained by InsertChild/DeleteChild), so the // behaviour is undefined: either a wrong child is returned silently, or the // call panics with an index-out-of-range error. func (n *FastNode[V]) MustGetChild(addr uint8) any { rank0 := n.childRankCache[addr] return n.Children.Items[rank0] } // AllChildren returns an iterator over all child nodes. // Each iteration yields the child's address (uint8) and the child node (any). func (n *FastNode[V]) AllChildren() iter.Seq2[uint8, any] { return func(yield func(addr uint8, child any) bool) { var buf [256]uint8 addrs := n.Children.AsSlice(&buf) for i, addr := range addrs { if !yield(addr, n.Children.Items[i]) { return } } } } // Contains returns true if an index (idx) has any matching longest-prefix // in the current node’s prefix table. // // This function performs a presence check without retrieving the associated value. // It is faster than a full lookup, as it only tests for intersection with the // backtracking bitset for the given index. // // The prefix table is structured as a complete binary tree (CBT), and LPM testing // is done via a bitset operation that maps the traversal path from the given index // toward its possible ancestors. func (n *FastNode[V]) Contains(idx uint8) bool { return n.Prefixes.Intersects(&lpm.LookupTbl[idx]) } // LookupIdx performs a longest-prefix match (LPM) lookup for the given index (idx) // within the 8-bit stride-based prefix table at this trie depth. // // The function returns the matched base index, associated value, and true if a // matching prefix exists at this level; otherwise, ok is false. // // Internally, the prefix table is organized as a complete binary tree (CBT) indexed // via the baseIndex function. Unlike the original ART algorithm, this implementation // does not use an allotment-based approach. Instead, it performs CBT backtracking // using a bitset-based operation with a precomputed backtracking pattern specific to idx. func (n *FastNode[V]) LookupIdx(idx uint8) (top uint8, val V, ok bool) { // top is the idx of the longest-prefix-match if top, ok = n.Prefixes.IntersectionTop(&lpm.LookupTbl[idx]); ok { return top, n.MustGetPrefix(top), true } return top, val, ok } // Lookup is just a simple wrapper for LookupIdx. func (n *FastNode[V]) Lookup(idx uint8) (val V, ok bool) { _, val, ok = n.LookupIdx(idx) return val, ok } // CloneFlat returns a shallow copy of the current node, optionally performing deep copies of values. // // If cloneFn is nil, the stored values in prefixes are copied directly without modification. // Otherwise, cloneFn is applied to each stored value for deep cloning. // Child nodes are cloned shallowly: LeafNode and FringeNode children are cloned via their clone methods, // but child nodes of type *FastNode[V] (subnodes) are assigned as-is without recursive cloning. // This method does not recursively clone descendants beyond the immediate children. // // Note: The returned node is a new instance with copied slices but only shallow copies of nested nodes, // except for LeafNode and FringeNode children which are cloned according to cloneFn. func (n *FastNode[V]) CloneFlat(cloneFn func(V) V) *FastNode[V] { if n == nil { return nil } c := new(FastNode[V]) if n.IsEmpty() { return c } // copy ... c.Prefixes = *(n.Prefixes.Copy()) // ... and clone the values if cloneFn != nil { for i, v := range c.Prefixes.Items { c.Prefixes.Items[i] = cloneFn(v) } } // copy ... c.childRankCache = n.childRankCache c.Children = *(n.Children.Copy()) // Iterate over children to flat clone leaf/fringe nodes; // for *FastNode[V] children, keep shallow references (no recursive clone) for i, anyKid := range c.Children.Items { switch kid := anyKid.(type) { case *FastNode[V]: // Shallow copy case *LeafNode[V]: // Clone leaf nodes, applying cloneFn as needed c.Children.Items[i] = kid.CloneLeaf(cloneFn) case *FringeNode[V]: // Clone fringe nodes, applying cloneFn as needed c.Children.Items[i] = kid.CloneFringe(cloneFn) default: panic("logic error, wrong node type") } } return c } // CloneRec performs a recursive deep copy of the node and all its descendants. // // If cloneFn is nil, the stored values are copied directly without modification. // Otherwise cloneFn is applied to each stored value for deep cloning. // // This method first creates a shallow clone of the current node using CloneFlat, // applying cloneFn to values as described there. Then it recursively clones all // child nodes of type *FastNode[V], performing a full deep clone down the subtree. // // Child nodes of type *LeafNode[V] and *FringeNode[V] are already cloned // by CloneFlat. // // Returns a new instance of FastNode[V] which is a complete deep clone of the // receiver node with all descendants. func (n *FastNode[V]) CloneRec(cloneFn func(V) V) *FastNode[V] { if n == nil { return nil } // Perform a flat clone of the current node. c := n.CloneFlat(cloneFn) // Recursively clone all child nodes of type *FastNode[V] for i, kidAny := range c.Children.Items { if kid, ok := kidAny.(*FastNode[V]); ok { c.Children.Items[i] = kid.CloneRec(cloneFn) } } return c }