diff --git a/circ/lib.go b/circ/lib.go deleted file mode 100644 index e97a5ea..0000000 --- a/circ/lib.go +++ /dev/null @@ -1,70 +0,0 @@ -// Package circ implements a circular FIFO buffer. -package circ - -// B is a circular FIFO buffer. -type B[T any] struct { - buf []T - len int - head int - tail int -} - -// Make creates a new buffer. -func Make[T any](cap int) B[T] { - buf := make([]T, cap) - return B[T]{buf: buf} -} - -// Capacity returns the total capacity of the buffer. -func (b *B[T]) Capacity() int { - return cap(b.buf) -} - -// Len returns the number of items in the buffer. -func (b *B[T]) Len() int { - return b.len -} - -// CanPush returns true if the buffer has space for new items. -func (b *B[T]) CanPush() bool { - return cap(b.buf)-b.len != 0 -} - -// CanPop returns true if the buffer has one or more items. -func (b *B[T]) CanPop() bool { - return b.len != 0 -} - -// PopFront returns the front-most item from the buffer. -// -// If the buffer is empty, it panics. -func (b *B[T]) PopFront() T { - if !b.CanPop() { - panic("cannot pop from empty buffer") - } - item := b.buf[b.head] - // clear buffer slot so as not to hold on to garbage - var empty T - b.buf[b.head] = empty - b.len-- - b.head++ - if b.head == cap(b.buf) { - b.head = 0 - } - return item -} - -// PushBack adds an item to the end of the buffer. -// -// If the buffer is full, it panics. -func (b *B[T]) PushBack(value T) { - if !b.CanPush() { - panic("cannot push back to full buffer") - } - b.buf[b.tail] = value - b.len++ - b.tail++ - if b.tail == cap(b.buf) { - b.tail = 0 - } -} diff --git a/mq/lib.go b/mq/lib.go index b1cc221..2fbbca6 100644 --- a/mq/lib.go +++ b/mq/lib.go @@ -31,7 +31,7 @@ import ( "sync" "gogs.humancabbage.net/sam/priorityq" - "gogs.humancabbage.net/sam/priorityq/circ" + "gogs.humancabbage.net/sam/priorityq/queue" ) // Q is a concurrent, dual-priority message queue. @@ -41,8 +41,8 @@ type Q[T any] struct { // Make a new queue. func Make[T any](cap int) Q[T] { - high := circ.Make[T](cap) - low := circ.Make[T](cap) + high := queue.Make[T](cap) + low := queue.Make[T](cap) s := &state[T]{ high: high, low: low, @@ -55,8 +55,8 @@ func Make[T any](cap int) Q[T] { type state[T any] struct { mu sync.Mutex - high circ.B[T] - low circ.B[T] + high queue.Q[T] + low queue.Q[T] canSendHigh sync.Cond canSendLow sync.Cond canRecv sync.Cond @@ -163,7 +163,7 @@ func (s *state[T]) TrySendLow(value T) error { return s.trySend(value, &s.low) } -func (s *state[T]) send(value T, buf *circ.B[T], cond *sync.Cond) { +func (s *state[T]) send(value T, buf *queue.Q[T], cond *sync.Cond) { s.mu.Lock() defer s.mu.Unlock() for { @@ -181,7 +181,7 @@ func (s *state[T]) send(value T, buf *circ.B[T], cond *sync.Cond) { } } -func (s *state[T]) trySend(value T, buf *circ.B[T]) error { +func (s *state[T]) trySend(value T, buf *queue.Q[T]) error { s.mu.Lock() defer s.mu.Unlock() if s.closed { diff --git a/queue/lib.go b/queue/lib.go new file mode 100644 index 0000000..2ba2d9c --- /dev/null +++ b/queue/lib.go @@ -0,0 +1,75 @@ +// Package q implements a non-concurrent queue. +// +// # Implementation +// +// [Q] is a classic ring buffer. It tracks its head, tail, and length. This +// makes determining whether the queue is full or empty trivial. +package queue + +// Q is a non-concurrent queue. +type Q[T any] struct { + buf []T + len int + head int + tail int +} + +// Make creates a new queue. +func Make[T any](cap int) Q[T] { + buf := make([]T, cap) + return Q[T]{buf: buf} +} + +// Capacity returns the total capacity of the queue. +func (b *Q[T]) Capacity() int { + return cap(b.buf) +} + +// Len returns the number of items in the queue. +func (b *Q[T]) Len() int { + return b.len +} + +// CanPush returns true if the queue has space for new items. +func (b *Q[T]) CanPush() bool { + return cap(b.buf)-b.len != 0 +} + +// CanPop returns true if the queue has one or more items. +func (b *Q[T]) CanPop() bool { + return b.len != 0 +} + +// PopFront returns the front-most item from the queue. +// +// If the queue is empty, it panics. +func (b *Q[T]) PopFront() T { + if !b.CanPop() { + panic("cannot pop from empty queue") + } + item := b.buf[b.head] + // clear queue slot so as not to hold on to garbage + var empty T + b.buf[b.head] = empty + b.len-- + b.head++ + if b.head == cap(b.buf) { + b.head = 0 + } + return item +} + +// PushBack adds an item to the end of the queue. +// +// If the queue is full, it panics. +func (b *Q[T]) PushBack(value T) { + if !b.CanPush() { + panic("cannot push back to full queue") + } + b.buf[b.tail] = value + b.len++ + b.tail++ + if b.tail == cap(b.buf) { + b.tail = 0 + } +} diff --git a/circ/lib_test.go b/queue/lib_test.go similarity index 66% rename from circ/lib_test.go rename to queue/lib_test.go index fa1625b..90219af 100644 --- a/circ/lib_test.go +++ b/queue/lib_test.go @@ -1,28 +1,28 @@ -package circ_test +package queue_test import ( "math/rand" "testing" - "gogs.humancabbage.net/sam/priorityq/circ" + "gogs.humancabbage.net/sam/priorityq/queue" ) func TestRepeatPushPop(t *testing.T) { t.Parallel() - cb := circ.Make[int](4) - if cb.Capacity() != 4 { + q := queue.Make[int](4) + if q.Capacity() != 4 { t.Errorf("wrong capacity") } for i := 0; i < 50; i++ { - cb.PushBack(1) - cb.PushBack(2) - cb.PushBack(3) - cb.PushBack(4) - if cb.Len() != 4 { + q.PushBack(1) + q.PushBack(2) + q.PushBack(3) + q.PushBack(4) + if q.Len() != 4 { t.Errorf("wrong length") } checkPop := func(n int) { - if v := cb.PopFront(); v != n { + if v := q.PopFront(); v != n { t.Errorf("popped %d, expected %d", v, n) } } @@ -35,18 +35,18 @@ func TestRepeatPushPop(t *testing.T) { func TestInterleavedPushPop(t *testing.T) { t.Parallel() - cb := circ.Make[int](4) + q := queue.Make[int](4) checkPop := func(n int) { - if v := cb.PopFront(); v != n { + if v := q.PopFront(); v != n { t.Errorf("popped %d, expected %d", v, n) } } - cb.PushBack(1) - cb.PushBack(2) - cb.PushBack(3) + q.PushBack(1) + q.PushBack(2) + q.PushBack(3) checkPop(1) - cb.PushBack(4) - cb.PushBack(5) + q.PushBack(4) + q.PushBack(5) checkPop(2) } @@ -57,8 +57,8 @@ func TestEmptyPopPanic(t *testing.T) { } }() t.Parallel() - cb := circ.Make[int](4) - cb.PopFront() + q := queue.Make[int](4) + q.PopFront() } func TestFullPushPanic(t *testing.T) { @@ -68,13 +68,13 @@ func TestFullPushPanic(t *testing.T) { } }() t.Parallel() - cb := circ.Make[int](1) - cb.PushBack(1) - cb.PushBack(2) + q := queue.Make[int](1) + q.PushBack(1) + q.PushBack(2) } func BenchmarkPush(b *testing.B) { - cb := circ.Make[int](b.N) + q := queue.Make[int](b.N) rs := rand.NewSource(0) r := rand.New(rs) items := make([]int, b.N) @@ -83,19 +83,19 @@ func BenchmarkPush(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - cb.PushBack(items[i]) + q.PushBack(items[i]) } } func BenchmarkPop(b *testing.B) { - cb := circ.Make[int](b.N) + q := queue.Make[int](b.N) rs := rand.NewSource(0) r := rand.New(rs) for i := 0; i < b.N; i++ { - cb.PushBack(r.Int()) + q.PushBack(r.Int()) } b.ResetTimer() for i := 0; i < b.N; i++ { - cb.PopFront() + q.PopFront() } }