This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+265
View File
@@ -0,0 +1,265 @@
// 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"
)
// BartNode represents a single trie level in the multibit routing table.
//
// Unlike the original ART algorithm, this implementation uses popcount-compressed
// sparse arrays instead of fixed-size allocations. Insertions and lookups rely on
// fast bitset operations and precomputed lookup tables to maximize CPU pipelining
// and cache efficiency.
//
// Each BartNode maintains two distinct sparse arrays:
//
// 1. Prefixes: Stores routing entries (prefix -> value) for the current stride.
// These are laid out as a complete binary tree using the baseIndex()
// function mapping from the ART algorithm. Prefixes that match exactly at
// the maximum trie depth are always stored here.
//
// 2. Children: Holds pointers to the next logical levels with a branching
// factor of 256 (8 bits per stride).
//
// A slot in the Children array may contain one of three types:
// - *BartNode[V]: An internal intermediate node for further trie traversal.
// - *FringeNode[V]: A path-compressed node if it qualifies as fringe [IsFringe]
// - *LeafNode[V]: A path-compressed node otherwise.
//
// Note: LeafNode and FringeNode are created through path compression and
// are automatically split into regular BartNodes when a more specific prefix
// is inserted that requires further branching.
type BartNode[V any] struct {
Prefixes sparse.Array256[V]
Children sparse.Array256[any]
}
// 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 *BartNode[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 *BartNode[V]) PrefixCount() int {
return n.Prefixes.Len()
}
// ChildCount returns the number of slots used in this node.
func (n *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[V], *LeafNode[V], or *FringeNode[V].
// Returns true if a child already existed at that address.
func (n *BartNode[V]) InsertChild(addr uint8, child any) (exists bool) {
_, exists = n.Children.InsertAt(addr, child)
return
}
// 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 *BartNode[V]) GetChild(addr uint8) (any, bool) {
return n.Children.Get(addr)
}
// MustGetChild retrieves the child at the specified address, panicking if not found.
// This method should only be used when the caller is certain the child exists.
func (n *BartNode[V]) MustGetChild(addr uint8) any {
return n.Children.MustGet(addr)
}
// AllChildren returns an iterator over all child nodes.
// Each iteration yields the child's address (uint8) and the child node (any).
func (n *BartNode[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
}
}
}
}
// DeleteChild removes the child node at the specified address.
// This operation is idempotent - removing a non-existent child is safe.
func (n *BartNode[V]) DeleteChild(addr uint8) (exists bool) {
_, exists = n.Children.DeleteAt(addr)
return exists
}
// 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 *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[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 *BartNode[V]) CloneFlat(cloneFn func(V) V) *BartNode[V] {
if n == nil {
return nil
}
c := new(BartNode[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.Children = *(n.Children.Copy())
// Iterate over children to flat clone leaf/fringe nodes;
// for *BartNode[V] children, keep shallow references (no recursive clone)
for i, anyKid := range c.Children.Items {
switch kid := anyKid.(type) {
case *BartNode[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 *BartNode[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 BartNode[V] which is a complete deep clone of the
// receiver node with all descendants.
func (n *BartNode[V]) CloneRec(cloneFn func(V) V) *BartNode[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 *BartNode[V]
for i, kidAny := range c.Children.Items {
if kid, ok := kidAny.(*BartNode[V]); ok {
c.Children.Items[i] = kid.CloneRec(cloneFn)
}
}
return c
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+286
View File
@@ -0,0 +1,286 @@
// 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
}
File diff suppressed because it is too large. Load diff
+221
View File
@@ -0,0 +1,221 @@
// Copyright (c) 2026 Karl Gaissmaier
// SPDX-License-Identifier: MIT
package nodes
import (
"iter"
"github.com/gaissmai/bart/internal/bitset"
"github.com/gaissmai/bart/internal/lpm"
"github.com/gaissmai/bart/internal/sparse"
)
// LiteNode is a space-optimized version of [BartNode] that tracks prefix existence
// without storing associated values.
type LiteNode[V any] struct {
Children sparse.Array256[any]
Prefixes struct {
// BitSet256 tracks the presence of prefixes at this level.
bitset.BitSet256
// Count maintains the current number of set bits, updated on modification
// to avoid expensive population counting.
Count uint16
}
}
// 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 *LiteNode[V]) IsEmpty() bool {
if n == nil {
return true
}
return n.Prefixes.Count == 0 && n.Children.Len() == 0
}
// PrefixCount returns the number of prefixes stored in this node.
func (n *LiteNode[V]) PrefixCount() int {
return int(n.Prefixes.Count)
}
// ChildCount returns the number of slots used in this node.
func (n *LiteNode[V]) ChildCount() int {
return n.Children.Len()
}
// InsertPrefix adds a routing entry at the specified index.
// It returns true if a prefix already existed at that index,
// false if this is a new insertion.
func (n *LiteNode[V]) InsertPrefix(idx uint8, _ V) (exists bool) {
if exists = n.Prefixes.Test(idx); exists {
return exists
}
n.Prefixes.Set(idx)
n.Prefixes.Count++
return exists
}
func (n *LiteNode[V]) GetPrefix(idx uint8) (_ V, exists bool) {
// no docstring by intention
exists = n.Prefixes.Test(idx)
return
}
func (n *LiteNode[V]) MustGetPrefix(idx uint8) (_ V) {
// no docstring by intention
return
}
// AllIndices returns an iterator over all prefix entries.
// Each iteration yields the prefix index (uint8) and its associated value (V).
func (n *LiteNode[V]) AllIndices() iter.Seq2[uint8, V] {
var zero V
return func(yield func(uint8, V) bool) {
var buf [256]uint8
for _, idx := range n.Prefixes.AsSlice(&buf) {
if !yield(idx, zero) {
return
}
}
}
}
// DeletePrefix removes the prefix at the specified index.
// Returns true if the prefix existed, and false otherwise.
func (n *LiteNode[V]) DeletePrefix(idx uint8) (exists bool) {
if exists = n.Prefixes.Test(idx); !exists {
return false
}
n.Prefixes.Clear(idx)
n.Prefixes.Count--
return true
}
// InsertChild adds a child node at the specified address (0-255).
// The child can be a *LiteNode[V], *LeafNode, or *FringeNode.
// Returns true if a child already existed at that address.
func (n *LiteNode[V]) InsertChild(addr uint8, child any) (exists bool) {
_, exists = n.Children.InsertAt(addr, child)
return
}
// 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 *LiteNode[V]) GetChild(addr uint8) (any, bool) {
return n.Children.Get(addr)
}
// MustGetChild retrieves the child at the specified address, panicking if not found.
// This method should only be used when the caller is certain the child exists.
func (n *LiteNode[V]) MustGetChild(addr uint8) any {
return n.Children.MustGet(addr)
}
// AllChildren returns an iterator over all child nodes.
// Each iteration yields the child's address (uint8) and the child node (any).
func (n *LiteNode[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
}
}
}
}
// DeleteChild removes the child node at the specified address.
// This operation is idempotent - removing a non-existent child is safe.
func (n *LiteNode[V]) DeleteChild(addr uint8) (exists bool) {
_, exists = n.Children.DeleteAt(addr)
return exists
}
// 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.
//
// 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 *LiteNode[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 index and whether a matching prefix
// exists at this level. The value type parameter exists only to satisfy interfaces.
//
// 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 *LiteNode[V]) LookupIdx(idx uint8) (top uint8, _ V, ok bool) {
top, ok = n.Prefixes.IntersectionTop(&lpm.LookupTbl[idx])
return
}
// Lookup is just a simple wrapper for LookupIdx.
func (n *LiteNode[V]) Lookup(idx uint8) (_ V, ok bool) {
_, _, ok = n.LookupIdx(idx)
return
}
// CloneFlat returns a shallow copy of the current node.
//
// CloneFn is only used for interface satisfaction.
func (n *LiteNode[V]) CloneFlat(_ func(V) V) *LiteNode[V] {
if n == nil {
return nil
}
c := new(LiteNode[V])
if n.IsEmpty() {
return c
}
// copy simple values
c.Prefixes = n.Prefixes
// sparse array
c.Children = *(n.Children.Copy())
// no values to copy
return c
}
// CloneRec performs a recursive deep copy of the node and all its descendants.
//
// cloneFn is only used for interface satisfaction.
//
// It first creates a shallow clone of the current node using CloneFlat.
// Then it recursively clones all child nodes of type *LiteNode[V],
// performing a full deep clone down the subtree.
//
// Child nodes of type *LeafNode and *FringeNode are already copied
// by CloneFlat.
//
// Returns a new instance of LiteNode[V] which is a complete deep clone of the
// receiver node with all descendants.
func (n *LiteNode[V]) CloneRec(_ func(V) V) *LiteNode[V] {
if n == nil {
return nil
}
// Perform a flat clone of the current node.
c := n.CloneFlat(nil)
// Recursively clone all child nodes of type *LiteNode[V]
for i, kidAny := range c.Children.Items {
if kid, ok := kidAny.(*LiteNode[V]); ok {
c.Children.Items[i] = kid.CloneRec(nil)
}
}
return c
}
File diff suppressed because it is too large. Load diff
+337
View File
@@ -0,0 +1,337 @@
// Copyright (c) 2026 Karl Gaissmaier
// SPDX-License-Identifier: MIT
package nodes
import (
"cmp"
"fmt"
"net/netip"
"strconv"
"strings"
"github.com/gaissmai/bart/internal/art"
)
// strideLen represents the byte stride length for the multibit trie.
// Each stride processes 8 bits (1 byte) at a time.
const strideLen = 8
// MaxItems defines the maximum number of prefixes or children that can be stored in a single node.
// This corresponds to 256 possible values for an 8-bit stride.
const MaxItems = 256
// MaxTreeDepth represents the maximum depth of the trie structure.
// For IPv6 addresses, this allows up to 16 bytes of depth.
const MaxTreeDepth = 16
// DepthMask is used for bounds check elimination (BCE) when accessing depth-indexed arrays.
const DepthMask = MaxTreeDepth - 1
// StridePath represents a path through the trie, with a maximum depth of 16 octets for IPv6.
type StridePath [MaxTreeDepth]uint8
// TrieItem, a node has no path information about its predecessors,
// we collect this during the recursive descent.
type TrieItem[V any] struct {
// for traversing, Path/Depth/Idx is needed to get the CIDR back from the trie.
Node any // BartNode, FastNode, LiteNode
Is4 bool
Path StridePath
Depth int
Idx uint8
// for printing
Cidr netip.Prefix
Val V
}
// StatsT, only used for dump, tests and benchmarks
type StatsT struct {
Prefixes int
Children int
SubNodes int
Leaves int
Fringes int
}
type nodeType byte
const (
nullNode nodeType = iota // empty node
fullNode // prefixes and children or path-compressed prefixes
halfNode // no prefixes, only children and path-compressed prefixes
pathNode // only children, no prefix nor path-compressed prefixes
stopNode // no children, only prefixes or path-compressed prefixes
)
// String implements Stringer for nodeType.
func (nt nodeType) String() string {
switch nt {
case nullNode:
return "NULL"
case fullNode:
return "FULL"
case halfNode:
return "HALF"
case pathNode:
return "PATH"
case stopNode:
return "STOP"
default:
return "unreachable"
}
}
// addrFmt, different format strings for IPv4 and IPv6, decimal versus hex.
func addrFmt(addr byte, is4 bool) string {
if is4 {
return fmt.Sprintf("%d", addr)
}
return fmt.Sprintf("0x%02x", addr)
}
// ip stride path, different formats for IPv4 and IPv6, dotted decimal or hex.
//
// 127.0.0
// 2001:0d
func ipStridePath(path StridePath, depth int, is4 bool) string {
buf := new(strings.Builder)
if is4 {
for i, b := range path[:depth] {
if i != 0 {
buf.WriteString(".")
}
buf.WriteString(strconv.Itoa(int(b)))
}
return buf.String()
}
for i, b := range path[:depth] {
if i != 0 && i%2 == 0 {
buf.WriteString(":")
}
fmt.Fprintf(buf, "%02x", b)
}
return buf.String()
}
// CmpPrefix, helper function, compare func for prefix sort,
// all cidrs are already normalized
func CmpPrefix(a, b netip.Prefix) int {
if cmpAddr := a.Addr().Compare(b.Addr()); cmpAddr != 0 {
return cmpAddr
}
return cmp.Compare(a.Bits(), b.Bits())
}
// LeafNode represents a path-compressed routing entry that stores both prefix and value.
// Leaf nodes are used when a prefix doesn't align with trie stride boundaries
// and needs to be stored as a compressed path to save memory.
type LeafNode[V any] struct {
Value V
Prefix netip.Prefix
}
// NewLeafNode creates a new leaf node with the specified prefix and value.
func NewLeafNode[V any](pfx netip.Prefix, val V) *LeafNode[V] {
return &LeafNode[V]{Prefix: pfx, Value: val}
}
// FringeNode represents a path-compressed routing entry that stores only a value.
// The prefix is implicitly defined by the node's position in the trie.
// Fringe nodes are used for prefixes that align exactly with stride boundaries
// (/8, /16, /24, etc.) to save memory by not storing redundant prefix information.
type FringeNode[V any] struct {
Value V
}
// NewFringeNode creates a new fringe node with the specified value.
func NewFringeNode[V any](val V) *FringeNode[V] {
return &FringeNode[V]{Value: val}
}
// IsFringe determines whether a prefix qualifies as a "FringeNode".
// Only prefixes that are stride-aligned (i.e., /8, /16, ..., /128)
// can be fringe-compressed. If these prefixes are inserted at a position
// where depth == (strideCount-1), they are treated as FringeNodes;
// at positions where depth < (strideCount-1), they are treated as LeafNodes.
//
// Example for a stride-aligned prefix like 192.168.1.0/24 (strideCount = 3, modBits = 0):
//
// depth = 3, depth == strideCount : A direct prefix with 0/0 (default route for subtrie).
// depth = 2, depth == (strideCount-1) : A path-compressed fringe.
// depth < 2, depth < (strideCount-1) : A path-compressed leaf.
func IsFringe(depth int, pfxLen int) bool {
strideCount, modBits := DivMod8(pfxLen)
return depth == strideCount-1 && modBits == 0
}
// CmpIndexRank, sort indexes in prefix sort order.
func CmpIndexRank(aIdx, bIdx uint8) int {
// convert idx [1..255] to prefix
aOctet, aBits := art.IdxToPfx(aIdx)
bOctet, bBits := art.IdxToPfx(bIdx)
// cmp the prefixes, first by address and then by bits
if aOctet == bOctet {
return cmp.Compare(aBits, bBits)
}
return cmp.Compare(aOctet, bOctet)
}
// CidrFromPath reconstructs a CIDR prefix from a stride path, depth, and index.
// The prefix is determined by the node's position in the trie and the base index
// from the ART algorithm's complete binary tree representation.
//
// Parameters:
// - path: The stride path through the trie
// - depth: Current depth in the trie
// - is4: True for IPv4 processing, false for IPv6
// - idx: The base index from the prefix table
//
// Returns the reconstructed netip.Prefix.
func CidrFromPath(path StridePath, depth int, is4 bool, idx uint8) netip.Prefix {
depth &= DepthMask // BCE
// retrieve the last octet and pfxLen
octet, pfxLen := art.IdxToPfx(idx)
// set byte in path at depth with last octet
path[depth] = octet
// canonicalize
clear(path[depth+1:])
// make ip addr from octets
var ip netip.Addr
if is4 {
ip = netip.AddrFrom4([4]byte(path[:4]))
} else {
ip = netip.AddrFrom16(path)
}
// calc bits with pathLen and pfxLen
bits := depth<<3 + int(pfxLen)
// PrefixFrom does not allocate and does not mask off the host bits of ip.
// With the clear(), the non-canonical bytes have already been removed.
return netip.PrefixFrom(ip, bits)
}
// CidrForFringe reconstructs a CIDR prefix for a fringe node from the traversal path.
// Since fringe nodes don't store their prefix explicitly, it's derived entirely
// from the node's position in the trie and its final byte value.
//
// Parameters:
// - octets: The path of previous bytes leading up to the fringe.
// - depth: Current depth in the trie (which equals strideCount - 1).
// - is4: True for IPv4 processing, false for IPv6.
// - fringeByte: The actual 8-bit value (0-255) of the prefix at this final stride.
//
// Returns the reconstructed netip.Prefix for the fringe.
func CidrForFringe(octets []byte, depth int, is4 bool, fringeByte uint8) netip.Prefix {
depth &= DepthMask // BCE
var path StridePath
copy(path[:], octets)
path[depth] = fringeByte
// canonicalize, fringe bit boundaries are always a multiple of a byte
clear(path[depth+1:])
// make ip addr from octets
var ip netip.Addr
if is4 {
ip = netip.AddrFrom4([4]byte(path[:4]))
} else {
ip = netip.AddrFrom16(path)
}
// it's a fringe, bits are always aligned on stride boundaries (/8, /16, /24, ...)
bits := (depth + 1) << 3
// PrefixFrom does not allocate and does not mask off the host bits of ip.
// With the clear(), the non-canonical bytes have already been removed.
return netip.PrefixFrom(ip, bits)
}
// DivMod8 returns the count of full 8‑bit strides (bits/8)
// and the remaining bits in the final stride (bits%8) for pfxLen.
//
// ATTENTION: Split the IP prefixes at 8-bit borders, count from 0.
//
// /7, /15, /23, /31, ..., /127
//
// BitPos: [0-7],[8-15],[16-23],[24-31],[32]
// BitPos: [0-7],[8-15],[16-23],[24-31],[32-39],[40-47],[48-55],[56-63],...,[120-127],[128]
//
// 0.0.0.0/0 => strideCount: 0, modBits: 0 (default route)
// 0.0.0.0/7 => strideCount: 0, modBits: 7
// 0.0.0.0/8 => strideCount: 1, modBits: 0 (fringe candidate)
// 10.0.0.0/8 => strideCount: 1, modBits: 0 (fringe candidate)
// 10.0.0.0/22 => strideCount: 2, modBits: 6
// 10.0.0.0/29 => strideCount: 3, modBits: 5
// 10.0.0.0/32 => strideCount: 4, modBits: 0 (fringe candidate)
//
// ::/0 => strideCount: 0, modBits: 0 (default route)
// ::1/128 => strideCount: 16, modBits: 0 (fringe candidate)
// 2001:db8::/42 => strideCount: 5, modBits: 2
// 2001:db8::/56 => strideCount: 7, modBits: 0 (fringe candidate)
//
// /32 and /128 prefixes are special, they never form a new node,
// At the end of the trie (IPv4: depth 4, IPv6: depth 16) they are always
// inserted as a path‑compressed fringe.
//
// We are not splitting at /8, /16, ..., because this would mean that the
// first node would have 512 prefixes, 9 bits from [0-8]. All remaining nodes
// would then only have 8 bits from [9-16], [17-24], [25..32], ...
// but the algorithm would then require a variable length bitset
// or imply a double-sized bitset.
//
// If you can commit to a fixed size of [4]uint64, then the algorithm is
// much faster due to modern CPUs.
//
// Perhaps a future Go version that supports SIMD instructions for the [4]uint64 vectors
// will make the algorithm even faster on suitable hardware.
func DivMod8(pfxLen int) (strideCount int, modBits uint8) {
// strideCount: range from 0..4 or 0..16
// modBits: range from 0..7
return pfxLen >> 3, uint8(pfxLen & 7)
}
// CloneLeaf creates and returns a copy of the leafNode receiver.
// If cloneFn is nil, the value is copied directly without modification.
// Otherwise, cloneFn is applied to the value for deep cloning.
// The prefix field is always copied as is.
func (l *LeafNode[V]) CloneLeaf(cloneFn func(V) V) *LeafNode[V] {
if l == nil {
return nil
}
if cloneFn == nil {
return &LeafNode[V]{Prefix: l.Prefix, Value: l.Value}
}
return &LeafNode[V]{Prefix: l.Prefix, Value: cloneFn(l.Value)}
}
// CloneFringe creates and returns a copy of the FringeNode receiver.
// If cloneFn is nil, the value is copied directly without modification.
// Otherwise, cloneFn is applied to the value for deep cloning.
func (l *FringeNode[V]) CloneFringe(cloneFn func(V) V) *FringeNode[V] {
if l == nil {
return nil
}
if cloneFn == nil {
return &FringeNode[V]{Value: l.Value}
}
return &FringeNode[V]{Value: cloneFn(l.Value)}
}