organize, add mem package

This commit is contained in:
Judah Caruso 2025-12-06 22:32:37 -07:00
parent bc751cc791
commit 19effc40a6
13 changed files with 454 additions and 68 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
.idea/

1
assert.go Normal file
View file

@ -0,0 +1 @@
package xx

1
assert_disabled.go Normal file
View file

@ -0,0 +1 @@
package xx

74
mem/mem.go Normal file
View file

@ -0,0 +1,74 @@
package mem
import (
"unsafe"
)
// SizeOf returns the size (in bytes) of the given type.
//
// Not to be confused with [unsafe.Sizeof] which returns the size of a type via an expression.
func SizeOf[T any]() uintptr {
var zero T
return unsafe.Sizeof(zero)
}
// AlignOf returns the alignment (in bytes) of the given type.
//
// Not to be confused with [unsafe.AlignOf] which returns the alignment of a type via an expression.
func AlignOf[T any]() uintptr {
var zero T
return unsafe.Alignof(zero)
}
// BitCast performs a bit conversion between two types of the same size.
//
// BitCast panics if the sizes of the types differ.
func BitCast[TOut any, TIn any](value *TIn) TOut {
if SizeOf[TOut]() != SizeOf[TIn]() {
panic("bitcast: sizes of types must match")
}
return *((*TOut)(unsafe.Pointer(value)))
}
// Copy copies size number of bytes from src into dst.
//
// Returns dst.
func Copy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer {
copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size))
return dst
}
// Clear overwrites 'count' number of bytes in 'dst' with a particular value.
//
// Returns dst.
func Clear(dst unsafe.Pointer, value byte, count uintptr) unsafe.Pointer {
b := (*byte)(dst)
for range count {
*b = value
b = (*byte)(unsafe.Add(dst, 1))
}
return dst
}
// Zero overwrites 'count' number of bytes in 'dst' with zeros.
//
// Returns dst.
func Zero(dst unsafe.Pointer, count uintptr) unsafe.Pointer {
return Clear(dst, 0, count)
}
// AlignForward returns an address align to the next power-of-two alignment.
func AlignForward(address uintptr, alignment uintptr) uintptr {
if alignment == 0 || (alignment&(alignment-1)) != 0 {
panic("alignforward: alignment must be a power of two")
}
return (address + alignment - 1) &^ (alignment - 1)
}
// AlignBackward returns an address align to the previous power-of-two alignment.
func AlignBackward(address uintptr, alignment uintptr) uintptr {
if alignment == 0 || (alignment&(alignment-1)) != 0 {
panic("alignbackward: alignment must be a power of two")
}
return address &^ (alignment - 1)
}

26
mem/mem_test.go Normal file
View file

@ -0,0 +1,26 @@
package mem_test
import (
"testing"
"git.brut.systems/judah/xx/mem"
)
func TestBitCast(t *testing.T) {
a := uint32(0xFFFF_FFFF)
b := mem.BitCast[float32](&a)
c := mem.BitCast[uint32](&b)
if a != c {
t.Fail()
}
v := uint8(0xFF)
d := mem.BitCast[int8](&v)
if d != -1 {
t.Fail()
}
e := mem.BitCast[uint8](&d)
if e != 255 {
t.Fail()
}
}

112
stable/array.go Normal file
View file

@ -0,0 +1,112 @@
package stable
import (
"iter"
)
const DefaultElementsPerBucket = 32
// Array is a resizable array whose values will never move in memory.
// This means it is safe to take a pointer to a value within the array
// while continuing to append to it.
type Array[T any] struct {
buckets []bucket[T]
last int
elements_per_bucket int
}
func (s *Array[T]) Init() {
s.InitWithCapacity(DefaultElementsPerBucket)
}
func (s *Array[T]) InitWithCapacity(elements_per_bucket int) {
if elements_per_bucket <= 0 {
elements_per_bucket = DefaultElementsPerBucket
}
s.elements_per_bucket = elements_per_bucket
s.buckets = s.buckets[:0]
s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket))
s.last = 0
}
func (s *Array[T]) Reset() {
s.buckets = s.buckets[:0]
s.last = 0
}
func (s *Array[T]) Append(value T) *T {
if len(s.buckets) == 0 {
s.Init()
}
if len(s.buckets[s.last]) == cap(s.buckets[s.last]) {
s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket))
s.last += 1
}
s.buckets[s.last] = append(s.buckets[s.last], value)
return &s.buckets[s.last][len(s.buckets[s.last])-1]
}
func (s *Array[T]) AppendMany(values ...T) (first *T) {
if len(values) == 0 {
return nil
}
first = s.Append(values[0])
if len(values) > 1 {
for _, v := range values[1:] {
s.Append(v)
}
}
return
}
func (s *Array[T]) Get(index int) *T {
b := s.buckets[index/s.elements_per_bucket]
return &b[index%s.elements_per_bucket]
}
func (s *Array[T]) Set(index int, value T) {
*s.Get(index) = value
}
func (s *Array[T]) Len() int {
return s.Cap() - (cap(s.buckets[s.last]) - len(s.buckets[s.last]))
}
func (s *Array[T]) Cap() int {
return len(s.buckets) * s.elements_per_bucket
}
func (s *Array[T]) Pointers() iter.Seq2[int, *T] {
return func(yield func(int, *T) bool) {
for bi := range s.buckets {
startIdx := bi * s.elements_per_bucket
for i := range s.buckets[bi] {
if !yield(startIdx+i, &s.buckets[bi][i]) {
return
}
}
}
}
}
func (s *Array[T]) Values() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for bi, b := range s.buckets {
startIdx := bi * s.elements_per_bucket
for i := range b {
if !yield(startIdx+i, b[i]) {
return
}
}
}
}
}
type bucket[T any] = []T

