Skip to content

Commit 51b2cc7

Browse files
authored
fix(iterator): ensure HasNext does not advance state (#47)
- Refactored bufferedIterator to make HasNext idempotent. - Added peek logic to prefetch the next valid node without advancing. - Updated HasNext and Next to align with the expected behavior in docs. - Added test cases to verify HasNext idempotency and empty iterator behavior. Signed-off-by: Pavel Larkin <[email protected]>
1 parent a91325c commit 51b2cc7

File tree

2 files changed

+113
-28
lines changed

2 files changed

+113
-28
lines changed

tree_iterator.go

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package art
22

3+
import "errors"
4+
35
// state represents the iteration state during tree traversal.
46
type state struct {
57
items []*iteratorContext
68
}
79

10+
// push adds a new iterator context to the state.
811
func (s *state) push(ctx *iteratorContext) {
912
s.items = append(s.items, ctx)
1013
}
1114

15+
// current returns the current iterator context and a flag indicating if there is any.
1216
func (s *state) current() (*iteratorContext, bool) {
1317
if len(s.items) == 0 {
1418
return nil, false
@@ -17,6 +21,7 @@ func (s *state) current() (*iteratorContext, bool) {
1721
return s.items[len(s.items)-1], true
1822
}
1923

24+
// discard removes the last iterator context from the state.
2025
func (s *state) discard() {
2126
if len(s.items) == 0 {
2227
return
@@ -67,6 +72,7 @@ type iterator struct {
6772
// assert that iterator implements the Iterator interface.
6873
var _ Iterator = (*iterator)(nil)
6974

75+
// newTreeIterator creates a new tree iterator.
7076
func newTreeIterator(tr *tree, opts traverseOpts) Iterator {
7177
state := &state{}
7278
state.push(newIteratorContext(tr.root, opts.hasReverse()))
@@ -83,10 +89,15 @@ func newTreeIterator(tr *tree, opts traverseOpts) Iterator {
8389
return it
8490
}
8591

86-
return &bufferedIterator{
92+
bit := &bufferedIterator{
8793
opts: opts,
8894
it: it,
8995
}
96+
97+
// peek the first node or leaf
98+
bit.peek()
99+
100+
return bit
90101
}
91102

92103
// hasConcurrentModification checks if the tree has been modified concurrently.
@@ -148,52 +159,67 @@ type bufferedIterator struct {
148159
nextErr error
149160
}
150161

162+
// HasNext returns true if there are more nodes to iterate.
151163
func (bit *bufferedIterator) HasNext() bool {
152-
for bit.hasNext() {
153-
nxt, err := bit.peek()
154-
if err != nil {
155-
return true
156-
}
164+
return bit.nextNode != nil
165+
}
157166

158-
// are we looking for a leaf node?
159-
if bit.hasLeafIterator() && nxt.Kind() == Leaf {
160-
return true
161-
}
167+
// Next returns the next node or leaf node and an error if any.
168+
// ErrNoMoreNodes is returned if there are no more nodes to iterate.
169+
// ErrConcurrentModification is returned if the tree has been modified concurrently.
170+
func (bit *bufferedIterator) Next() (Node, error) {
171+
current := bit.nextNode
162172

163-
// are we looking for a non-leaf node?
164-
if bit.hasNodeIterator() && nxt.Kind() != Leaf {
165-
return true
166-
}
173+
if !bit.HasNext() {
174+
return nil, bit.nextErr
167175
}
168176

169-
bit.resetNext()
177+
bit.peek()
170178

171-
return false
172-
}
179+
// ErrConcurrentModification should be returned immediately.
180+
// ErrNoMoreNodes will be return on the next call.
181+
if errors.Is(bit.nextErr, ErrConcurrentModification) {
182+
return nil, bit.nextErr
183+
}
173184

174-
func (bit *bufferedIterator) Next() (Node, error) {
175-
return bit.nextNode, bit.nextErr
185+
return current, nil
176186
}
177187

188+
// hasLeafIterator checks if the iterator is for leaf nodes.
178189
func (bit *bufferedIterator) hasLeafIterator() bool {
179190
return bit.opts&TraverseLeaf == TraverseLeaf
180191
}
181192

193+
// hasNodeIterator checks if the iterator is for non-leaf nodes.
182194
func (bit *bufferedIterator) hasNodeIterator() bool {
183195
return bit.opts&TraverseNode == TraverseNode
184196
}
185197

186-
func (bit *bufferedIterator) hasNext() bool {
187-
return bit.it.HasNext()
198+
// peek looks for the next node or leaf node to iterate.
199+
func (bit *bufferedIterator) peek() {
200+
for {
201+
bit.nextNode, bit.nextErr = bit.it.Next()
202+
if bit.nextErr != nil {
203+
return
204+
}
205+
206+
if bit.matchesFilter() {
207+
return
208+
}
209+
}
188210
}
189211

190-
func (bit *bufferedIterator) peek() (Node, error) {
191-
bit.nextNode, bit.nextErr = bit.it.Next()
212+
// matchesFilter checks if the next node matches the iterator filter.
213+
func (bit *bufferedIterator) matchesFilter() bool {
214+
// check if the iterator is looking for leaf nodes
215+
if bit.hasLeafIterator() && bit.nextNode.Kind() == Leaf {
216+
return true
217+
}
192218

193-
return bit.nextNode, bit.nextErr
194-
}
219+
// check if the iterator is looking for non-leaf nodes
220+
if bit.hasNodeIterator() && bit.nextNode.Kind() != Leaf {
221+
return true
222+
}
195223

196-
func (bit *bufferedIterator) resetNext() {
197-
bit.nextNode = nil
198-
bit.nextErr = nil
224+
return false
199225
}

tree_traversal_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,63 @@ func TestTreeIterateWordsStats(t *testing.T) {
529529

530530
stats = collectStats(tree.Iterator(TraverseNode))
531531
assert.Equal(t, treeStats{0, 113419, 10433, 403, 1}, stats)
532+
533+
// by default Iterator traverses only leaf nodes
534+
stats = collectStats(tree.Iterator())
535+
assert.Equal(t, treeStats{235886, 0, 0, 0, 0}, stats)
536+
}
537+
538+
func TestIteratorHasNextDoesNotAdvanceState(t *testing.T) {
539+
t.Parallel()
540+
541+
tree := newTree()
542+
tree.Insert(Key("1"), []byte{1})
543+
tree.Insert(Key("2"), []byte{2})
544+
545+
iter := tree.Iterator()
546+
547+
// HasNext should not advance the iterator state
548+
assert.True(t, iter.HasNext())
549+
assert.True(t, iter.HasNext())
550+
551+
// change the iterator state
552+
n, err := iter.Next()
553+
require.NoError(t, err)
554+
assert.Equal(t, Key("1"), n.Key())
555+
556+
// HasNext remains idempotent
557+
assert.True(t, iter.HasNext())
558+
assert.True(t, iter.HasNext())
559+
560+
// advance to the second key
561+
n, err = iter.Next()
562+
require.NoError(t, err)
563+
assert.Equal(t, Key("2"), n.Key())
564+
565+
// HasNext returns false at the end
566+
assert.False(t, iter.HasNext())
567+
assert.False(t, iter.HasNext())
568+
569+
// calling Next after the iterator is exhausted
570+
for i := 0; i < 2; i++ {
571+
n, err = iter.Next()
572+
assert.Nil(t, n, "Next() should return nil after exhaustion")
573+
assert.Equal(t, ErrNoMoreNodes, err, "Next() should return ErrNoMoreNodes after exhaustion")
574+
}
575+
}
576+
577+
func TestIteratorEmptyTreeBehavior(t *testing.T) {
578+
t.Parallel()
579+
580+
tree := New()
581+
iter := tree.Iterator()
582+
583+
// HasNext should return false for an empty tree
584+
assert.False(t, iter.HasNext())
585+
assert.False(t, iter.HasNext())
586+
587+
// Next should return nil and ErrNoMoreNodes for an empty tree
588+
n, err := iter.Next()
589+
assert.Nil(t, n)
590+
assert.Equal(t, ErrNoMoreNodes, err)
532591
}

0 commit comments

Comments
 (0)