Skip to content

Commit 3a19ea5

Browse files
authored
Add various new standard library extensions (#457)
This PR aggregates new additions to the `exts` directory form various in-progress PRs, aiming to try to get those changes out of in-progress PRs and into their own, easier-to-review PR.
1 parent 9b34a89 commit 3a19ea5

File tree

15 files changed

+647
-92
lines changed

15 files changed

+647
-92
lines changed

experimental/ast/path.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"github.com/bufbuild/protocompile/experimental/report"
2323
"github.com/bufbuild/protocompile/experimental/token"
2424
"github.com/bufbuild/protocompile/internal/ext/iterx"
25-
"github.com/bufbuild/protocompile/internal/ext/slicesx"
2625
)
2726

2827
// Path represents a multi-part identifier.
@@ -70,12 +69,11 @@ func (p Path) ToRelative() Path {
7069
// AsIdent returns the single identifier that comprises this path, or
7170
// the zero token.
7271
func (p Path) AsIdent() token.Token {
73-
var buf [2]PathComponent
74-
prefix := slicesx.AppendSeq(buf[:0], iterx.Limit(2, p.Components))
75-
if len(prefix) != 1 || !prefix[0].Separator().IsZero() {
72+
first, _ := iterx.OnlyOne(p.Components)
73+
if !first.Separator().IsZero() {
7674
return token.Zero
7775
}
78-
return prefix[0].AsIdent()
76+
return first.AsIdent()
7977
}
8078

8179
// AsPredeclared returns the [predeclared.Name] that this path represents.

experimental/report/diff.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,19 @@ func unifiedDiff(span Span, edits []Edit) (Span, []hunk) {
8787

8888
// Partition offsets into overlapping lines. That is, this connects together
8989
// all edit spans whose end and start are not separated by a newline.
90-
prev := 0
91-
parts := slicesx.SplitFunc(edits, func(i int, next Edit) bool {
92-
if i == prev {
93-
return false
94-
}
95-
96-
chunk := src[edits[i-1].End:next.Start]
97-
if !strings.Contains(chunk, "\n") {
98-
return false
99-
}
100-
101-
prev = i
102-
return true
90+
parts := slicesx.SplitAfterFunc(edits, func(i int, edit Edit) bool {
91+
next, ok := slicesx.Get(edits, i+1)
92+
return ok && edit.End < next.Start && // Go treats str[x:y] for x > y as an error.
93+
strings.Contains(src[edit.End:next.Start], "\n")
10394
})
10495

10596
var out []hunk
10697
var prevHunk int
10798
parts(func(edits []Edit) bool {
99+
if len(edits) == 0 {
100+
return true
101+
}
102+
108103
// First, figure out the start and end of the modified region.
109104
start, end := edits[0].Start, edits[0].End
110105
for _, edit := range edits[1:] {

internal/ext/iterx/consume.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package iterx contains extensions to Go's package iter.
16+
package iterx
17+
18+
import (
19+
"fmt"
20+
"strings"
21+
22+
"github.com/bufbuild/protocompile/internal/iter"
23+
)
24+
25+
// Count counts the number of elements in seq that match the given predicate.
26+
//
27+
// If p is nil, it is treated as func(_ T) bool { return true }.
28+
func Count[T any](seq iter.Seq[T]) int {
29+
var total int
30+
seq(func(_ T) bool {
31+
total++
32+
return true
33+
})
34+
return total
35+
}
36+
37+
// Join is like [strings.Join], but works on an iterator. Elements are
38+
// stringified as if by [fmt.Print].
39+
func Join[T any](seq iter.Seq[T], sep string) string {
40+
var out strings.Builder
41+
first := true
42+
seq(func(v T) bool {
43+
if !first {
44+
out.WriteString(sep)
45+
}
46+
first = false
47+
48+
fmt.Fprint(&out, v)
49+
return true
50+
})
51+
return out.String()
52+
}
53+
54+
// Every returns whether every element of an iterator satisfies the given
55+
// predicate. Returns true if seq yields no values.
56+
func Every[T any](seq iter.Seq[T], p func(T) bool) bool {
57+
all := true
58+
seq(func(v T) bool {
59+
all = p(v)
60+
return all
61+
})
62+
return all
63+
}

internal/ext/iterx/filtermap.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iterx
16+
17+
import (
18+
"github.com/bufbuild/protocompile/internal/iter"
19+
)
20+
21+
// This file contains the matrix of {Map, Filter, FilterMap} x {1, 2, 1to2, 2to1},
22+
// except that Filter1to2 and Filter2to1 don't really make sense.
23+
24+
// Map returns a new iterator applying f to each element of seq.
25+
func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] {
26+
return FilterMap(seq, func(v T) (U, bool) { return f(v), true })
27+
}
28+
29+
// Filter returns a new iterator that only includes values satisfying p.
30+
func Filter[T any](seq iter.Seq[T], p func(T) bool) iter.Seq[T] {
31+
return FilterMap(seq, func(v T) (T, bool) { return v, p(v) })
32+
}
33+
34+
// FilterMap combines the operations of [Map] and [Filter].
35+
func FilterMap[T, U any](seq iter.Seq[T], f func(T) (U, bool)) iter.Seq[U] {
36+
return func(yield func(U) bool) {
37+
seq(func(v T) bool {
38+
v2, ok := f(v)
39+
return !ok || yield(v2)
40+
})
41+
}
42+
}
43+
44+
// Map2 returns a new iterator applying f to each element of seq.
45+
func Map2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W)) iter.Seq2[V, W] {
46+
return FilterMap2(seq, func(v1 T, v2 U) (V, W, bool) {
47+
x1, x2 := f(v1, v2)
48+
return x1, x2, true
49+
})
50+
}
51+
52+
// Filter2 returns a new iterator that only includes values satisfying p.
53+
func Filter2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) iter.Seq2[T, U] {
54+
return FilterMap2(seq, func(v1 T, v2 U) (T, U, bool) { return v1, v2, p(v1, v2) })
55+
}
56+
57+
// FilterMap2 combines the operations of [Map] and [Filter].
58+
func FilterMap2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W, bool)) iter.Seq2[V, W] {
59+
return func(yield func(V, W) bool) {
60+
seq(func(v1 T, v2 U) bool {
61+
x1, x2, ok := f(v1, v2)
62+
return !ok || yield(x1, x2)
63+
})
64+
}
65+
}
66+
67+
// Map2to1 is like [Map], but it also acts a Y pipe for converting a two-element
68+
// iterator into a one-element iterator.
69+
func Map2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) V) iter.Seq[V] {
70+
return FilterMap2to1(seq, func(v1 T, v2 U) (V, bool) {
71+
return f(v1, v2), true
72+
})
73+
}
74+
75+
// FilterMap2to1 is like [FilterMap], but it also acts a Y pipe for converting
76+
// a two-element iterator into a one-element iterator.
77+
func FilterMap2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) (V, bool)) iter.Seq[V] {
78+
return func(yield func(V) bool) {
79+
seq(func(v1 T, v2 U) bool {
80+
v, ok := f(v1, v2)
81+
return !ok || yield(v)
82+
})
83+
}
84+
}
85+
86+
// Map1To2 is like [Map], but it also acts a Y pipe for converting a one-element
87+
// iterator into a two-element iterator.
88+
func Map1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V)) iter.Seq2[U, V] {
89+
return FilterMap1To2(seq, func(v T) (U, V, bool) {
90+
x1, x2 := f(v)
91+
return x1, x2, true
92+
})
93+
}
94+
95+
// FilterMap1To2 is like [FilterMap], but it also acts a Y pipe for converting
96+
// a one-element iterator into a two-element iterator.
97+
func FilterMap1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V, bool)) iter.Seq2[U, V] {
98+
return func(yield func(U, V) bool) {
99+
seq(func(v T) bool {
100+
x1, x2, ok := f(v)
101+
return !ok || yield(x1, x2)
102+
})
103+
}
104+
}