82
stable/array_test.go Normal file
View file

@ -0,0 +1,82 @@
package stable_test
import (
"runtime"
"testing"
"git.brut.systems/judah/xx/stable"
)
func TestArray_StableWithGC(t *testing.T) {
type valuewithptr struct {
value int
ptr *int
}
var arr stable.Array[valuewithptr]
aptr := arr.Append(valuewithptr{value: 10, ptr: nil})
bptr := arr.Append(valuewithptr{value: 20, ptr: &aptr.value})
const N = 1000
for i := range N {
arr.Append(valuewithptr{value: i})
runtime.GC()
}
expect(t, arr.Get(0) == aptr)
expect(t, arr.Get(1) == bptr)
expect(t, arr.Len() == N+2, "len was %d", arr.Len())
expect(t, bptr.ptr != nil && bptr.value == 20)
expect(t, bptr.ptr == &aptr.value, "%p vs. %p", bptr.ptr, &aptr.value)
}
func BenchmarkArray_RandomAccess(b *testing.B) {
var arr stable.Array[int]
for i := range b.N {
arr.Append(i * i)
}
b.ResetTimer()
for i := range b.N {
arr.Get(i % 10000)
}
}
func BenchmarkArray_Append(b *testing.B) {
var arr stable.Array[int]
for i := range b.N {
arr.Append(i * i)
}
arr.Reset()
for i := range b.N {
arr.Append(i * i)
}
}
func BenchmarkArray_Iteration(b *testing.B) {
var arr stable.Array[int]
for i := range b.N {
arr.Append(i * i)
}
b.ResetTimer()
sum := 0
for _, v := range arr.Values() {
sum += v
}
}
func expect(t *testing.T, cond bool, message ...any) {
t.Helper()
if !cond {
if len(message) == 0 {
message = append(message, "assertion failed")
}
str := message[0].(string)
t.Fatalf(str, message[1:]...)
}
}

117
stable/pointer_test.go Normal file
View file

@ -0,0 +1,117 @@
package pointer
import (
"testing"
)
func TestAlignForward(t *testing.T) {
tests := []struct {
name string
offset uintptr
alignment uintptr
}{
{"align 8 bytes", 1, 8},
{"align 16 bytes", 3, 16},
{"align 32 bytes", 7, 32},
{"align 64 bytes", 15, 64},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := int32(789)
pinned := Pin(&value)
defer pinned.Unpin()
// Add offset to misalign
misaligned := pinned.Add(tt.offset)
aligned := misaligned.AlignForward(tt.alignment)
// Check alignment
if aligned.Address()%tt.alignment != 0 {
t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment)
}
// Check it's forward aligned (greater or equal)
if aligned.Address() < misaligned.Address() {
t.Errorf("Forward aligned address %d should be >= original %d", aligned.Address(), misaligned.Address())
}
})
}
}
func TestAlignBackward(t *testing.T) {
tests := []struct {
name string
offset uintptr
alignment uintptr
}{
{"align 8 bytes", 5, 8},
{"align 16 bytes", 10, 16},
{"align 32 bytes", 20, 32},
{"align 64 bytes", 40, 64},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := int32(321)
pinned := Pin(&value)
defer pinned.Unpin()
// Add offset to misalign
misaligned := pinned.Add(tt.offset)
aligned := misaligned.AlignBackward(tt.alignment)
// Check alignment
if aligned.Address()%tt.alignment != 0 {
t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment)
}
// Check it's backward aligned (less or equal)
if aligned.Address() > misaligned.Address() {
t.Errorf("Backward aligned address %d should be <= original %d", aligned.Address(), misaligned.Address())
}
})
}
}
func TestNth(t *testing.T) {
// Test with int32 array
arr := []int32{10, 20, 30, 40, 50}
pinned := Pin(&arr[0])
defer pinned.Unpin()
for i := 0; i < len(arr); i++ {
value := Nth[int32](pinned, i)
if value != arr[i] {
t.Errorf("Index %d: expected %d, got %d", i, arr[i], value)
}
}
}
func TestNthFloat64(t *testing.T) {
// Test with a float64 array
arr := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
pinned := Pin(&arr[0])
defer pinned.Unpin()
for i := 0; i < len(arr); i++ {
value := Nth[float64](pinned, i)
if value != arr[i] {
t.Errorf("Index %d: expected %f, got %f", i, arr[i], value)
}
}
}
func TestPointerArithmeticChain(t *testing.T) {
value := int32(888)
pinned := Pin(&value)
defer pinned.Unpin()
// Test chaining operations
result := pinned.Add(16).Add(8).Sub(4)
expected := pinned.Address() + 16 + 8 - 4
if result.Address() != expected {
t.Errorf("Expected address %d, got %d", expected, result.Address())
}
}

