Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion common.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"net/netip"

"github.com/gaissmai/bart/internal/nodes"
"github.com/gaissmai/bart/internal/value"

// inlining hint, see also the TestInlineBitSet256Functions.
// without this silent import the BitSet256 functions are not inlined
Expand Down Expand Up @@ -90,7 +91,7 @@ func cloneVal[V any](val V) V {
// panicOnZST panics if V is a zero sized type.
// bart.Fast must reject zero-sized types as payload.
func panicOnZST[V any]() {
if nodes.IsZST[V]() {
if value.IsZST[V]() {
panic(fmt.Errorf("%T is a zero-sized type, not allowed as payload for bart.Fast", *new(V)))
}
}
14 changes: 9 additions & 5 deletions internal/nodes/bartmethodsgenerated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions internal/nodes/commonmethods_tmpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/gaissmai/bart/internal/allot"
"github.com/gaissmai/bart/internal/art"
"github.com/gaissmai/bart/internal/bitset"
"github.com/gaissmai/bart/internal/value"
)

type _NODE_TYPE[V any] struct {
Expand Down Expand Up @@ -837,7 +838,7 @@ func (n *_NODE_TYPE[V]) Dump(w io.Writer, path StridePath, depth int, is4 bool)
indent := strings.Repeat(".", depth)

// printing values if V is not zero-sized
printVal := !IsZST[V]()
printVal := !value.IsZST[V]()

// node type with depth and octet path and bits.
fmt.Fprintf(w, "\n%s[%s] depth: %d path: [%s] / %d\n",
Expand Down Expand Up @@ -1113,6 +1114,9 @@ func (n *_NODE_TYPE[V]) FprintRec(w io.Writer, parent TrieItem[V], pad string) e
return CmpPrefix(a.Cidr, b.Cidr)
})

// printing values if V is not zero-sized
printVal := !value.IsZST[V]()

// for all direct item under this node ...
for i, item := range directItems {
// symbols used in tree
Expand All @@ -1127,11 +1131,11 @@ func (n *_NODE_TYPE[V]) FprintRec(w io.Writer, parent TrieItem[V], pad string) e

var err error
// val is the empty struct, don't print it
switch {
case IsZST[V](): // skip printing values if V is zero-sized
_, err = fmt.Fprintf(w, "%s%s\n", pad+glyph, item.Cidr)
default:
if printVal {
_, 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 {
Expand Down
14 changes: 9 additions & 5 deletions internal/nodes/fastmethodsgenerated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions internal/nodes/litemethodsgenerated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 0 additions & 30 deletions internal/nodes/nodebasics.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,36 +84,6 @@ func (nt nodeType) String() string {
}
}

// IsZST reports whether type V is a zero-sized type (ZST).
//
// Zero-sized types such as struct{}, [0]byte, or structs/arrays with no fields
// occupy no memory. The Go runtime optimizes allocations of ZSTs by returning
// pointers to the same memory address (typically runtime.zerobase).
//
// This function exploits that optimization: it allocates two instances of V
// and compares their addresses. If the addresses are equal, V must be a ZST,
// since distinct non-zero-sized allocations would have different addresses.
//
// The helper escapeToHeap ensures both allocations reach the heap and prevents
// the compiler from proving address equality at compile time, which would
// invalidate the runtime check.
func IsZST[V any]() bool {
a, b := escapeToHeap[V]()
return a == b
}

// escapeToHeap forces two allocations of type V to escape to the heap.
//
// The go:noinline directive is critical: it prevents the compiler from inlining
// this function and optimizing away the allocations or proving that a == b at
// compile time. Without it, the compiler could elide one allocation or determine
// the result statically, breaking the ZST detection heuristic.
//
//go:noinline
func escapeToHeap[V any]() (*V, *V) {
return new(V), new(V)
}

// addrFmt, different format strings for IPv4 and IPv6, decimal versus hex.
func addrFmt(addr byte, is4 bool) string {
if is4 {
Expand Down
34 changes: 34 additions & 0 deletions internal/value/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2025 Karl Gaissmaier
// SPDX-License-Identifier: MIT

package value

// IsZST reports whether type V is a zero-sized type (ZST).
//
// Zero-sized types such as struct{}, [0]byte, or structs/arrays with no fields
// occupy no memory. The Go runtime optimizes allocations of ZSTs by returning
// pointers to the same memory address (typically runtime.zerobase).
//
// This function exploits that optimization: it allocates two instances of V
// and compares their addresses. If the addresses are equal, V must be a ZST,
// since distinct non-zero-sized allocations would have different addresses.
//
// The helper escapeToHeap ensures both allocations reach the heap and prevents
// the compiler from proving address equality at compile time, which would
// invalidate the runtime check.
func IsZST[V any]() bool {
a, b := escapeToHeap[V]()
return a == b
}

// escapeToHeap forces two allocations of type V to escape to the heap.
//
// The go:noinline directive is critical: it prevents the compiler from inlining
// this function and optimizing away the allocations or proving that a == b at
// compile time. Without it, the compiler could elide one allocation or determine
// the result statically, breaking the ZST detection heuristic.
//
//go:noinline
func escapeToHeap[V any]() (*V, *V) {
return new(V), new(V)
}
39 changes: 39 additions & 0 deletions internal/value/value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2024 Karl Gaissmaier
// SPDX-License-Identifier: MIT

package value

import (
"testing"
)

func TestIsZeroSizedType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
got bool
want bool
}{
{
name: "struct{}",
got: IsZST[struct{}](),
want: true,
},
{
name: "[0]byte",
got: IsZST[[0]byte](),
want: true,
},
{
name: "int",
got: IsZST[int](),
want: false,
},
}

for _, tt := range tests {
if tt.got != tt.want {
t.Errorf("%s, want %v, got %v", tt.name, tt.want, tt.got)
}
}
}
Loading