Skip to content

Commit efac672

Browse files
authored
starlark: new API for go1.23 push iterators (#527)
This change defines two helpers, Elements(seq) and Entries(mapping), that adapt the Iterable and IterableMapping interfaces to one- and two-variable go1.23 range loops. The new syntax is more concise and frees the caller from worrying about Iterator.Done. We do not yet update any calls to use them, so that the project can continue to be build with pre-go1.23 releases of Go. Also, define Elements methods on {*List,Tuple,*Set} and an Entries method on *Dict. These optimized iterators (which can fetch both k and v in one go) will be used by the Elements and Entries standalone functions when the operand supports it. (User-defined types can implement these methods too, although the interface isn't currently documented.) Also, a go1.23-only test of the new iteration.
1 parent 981680b commit efac672

File tree

3 files changed

+239
-3
lines changed

3 files changed

+239
-3
lines changed

starlark/hashtable.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,16 @@ func (it *keyIterator) Done() {
416416
}
417417
}
418418

419+
// entries is a go1.23 iterator over the entries of the hash table.
420+
func (ht *hashtable) entries(yield func(k, v Value) bool) {
421+
if !ht.frozen {
422+
ht.itercount++
423+
defer func() { ht.itercount-- }()
424+
}
425+
for e := ht.head; e != nil && yield(e.key, e.value); e = e.next {
426+
}
427+
}
428+
419429
var seed = maphash.MakeSeed()
420430

421431
// hashString computes the hash of s.

starlark/iterator_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2024 The Bazel Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.23
6+
7+
package starlark_test
8+
9+
// This file defines tests of the starlark.Value Go API's go1.23 iterators:
10+
//
11+
// ({Tuple,*List,(Set}).Elements
12+
// Elements
13+
// (*Dict).Entries
14+
// Entries
15+
16+
import (
17+
"fmt"
18+
"reflect"
19+
"testing"
20+
21+
. "go.starlark.net/starlark"
22+
)
23+
24+
func TestTupleElements(t *testing.T) {
25+
tuple := Tuple{MakeInt(1), MakeInt(2), MakeInt(3)}
26+
27+
var got []string
28+
for elem := range tuple.Elements {
29+
got = append(got, fmt.Sprint(elem))
30+
if len(got) == 2 {
31+
break // skip 3
32+
}
33+
}
34+
for elem := range Elements(tuple) {
35+
got = append(got, fmt.Sprint(elem))
36+
if len(got) == 4 {
37+
break // skip 3
38+
}
39+
}
40+
want := []string{"1", "2", "1", "2"}
41+
if !reflect.DeepEqual(got, want) {
42+
t.Errorf("got %v, want %v", got, want)
43+
}
44+
}
45+
46+
func TestListElements(t *testing.T) {
47+
list := NewList([]Value{MakeInt(1), MakeInt(2), MakeInt(3)})
48+
49+
var got []string
50+
for elem := range list.Elements {
51+
got = append(got, fmt.Sprint(elem))
52+
if len(got) == 2 {
53+
break // skip 3
54+
}
55+
}
56+
for elem := range Elements(list) {
57+
got = append(got, fmt.Sprint(elem))
58+
if len(got) == 4 {
59+
break // skip 3
60+
}
61+
}
62+
want := []string{"1", "2", "1", "2"}
63+
if !reflect.DeepEqual(got, want) {
64+
t.Errorf("got %v, want %v", got, want)
65+
}
66+
}
67+
68+
func TestSetElements(t *testing.T) {
69+
set := NewSet(3)
70+
set.Insert(MakeInt(1))
71+
set.Insert(MakeInt(2))
72+
set.Insert(MakeInt(3))
73+
74+
var got []string
75+
for elem := range set.Elements {
76+
got = append(got, fmt.Sprint(elem))
77+
if len(got) == 2 {
78+
break // skip 3
79+
}
80+
}
81+
for elem := range Elements(set) {
82+
got = append(got, fmt.Sprint(elem))
83+
if len(got) == 4 {
84+
break // skip 3
85+
}
86+
}
87+
want := []string{"1", "2", "1", "2"}
88+
if !reflect.DeepEqual(got, want) {
89+
t.Errorf("got %v, want %v", got, want)
90+
}
91+
}
92+
93+
func TestDictEntries(t *testing.T) {
94+
dict := NewDict(2)
95+
dict.SetKey(String("one"), MakeInt(1))
96+
dict.SetKey(String("two"), MakeInt(2))
97+
dict.SetKey(String("three"), MakeInt(3))
98+
99+
var got []string
100+
for k, v := range dict.Entries {
101+
got = append(got, fmt.Sprintf("%v %v", k, v))
102+
if len(got) == 2 {
103+
break // skip 3
104+
}
105+
}
106+
for k, v := range Entries(dict) {
107+
got = append(got, fmt.Sprintf("%v %v", k, v))
108+
if len(got) == 4 {
109+
break // skip 3
110+
}
111+
}
112+
want := []string{`"one" 1`, `"two" 2`, `"one" 1`, `"two" 2`}
113+
if !reflect.DeepEqual(got, want) {
114+
t.Errorf("got %v, want %v", got, want)
115+
}
116+
}

starlark/value.go

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,17 @@ var (
254254
//
255255
// Example usage:
256256
//
257-
// iter := iterable.Iterator()
257+
// var seq Iterator = ...
258+
// iter := seq.Iterate()
258259
// defer iter.Done()
259-
// var x Value
260-
// for iter.Next(&x) {
260+
// var elem Value
261+
// for iter.Next(elem) {
261262
// ...
262263
// }
264+
//
265+
// Or, using go1.23 iterators:
266+
//
267+
// for elem := range Elements(seq) { ... }
263268
type Iterator interface {
264269
// If the iterator is exhausted, Next returns false.
265270
// Otherwise it sets *p to the current element of the sequence,
@@ -283,6 +288,8 @@ type Mapping interface {
283288
}
284289

285290
// An IterableMapping is a mapping that supports key enumeration.
291+
//
292+
// See [Entries] for example use.
286293
type IterableMapping interface {
287294
Mapping
288295
Iterate() Iterator // see Iterable interface
@@ -847,6 +854,7 @@ func (d *Dict) Type() string { return "dict"
847854
func (d *Dict) Freeze() { d.ht.freeze() }
848855
func (d *Dict) Truth() Bool { return d.Len() > 0 }
849856
func (d *Dict) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: dict") }
857+
func (d *Dict) Entries(yield func(k, v Value) bool) { d.ht.entries(yield) }
850858

851859
func (x *Dict) Union(y *Dict) *Dict {
852860
z := new(Dict)
@@ -954,6 +962,23 @@ func (l *List) Iterate() Iterator {
954962
return &listIterator{l: l}
955963
}
956964

965+
// Elements is a go1.23 iterator over the elements of the list.
966+
//
967+
// Example:
968+
//
969+
// for elem := range list.Elements { ... }
970+
func (l *List) Elements(yield func(Value) bool) {
971+
if !l.frozen {
972+
l.itercount++
973+
defer func() { l.itercount-- }()
974+
}
975+
for _, x := range l.elems {
976+
if !yield(x) {
977+
break
978+
}
979+
}
980+
}
981+
957982
func (x *List) CompareSameType(op syntax.Token, y_ Value, depth int) (bool, error) {
958983
y := y_.(*List)
959984
// It's tempting to check x == y as an optimization here,
@@ -1053,6 +1078,20 @@ func (t Tuple) Slice(start, end, step int) Value {
10531078
}
10541079

10551080
func (t Tuple) Iterate() Iterator { return &tupleIterator{elems: t} }
1081+
1082+
// Elements is a go1.23 iterator over the elements of the tuple.
1083+
//
1084+
// (A Tuple is a slice, so it is of course directly iterable. This
1085+
// method exists to provide a fast path for the [Elements] standalone
1086+
// function.)
1087+
func (t Tuple) Elements(yield func(Value) bool) {
1088+
for _, x := range t {
1089+
if !yield(x) {
1090+
break
1091+
}
1092+
}
1093+
}
1094+
10561095
func (t Tuple) Freeze() {
10571096
for _, elem := range t {
10581097
elem.Freeze()
@@ -1124,6 +1163,9 @@ func (s *Set) Truth() Bool { return s.Len() > 0 }
11241163

11251164
func (s *Set) Attr(name string) (Value, error) { return builtinAttr(s, name, setMethods) }
11261165
func (s *Set) AttrNames() []string { return builtinAttrNames(setMethods) }
1166+
func (s *Set) Elements(yield func(k Value) bool) {
1167+
s.ht.entries(func(k, _ Value) bool { return yield(k) })
1168+
}
11271169

11281170
func (x *Set) CompareSameType(op syntax.Token, y_ Value, depth int) (bool, error) {
11291171
y := y_.(*Set)
@@ -1561,6 +1603,74 @@ func Iterate(x Value) Iterator {
15611603
return nil
15621604
}
15631605

1606+
// Elements returns an iterator for the elements of the iterable value.
1607+
//
1608+
// Example of go1.23 iteration:
1609+
//
1610+
// for elem := range Elements(iterable) { ... }
1611+
//
1612+
// Push iterators are provided as a convience for Go client code. The
1613+
// core iteration behavior of Starlark for-loops is defined by the
1614+
// [Iterable] interface.
1615+
//
1616+
// TODO(adonovan): change return type to go1.23 iter.Seq[Value].
1617+
func Elements(iterable Iterable) func(yield func(Value) bool) {
1618+
// Use specialized push iterator if available (*List, Tuple, *Set).
1619+
type hasElements interface {
1620+
Elements(yield func(k Value) bool)
1621+
}
1622+
if iterable, ok := iterable.(hasElements); ok {
1623+
return iterable.Elements
1624+
}
1625+
1626+
iter := iterable.Iterate()
1627+
return func(yield func(Value) bool) {
1628+
defer iter.Done()
1629+
var x Value
1630+
for iter.Next(&x) && yield(x) {
1631+
}
1632+
}
1633+
}
1634+
1635+
// Entries returns an iterator over the entries (key/value pairs) of
1636+
// the iterable mapping.
1637+
//
1638+
// Example of go1.23 iteration:
1639+
//
1640+
// for k, v := range Entries(mapping) { ... }
1641+
//
1642+
// Push iterators are provided as a convience for Go client code. The
1643+
// core iteration behavior of Starlark for-loops is defined by the
1644+
// [Iterable] interface.
1645+
//
1646+
// TODO(adonovan): change return type to go1.23 iter.Seq2[Value, Value].
1647+
func Entries(mapping IterableMapping) func(yield func(k, v Value) bool) {
1648+
// If available (e.g. *Dict), use specialized push iterator,
1649+
// as it gets k and v in one shot.
1650+
type hasEntries interface {
1651+
Entries(yield func(k, v Value) bool)
1652+
}
1653+
if mapping, ok := mapping.(hasEntries); ok {
1654+
return mapping.Entries
1655+
}
1656+
1657+
iter := mapping.Iterate()
1658+
return func(yield func(k, v Value) bool) {
1659+
defer iter.Done()
1660+
var k Value
1661+
for iter.Next(&k) {
1662+
v, found, err := mapping.Get(k)
1663+
if err != nil || !found {
1664+
panic(fmt.Sprintf("Iterate and Get are inconsistent (mapping=%v, key=%v)",
1665+
mapping.Type(), k.Type()))
1666+
}
1667+
if !yield(k, v) {
1668+
break
1669+
}
1670+
}
1671+
}
1672+
}
1673+
15641674
// Bytes is the type of a Starlark binary string.
15651675
//
15661676
// A Bytes encapsulates an immutable sequence of bytes.

0 commit comments

Comments
 (0)