1
stable/xar.go Normal file
View file

@ -0,0 +1 @@
package stable

1
stable/xar_test.go Normal file
View file

@ -0,0 +1 @@
package stable

View file

@ -1,49 +0,0 @@
package xx
import (
"unsafe"
)
// New returns a newly allocated value with an initial value.
func New[T any](expr T) *T {
p := new(T)
*p = expr
return p
}
// Bitcast performs a bit conversion between two types of the same size.
//
// Bitcast panics if the sizes of the types differ.
func Bitcast[TOut any, TIn any](value TIn) TOut {
if SizeOf[TOut]() != SizeOf[TIn]() {
panic("bitcast: sizes of types must match")
}
return *((*TOut)(unsafe.Pointer(&value)))
}
// Copy copies src number of bytes into dst.
// Returns dst.
//
// Copy panics if src is smaller than dst.
func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst {
if SizeOf[TSrc]() < SizeOf[TDst]() {
panic("copy: size of src must be >= dst")
}
MemCopy(unsafe.Pointer(dst), unsafe.Pointer(src), SizeOf[TDst]())
return dst
}
// MemCopy copies size number of bytes from src into dst.
// Returns dst.
func MemCopy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer {
copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size))
return dst
}
// SizeOf returns the size in bytes of the given type.
//
// Not to be confused with [unsafe.Sizeof] which returns the size of an expression.
func SizeOf[T any]() uintptr {
var zero T
return unsafe.Sizeof(zero)
}

35
xx.go Normal file
View file

@ -0,0 +1,35 @@
package xx
import (
"unsafe"
"git.brut.systems/judah/xx/mem"
)
// New returns a newly allocated value with an initial value.
func New[T any](expr T) *T {
p := new(T)
*p = expr
return p
}
// Copy copies src number of bytes into dst.
// Returns dst.
//
// Copy panics if src is smaller than dst.
func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst {
if mem.SizeOf[TSrc]() < mem.SizeOf[TDst]() {
panic("copy: size of src must be >= dst")
}
mem.Copy(unsafe.Pointer(dst), unsafe.Pointer(src), mem.SizeOf[TDst]())
return dst
}
// Clone returns a newly allocated shallow copy of the given value.
func Clone[T any](value *T) *T {
return Copy(new(T), value)
}
func BoolUint(b bool) uint {
return uint(*(*uint8)(unsafe.Pointer(&b)))
}

View file

@ -5,6 +5,7 @@ import (
"unsafe"
"git.brut.systems/judah/xx"
"git.brut.systems/judah/xx/mem"
)
func TestNew(t *testing.T) {
@ -13,7 +14,7 @@ func TestNew(t *testing.T) {
t.Fail()
}
if unsafe.Sizeof(*a) != xx.SizeOf[uint32]() {
if unsafe.Sizeof(*a) != mem.SizeOf[uint32]() {
t.Fail()
}
@ -42,21 +43,3 @@ func TestNew(t *testing.T) {
t.Fail()
}
}
func TestBitcast(t *testing.T) {
a := uint32(0xFFFF_FFFF)
b := xx.Bitcast[float32](a)
c := xx.Bitcast[uint32](b)
if a != c {
t.Fail()
}
d := xx.Bitcast[int8](uint8(0xFF))
if d != -1 {
t.Fail()
}
e := xx.Bitcast[uint8](d)
if e != 255 {
t.Fail()
}
}