Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+265
@@ -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
|
||||
}
|
||||
+2427
File diff suppressed because it is too large.
Load diff
+2461
File diff suppressed because it is too large.
Load diff
+1266
File diff suppressed because it is too large.
Load diff
+286
@@ -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
|
||||
}
|
||||
+2427
File diff suppressed because it is too large.
Load diff
+221
@@ -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
|
||||
}
|
||||
+2427
File diff suppressed because it is too large.
Load diff
+337
@@ -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)}
|
||||
}
|
||||
Reference in new issue
Block a user