Files
tsnet-proxy/vendor/github.com/gaissmai/bart/internal/nodes/litemethodsgenerated.go
T
2026-10-22 12:27:19 +01:00

2428 lines
72 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Code generated from file "commonmethods_tmpl.go"; DO NOT EDIT.
// Copyright (c) 2026 Karl Gaissmaier
// SPDX-License-Identifier: MIT
package nodes
import (
"fmt"
"io"
"net/netip"
"slices"
"strings"
"github.com/gaissmai/bart/internal/allot"
"github.com/gaissmai/bart/internal/art"
"github.com/gaissmai/bart/internal/value"
)
// Insert adds or updates a network prefix and its associated value in the trie.
// Traversal begins at the specified byte depth.
//
// The trie utilizes path compression to conserve memory. A prefix is inserted:
// - Uncompressed into a node's prefix table at depth == strideCount.
// - As a path-compressed FringeNode if it qualifies as fringe [IsFringe].
// - As a path-compressed LeafNode otherwise.
//
// When a new prefix collides with an existing compressed node (Leaf or Fringe),
// Insert resolves the collision by creating a new intermediate node, pushing
// the existing entry down to the next level, and continuing traversal.
//
// Parameters:
// - pfx: The network prefix to insert (must be in canonical/masked form).
// - val: The value to associate with the prefix.
// - depth: The current depth in the trie (0-based byte index).
//
// Returns true if an existing prefix was updated, false if a new insertion occurred.
func (n *LiteNode[V]) Insert(pfx netip.Prefix, val V, depth int) (exists bool) {
ip := pfx.Addr() // the pfx must be in canonical form
pfxLen := pfx.Bits()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// Traverse the prefix's octets. Each depth corresponds to an 8-bit stride.
// We descend through the trie until we either reach the final stride (depth == strideCount)
// or find an empty child slot where we can path-compress the remaining strides.
for ; depth < len(octets); depth++ {
octet := octets[depth]
// The current depth matches the prefix's stride count, meaning this is the final
// node for this prefix. We insert it directly into this node's prefix table.
if depth == strideCount {
return n.InsertPrefix(art.PfxToIdx(octet, modBits), val)
}
// No child exists at this octet path. Instead of creating intermediate nodes
// for the remaining strides, we path-compress the rest of the prefix into a single child slot.
if !n.Children.Test(octet) {
// If the prefix is perfectly aligned with the next stride boundary (e.g., /16 at depth 1),
// it acts as a default route for everything below it. We store it as a FringeNode.
// Otherwise, it has trailing bits or crosses boundaries, so we store it as a LeafNode.
if IsFringe(depth, pfxLen) {
return n.InsertChild(octet, NewFringeNode(val))
}
return n.InsertChild(octet, NewLeafNode(pfx, val))
}
// A child already exists at this octet path. Retrieve it to either continue
// our descent along the strides or resolve a structural collision with a compressed node.
kid := n.MustGetChild(octet)
switch kid := kid.(type) {
case *LiteNode[V]:
// Standard intermediate node: descend to the next trie level.
n = kid
case *LeafNode[V]:
// Collision with an existing path-compressed LeafNode.
// If it's the exact same prefix, simply update the value.
if kid.Prefix == pfx {
kid.Value = val
return true
}
// Collision resolution: the paths diverge.
// 1. Create a new intermediate node.
// 2. Push the existing leaf down into this new node.
// 3. Replace the current child slot with the new node.
// 4. Descend into the new node to continue inserting 'pfx'.
newNode := new(LiteNode[V])
newNode.Insert(kid.Prefix, kid.Value, depth+1)
n.InsertChild(octet, newNode)
n = newNode
case *FringeNode[V]:
// Collision with an existing path-compressed FringeNode.
// If the incoming prefix is also a fringe at this depth, update the value.
if IsFringe(depth, pfxLen) {
kid.Value = val
return true
}
// Collision resolution:
// The existing FringeNode acts as a catch-all (default route) for this sub-trie.
// To allow the incoming prefix to branch further, we expand the FringeNode
// into a full intermediate node and place its value at the default route index (1).
newNode := new(LiteNode[V])
newNode.InsertPrefix(1, kid.Value)
n.InsertChild(octet, newNode)
n = newNode
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// InsertPersist adds or updates a network prefix and its associated value in the trie
// using Copy-On-Write (COW) semantics. Traversal begins at the specified byte depth.
//
// Unlike [Insert], InsertPersist ensures structural integrity of the existing tree
// by cloning internal nodes along the descent path (Copy-On-Write) before mutation.
//
// Parameters:
// - cloneFn: The function used to clone values (V).
// - pfx: The network prefix to insert (must be in canonical/masked form).
// - val: The value to associate with the prefix.
// - depth: The current depth in the trie (0-based byte index).
//
// Returns true if an existing prefix was updated, false if a new insertion occurred.
func (n *LiteNode[V]) InsertPersist(cloneFn func(V) V, pfx netip.Prefix, val V, depth int) (exists bool) {
ip := pfx.Addr() // the pfx must be in canonical form
pfxLen := pfx.Bits()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// Traverse the prefix's octets. Each depth corresponds to an 8-bit stride.
// We descend through the trie until we either reach the final stride (depth == strideCount)
// or find an empty child slot where we can path-compress the remaining strides.
for ; depth < len(octets); depth++ {
octet := octets[depth]
// The current depth matches the prefix's stride count, meaning this is the final
// node for this prefix. We insert it directly into this node's prefix table.
if depth == strideCount {
return n.InsertPrefix(art.PfxToIdx(octet, modBits), val)
}
// No child exists at this octet path. Instead of creating intermediate nodes
// for the remaining strides, we path-compress the rest of the prefix into a single child slot.
if !n.Children.Test(octet) {
// If the prefix is perfectly aligned with the next stride boundary (e.g., /16 at depth 1),
// it acts as a default route for everything below it. We store it as a FringeNode.
// Otherwise, it has trailing bits or crosses boundaries, so we store it as a LeafNode.
if IsFringe(depth, pfxLen) {
return n.InsertChild(octet, NewFringeNode(val))
}
return n.InsertChild(octet, NewLeafNode(pfx, val))
}
// A child already exists at this octet path. Retrieve it to either continue
// our descent along the strides or resolve a structural collision with a compressed node.
kid := n.MustGetChild(octet)
switch kid := kid.(type) {
case *LiteNode[V]:
// Standard intermediate node: Clone the traversed path to maintain persistence (COW).
// We clone the child node before modifying it, then replace the current child slot.
kid = kid.CloneFlat(cloneFn)
n.InsertChild(octet, kid)
n = kid
case *LeafNode[V]:
// Collision with an existing path-compressed LeafNode.
// If it's the exact same prefix, simply update the value.
if kid.Prefix == pfx {
kid.Value = val
return true
}
// Collision resolution: the paths diverge.
// 1. Create a new intermediate node.
// 2. Push the existing leaf down into this new node.
// 3. Replace the current child slot with the new node.
// 4. Descend into the new node to continue inserting 'pfx'.
newNode := new(LiteNode[V])
newNode.Insert(kid.Prefix, kid.Value, depth+1)
n.InsertChild(octet, newNode)
n = newNode
case *FringeNode[V]:
// Collision with an existing path-compressed FringeNode.
// If the incoming prefix is also a fringe at this depth, update the value.
if IsFringe(depth, pfxLen) {
kid.Value = val
return true
}
// Collision resolution:
// The existing FringeNode acts as a catch-all (default route) for this sub-trie.
// To allow the incoming prefix to branch further, we expand the FringeNode
// into a full intermediate node and place its value at the default route index (1).
newNode := new(LiteNode[V])
newNode.InsertPrefix(1, kid.Value)
n.InsertChild(octet, newNode)
n = newNode
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// PurgeAndCompress performs bottom-up trie maintenance to restore path compression
// after a deletion. It unwinds the provided stack of parent nodes, identifying
// nodes that have become sparse (i.e., containing only a single prefix or a single
// child node) and prunes them by promoting the underlying entries to the parent level.
//
// This ensures the trie remains memory-efficient by collapsing redundant intermediate
// nodes back into path-compressed LeafNodes or FringeNodes whenever possible.
//
// Parameters:
// - stack: Array of parent nodes to process during bottom-up unwinding.
// - octets: The full path of octets leading to the current node.
// - is4: True for IPv4 processing, false for IPv6.
func (n *LiteNode[V]) PurgeAndCompress(stack []*LiteNode[V], octets []uint8, is4 bool) {
// Iterate backwards through the ancestor stack to prune nodes from the bottom up.
for depth := len(stack) - 1; depth >= 0; depth-- {
parent := stack[depth]
octet := octets[depth]
// Check if the current node is redundant.
// A node may be redundant if it contains exactly one entry (either a prefix or
// a compressed child node).
pfxCount := n.PrefixCount()
childCount := n.ChildCount()
// If it contains more than one entry, it is always structurally significant and cannot be pruned.
if pfxCount+childCount > 1 {
return
}
switch {
case childCount == 1:
// The node has exactly one child. We determine if it is a path node,
// a compressed LeafNode, or a compressed FringeNode.
singleAddr, _ := n.Children.FirstSet()
anyKid := n.MustGetChild(singleAddr)
switch kid := anyKid.(type) {
case *LiteNode[V]:
// The child is an intermediate path node; the tree structure is required
// at this level. Compression cannot proceed further up.
return
case *LeafNode[V]:
// The child is a compressed LeafNode. Prune the current node
// and re-insert the leaf into the parent to elevate it.
parent.DeleteChild(octet)
parent.Insert(kid.Prefix, kid.Value, depth)
case *FringeNode[V]:
// The child is a compressed FringeNode. Prune the current node
// and re-insert the fringe as a leaf into the parent.
parent.DeleteChild(octet)
// Reconstruct the full prefix for the fringe, as path compression
// requires the entire CIDR path, not just the remainder.
// depth is the parent's depth, so we offset by 1 for the kid's position.
fringePfx := CidrForFringe(octets, depth+1, is4, singleAddr)
parent.Insert(fringePfx, kid.Value, depth)
}
case pfxCount == 1:
// The node has exactly one prefix. Prune the node and elevate the
// prefix to the parent level as a leaf/fringe.
parent.DeleteChild(octet)
// Retrieve the single prefix stored in this node.
idx, _ := n.Prefixes.FirstSet()
val := n.MustGetPrefix(idx)
// Reconstruct the prefix from the path for re-insertion.
path := StridePath{}
copy(path[:], octets)
pfx := CidrFromPath(path, depth+1, is4, idx)
parent.Insert(pfx, val, depth)
default:
panic("unreachable")
}
// Move up to the next parent in the stack to continue pruning.
n = parent
}
}
// Delete removes the prefix from the trie rooted at n and returns true if the
// prefix existed, false if it was not found. The prefix must be in canonical
// (masked) form.
//
// The trie uses path compression, so a prefix may be stored in one of three ways:
// - In the current node's prefix table when the prefix length aligns exactly
// with the stride boundary at this depth (depth == strideCount).
// - As a path-compressed FringeNode in a child slot for stride-aligned prefixes
// (e.g. /8, /16, /24); occurs at depth == strideCount-1.
// - As a path-compressed LeafNode in a child slot for prefixes otherwise.
//
// After a successful deletion, PurgeAndCompress walks the ancestor stack to prune
// now-empty nodes and restore path compression upward.
func (n *LiteNode[V]) Delete(pfx netip.Prefix) (exists bool) {
ip := pfx.Addr() // pfx must be in canonical (masked) form
pfxLen := pfx.Bits()
is4 := ip.Is4()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// Record ancestor nodes as we descend; PurgeAndCompress uses this stack to
// walk back up and clean up empty or re-compressible nodes after deletion.
stack := [MaxTreeDepth]*LiteNode[V]{}
for depth, octet := range octets {
depth &= DepthMask // BCE hint; keep Delete on the fast path
stack[depth] = n // record current node before descending
// At the stride boundary, the prefix is stored directly in this node's
// prefix table.
if depth == strideCount {
if exists = n.DeletePrefix(art.PfxToIdx(octet, modBits)); !exists {
return false
}
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
}
// no child node exists at this octet; the prefix is not in the trie
if !n.Children.Test(octet) {
return false
}
// A child node exists at this octet path, retrieve it.
kid := n.MustGetChild(octet)
switch kid := kid.(type) {
case *LiteNode[V]:
n = kid // descend to the next trie level
case *FringeNode[V]:
// A FringeNode holds a single stride-aligned prefix (/8, /16, ...).
// If pfx does not qualify as a fringe at this depth, it cannot be here.
if !IsFringe(depth, pfxLen) {
return false
}
n.DeleteChild(octet)
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
case *LeafNode[V]:
// A LeafNode holds exactly one path-compressed prefix.
// Compare using the canonical (masked) form for an exact match.
if kid.Prefix != pfx {
return false
}
n.DeleteChild(octet)
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// DeletePersist removes the prefix from the trie rooted at n using Copy-On-Write (COW) semantics.
// It returns true if the prefix existed, false if it was not found. The prefix must be in
// canonical (masked) form.
//
// Like [Delete], this method uses path compression. However, DeletePersist ensures
// the structural integrity of the existing tree by cloning internal nodes along the
// descent path (COW) before mutation.
//
// After a successful deletion, PurgeAndCompress walks the ancestor stack to prune
// now-empty nodes and restore path compression upward.
func (n *LiteNode[V]) DeletePersist(cloneFn func(V) V, pfx netip.Prefix) (exists bool) {
ip := pfx.Addr() // pfx must be in canonical (masked) form
pfxLen := pfx.Bits()
is4 := ip.Is4()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// Record ancestor nodes as we descend; PurgeAndCompress uses this stack to
// walk back up and clean up empty or re-compressible nodes after deletion.
// Since this is a COW operation, we store the cloned nodes here.
stack := [MaxTreeDepth]*LiteNode[V]{}
for depth, octet := range octets {
depth &= DepthMask // BCE hint; keep DeletePersist on the fast path
stack[depth] = n // record current node before descending
// At the stride boundary, the prefix is stored directly in this node's
// prefix table.
if depth == strideCount {
if exists = n.DeletePrefix(art.PfxToIdx(octet, modBits)); !exists {
return false
}
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
}
// no child node exists at this octet; the prefix is not in the trie
if !n.Children.Test(octet) {
return false
}
// A child node exists at this octet path, retrieve it to either continue
// our descent or perform a persistent delete on a compressed node.
kid := n.MustGetChild(octet)
switch kid := kid.(type) {
case *LiteNode[V]:
// Standard intermediate node: Clone the traversed path to maintain
// persistence (COW). We clone the child node before modifying it,
// then replace the current child slot.
kid = kid.CloneFlat(cloneFn)
n.InsertChild(octet, kid)
n = kid
continue
case *FringeNode[V]:
// A FringeNode holds a single stride-aligned prefix (/8, /16, ...).
// If pfx does not qualify as a fringe at this depth, it cannot be here.
if !IsFringe(depth, pfxLen) {
return false
}
n.DeleteChild(octet)
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
case *LeafNode[V]:
// A LeafNode holds exactly one path-compressed prefix.
// Compare using the canonical (masked) form for an exact match.
if kid.Prefix != pfx {
return false
}
n.DeleteChild(octet)
// prune now-empty nodes and re-compress the path upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return true
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// Get retrieves the value associated with the given network prefix.
// Traversal descends through the trie using the prefix's octets.
//
// The lookup handles path compression transparently:
// - If the path matches an internal node at the target stride, the value is retrieved
// from the node's prefix table.
// - If the path leads to a compressed LeafNode or FringeNode, the function verifies
// the prefix match before returning the value.
//
// Parameters:
// - pfx: The network prefix to look up (must be in canonical form).
//
// Returns:
// - val: The value associated with the prefix (zero value if not found).
// - exists: True if the prefix was found, false otherwise.
func (n *LiteNode[V]) Get(pfx netip.Prefix) (val V, exists bool) {
// The prefix must be provided in canonical (masked) form for correct trie traversal.
ip := pfx.Addr()
pfxLen := pfx.Bits()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// Traverse the trie octet by octet based on the prefix path.
for depth, octet := range octets {
// At the target stride boundary, the prefix is expected in this node's
// prefix table.
if depth == strideCount {
return n.GetPrefix(art.PfxToIdx(octet, modBits))
}
// If no child exists at this path, the prefix is not in the trie.
kidAny, ok := n.GetChild(octet)
if !ok {
return val, false
}
// Identify the node type at this path segment.
switch kid := kidAny.(type) {
case *LiteNode[V]:
// Standard intermediate node: descend to the next level.
n = kid
case *FringeNode[V]:
// Reached a path-compressed FringeNode.
// Verify if the prefix qualifies as a fringe at this depth to return a match.
if IsFringe(depth, pfxLen) {
return kid.Value, true
}
return val, false
case *LeafNode[V]:
// Reached a path-compressed LeafNode.
// Check if the stored prefix matches the lookup prefix exactly.
if kid.Prefix == pfx {
return kid.Value, true
}
return val, false
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// Modify performs an in-place modification of a prefix using the provided callback function.
// The callback receives the current value (if found) and existence flag, and returns
// a new value and deletion flag.
//
// Modify returns the size delta (-1, 0, +1).
// This method handles path traversal, node creation for new paths, and automatic
// purge/compress operations after deletions.
//
// Parameters:
// - pfx: The network prefix to modify (must be in canonical form)
// - cb: Callback function that receives (currentValue, exists) and returns (newValue, deleteFlag)
//
// Returns:
// - delta: Size change (-1 for delete, 0 for update/noop, +1 for insert)
func (n *LiteNode[V]) Modify(pfx netip.Prefix, cb func(val V, found bool) (_ V, del bool)) (delta int) {
var zero V
ip := pfx.Addr()
pfxLen := pfx.Bits()
is4 := ip.Is4()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// record the nodes on the path to the deleted node, needed to purge
// and/or path compress nodes after the deletion of a prefix
stack := [MaxTreeDepth]*LiteNode[V]{}
// find the proper trie node to update prefix
for depth, octet := range octets {
depth &= DepthMask // BCE
// push current node on stack for path recording
stack[depth] = n
// The current depth matches the prefix's stride count, meaning this is the final
// stride for this prefix. Insert or update it directly in this node's prefix table.
if depth == strideCount {
idx := art.PfxToIdx(octet, modBits)
oldVal, existed := n.GetPrefix(idx)
newVal, del := cb(oldVal, existed)
// update size if necessary
switch {
case !existed && del: // no-op
return 0
case existed && del: // delete
n.DeletePrefix(idx)
// remove now-empty nodes and re-path-compress upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return -1
case !existed: // insert
n.InsertPrefix(idx, newVal)
return 1
case existed: // update
n.InsertPrefix(idx, newVal)
return 0
default:
panic("unreachable")
}
}
// go down in tight loop to last octet
if !n.Children.Test(octet) {
// insert prefix path compressed
newVal, del := cb(zero, false)
if del {
return 0
}
// insert
if IsFringe(depth, pfxLen) {
n.InsertChild(octet, NewFringeNode(newVal))
} else {
n.InsertChild(octet, NewLeafNode(pfx, newVal))
}
return 1
}
// n.children.Test(octet) == true
kid := n.MustGetChild(octet)
// kid is node or leaf or fringe at octet
switch kid := kid.(type) {
case *LiteNode[V]:
n = kid // descend down to next trie level
continue
case *LeafNode[V]:
oldVal := kid.Value
// update existing value if prefixes are equal
if kid.Prefix == pfx {
newVal, del := cb(oldVal, true)
if !del {
kid.Value = newVal
return 0
}
// delete
n.DeleteChild(octet)
// remove now-empty nodes and re-path-compress upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return -1
}
// stop if this is a no-op for zero values
newVal, del := cb(zero, false)
if del {
return 0
}
// create new node
// insert new child at current leaf position (octet)
newNode := new(LiteNode[V])
n.InsertChild(octet, newNode)
// push the leaf down
// insert pfx with newVal in new node
newNode.Insert(kid.Prefix, kid.Value, depth+1)
newNode.Insert(pfx, newVal, depth+1)
return 1
case *FringeNode[V]:
// update existing value if prefix is fringe
if IsFringe(depth, pfxLen) {
newVal, del := cb(kid.Value, true)
if !del {
kid.Value = newVal
return 0
}
// delete
n.DeleteChild(octet)
// remove now-empty nodes and re-path-compress upwards
n.PurgeAndCompress(stack[:depth], octets, is4)
return -1
}
// stop if this is a no-op for zero values
newVal, del := cb(zero, false)
if del {
return 0
}
// create new node
// insert new child at current leaf position (octet)
newNode := new(LiteNode[V])
n.InsertChild(octet, newNode)
// push the fringe down, it becomes a default route (idx=1)
// insert pfx with newVal in new node
newNode.InsertPrefix(1, kid.Value)
newNode.Insert(pfx, newVal, depth+1)
return 1
default:
panic("logic error, wrong node type")
}
}
panic("unreachable")
}
// EqualRec performs recursive structural equality comparison between two nodes.
// Compares prefix and child bitsets, then recursively compares all stored values
// and child nodes. Returns true if the nodes and their entire subtrees are
// structurally and semantically identical, false otherwise.
//
// The comparison handles different node types (internal nodes, leafNodes, fringeNodes)
// and uses the equal function for value comparisons to support custom equality logic.
func (n *LiteNode[V]) EqualRec(o *LiteNode[V]) bool {
if n == nil || o == nil {
return n == o
}
if n == o {
return true
}
if n.Prefixes.BitSet256 != o.Prefixes.BitSet256 {
return false
}
if n.Children.BitSet256 != o.Children.BitSet256 {
return false
}
for idx, nVal := range n.AllIndices() {
oVal := o.MustGetPrefix(idx) // mustGet is ok, bitsets are equal
if !value.Equal(nVal, oVal) {
return false
}
}
for addr, nKid := range n.AllChildren() {
oKid := o.MustGetChild(addr) // mustGet is ok, bitsets are equal
switch nKid := nKid.(type) {
case *LiteNode[V]:
// oKid must also be a node
oKid, ok := oKid.(*LiteNode[V])
if !ok {
return false
}
// compare rec-descent
if !nKid.EqualRec(oKid) {
return false
}
case *LeafNode[V]:
// oKid must also be a leaf
oKid, ok := oKid.(*LeafNode[V])
if !ok {
return false
}
// compare prefixes
if nKid.Prefix != oKid.Prefix {
return false
}
// compare values
if !value.Equal(nKid.Value, oKid.Value) {
return false
}
case *FringeNode[V]:
// oKid must also be a fringe
oKid, ok := oKid.(*FringeNode[V])
if !ok {
return false
}
// compare values
if !value.Equal(nKid.Value, oKid.Value) {
return false
}
default:
panic("logic error, wrong node type")
}
}
return true
}
// DumpRec recursively descends the trie rooted at n and writes a human-readable
// representation of each visited node to w.
//
// It returns immediately if n is nil or empty. For each visited internal node
// it calls dump to write the node's representation, then iterates its child
// addresses and recurses into children of type *LiteNode[V] (internal subnodes).
// The path slice and depth together represent the byte-wise path
// from the root to the current node; depth is incremented for each recursion.
// The is4 flag controls IPv4/IPv6 formatting used by dump.
func (n *LiteNode[V]) DumpRec(w io.Writer, path StridePath, depth int, is4 bool) {
if n == nil || n.IsEmpty() {
return
}
// dump this node
n.dump(w, path, depth, is4)
// node may have children, rec-descent down
for addr, child := range n.AllChildren() {
if kid, ok := child.(*LiteNode[V]); ok {
path[depth] = addr
kid.DumpRec(w, path, depth+1, is4)
}
}
}
// dump writes a human-readable representation of the node to `w`.
// It prints the node type, depth, formatted path (IPv4 vs IPv6 controlled by `is4`),
// and bit count, followed by any stored prefixes (and their values when applicable),
// the set of child octets, and any path-compressed leaves or fringe entries.
func (n *LiteNode[V]) dump(w io.Writer, path StridePath, depth int, is4 bool) {
bits := depth * strideLen
indent := strings.Repeat(".", depth)
// printing values if V is not the empty struct{}
printValues := !value.IsEmptyStruct[V]()
// node type with depth and octet path and bits.
fmt.Fprintf(w, "\n%s[%s] depth: %d path: [%s] / %d\n",
indent, n.hasType(), depth, ipStridePath(path, depth, is4), bits)
if nPfxCount := n.PrefixCount(); nPfxCount != 0 {
var buf [256]uint8
allIndices := n.Prefixes.AsSlice(&buf)
// print the baseIndices for this node.
fmt.Fprintf(w, "%sindexs(#%d): %v\n", indent, nPfxCount, allIndices)
// print the prefixes for this node
fmt.Fprintf(w, "%sprefxs(#%d):", indent, nPfxCount)
for _, idx := range allIndices {
pfx := CidrFromPath(path, depth, is4, idx)
fmt.Fprintf(w, " %s", pfx)
}
fmt.Fprintln(w)
// skip printing values if V is zero-sized
if printValues {
// print the values for this node
fmt.Fprintf(w, "%svalues(#%d):", indent, nPfxCount)
for _, idx := range allIndices {
val := n.MustGetPrefix(idx)
fmt.Fprintf(w, " %#v", val)
}
fmt.Fprintln(w)
}
}
if cc := n.ChildCount(); cc != 0 {
allAddrs := make([]uint8, 0, cc)
childAddrs := make([]uint8, 0, cc)
leafAddrs := make([]uint8, 0, cc)
fringeAddrs := make([]uint8, 0, cc)
// the node has recursive child nodes or path-compressed leaves
for addr, child := range n.AllChildren() {
allAddrs = append(allAddrs, addr)
switch child.(type) {
case *LiteNode[V]:
childAddrs = append(childAddrs, addr)
continue
case *FringeNode[V]:
fringeAddrs = append(fringeAddrs, addr)
case *LeafNode[V]:
leafAddrs = append(leafAddrs, addr)
default:
panic("logic error, wrong node type")
}
}
// print the children for this node.
fmt.Fprintf(w, "%soctets(#%d): %v\n", indent, len(allAddrs), allAddrs)
if leafCount := len(leafAddrs); leafCount > 0 {
// print the pathcomp prefixes for this node
fmt.Fprintf(w, "%sleaves(#%d):", indent, leafCount)
for _, addr := range leafAddrs {
kid := n.MustGetChild(addr).(*LeafNode[V])
// skip printing values if V is zero-sized
if printValues {
fmt.Fprintf(w, " %s:{%s, %v}", addrFmt(addr, is4), kid.Prefix, kid.Value)
} else {
fmt.Fprintf(w, " %s:{%s}", addrFmt(addr, is4), kid.Prefix)
}
}
fmt.Fprintln(w)
}
if fringeCount := len(fringeAddrs); fringeCount > 0 {
// print the pathcomp prefixes for this node
fmt.Fprintf(w, "%sfringe(#%d):", indent, fringeCount)
for _, addr := range fringeAddrs {
fringePfx := CidrForFringe(path[:], depth, is4, addr)
kid := n.MustGetChild(addr).(*FringeNode[V])
// skip printing values if V is zero-sized
if printValues {
fmt.Fprintf(w, " %s:{%s, %v}", addrFmt(addr, is4), fringePfx, kid.Value)
} else {
fmt.Fprintf(w, " %s:{%s}", addrFmt(addr, is4), fringePfx)
}
}
fmt.Fprintln(w)
}
if childCount := len(childAddrs); childCount > 0 {
// print the next child
fmt.Fprintf(w, "%schilds(#%d):", indent, childCount)
for _, addr := range childAddrs {
fmt.Fprintf(w, " %s", addrFmt(addr, is4))
}
fmt.Fprintln(w)
}
}
}
// DumpString traverses the trie to the node at the specified depth along the given
// octet path and returns its string representation via Dump.
//
// If the path is invalid or encounters an unexpected node type during traversal,
// it returns an error message string instead.
//
// Parameters:
// - octets: The path of octets to follow from the root
// - depth: Target depth to reach before dumping (0-based byte index)
// - is4: True for IPv4 formatting, false for IPv6
//
// Returns a formatted string representation of the target node or an error message.
func (n *LiteNode[V]) DumpString(octets []uint8, depth int, is4 bool) string {
path := StridePath{}
copy(path[:], octets)
buf := new(strings.Builder)
for i := range depth {
anyKid, ok := n.GetChild(path[i])
if !ok {
return fmt.Sprintf("ERROR: kid for %v[%d] is NOT set in node\n", octets, i)
}
kid, ok := anyKid.(*LiteNode[V])
if !ok {
return fmt.Sprintf("ERROR: kid for %v[%d] is NO %s\n", octets, i, "LiteNode[V]")
}
// traverse
n = kid
}
n.dump(buf, path, depth, is4)
return buf.String()
}
// hasType classifies the given node into one of the nodeType values.
//
// It inspects immediate statistics (prefix count, child count, node, leaf and
// fringe counts) for the node and returns:
// - nullNode: no prefixes and no children
// - stopNode: has children but no subnodes (nodes == 0)
// - halfNode: contains at least one leaf or fringe and also has subnodes, but
// no prefixes
// - fullNode: has prefixes and also has subnodes
// - pathNode: has subnodes only (no prefixes, leaves, or fringes)
//
// The order of these checks is significant to ensure the correct classification.
func (n *LiteNode[V]) hasType() nodeType {
s := n.Stats()
// the order is important
switch {
case s.Prefixes == 0 && s.Children == 0:
return nullNode
case s.SubNodes == 0:
return stopNode
case (s.Leaves > 0 || s.Fringes > 0) && s.SubNodes > 0 && s.Prefixes == 0:
return halfNode
case (s.Prefixes > 0 || s.Leaves > 0 || s.Fringes > 0) && s.SubNodes > 0:
return fullNode
case (s.Prefixes == 0 && s.Leaves == 0 && s.Fringes == 0) && s.SubNodes > 0:
return pathNode
default:
panic(fmt.Sprintf("UNREACHABLE: pfx: %d, chld: %d, node: %d, leaf: %d, fringe: %d",
s.Prefixes, s.Children, s.SubNodes, s.Leaves, s.Fringes))
}
}
// Stats returns immediate statistics for n: counts of prefixes and children,
// and a classification of each child into nodes, leaves, or fringes.
// It inspects only the direct children of n (not the whole subtree).
// Panics if a child has an unexpected concrete type.
func (n *LiteNode[V]) Stats() (s StatsT) {
s.Prefixes = n.PrefixCount()
s.Children = n.ChildCount()
for _, child := range n.AllChildren() {
switch child.(type) {
case *LiteNode[V]:
s.SubNodes++
case *FringeNode[V]:
s.Fringes++
case *LeafNode[V]:
s.Leaves++
default:
panic("logic error, wrong node type")
}
}
return s
}
// StatsRec returns aggregated statistics for the subtree rooted at n.
//
// It walks the node tree recursively and sums immediate counts (prefixes and
// child slots) plus the number of nodes, leaves, and fringe nodes in the
// subtree. If n is nil or empty, a zeroed stats is returned. The returned
// SubNodes count includes the current node. The function will panic if a child
// has an unexpected concrete type.
func (n *LiteNode[V]) StatsRec() (s StatsT) {
if n == nil || n.IsEmpty() {
return s
}
s.Prefixes = n.PrefixCount()
s.Children = n.ChildCount()
s.SubNodes = 1 // this node
s.Leaves = 0
s.Fringes = 0
for _, child := range n.AllChildren() {
switch kid := child.(type) {
case *LiteNode[V]:
// rec-descent
rs := kid.StatsRec()
s.Prefixes += rs.Prefixes
s.Children += rs.Children
s.SubNodes += rs.SubNodes
s.Leaves += rs.Leaves
s.Fringes += rs.Fringes
case *FringeNode[V]:
s.Fringes++
case *LeafNode[V]:
s.Leaves++
default:
panic("logic error, wrong node type")
}
}
return s
}
// FprintRec recursively prints a hierarchical CIDR tree representation
// starting from this node to the provided writer. The output shows the
// routing table structure in human-readable format for debugging and analysis.
func (n *LiteNode[V]) FprintRec(w io.Writer, parent TrieItem[V], pad string) error {
// recursion stop condition
if n == nil || n.IsEmpty() {
return nil
}
// get direct covered childs for this parent ...
directItems := n.DirectItemsRec(parent.Idx, parent.Path, parent.Depth, parent.Is4)
// sort them by netip.Prefix, not by baseIndex
slices.SortFunc(directItems, func(a, b TrieItem[V]) int {
return CmpPrefix(a.Cidr, b.Cidr)
})
// printing values if V is not the empty struct{}
printValues := !value.IsEmptyStruct[V]()
// for all direct item under this node ...
for i, item := range directItems {
// symbols used in tree
glyph := "├─ "
space := "│ "
// ... treat last kid special
if i == len(directItems)-1 {
glyph = "└─ "
space = " "
}
var err error
// val is the empty struct, don't print it
if printValues {
_, err = fmt.Fprintf(w, "%s%s (%v)\n", pad+glyph, item.Cidr, item.Val)
} else {
// skip printing values if V is zero-sized
_, err = fmt.Fprintf(w, "%s%s\n", pad+glyph, item.Cidr)
}
if err != nil {
return err
}
// rec-descent with this item as parent
nextNode, _ := item.Node.(*LiteNode[V])
if err = nextNode.FprintRec(w, item, pad+space); err != nil {
return err
}
}
return nil
}
// DirectItemsRec, returns the direct covered items by parent.
// It's a complex recursive function, you have to know the data structure
// by heart to understand this function!
func (n *LiteNode[V]) DirectItemsRec(parentIdx uint8, path StridePath, depth int, is4 bool) (directItems []TrieItem[V]) {
// recursion stop condition
if n == nil || n.IsEmpty() {
return nil
}
// prefixes:
// for all idx's (prefixes mapped by baseIndex) in this node
// do a longest-prefix-match
for idx, val := range n.AllIndices() {
// tricky part, skip self
// test with next possible lpm (idx>>1), it's a complete binary tree
nextIdx := idx >> 1
// fast skip, lpm not possible
if nextIdx < parentIdx {
continue
}
// do a longest-prefix-match
lpm, _, _ := n.LookupIdx(nextIdx)
// be aware, 0 is here a possible value for parentIdx and lpm (if not found)
if lpm == parentIdx {
// prefix is directly covered by parent
item := TrieItem[V]{
Node: n,
Is4: is4,
Path: path,
Depth: depth,
Idx: idx,
// get the prefix back from trie
Cidr: CidrFromPath(path, depth, is4, idx),
Val: val,
}
directItems = append(directItems, item)
}
}
// children:
for addr, child := range n.AllChildren() {
hostIdx := art.OctetToIdx(addr)
// do a longest-prefix-match
lpm, _, _ := n.LookupIdx(hostIdx)
// be aware, 0 is here a possible value for parentIdx and lpm (if not found)
if lpm == parentIdx {
// child is directly covered by parent
switch kid := child.(type) {
case *LiteNode[V]: // traverse rec-descent, call with next child node,
// next trie level, set parentIdx to 0, adjust path and depth
path[depth] = addr
directItems = append(directItems, kid.DirectItemsRec(0, path, depth+1, is4)...)
case *LeafNode[V]: // path-compressed child, stops recursion for this child
item := TrieItem[V]{
Node: nil,
Is4: is4,
Cidr: kid.Prefix,
Val: kid.Value,
}
directItems = append(directItems, item)
case *FringeNode[V]: // path-compressed fringe, stops recursion for this child
item := TrieItem[V]{
Node: nil,
Is4: is4,
// get the prefix back from trie
Cidr: CidrForFringe(path[:], depth, is4, addr),
Val: kid.Value,
}
directItems = append(directItems, item)
default:
panic("logic error, wrong node type")
}
}
}
return directItems
}
// UnionRec recursively merges another node o into the receiver node n.
//
// All prefix and child entries from o are cloned and inserted into n.
// If a prefix already exists in n, its value is overwritten by the value from o,
// and the duplicate is counted in the return value. This count can later be used
// to update size-related metadata in the parent trie.
//
// The union handles all possible combinations of child node types (node, leaf, fringe)
// between the two nodes. Structural conflicts are resolved by creating new intermediate
// *LiteNode[V] objects and pushing both children further down the trie. Leaves and fringes
// are also recursively relocated as needed to preserve prefix semantics.
//
// The merge operation is destructive on the receiver n, but leaves the source node o unchanged.
//
// Returns the number of duplicate prefixes that were overwritten during merging.
func (n *LiteNode[V]) UnionRec(cloneFn func(V) V, o *LiteNode[V], depth int) (duplicates int) {
if cloneFn == nil {
cloneFn = func(v V) V { return v }
}
buf := [256]uint8{}
// for all prefixes in other node do ...
for _, oIdx := range o.Prefixes.AsSlice(&buf) {
// clone/copy the value from other node at idx
val := o.MustGetPrefix(oIdx)
clonedVal := cloneFn(val)
// insert/overwrite cloned value from o into n
if n.InsertPrefix(oIdx, clonedVal) {
// this prefix is duplicate in n and o
duplicates++
}
}
// for all child addrs in other node do ...
for _, addr := range o.Children.AsSlice(&buf) {
otherChild := o.MustGetChild(addr)
thisChild, thisExists := n.GetChild(addr)
// Use helper function to handle all 4x3 combinations
duplicates += n.handleMatrix(cloneFn, thisExists, thisChild, otherChild, addr, depth)
}
return duplicates
}
// UnionRecPersist is similar to unionRec but performs an immutable union of nodes.
func (n *LiteNode[V]) UnionRecPersist(cloneFn func(V) V, o *LiteNode[V], depth int) (duplicates int) {
if cloneFn == nil {
cloneFn = func(v V) V { return v }
}
buf := [256]uint8{}
// for all prefixes in other node do ...
for _, oIdx := range o.Prefixes.AsSlice(&buf) {
// clone/copy the value from other node
val := o.MustGetPrefix(oIdx)
clonedVal := cloneFn(val)
// insert/overwrite cloned value from o into n
if exists := n.InsertPrefix(oIdx, clonedVal); exists {
// this prefix is duplicate in n and o
duplicates++
}
}
// for all child addrs in other node do ...
for _, addr := range o.Children.AsSlice(&buf) {
otherChild := o.MustGetChild(addr)
thisChild, thisExists := n.GetChild(addr)
// Use helper function to handle all 4x3 combinations
duplicates += n.handleMatrixPersist(cloneFn, thisExists, thisChild, otherChild, addr, depth)
}
return duplicates
}
// handleMatrix, 12 possible combinations to union this child and other child
//
// THIS, OTHER: (always clone the other kid!)
// --------------
// NULL, node <-- insert node at addr
// NULL, leaf <-- insert leaf at addr
// NULL, fringe <-- insert fringe at addr
//
// node, node <-- union rec-descent with node
// node, leaf <-- insert leaf at depth+1
// node, fringe <-- insert fringe at depth+1
//
// leaf, node <-- insert new node, push this leaf down, union rec-descent
// leaf, leaf <-- insert new node, push both leaves down (!first check equality)
// leaf, fringe <-- insert new node, push this leaf and fringe down
//
// fringe, node <-- insert new node, push this fringe down, union rec-descent
// fringe, leaf <-- insert new node, push this fringe down, insert other leaf at depth+1
// fringe, fringe <-- just overwrite value
func (n *LiteNode[V]) handleMatrix(cloneFn func(V) V, thisExists bool, thisChild, otherChild any, addr uint8, depth int) int {
// Do ALL type assertions upfront - reduces line noise
var (
thisNode, thisIsNode = thisChild.(*LiteNode[V])
thisLeaf, thisIsLeaf = thisChild.(*LeafNode[V])
thisFringe, thisIsFringe = thisChild.(*FringeNode[V])
otherNode, otherIsNode = otherChild.(*LiteNode[V])
otherLeaf, otherIsLeaf = otherChild.(*LeafNode[V])
otherFringe, otherIsFringe = otherChild.(*FringeNode[V])
)
// just insert cloned child at this empty slot
if !thisExists {
switch {
case otherIsNode:
n.InsertChild(addr, otherNode.CloneRec(cloneFn))
case otherIsLeaf:
n.InsertChild(addr, &LeafNode[V]{Prefix: otherLeaf.Prefix, Value: cloneFn(otherLeaf.Value)})
case otherIsFringe:
n.InsertChild(addr, &FringeNode[V]{Value: cloneFn(otherFringe.Value)})
default:
panic("logic error, wrong node type")
}
return 0
}
// Case 1: Special cases that DON'T need a new node
// Special case: fringe + fringe -> just overwrite value
if thisIsFringe && otherIsFringe {
thisFringe.Value = cloneFn(otherFringe.Value)
return 1
}
// Special case: leaf + leaf with same prefix -> just overwrite value
if thisIsLeaf && otherIsLeaf && thisLeaf.Prefix == otherLeaf.Prefix {
thisLeaf.Value = cloneFn(otherLeaf.Value)
return 1
}
// Case 2: thisChild is already a node - insert into it, no new node needed
if thisIsNode {
switch {
case otherIsNode:
return thisNode.UnionRec(cloneFn, otherNode, depth+1)
case otherIsLeaf:
if thisNode.Insert(otherLeaf.Prefix, cloneFn(otherLeaf.Value), depth+1) {
return 1
}
return 0
case otherIsFringe:
if thisNode.InsertPrefix(1, cloneFn(otherFringe.Value)) {
return 1
}
return 0
default:
panic("logic error, wrong node type")
}
}
// Case 3: All remaining cases need a new node
// (thisChild is leaf or fringe, and we didn't hit the special cases above)
nc := new(LiteNode[V])
// Push existing child down into new node
switch {
case thisIsLeaf:
nc.Insert(thisLeaf.Prefix, thisLeaf.Value, depth+1)
case thisIsFringe:
nc.InsertPrefix(1, thisFringe.Value)
default:
panic("logic error, unexpected this child type")
}
// Replace child with new node
n.InsertChild(addr, nc)
// Now handle other child
switch {
case otherIsNode:
return nc.UnionRec(cloneFn, otherNode, depth+1)
case otherIsLeaf:
if nc.Insert(otherLeaf.Prefix, cloneFn(otherLeaf.Value), depth+1) {
return 1
}
return 0
case otherIsFringe:
if nc.InsertPrefix(1, cloneFn(otherFringe.Value)) {
return 1
}
return 0
default:
panic("logic error, wrong other node type")
}
}
// handleMatrixPersist, 12 possible combinations to union this child and other child
//
// THIS, OTHER: (always clone the other kid!)
// --------------
// NULL, node <-- insert node at addr
// NULL, leaf <-- insert leaf at addr
// NULL, fringe <-- insert fringe at addr
//
// node, node <-- union rec-descent with node
// node, leaf <-- insert leaf at depth+1
// node, fringe <-- insert fringe at depth+1
//
// leaf, node <-- insert new node, push this leaf down, union rec-descent
// leaf, leaf <-- insert new node, push both leaves down (!first check equality)
// leaf, fringe <-- insert new node, push this leaf and fringe down
//
// fringe, node <-- insert new node, push this fringe down, union rec-descent
// fringe, leaf <-- insert new node, push this fringe down, insert other leaf at depth+1
// fringe, fringe <-- just overwrite value
func (n *LiteNode[V]) handleMatrixPersist(cloneFn func(V) V, thisExists bool, thisChild, otherChild any, addr uint8, depth int) int {
// Do ALL type assertions upfront - reduces line noise
var (
thisNode, thisIsNode = thisChild.(*LiteNode[V])
thisLeaf, thisIsLeaf = thisChild.(*LeafNode[V])
thisFringe, thisIsFringe = thisChild.(*FringeNode[V])
otherNode, otherIsNode = otherChild.(*LiteNode[V])
otherLeaf, otherIsLeaf = otherChild.(*LeafNode[V])
otherFringe, otherIsFringe = otherChild.(*FringeNode[V])
)
// just insert cloned child at this empty slot
if !thisExists {
switch {
case otherIsNode:
n.InsertChild(addr, otherNode.CloneRec(cloneFn))
case otherIsLeaf:
n.InsertChild(addr, &LeafNode[V]{Prefix: otherLeaf.Prefix, Value: cloneFn(otherLeaf.Value)})
case otherIsFringe:
n.InsertChild(addr, &FringeNode[V]{Value: cloneFn(otherFringe.Value)})
default:
panic("logic error, wrong node type")
}
return 0
}
// Case 1: Special cases that DON'T need a new node
// Special case: fringe + fringe -> just overwrite value
if thisIsFringe && otherIsFringe {
thisFringe.Value = cloneFn(otherFringe.Value)
return 1
}
// Special case: leaf + leaf with same prefix -> just overwrite value
if thisIsLeaf && otherIsLeaf && thisLeaf.Prefix == otherLeaf.Prefix {
thisLeaf.Value = cloneFn(otherLeaf.Value)
return 1
}
// Case 2: thisChild is already a node - clone this node, insert into it
if thisIsNode {
// CLONE the node
// thisNode points now to cloned kid
thisNode = thisNode.CloneFlat(cloneFn)
// replace kid with cloned thisKid
n.InsertChild(addr, thisNode)
switch {
case otherIsNode:
return thisNode.UnionRecPersist(cloneFn, otherNode, depth+1)
case otherIsLeaf:
if thisNode.InsertPersist(cloneFn, otherLeaf.Prefix, cloneFn(otherLeaf.Value), depth+1) {
return 1
}
return 0
case otherIsFringe:
if thisNode.InsertPrefix(1, cloneFn(otherFringe.Value)) {
return 1
}
return 0
default:
panic("logic error, wrong node type")
}
}
// Case 3: All remaining cases need a new node
// (thisChild is leaf or fringe, and we didn't hit the special cases above)
nc := new(LiteNode[V])
// Push existing child down into new node
switch {
case thisIsLeaf:
nc.Insert(thisLeaf.Prefix, thisLeaf.Value, depth+1)
case thisIsFringe:
nc.InsertPrefix(1, thisFringe.Value)
default:
panic("logic error, unexpected this child type")
}
// Replace child with new node
n.InsertChild(addr, nc)
// Now handle other child
switch {
case otherIsNode:
return nc.UnionRec(cloneFn, otherNode, depth+1)
case otherIsLeaf:
if nc.Insert(otherLeaf.Prefix, cloneFn(otherLeaf.Value), depth+1) {
return 1
}
return 0
case otherIsFringe:
if nc.InsertPrefix(1, cloneFn(otherFringe.Value)) {
return 1
}
return 0
default:
panic("logic error, wrong other node type")
}
}
// AllRec recursively traverses the trie starting at the current node,
// applying the provided yield function to every stored prefix and value.
//
// For each route entry (prefix and value), yield is invoked. If yield returns false,
// the traversal stops immediately, and false is propagated upwards,
// enabling early termination.
//
// The function handles all prefix entries in the current node, as well as any children -
// including sub-nodes, leaf nodes with full prefixes, and fringe nodes
// representing path-compressed prefixes. IP prefix reconstruction is performed on-the-fly
// from the current path and depth.
//
// The traversal order is not defined. This implementation favors simplicity
// and runtime efficiency over consistency of iteration sequence.
func (n *LiteNode[V]) AllRec(path StridePath, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool {
var buf [256]uint8
for _, idx := range n.Prefixes.AsSlice(&buf) {
cidr := CidrFromPath(path, depth, is4, idx)
val := n.MustGetPrefix(idx)
// callback for this prefix and val
if !yield(cidr, val) {
// early exit
return false
}
}
// for all children (nodes and leaves) in this node do ...
for _, addr := range n.Children.AsSlice(&buf) {
anyKid := n.MustGetChild(addr)
switch kid := anyKid.(type) {
case *LiteNode[V]:
// rec-descent with this node
path[depth] = addr
if !kid.AllRec(path, depth+1, is4, yield) {
// early exit
return false
}
case *LeafNode[V]:
// callback for this leaf
if !yield(kid.Prefix, kid.Value) {
// early exit
return false
}
case *FringeNode[V]:
fringePfx := CidrForFringe(path[:], depth, is4, addr)
// callback for this fringe
if !yield(fringePfx, kid.Value) {
// early exit
return false
}
default:
panic("logic error, wrong node type")
}
}
return true
}
// AllRecSorted recursively traverses the trie in prefix-sorted order and applies
// the given yield function to each stored prefix and value.
//
// Unlike AllRec, this implementation ensures that route entries are visited in
// canonical prefix sort order. To achieve this,
// both the prefixes and children of the current node are gathered, sorted,
// and then interleaved during traversal based on logical octet positioning.
//
// The function first sorts relevant entries by their prefix index and address value,
// using a comparison function that ranks prefixes according to their mask length and position.
// Then it walks the trie, always yielding child entries that fall before the current prefix,
// followed by the prefix itself. Remaining children are processed once all prefixes have been visited.
//
// Prefixes are reconstructed on-the-fly from the traversal path, and iteration includes all child types:
// inner nodes (recursive descent), leaf nodes, and fringe (compressed) prefixes.
//
// The order is stable and predictable, making the function suitable for use cases
// like table exports, comparisons or serialization.
//
// Parameters:
// - path: the current traversal path through the trie
// - depth: current depth in the trie (0-based)
// - is4: true for IPv4 processing, false for IPv6
// - yield: callback function invoked for each prefix/value pair
//
// Returns false if yield function requests early termination.
func (n *LiteNode[V]) AllRecSorted(path StridePath, depth int, is4 bool, yield func(netip.Prefix, V) bool) bool {
// get slice of all child octets, sorted by addr
var childBuf [256]uint8
allChildAddrs := n.Children.AsSlice(&childBuf)
// get slice of all indexes, sorted by idx
var idxBuf [256]uint8
allIndices := n.Prefixes.AsSlice(&idxBuf)
// sort indices in CIDR sort order
slices.SortFunc(allIndices, CmpIndexRank)
childCursor := 0
// yield indices and children in CIDR sort order
for _, pfxIdx := range allIndices {
pfxOctet, _ := art.IdxToPfx(pfxIdx)
// yield all children before idx
for j := childCursor; j < len(allChildAddrs); j++ {
childAddr := allChildAddrs[j]
if childAddr >= pfxOctet {
break
}
// yield the node (rec-descent) or leaf
anyKid := n.MustGetChild(childAddr)
switch kid := anyKid.(type) {
case *LiteNode[V]:
path[depth] = childAddr
if !kid.AllRecSorted(path, depth+1, is4, yield) {
return false
}
case *LeafNode[V]:
if !yield(kid.Prefix, kid.Value) {
return false
}
case *FringeNode[V]:
fringePfx := CidrForFringe(path[:], depth, is4, childAddr)
if !yield(fringePfx, kid.Value) {
return false
}
default:
panic("logic error, wrong node type")
}
childCursor++
}
// yield the prefix for this idx
cidr := CidrFromPath(path, depth, is4, pfxIdx)
// n.prefixes.Items[i] not possible after sorting allIndices
if !yield(cidr, n.MustGetPrefix(pfxIdx)) {
return false
}
}
// yield the rest of leaves and nodes (rec-descent)
for j := childCursor; j < len(allChildAddrs); j++ {
addr := allChildAddrs[j]
anyKid := n.MustGetChild(addr)
switch kid := anyKid.(type) {
case *LiteNode[V]:
path[depth] = addr
if !kid.AllRecSorted(path, depth+1, is4, yield) {
return false
}
case *LeafNode[V]:
if !yield(kid.Prefix, kid.Value) {
return false
}
case *FringeNode[V]:
fringePfx := CidrForFringe(path[:], depth, is4, addr)
if !yield(fringePfx, kid.Value) {
return false
}
default:
panic("logic error, wrong node type")
}
}
return true
}
// EachLookupPrefix performs a hierarchical lookup of all matching prefixes
// in the current node’s 8-bit stride-based prefix table.
//
// The function walks up the trie-internal complete binary tree (CBT),
// testing each possible prefix length mask (in decreasing order of specificity),
// and invokes the yield function for every matching entry.
//
// The given idx refers to the position for this stride's prefix and is used
// to derive a backtracking path through the CBT by repeatedly halving the index.
// At each step, if a prefix exists in the table, its corresponding CIDR is
// reconstructed and yielded. If yield returns false, traversal stops early.
//
// This function is intended for internal use during supernet traversal and
// does not descend the trie further.
func (n *LiteNode[V]) EachLookupPrefix(ip netip.Addr, depth int, pfxIdx uint8, yield func(netip.Prefix, V) bool) (ok bool) {
for ; pfxIdx > 0; pfxIdx >>= 1 {
if n.Prefixes.Test(pfxIdx) {
val := n.MustGetPrefix(pfxIdx)
// get the CIDR back
_, pfxLen := art.IdxToPfx(pfxIdx)
cidr, _ := ip.Prefix(depth<<3 + int(pfxLen))
if !yield(cidr, val) {
return false
}
}
}
return true
}
// EachSubnet yields all prefix entries and child nodes covered by a given parent prefix,
// sorted in natural CIDR order, within the current node.
//
// The function iterates through all prefixes and children from the node’s stride tables.
// Only entries that fall within the address range defined by the parent prefix index (pfxIdx)
// are included. Matching entries are buffered, sorted, and passed through to the yield function.
//
// Child entries (nodes, leaves, fringes) that fall under the covered address range
// are processed recursively via AllRecSorted to ensure sorted traversal.
//
// This function is intended for internal use by Subnets(), and it assumes the
// current node is positioned at the point in the trie corresponding to the parent prefix.
func (n *LiteNode[V]) EachSubnet(octets []byte, depth int, is4 bool, pfxIdx uint8, yield func(netip.Prefix, V) bool) bool {
// octets as array, needed below more than once
var path StridePath
copy(path[:], octets)
pfxFirstAddr, pfxLastAddr := art.IdxToRange(pfxIdx)
allCoveredIndices := make([]uint8, 0, n.PrefixCount())
var buf [256]uint8
for _, idx := range n.Prefixes.AsSlice(&buf) {
thisFirstAddr, thisLastAddr := art.IdxToRange(idx)
if thisFirstAddr >= pfxFirstAddr && thisLastAddr <= pfxLastAddr {
allCoveredIndices = append(allCoveredIndices, idx)
}
}
// sort indices in CIDR sort order
slices.SortFunc(allCoveredIndices, CmpIndexRank)
// 2. collect all covered child addrs by prefix
allCoveredChildAddrs := make([]uint8, 0, n.ChildCount())
for _, addr := range n.Children.AsSlice(&buf) {
if addr >= pfxFirstAddr && addr <= pfxLastAddr {
allCoveredChildAddrs = append(allCoveredChildAddrs, addr)
}
}
// 3. yield covered indices, path-compressed prefixes
// and children in CIDR sort order
addrCursor := 0
// yield indices and children in CIDR sort order
for _, pfxIdx := range allCoveredIndices {
pfxOctet, _ := art.IdxToPfx(pfxIdx)
// yield all children before idx
for j := addrCursor; j < len(allCoveredChildAddrs); j++ {
addr := allCoveredChildAddrs[j]
if addr >= pfxOctet {
break
}
// yield the node or leaf?
switch kid := n.MustGetChild(addr).(type) {
case *LiteNode[V]:
path[depth] = addr
if !kid.AllRecSorted(path, depth+1, is4, yield) {
return false
}
case *LeafNode[V]:
if !yield(kid.Prefix, kid.Value) {
return false
}
case *FringeNode[V]:
fringePfx := CidrForFringe(path[:], depth, is4, addr)
// callback for this fringe
if !yield(fringePfx, kid.Value) {
// early exit
return false
}
default:
panic("logic error, wrong node type")
}
addrCursor++
}
// yield the prefix for this idx
cidr := CidrFromPath(path, depth, is4, pfxIdx)
// n.prefixes.Items[i] not possible after sorting allIndices
if !yield(cidr, n.MustGetPrefix(pfxIdx)) {
return false
}
}
// yield the rest of leaves and nodes (rec-descent)
for _, addr := range allCoveredChildAddrs[addrCursor:] {
// yield the node or leaf?
switch kid := n.MustGetChild(addr).(type) {
case *LiteNode[V]:
path[depth] = addr
if !kid.AllRecSorted(path, depth+1, is4, yield) {
return false
}
case *LeafNode[V]:
if !yield(kid.Prefix, kid.Value) {
return false
}
case *FringeNode[V]:
fringePfx := CidrForFringe(path[:], depth, is4, addr)
// callback for this fringe
if !yield(fringePfx, kid.Value) {
// early exit
return false
}
default:
panic("logic error, wrong node type")
}
}
return true
}
// Supernets yields all supernet prefixes of pfx that exist in the trie,
// in reverse order (most-specific first, least-specific last).
//
// It traverses upward from the given prefix toward the root, collecting
// matching prefixes along the path. The traversal uses a stack to yield
// results in reverse order, so that more-specific supernets appear before
// less-specific ones.
//
// The function handles all node types (internal nodes, leaves, and fringes)
// and stops early if the yield callback returns false.
//
// Parameters:
// - pfx: The prefix for which to find supernets
// - yield: Callback function invoked for each supernet prefix/value pair
//
// The yield function receives prefix/value pairs and returns false to stop
// the iteration early.
func (n *LiteNode[V]) Supernets(pfx netip.Prefix, yield func(netip.Prefix, V) bool) {
ip := pfx.Addr()
pfxLen := pfx.Bits()
is4 := ip.Is4()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// stack of the traversed nodes for reverse ordering of supernets
stack := [MaxTreeDepth]*LiteNode[V]{}
// run variable, used after for loop
var depth int
var octet byte
// find last node along this octet path
LOOP:
for depth, octet = range octets {
// stepped one past the last stride of interest; back up to last and exit
if depth > strideCount {
depth--
break
}
// push current node on stack
stack[depth] = n
// descend down the trie
if !n.Children.Test(octet) {
break LOOP
}
kid := n.MustGetChild(octet)
// kid is node or leaf or fringe at octet
switch kid := kid.(type) {
case *LiteNode[V]:
n = kid
continue LOOP // descend down to next trie level
case *LeafNode[V]:
if kid.Prefix.Bits() > pfx.Bits() {
break LOOP
}
if kid.Prefix.Overlaps(pfx) {
if !yield(kid.Prefix, kid.Value) {
// early exit
return
}
}
// end of trie along this octets path
break LOOP
case *FringeNode[V]:
fringePfx := CidrForFringe(octets, depth, is4, octet)
if fringePfx.Bits() > pfx.Bits() {
break LOOP
}
if fringePfx.Overlaps(pfx) {
if !yield(fringePfx, kid.Value) {
// early exit
return
}
}
// end of trie along this octets path
break LOOP
default:
panic("logic error, wrong node type")
}
}
// start backtracking, unwind the stack
for ; depth >= 0; depth-- {
n = stack[depth]
// only the lastOctet may have a different prefix len
// all others are just host routes
var idx uint8
octet = octets[depth]
// Last “octet” from prefix
// Note: For /32 and /128, depth never reaches strideCount (4/16),
if depth == strideCount {
idx = art.PfxToIdx(octet, modBits)
} else {
idx = art.OctetToIdx(octet)
}
// micro benchmarking, skip if there is no match
if !n.Contains(idx) {
continue
}
// yield all the matching prefixes, not just the lpm
if !n.EachLookupPrefix(ip, depth, idx, yield) {
// early exit
return
}
}
}
// Subnets yields all subnet prefixes covered by pfx that exist in the trie,
// in CIDR sort order.
//
// It first locates the trie node corresponding to pfx, then recursively
// yields all prefixes and child entries contained within that subtree.
// The traversal uses sorted iteration to maintain canonical CIDR ordering.
//
// The function handles various node types (internal nodes, leaves, and fringes)
// and uses EachSubnet and AllRecSorted for sorted traversal of covered prefixes.
//
// Parameters:
// - pfx: The parent prefix whose subnets should be yielded
// - yield: Callback function invoked for each subnet prefix/value pair
//
// The yield function receives prefix/value pairs and returns false to stop
// the iteration early. If pfx doesn't exist in the trie, no prefixes are yielded.
func (n *LiteNode[V]) Subnets(pfx netip.Prefix, yield func(netip.Prefix, V) bool) {
// values derived from pfx
ip := pfx.Addr()
pfxLen := pfx.Bits()
is4 := ip.Is4()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
// find the trie node
for depth, octet := range octets {
// Last “octet” from prefix
// Note: For /32 and /128, depth never reaches strideCount (4/16),
// so those are handled below via the fringe/leaf path.
if depth == strideCount {
idx := art.PfxToIdx(octet, modBits)
n.EachSubnet(octets, depth, is4, idx, yield)
return
}
if !n.Children.Test(octet) {
return
}
kid := n.MustGetChild(octet)
// kid is node or leaf or fringe at octet
switch kid := kid.(type) {
case *LiteNode[V]:
n = kid
continue // descend down to next trie level
case *LeafNode[V]:
if pfx.Bits() <= kid.Prefix.Bits() && pfx.Overlaps(kid.Prefix) {
yield(kid.Prefix, kid.Value)
}
return // immediate return
case *FringeNode[V]:
// get the LPM prefix back from ip and depth
// it's a fringe, bits are always /8, /16, /24, ...
fringePfx, _ := ip.Prefix((depth + 1) << 3)
if pfx.Bits() <= fringePfx.Bits() && pfx.Overlaps(fringePfx) {
yield(fringePfx, kid.Value)
}
return // immediate return
default:
panic("logic error, wrong node type")
}
}
}
// Overlaps recursively compares two trie nodes and returns true
// if any of their prefixes or descendants overlap.
//
// The implementation checks for:
// 1. Direct overlapping prefixes on this node level
// 2. Prefixes in one node overlapping with children in the other
// 3. Matching child addresses in both nodes, which are recursively compared
//
// All 12 possible type combinations for child entries (node, leaf, fringe) are supported.
//
// The function is optimized for early exit on first match and uses heuristics to
// choose between set-based and loop-based matching for performance.
func (n *LiteNode[V]) Overlaps(o *LiteNode[V], depth int) bool {
nPfxCount := n.PrefixCount()
oPfxCount := o.PrefixCount()
nChildCount := n.ChildCount()
oChildCount := o.ChildCount()
// ##############################
// 1. Test if any routes overlaps
// ##############################
// full cross check
if nPfxCount > 0 && oPfxCount > 0 {
if n.OverlapsRoutes(o) {
return true
}
}
// ####################################
// 2. Test if routes overlaps any child
// ####################################
// swap nodes to help chance on its way,
// if the first call to expensive overlapsChildrenIn() is already true,
// if both orders are false it doesn't help either
if nChildCount > oChildCount {
n, o = o, n
nPfxCount = n.PrefixCount()
oPfxCount = o.PrefixCount()
nChildCount = n.ChildCount()
oChildCount = o.ChildCount()
}
if nPfxCount > 0 && oChildCount > 0 {
if n.OverlapsChildrenIn(o) {
return true
}
}
// symmetric reverse
if oPfxCount > 0 && nChildCount > 0 {
if o.OverlapsChildrenIn(n) {
return true
}
}
// ############################################
// 3. children with same octet in nodes n and o
// ############################################
// stop condition, n or o have no children
if nChildCount == 0 || oChildCount == 0 {
return false
}
// stop condition, no child with identical octet in n and o
if !n.Children.Intersects(&o.Children.BitSet256) {
return false
}
return n.OverlapsSameChildren(o, depth)
}
// OverlapsRoutes compares the prefix sets of two nodes (n and o).
//
// It first checks for direct bitset intersection (identical indices),
// then walks both prefix sets using the Contains method to detect if any
// of the n-prefixes is contained in o, or vice versa.
func (n *LiteNode[V]) OverlapsRoutes(o *LiteNode[V]) bool {
// some prefixes are identical, trivial overlap
if n.Prefixes.Intersects(&o.Prefixes.BitSet256) {
return true
}
// get the lowest idx (biggest prefix)
nFirstIdx, _ := n.Prefixes.FirstSet()
oFirstIdx, _ := o.Prefixes.FirstSet()
// start with other min value
nIdx := oFirstIdx
oIdx := nFirstIdx
nOK := true
oOK := true
// zip, range over n and o together to help chance on its way
for nOK || oOK {
if nOK {
// does any route in o overlap this prefix from n
if nIdx, nOK = n.Prefixes.NextSet(nIdx); nOK {
if o.Contains(nIdx) {
return true
}
if nIdx == 255 {
// stop, don't overflow uint8!
nOK = false
} else {
nIdx++
}
}
}
if oOK {
// does any route in n overlap this prefix from o
if oIdx, oOK = o.Prefixes.NextSet(oIdx); oOK {
if n.Contains(oIdx) {
return true
}
if oIdx == 255 {
// stop, don't overflow uint8!
oOK = false
} else {
oIdx++
}
}
}
}
return false
}
// OverlapsChildrenIn checks whether the prefixes in node n
// overlap with any children (by address range) in node o.
//
// Uses bitset intersection or manual iteration heuristically,
// depending on prefix and child count.
//
// Bitset-based matching uses precomputed coverage tables
// to avoid per-address looping. This is critical for high fan-out nodes.
func (n *LiteNode[V]) OverlapsChildrenIn(o *LiteNode[V]) bool {
pfxCount := n.PrefixCount()
childCount := o.ChildCount()
// heuristic: 15 is the crossover point where bitset operations become
// more efficient than iteration, determined by micro benchmarks on typical
// routing table distributions
const overlapsRangeCutoff = 15
doRange := childCount < overlapsRangeCutoff || pfxCount > overlapsRangeCutoff
// do range over, not so many children and maybe too many prefixes for other algo below
var buf [256]uint8
if doRange {
for _, addr := range o.Children.AsSlice(&buf) {
if n.Contains(art.OctetToIdx(addr)) {
return true
}
}
return false
}
// do bitset intersection, alloted route table with child octets
// maybe too many children for range-over or not so many prefixes to
// build the alloted routing table from them
// use allot table with prefixes as bitsets, bitsets are precalculated.
for _, idx := range n.Prefixes.AsSlice(&buf) {
if o.Children.Intersects(&allot.FringeRoutesLookupTbl[idx]) {
return true
}
}
return false
}
// OverlapsSameChildren compares all matching child addresses (octets)
// between node n and node o recursively.
//
// For each shared address, the corresponding child nodes (of any type)
// are compared using LiteNodeOverlapsTwoChildren, which handles all
// node/leaf/fringe combinations.
func (n *LiteNode[V]) OverlapsSameChildren(o *LiteNode[V], depth int) bool {
// intersect the child bitsets from n with o
commonChildren := n.Children.Intersection(&o.Children.BitSet256)
for addr, ok := commonChildren.NextSet(0); ok; {
nChild := n.MustGetChild(addr)
oChild := o.MustGetChild(addr)
if n.OverlapsTwoChildren(nChild, oChild, depth+1) {
return true
}
if addr == 255 {
break // Prevent uint8 overflow
}
addr, ok = commonChildren.NextSet(addr + 1)
}
return false
}
// OverlapsPrefixAtDepth returns true if any route in the subtree rooted at this node
// overlaps with the given pfx, starting the comparison at the specified depth.
//
// This function supports structural overlap detection even in compressed or sparse
// paths within the trie, including fringe and leaf nodes. Matching is directional:
// it returns true if a route fully covers pfx, or if pfx covers an existing route.
//
// At each step, it checks for visible prefixes and children that may intersect the
// target prefix via stride-based longest-prefix test. The walk terminates early as
// soon as a structural overlap is found.
//
// This function underlies the top-level OverlapsPrefix behavior and handles details of
// trie traversal across varying prefix lengths and compression levels.
func (n *LiteNode[V]) OverlapsPrefixAtDepth(pfx netip.Prefix, depth int) bool {
ip := pfx.Addr()
pfxLen := pfx.Bits()
octets := ip.AsSlice()
strideCount, modBits := DivMod8(pfxLen)
for ; depth < len(octets); depth++ {
if depth > strideCount {
break
}
octet := octets[depth]
// full octet path in node trie, check overlap with last prefix octet
if depth == strideCount {
return n.OverlapsIdx(art.PfxToIdx(octet, modBits))
}
// test if any route overlaps prefix´ so far
// no best match needed, forward tests without backtracking
if n.PrefixCount() != 0 && n.Contains(art.OctetToIdx(octet)) {
return true
}
if !n.Children.Test(octet) {
return false
}
// next child, node or leaf
switch kid := n.MustGetChild(octet).(type) {
case *LiteNode[V]:
n = kid
continue
case *LeafNode[V]:
return kid.Prefix.Overlaps(pfx)
case *FringeNode[V]:
return true
default:
panic("logic error, wrong node type")
}
}
panic("unreachable: " + pfx.String())
}
// OverlapsIdx returns true if the given prefix index overlaps with any entry in this node.
//
// The overlap detection considers three categories:
//
// 1. Whether any stored prefix in this node covers the requested prefix (LPM test)
// 2. Whether the requested prefix covers any stored route in the node
// 3. Whether the requested prefix overlaps with any fringe or child entry
//
// Internally, it leverages precomputed bitsets from the allotment model,
// using fast bitwise set intersections instead of explicit range comparisons.
// This enables high-performance overlap checks on a single stride level
// without descending further into the trie.
func (n *LiteNode[V]) OverlapsIdx(idx uint8) bool {
// 1. Test if any route in this node overlaps prefix?
if n.Contains(idx) {
return true
}
// 2. Test if prefix overlaps any route in this node
if n.Prefixes.Intersects(&allot.PfxRoutesLookupTbl[idx]) {
return true
}
// 3. Test if prefix overlaps any child in this node
return n.Children.Intersects(&allot.FringeRoutesLookupTbl[idx])
}
// OverlapsTwoChildren handles all 3x3 combinations of
// node kinds (node, leaf, fringe).
//
// 3x3 possible different combinations for n and o
//
// node, node --> overlaps rec descent
// node, leaf --> overlapsPrefixAtDepth
// node, fringe --> true
//
// leaf, node --> overlapsPrefixAtDepth
// leaf, leaf --> netip.Prefix.Overlaps
// leaf, fringe --> true
//
// fringe, node --> true
// fringe, leaf --> true
// fringe, fringe --> true
func (n *LiteNode[V]) OverlapsTwoChildren(nChild, oChild any, depth int) bool {
// child type detection
nNode, nIsNode := nChild.(*LiteNode[V])
nLeaf, nIsLeaf := nChild.(*LeafNode[V])
_, nIsFringe := nChild.(*FringeNode[V])
oNode, oIsNode := oChild.(*LiteNode[V])
oLeaf, oIsLeaf := oChild.(*LeafNode[V])
_, oIsFringe := oChild.(*FringeNode[V])
// Handle all 9 combinations with a single expression
switch {
// NODE cases
case nIsNode && oIsNode:
return nNode.Overlaps(oNode, depth)
case nIsNode && oIsLeaf:
return nNode.OverlapsPrefixAtDepth(oLeaf.Prefix, depth)
case nIsNode && oIsFringe:
return true
// LEAF cases
case nIsLeaf && oIsNode:
return oNode.OverlapsPrefixAtDepth(nLeaf.Prefix, depth)
case nIsLeaf && oIsLeaf:
return oLeaf.Prefix.Overlaps(nLeaf.Prefix)
case nIsLeaf && oIsFringe:
return true
// FRINGE cases
case nIsFringe:
return true // fringe overlaps with everything
default:
panic("logic error, wrong node type combination")
}
}