Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 0 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# todo

- Better function docs for the interfaces, example:
- What happens if deleting a key that doesn't exist? Perhaps use similar wording to the `delete` built-in: The delete built-in function deletes the element with the specified key (m[key]) from the map. If m is nil or there is no such element, delete is a no-op.
- Consider `Delete` in `Set`.
- Potentially
- Add last-in-first-out queue?
2 changes: 1 addition & 1 deletion map.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Map[K comparable, V any] interface {
Get(key K) (value V, loaded bool)
// Set stores a value for the given key.
Set(key K, value V)
// Delete removes the key from the map.
// Delete removes the key from the map. If the key doesn't exist, Delete is a no-op.
Delete(key K)
// Len returns the number of items in the map.
Len() int
Expand Down
21 changes: 21 additions & 0 deletions map_mutex.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func (m *MutexMap[K, V]) Set(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}
m.values[key] = value
}

Expand All @@ -37,6 +40,9 @@ func (m *MutexMap[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
return
}
delete(m.values, key)
}

Expand All @@ -62,6 +68,10 @@ func (m *MutexMap[K, V]) CompareAndSwap(key K, oldValue, newValue V) bool {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
return false
}

current, exists := m.values[key]
if !exists {
// Handle case where key doesn't exist
Expand All @@ -84,6 +94,10 @@ func (m *MutexMap[K, V]) Swap(key K, value V) (V, bool) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}

oldValue, loaded := m.values[key]
m.values[key] = value
if !loaded {
Expand All @@ -99,6 +113,10 @@ func (m *MutexMap[K, V]) LoadOrStore(key K, value V) (V, bool) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}

if v, ok := m.values[key]; ok {
return v, true
}
Expand Down Expand Up @@ -150,6 +168,9 @@ func (m *MutexMap[K, V]) SetMany(entries map[K]V) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}
maps.Insert(m.values, maps.All(entries))
}

Expand Down
21 changes: 21 additions & 0 deletions map_rwmutex.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func (m *RWMutexMap[K, V]) Set(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}
m.values[key] = value
}

Expand All @@ -37,6 +40,9 @@ func (m *RWMutexMap[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
return
}
delete(m.values, key)
}

Expand All @@ -62,6 +68,10 @@ func (m *RWMutexMap[K, V]) CompareAndSwap(key K, oldValue, newValue V) bool {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
return false
}

current, exists := m.values[key]
if !exists {
// Handle case where key doesn't exist
Expand All @@ -84,6 +94,10 @@ func (m *RWMutexMap[K, V]) Swap(key K, value V) (V, bool) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}

oldValue, loaded := m.values[key]
m.values[key] = value
if !loaded {
Expand All @@ -99,6 +113,10 @@ func (m *RWMutexMap[K, V]) LoadOrStore(key K, value V) (V, bool) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}

if v, ok := m.values[key]; ok {
return v, true
}
Expand Down Expand Up @@ -150,6 +168,9 @@ func (m *RWMutexMap[K, V]) SetMany(entries map[K]V) {
m.mu.Lock()
defer m.mu.Unlock()

if m.values == nil {
m.values = make(map[K]V)
}
maps.Insert(m.values, maps.All(entries))
}

Expand Down
81 changes: 81 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,87 @@ func TestConcurrentAccess(t *testing.T) {
}
}

func TestMapZeroValue(t *testing.T) {
t.Run("RWMutexMap", func(t *testing.T) {
var m RWMutexMap[string, int]

// Set on zero-value should initialize map
m.Set("key1", 1)
m.Set("key2", 2)
assert.Equal(t, 2, m.Len())

// Get should work
val, ok := m.Get("key1")
assert.True(t, ok)
assert.Equal(t, 1, val)

// Delete should work
m.Delete("key1")
assert.Equal(t, 1, m.Len())

// Read operations on zero-value
var m2 RWMutexMap[int, string]
_, ok = m2.Get(999)
assert.False(t, ok)
assert.Equal(t, 0, m2.Len())

// Delete on zero-value with nil map
var m3 RWMutexMap[string, int]
m3.Delete("anything") // Should not panic
assert.Equal(t, 0, m3.Len())
})

t.Run("MutexMap", func(t *testing.T) {
var m MutexMap[string, int]

// Set on zero-value should initialize map
m.Set("key1", 1)
m.Set("key2", 2)
assert.Equal(t, 2, m.Len())

// Get should work
val, ok := m.Get("key1")
assert.True(t, ok)
assert.Equal(t, 1, val)

// Delete should work
m.Delete("key1")
assert.Equal(t, 1, m.Len())

// Read operations on zero-value
var m2 MutexMap[int, string]
_, ok = m2.Get(999)
assert.False(t, ok)
assert.Equal(t, 0, m2.Len())

// Delete on zero-value with nil map
var m3 MutexMap[string, int]
m3.Delete("anything") // Should not panic
assert.Equal(t, 0, m3.Len())
})

t.Run("SyncMap", func(t *testing.T) {
// SyncMap is already zero-value safe (sync.Map is zero-value safe)
var m SyncMap[string, int]

// Set on zero-value
m.Set("key1", 1)
m.Set("key2", 2)
assert.Equal(t, 2, m.Len())

// Get should work
val, ok := m.Get("key1")
assert.True(t, ok)
assert.Equal(t, 1, val)

// Read operations work on zero-value
var m2 SyncMap[int, string]
_, ok = m2.Get(999)
assert.False(t, ok)
assert.Equal(t, 0, m2.Len())
})
}