internal/ext/iterx/get.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iterx
16+
17+
import (
18+
"github.com/bufbuild/protocompile/internal/iter"
19+
)
20+
21+
// First retrieves the first element of an iterator.
22+
func First[T any](seq iter.Seq[T]) (v T, ok bool) {
23+
seq(func(x T) bool {
24+
v = x
25+
ok = true
26+
return false
27+
})
28+
return v, ok
29+
}
30+
31+
// OnlyOne retrieves the only element of an iterator.
32+
func OnlyOne[T any](seq iter.Seq[T]) (v T, ok bool) {
33+
var found T
34+
seq(func(x T) bool {
35+
if !ok {
36+
found = x
37+
}
38+
ok = !ok
39+
return ok
40+
})
41+
if ok {
42+
// Ensure we return the zero value if there is more
43+
// than one element.
44+
v = found
45+
}
46+
return v, ok
47+
}
48+
49+
// Find returns the first element that matches a predicate.
50+
//
51+
// Returns the value and the index at which it was found, or -1 if it wasn't
52+
// found.
53+
func Find[T any](seq iter.Seq[T], p func(T) bool) (int, T) {
54+
var v T
55+
var idx int
56+
var found bool
57+
seq(func(x T) bool {
58+
if p(x) {
59+
v = x
60+
found = true
61+
return false
62+
}
63+
idx++
64+
return true
65+
})
66+
if !found {
67+
idx = -1
68+
}
69+
return idx, v
70+
}
71+
72+
// Find2 is like [Find] but for two-element iterators.
73+
func Find2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) (int, T, U) {
74+
var v1 T
75+
var v2 U
76+
var idx int
77+
var found bool
78+
seq(func(x1 T, x2 U) bool {
79+
if p(x1, x2) {
80+
v1, v2 = x1, x2
81+
found = true
82+
return false
83+
}
84+
idx++
85+
return true
86+
})
87+
if !found {
88+
idx = -1
89+
}
90+
return idx, v1, v2
91+
}
92+
93+
// Index returns the index of the first element of seq that satisfies p.
94+
//
95+
// if not found, returns -1.
96+
func Index[T any](seq iter.Seq[T], p func(T) bool) int {
97+
idx, _ := Find(seq, p)
98+
return idx
99+
}
100+
101+
// Index2 is like [Index], but for two-element iterators.
102+
func Index2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) int {
103+
idx, _, _ := Find2(seq, p)
104+
return idx
105+
}

0 commit comments

Comments
 (0)