diff --git a/brain/kvbrain/speak.go b/brain/kvbrain/speak.go index eae6bb6..3a578cb 100644 --- a/brain/kvbrain/speak.go +++ b/brain/kvbrain/speak.go @@ -9,17 +9,17 @@ import ( "github.com/dgraph-io/badger/v4" "github.com/zephyrtronium/robot/brain" - "github.com/zephyrtronium/robot/prepend" + "github.com/zephyrtronium/robot/deque" "github.com/zephyrtronium/robot/tpool" ) -var prependerPool tpool.Pool[*prepend.List[string]] +var prependerPool tpool.Pool[deque.Deque[string]] // Speak generates a full message and appends it to w. // The prompt is in reverse order and has entropy reduction applied. func (br *Brain) Speak(ctx context.Context, tag string, prompt []string, w *brain.Builder) error { - search := prependerPool.Get().Set(prompt...) - defer func() { prependerPool.Put(search) }() + search := prependerPool.Get().Prepend(prompt...) + defer func() { prependerPool.Put(search.Reset()) }() tb := hashTag(make([]byte, 0, tagHashLen), tag) b := make([]byte, 0, 128) @@ -41,7 +41,7 @@ func (br *Brain) Speak(ctx context.Context, tag string, prompt []string, w *brai break } w.Append(id, b) - search = search.Drop(search.Len() - l - 1).Prepend(brain.ReduceEntropy(string(b))) + search = search.DropEnd(search.Len() - l - 1).Prepend(brain.ReduceEntropy(string(b))) } return nil } diff --git a/brain/sqlbrain/speak.go b/brain/sqlbrain/speak.go index 1f8342f..de130d5 100644 --- a/brain/sqlbrain/speak.go +++ b/brain/sqlbrain/speak.go @@ -8,17 +8,17 @@ import ( "zombiezen.com/go/sqlite" "github.com/zephyrtronium/robot/brain" - "github.com/zephyrtronium/robot/prepend" + "github.com/zephyrtronium/robot/deque" "github.com/zephyrtronium/robot/tpool" ) -var prependerPool tpool.Pool[*prepend.List[string]] +var prependerPool tpool.Pool[deque.Deque[string]] // Speak generates a full message and appends it to w. // The prompt is in reverse order and has entropy reduction applied. func (br *Brain) Speak(ctx context.Context, tag string, prompt []string, w *brain.Builder) error { - search := prependerPool.Get().Set(prompt...) - defer func() { prependerPool.Put(search) }() + search := prependerPool.Get().Prepend(prompt...) + defer func() { prependerPool.Put(search.Reset()) }() conn, err := br.db.Take(ctx) defer br.db.Put(conn) @@ -39,7 +39,7 @@ func (br *Brain) Speak(ctx context.Context, tag string, prompt []string, w *brai break } w.Append(id, b) - search = search.Drop(search.Len() - l - 1).Prepend(brain.ReduceEntropy(string(b))) + search = search.DropEnd(search.Len() - l - 1).Prepend(brain.ReduceEntropy(string(b))) } return nil } diff --git a/deque/deque.go b/deque/deque.go new file mode 100644 index 0000000..18863ec --- /dev/null +++ b/deque/deque.go @@ -0,0 +1,90 @@ +// Package deque provides a slice-backed double-ended queue. +package deque + +import "slices" + +// Deque is a slice-backed double-ended queue. +type Deque[Elem any] struct { + el []Elem + // left is the position of the leftmost valid element in el. + // left >= len(el) implies the ring is empty. + left int +} + +// Len returns the number of elements in the deque. +func (d Deque[Elem]) Len() int { + return len(d.el) - d.left +} + +// Append adds elements tot he end of the deque. +func (d Deque[Elem]) Append(ee ...Elem) Deque[Elem] { + d.el = append(d.el, ee...) + return d +} + +// Prepend adds elements to the front of the deque. +func (d Deque[Elem]) Prepend(ee ...Elem) Deque[Elem] { + d = d.GrowFront(len(ee)) + d.left -= len(ee) + copy(d.Slice(), ee) + return d +} + +// GrowFront ensures there is space to [Prepend] at least n elements. +func (d Deque[Elem]) GrowFront(n int) Deque[Elem] { + if d.left >= n { + return d + } + // Grow the slice, then slide the existing elements to the end. + k := d.Len() + d.el = slices.Grow(d.el, n) + copy(d.el[cap(d.el)-k:cap(d.el)], d.Slice()) + d.el = d.el[:cap(d.el)] + d.left = cap(d.el) - k + return d +} + +// GrowEnd ensures there is space to [Append] at least n elements. +func (d Deque[Elem]) GrowEnd(n int) Deque[Elem] { + d.el = slices.Grow(d.el, n) + return d +} + +// DropEnd removes n elements from the end of the deque. +// If n is negative, there is no change. +// If n is larger than the deque's size, the result is empty. +func (d Deque[Elem]) DropEnd(n int) Deque[Elem] { + if n <= 0 { + return d + } + if n >= d.Len() { + return d.Reset() + } + d.el = d.el[:len(d.el)-n] + return d +} + +// DropEndWhile removes elements from the end of the deque until the predicate +// returns false. +// The pointer passed to the predicate is a view into the deque's memory. +func (d Deque[Elem]) DropEndWhile(pred func(Elem) bool) Deque[Elem] { + for len(d.el) > d.left { + if !pred(d.el[len(d.el)-1]) { + break + } + d.el = d.el[:len(d.el)-1] + } + return d +} + +// Reset removes all elements from the deque. +func (d Deque[Elem]) Reset() Deque[Elem] { + d.left = len(d.el) + return d +} + +// Slice returns a view into the deque's memory. +// Elements prepended to the deque appear at the beginning of the slice. +func (d Deque[Elem]) Slice() []Elem { + return d.el[d.left:] +} diff --git a/deque/deque_test.go b/deque/deque_test.go new file mode 100644 index 0000000..cc11e57 --- /dev/null +++ b/deque/deque_test.go @@ -0,0 +1,104 @@ +package deque_test + +import ( + "slices" + "testing" + + "github.com/zephyrtronium/robot/deque" +) + +func TestDeque(t *testing.T) { + cases := []struct { + name string + append []int + prepend []int + want []int + }{ + { + name: "empty", + append: nil, + prepend: nil, + want: nil, + }, + { + name: "append", + append: []int{1, 2}, + prepend: nil, + want: []int{1, 2}, + }, + { + name: "prepend", + append: nil, + prepend: []int{1, 2}, + want: []int{1, 2}, + }, + { + name: "both", + append: []int{1, 2}, + prepend: []int{3, 4}, + want: []int{3, 4, 1, 2}, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + var d deque.Deque[int] + invariants := func() { + if d.Len() != len(d.Slice()) { + t.Errorf("lens disagree: d.Len gave %d, len(d.Slice) gave %d", d.Len(), len(d.Slice())) + } + } + invariants() + d = d.Append(c.append...) + invariants() + d = d.Prepend(c.prepend...) + invariants() + if !slices.Equal(d.Slice(), c.want) { + t.Errorf("wrong result: want %v, got %v", c.want, d.Slice()) + } + }) + } +} + +func TestDropEndWhile(t *testing.T) { + cases := []struct { + name string + start []bool + want []bool + }{ + { + name: "empty", + start: nil, + want: nil, + }, + { + name: "none", + start: []bool{false, false}, + want: []bool{false, false}, + }, + { + name: "one", + start: []bool{false, true}, + want: []bool{false}, + }, + { + name: "end", + start: []bool{true, false}, + want: []bool{true, false}, + }, + { + name: "all", + start: []bool{true, true}, + want: nil, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + d := deque.Deque[bool]{}.Append(c.start...) + d = d.DropEndWhile(func(b bool) bool { return b }) + if !slices.Equal(d.Slice(), c.want) { + t.Errorf("wrong result: want %v, got %v", c.want, d.Slice()) + } + }) + } +} diff --git a/prepend/prepend.go b/prepend/prepend.go deleted file mode 100644 index 73cde0e..0000000 --- a/prepend/prepend.go +++ /dev/null @@ -1,83 +0,0 @@ -package prepend - -// List is a list that minimizes copying while prepending. -// A nil *List is useful; methods which modify the list return a possibly new -// value, similar to the append builtin function. -type List[E any] struct { - space []E - k int -} - -// Len returns the number of elements in the list. -func (p *List[E]) Len() int { - if p == nil { - return 0 - } - return len(p.space) - p.k -} - -// Slice returns the elements in the list as a Slice directly into the list's -// owned memory. -func (p *List[E]) Slice() []E { - if p == nil { - return nil - } - return p.space[p.k:] -} - -// Set sets the contents of the list. -func (p *List[E]) Set(ee ...E) *List[E] { - if len(ee) == 0 { - return p.Reset() - } - p = p.Reset() - if len(ee) > len(p.space) { - p.space = make([]E, len(ee)) - } - p.k = len(p.space) - len(ee) - copy(p.space[p.k:], ee) - return p -} - -// Prepend inserts elements in provided order at the start of the list. -func (p *List[E]) Prepend(ee ...E) *List[E] { - if p == nil { - p = new(List[E]) - } - if p.k < len(ee) { - // We don't expect enormous prompts, so a simple growth algorithm is fine. - b := make([]E, cap(p.space)*2+len(ee)) - p.k = len(b) - len(p.space) - copy(b[p.k:], p.space) - p.space = b - } - p.k -= len(ee) - copy(p.space[p.k:], ee) - return p -} - -// Drop removes the last n terms from the list. -// If n <= 0, there is no change. -// If n >= p.len(), the list becomes empty. -func (p *List[E]) Drop(n int) *List[E] { - if n <= 0 { - return p - } - if n >= p.Len() { - // As a special case, we can reset the entire list when we drop all. - // Note this branch also includes p == nil. - return p.Reset() - } - p.space = p.space[:len(p.space)-n] - return p -} - -// Reset removes all elements from the list. -func (p *List[E]) Reset() *List[E] { - if p == nil { - return new(List[E]) - } - p.space = p.space[:cap(p.space):cap(p.space)] - p.k = cap(p.space) - return p -} diff --git a/prepend/prepend_test.go b/prepend/prepend_test.go deleted file mode 100644 index c151976..0000000 --- a/prepend/prepend_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package prepend - -import ( - "fmt" - "slices" - "testing" -) - -func TestPrepender(t *testing.T) { - cases := []struct { - name string - set []int - pre [][]int - drop int - want []int - }{ - { - name: "empty", - set: nil, - pre: nil, - drop: 0, - want: nil, - }, - { - name: "set", - set: []int{1, 2}, - pre: nil, - drop: 0, - want: []int{1, 2}, - }, - { - name: "empty-pre", - set: nil, - pre: [][]int{{1}}, - drop: 0, - want: []int{1}, - }, - { - name: "pre", - set: []int{2}, - pre: [][]int{{1}}, - drop: 0, - want: []int{1, 2}, - }, - { - name: "many-pre", - set: nil, - pre: [][]int{{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}}, - drop: 0, - // prepending gives reverse order - want: []int{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, - }, - { - name: "multi-pre", - set: nil, - pre: [][]int{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, - drop: 0, - want: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, - }, - { - name: "empty-drop", - set: nil, - pre: nil, - drop: 1, - want: nil, - }, - { - name: "drop", - set: []int{1, 2}, - pre: nil, - drop: 1, - want: []int{1}, - }, - { - name: "drop-minus", - set: []int{1, 2}, - pre: nil, - drop: -1, - want: []int{1, 2}, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - var p *List[int] - invariants := func(step string) { - if want, got := len(p.Slice()), p.Len(); want != got { - t.Errorf("lengths disagree after %s: slice gives %d, len gives %d", step, want, got) - } - } - invariants("nil decl") - p = p.Set(c.set...) - invariants("set") - for _, x := range c.pre { - p = p.Prepend(x...) - invariants(fmt.Sprintf("prepend %d", x)) - } - p = p.Drop(c.drop) - invariants("drop") - if !slices.Equal(p.Slice(), c.want) { - t.Errorf("wrong final list:\nwant %v\ngot %v", c.want, p.Slice()) - } - p = p.Reset() - invariants("reset") - if len(p.Slice()) != 0 { - t.Errorf("not empty after reset: %v", p.Slice()) - } - }) - } -}