//
// BENCHMARKS
//
Expand Down
5 changes: 3 additions & 2 deletions queue_rwmutex.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,11 @@ func (q *RWMutexQueue[T]) Slice() []T {
// the queue or its items.
func (q *RWMutexQueue[T]) Range(f func(item T) bool) {
q.mu.RLock()
items := q.items[q.head:]
snapshot := make([]T, len(q.items)-q.head)
copy(snapshot, q.items[q.head:])
q.mu.RUnlock()

for _, it := range items {
for _, it := range snapshot {
if !f(it) {
break
}
Expand Down
101 changes: 98 additions & 3 deletions queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,32 @@ func (s *queueTestSuite[T]) TestAllIterator(t *testing.T) {
assert.Equal(t, 4, q.Len())
}

func (s *queueTestSuite[T]) TestRangeSnapshot(t *testing.T) {
q := s.newQueue()
q.Push(s.item1, s.item2, s.item3)

// Range should provide a snapshot - mutations during iteration shouldn't affect what we see
var observed []T
q.Range(func(item T) bool {
observed = append(observed, item)
// Mutate the queue during iteration
if len(observed) == 1 {
q.Push(s.item1) // Add a duplicate
}
return true
})

// Should only observe the original 3 items (snapshot behavior)
assert.Equal(t, []T{s.item1, s.item2, s.item3}, observed)
// But the queue should now have 4 items
assert.Equal(t, 4, q.Len())
}

func runQueueTestSuite[T any](t *testing.T, s *queueTestSuite[T]) {
t.Run("BasicOperations", s.TestBasicOperations)
t.Run("Slice", s.TestSlice)
t.Run("Range", s.TestRange)
t.Run("RangeSnapshot", s.TestRangeSnapshot)
t.Run("AllIterator", s.TestAllIterator)
}

Expand Down Expand Up @@ -180,10 +202,10 @@ func testConcurrentQueueAccess(t *testing.T, q Queue[string]) {

// Concurrent enqueues
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
for i := range goroutines {
go func(id int) {
defer wg.Done()
for j := 0; j < perGoroutine; j++ {
for j := range perGoroutine {
q.Push(strconv.Itoa(id*perGoroutine + j))
}
}(i)
Expand All @@ -194,7 +216,7 @@ func testConcurrentQueueAccess(t *testing.T, q Queue[string]) {

// Now dequeue everything sequentially
total := goroutines * perGoroutine
for i := 0; i < total; i++ {
for range total {
item, ok := q.Pop()
assert.True(t, ok)
_ = item // value not important for this test
Expand All @@ -208,3 +230,76 @@ func TestQueueConcurrentAccess(t *testing.T) {
q := NewRWMutexQueue[string]()
testConcurrentQueueAccess(t, q)
}

func TestQueueConcurrentRange(t *testing.T) {
q := NewRWMutexQueue[int]()

// Pre-populate the queue
for i := range 100 {
q.Push(i)
}

var wg sync.WaitGroup
// Goroutine 1: Concurrent Range calls
wg.Go(func() {
for range 20 {
count := 0
q.Range(func(int) bool {
count++
return true
})
// Verify we got some items (exact count may vary due to concurrent mutations)
assert.Greater(t, count, 0)
}
})

// Goroutine 2: Concurrent Push operations
wg.Go(func() {
for i := range 100 {
q.Push(i + 1000)
}
})

// Goroutine 3: Concurrent Pop operations
wg.Go(func() {
for range 50 {
q.Pop()
}
})

wg.Wait()
// Test should complete without data races
}

func TestRWMutexQueueZeroValue(t *testing.T) {
// RWMutexQueue documents that zero-value is ready to use
var q RWMutexQueue[int]

// Push on zero-value
q.Push(1, 2, 3)
assert.Equal(t, 3, q.Len())

// Peek should work
item, ok := q.Peek()
assert.True(t, ok)
assert.Equal(t, 1, item)

// Pop should work
item, ok = q.Pop()
assert.True(t, ok)
assert.Equal(t, 1, item)
assert.Equal(t, 2, q.Len())

// Read operations on empty zero-value
var q2 RWMutexQueue[string]
assert.Equal(t, 0, q2.Len())
_, ok = q2.Peek()
assert.False(t, ok)
_, ok = q2.Pop()
assert.False(t, ok)

// Clear on zero-value should not panic
var q3 RWMutexQueue[int]
q3.Clear()
assert.Equal(t, 0, q3.Len())
}
3 changes: 2 additions & 1 deletion set.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import "iter"
type Set[T comparable] interface {
// Add stores an item in the set.
Add(item T) (added bool)
// Delete removes an item from the set.
// Delete removes an item from the set. Returns true if the item was present and removed,
// false if it was not in the set. If the item doesn't exist, Delete is a no-op.
Delete(item T) (removed bool)
// Has returns true if the item is in the set, otherwise false.
Has(item T) bool
Expand Down
Loading