Skip to content

Commit d2a738b

Browse files
committed
refactor: change funds queue internals
1 parent a947bfb commit d2a738b

2 files changed

Lines changed: 100 additions & 131 deletions

File tree

Lines changed: 76 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package interpreter
22

33
import (
4+
"fmt"
45
"math/big"
6+
"slices"
57
)
68

79
type Sender struct {
@@ -10,103 +12,44 @@ type Sender struct {
1012
Color string
1113
}
1214

13-
type queue[T any] struct {
14-
Head T
15-
Tail *queue[T]
16-
17-
// Instead of keeping a single ref of the lastCell and updating the invariant on every push/pop operation,
18-
// we keep a cache of the last cell on every cell.
19-
// This makes code much easier and we don't risk breaking the invariant and producing wrong results and other subtle issues
20-
//
21-
// While, unlike keeping a single reference (like golang's queue `container/list` package does), this is not always O(1),
22-
// the amortized time should still be O(1) (the number of steps of traversal while searching the last elem is not higher than the number of .Push() calls)
23-
lastCell *queue[T]
24-
}
25-
26-
func (s *queue[T]) getLastCell() *queue[T] {
27-
// check if this is the last cell without reading cache first
28-
if s.Tail == nil {
29-
return s
30-
}
31-
32-
// if not, check if cache is present
33-
if s.lastCell != nil {
34-
// even if it is, it may be a stale value (as more values could have been pushed), so we check the value recursively
35-
lastCell := s.lastCell.getLastCell()
36-
// we do path compression so that next time we get the path immediately
37-
s.lastCell = lastCell
38-
return lastCell
39-
}
40-
41-
// if no last value is cached, we traverse recursively to find it
42-
s.lastCell = s.Tail.getLastCell()
43-
return s.lastCell
44-
}
45-
46-
func fromSlice[T any](slice []T) *queue[T] {
47-
var ret *queue[T]
48-
// TODO use https://pkg.go.dev/slices#Backward in golang 1.23
49-
for i := len(slice) - 1; i >= 0; i-- {
50-
ret = &queue[T]{
51-
Head: slice[i],
52-
Tail: ret,
53-
}
54-
}
55-
return ret
56-
}
57-
5815
type fundsQueue struct {
59-
senders *queue[Sender]
16+
senders []Sender
6017
}
6118

6219
func newFundsQueue(senders []Sender) fundsQueue {
63-
return fundsQueue{
64-
senders: fromSlice(senders),
65-
}
66-
}
67-
68-
func (s *fundsQueue) compactTop() {
69-
for s.senders != nil && s.senders.Tail != nil {
70-
71-
first := s.senders.Head
72-
second := s.senders.Tail.Head
73-
74-
if second.Amount.Cmp(big.NewInt(0)) == 0 {
75-
s.senders = &queue[Sender]{Head: first, Tail: s.senders.Tail.Tail}
76-
continue
77-
}
78-
79-
if first.Name != second.Name || first.Color != second.Color {
80-
return
81-
}
82-
83-
s.senders = &queue[Sender]{
84-
Head: Sender{
85-
Name: first.Name,
86-
Color: first.Color,
87-
Amount: new(big.Int).Add(first.Amount, second.Amount),
88-
},
89-
Tail: s.senders.Tail.Tail,
90-
}
20+
queue := fundsQueue{
21+
senders: []Sender{},
9122
}
23+
queue.Push(senders...)
24+
return queue
9225
}
9326

27+
// Pull everything from this queue
9428
func (s *fundsQueue) PullAll() []Sender {
95-
var senders []Sender
96-
for s.senders != nil {
97-
senders = append(senders, s.senders.Head)
98-
s.senders = s.senders.Tail
99-
}
29+
senders := s.senders
30+
s.senders = []Sender{} // TODO better heuristics for initial alloc
10031
return senders
10132
}
10233

10334
func (s *fundsQueue) Push(senders ...Sender) {
104-
newTail := fromSlice(senders)
105-
if s.senders == nil {
106-
s.senders = newTail
35+
for _, sender := range senders {
36+
s.PushOne(sender)
37+
}
38+
}
39+
40+
func (s *fundsQueue) PushOne(sender Sender) {
41+
if sender.Amount.Cmp(big.NewInt(0)) == 0 {
42+
return
43+
}
44+
if len(s.senders) == 0 {
45+
s.senders = []Sender{sender}
46+
return
47+
}
48+
last := s.senders[len(s.senders)-1]
49+
if last.Name == sender.Name && last.Color == sender.Color {
50+
last.Amount.Add(last.Amount, sender.Amount)
10751
} else {
108-
cell := s.senders.getLastCell()
109-
cell.Tail = newTail
52+
s.senders = append(s.senders, sender)
11053
}
11154
}
11255

@@ -121,68 +64,71 @@ func (s *fundsQueue) PullUncolored(requiredAmount *big.Int) []Sender {
12164
return s.PullColored(requiredAmount, "")
12265
}
12366

124-
func (s *fundsQueue) Pull(requiredAmount *big.Int, color *string) []Sender {
67+
// Pull at most maxAmount from this queue, with the given color
68+
func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender {
12569
// clone so that we can manipulate this arg
126-
requiredAmount = new(big.Int).Set(requiredAmount)
70+
maxAmount = new(big.Int).Set(maxAmount)
12771

12872
// TODO preallocate for perfs
129-
var out []Sender
130-
131-
for requiredAmount.Cmp(big.NewInt(0)) != 0 && s.senders != nil {
132-
s.compactTop()
73+
out := newFundsQueue([]Sender{})
74+
offset := 0
13375

134-
available := s.senders.Head
135-
s.senders = s.senders.Tail
76+
for maxAmount.Sign() > 0 && len(s.senders) > offset {
77+
frontSender := s.senders[offset]
13678

137-
if color != nil && available.Color != *color {
138-
out1 := s.Pull(requiredAmount, color)
139-
s.senders = &queue[Sender]{
140-
Head: available,
141-
Tail: s.senders,
142-
}
143-
out = append(out, out1...)
144-
break
79+
if color != nil && frontSender.Color != *color {
80+
offset += 1
81+
continue
14582
}
14683

147-
switch available.Amount.Cmp(requiredAmount) {
148-
case -1: // not enough:
149-
out = append(out, available)
150-
requiredAmount.Sub(requiredAmount, available.Amount)
151-
152-
case 1: // more than enough
153-
s.senders = &queue[Sender]{
154-
Head: Sender{
155-
Name: available.Name,
156-
Color: available.Color,
157-
Amount: new(big.Int).Sub(available.Amount, requiredAmount),
158-
},
159-
Tail: s.senders,
84+
switch frontSender.Amount.Cmp(maxAmount) {
85+
case -1: // not enough
86+
maxAmount.Sub(maxAmount, frontSender.Amount)
87+
out.Push(frontSender)
88+
if offset == 0 {
89+
s.senders = s.senders[1:]
90+
} else {
91+
s.senders = slices.Delete(s.senders, offset, offset+1)
16092
}
161-
fallthrough
162-
163-
case 0: // exactly the same
164-
out = append(out, Sender{
165-
Name: available.Name,
166-
Color: available.Color,
167-
Amount: new(big.Int).Set(requiredAmount),
93+
case 1: // more than enough
94+
out.Push(Sender{
95+
Name: frontSender.Name,
96+
Amount: maxAmount,
97+
Color: frontSender.Color,
16898
})
169-
return out
99+
s.senders[offset].Amount.Sub(s.senders[offset].Amount, maxAmount)
100+
return out.senders
101+
case 0: // exactly enough
102+
out.Push(s.senders[offset])
103+
if offset == 0 {
104+
s.senders = s.senders[1:]
105+
} else {
106+
s.senders = slices.Delete(s.senders, offset, offset+1)
107+
}
108+
return out.senders
170109
}
171-
172110
}
173111

174-
return out
112+
return out.senders
175113
}
176114

177115
// Clone the queue so that you can safely mutate one without mutating the other
178116
func (s fundsQueue) Clone() fundsQueue {
179-
fq := newFundsQueue(nil)
117+
return newFundsQueue(s.senders)
118+
}
180119

181-
senders := s.senders
182-
for senders != nil {
183-
fq.Push(senders.Head)
184-
senders = senders.Tail
120+
func (s fundsQueue) String() string {
121+
out := "<"
122+
for i, sender := range s.senders {
123+
if sender.Color == "" {
124+
out += fmt.Sprintf("%v from %v", sender.Amount, sender.Name)
125+
} else {
126+
out += fmt.Sprintf("%v from %v\\%v", sender.Amount, sender.Name, sender.Color)
127+
}
128+
if i != len(s.senders)-1 {
129+
out += ", "
130+
}
185131
}
186-
187-
return fq
132+
out += ">"
133+
return out
188134
}

internal/interpreter/funds_queue_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func TestPullZero(t *testing.T) {
5555
})
5656

5757
out := queue.PullAnything(big.NewInt(0))
58-
require.Equal(t, []Sender(nil), out)
58+
require.Len(t, out, 0)
5959
}
6060

6161
func TestCompactFunds(t *testing.T) {
@@ -161,6 +161,29 @@ func TestPullColored(t *testing.T) {
161161
}, queue.PullAll())
162162
}
163163

164+
func TestOrdering(t *testing.T) {
165+
queue := newFundsQueue(nil)
166+
queue.PushOne(Sender{
167+
Name: "users:001",
168+
Amount: big.NewInt(15),
169+
})
170+
queue.PushOne(Sender{
171+
Name: "users:002",
172+
Amount: big.NewInt(15),
173+
})
174+
175+
out := queue.PullUncolored(big.NewInt(10))
176+
require.Equal(t, []Sender{
177+
{Name: "users:001", Amount: big.NewInt(10)},
178+
}, out)
179+
180+
out = queue.PullUncolored(big.NewInt(20))
181+
require.Equal(t, []Sender{
182+
{Name: "users:001", Amount: big.NewInt(5)},
183+
{Name: "users:002", Amount: big.NewInt(15)},
184+
}, out)
185+
}
186+
164187
func TestPullColoredComplex(t *testing.T) {
165188
queue := newFundsQueue([]Sender{
166189
{"s1", big.NewInt(1), "c1"},

0 commit comments

Comments
 (0)