Skip to content

Commit b04f563

Browse files
authored
Factor ast.Slice out into its own package (#416)
I plan to use this pattern when I implement the IR package, so having it not live in the AST package is a good idea. I've also refactored the interfaces to be simpler, and to reduce the number of methods that actually need to be implemented: almost all types (except for `ast.TypeList`) that were previously an `ast.Slice` now have an accessor for it. The `ast.Slice.Iter` function has been dropped, and iteration is now done with dedicated iterators in `seq`.
1 parent 4387357 commit b04f563

File tree

17 files changed

+456
-362
lines changed

17 files changed

+456
-362
lines changed

experimental/ast/commas.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 ast
16+
17+
import (
18+
"slices"
19+
20+
"github.com/bufbuild/protocompile/experimental/seq"
21+
"github.com/bufbuild/protocompile/experimental/token"
22+
)
23+
24+
// Commas is like [Slice], but it's for a comma-delimited list of some kind.
25+
//
26+
// This makes it easy to work with the list as though it's a slice, while also
27+
// allowing access to the commas.
28+
type Commas[T any] interface {
29+
seq.Inserter[T]
30+
31+
// Comma is like [seq.Indexer.At] but returns the comma that follows the nth
32+
// element.
33+
//
34+
// May be [token.Zero], either because it's the last element
35+
// (a common situation where there is no comma) or it was added with
36+
// Insert() rather than InsertComma().
37+
Comma(n int) token.Token
38+
39+
// AppendComma is like [seq.Append], but includes an explicit comma.
40+
AppendComma(value T, comma token.Token)
41+
42+
// InsertComma is like [seq.Inserter.Insert], but includes an explicit comma.
43+
InsertComma(n int, value T, comma token.Token)
44+
}
45+
46+
type withComma[T any] struct {
47+
Value T
48+
Comma token.ID
49+
}
50+
51+
type commas[T, E any] struct {
52+
seq.SliceInserter[T, withComma[E]]
53+
ctx Context
54+
}
55+
56+
func (c commas[T, _]) Comma(n int) token.Token {
57+
return (*c.SliceInserter.Slice)[n].Comma.In(c.ctx)
58+
}
59+
60+
func (c commas[T, _]) AppendComma(value T, comma token.Token) {
61+
c.InsertComma(c.Len(), value, comma)
62+
}
63+
64+
func (c commas[T, _]) InsertComma(n int, value T, comma token.Token) {
65+
c.ctx.Nodes().panicIfNotOurs(comma)
66+
v := c.SliceInserter.Unwrap(value)
67+
v.Comma = comma.ID()
68+
69+
*c.Slice = slices.Insert(*c.Slice, n, v)
70+
}

experimental/ast/decl_body.go

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
package ast
1616

1717
import (
18-
"slices"
19-
2018
"github.com/bufbuild/protocompile/experimental/report"
19+
"github.com/bufbuild/protocompile/experimental/seq"
2120
"github.com/bufbuild/protocompile/experimental/token"
2221
"github.com/bufbuild/protocompile/internal/arena"
2322
)
@@ -28,8 +27,6 @@ import (
2827
// "orphaned" field or oneof outside of a message, or an RPC method inside of an enum, and
2928
// so on.
3029
//
31-
// DeclBody implements [Slice], providing access to its declarations.
32-
//
3330
// # Grammar
3431
//
3532
// DeclBody := `{` DeclAny* `}`
@@ -48,10 +45,6 @@ type rawDeclBody struct {
4845
ptrs []arena.Untyped
4946
}
5047

51-
var (
52-
_ Inserter[DeclAny] = DeclBody{}
53-
)
54-
5548
// Braces returns this body's surrounding braces, if it has any.
5649
func (d DeclBody) Braces() token.Token {
5750
if d.IsZero() {
@@ -63,64 +56,39 @@ func (d DeclBody) Braces() token.Token {
6356

6457
// Span implements [report.Spanner].
6558
func (d DeclBody) Span() report.Span {
59+
decls := d.Decls()
6660
switch {
6761
case d.IsZero():
6862
return report.Span{}
6963
case !d.Braces().IsZero():
7064
return d.Braces().Span()
71-
case d.Len() == 0:
65+
case decls.Len() == 0:
7266
return report.Span{}
7367
default:
74-
return report.Join(d.At(0), d.At(d.Len()-1))
68+
return report.Join(decls.At(0), decls.At(decls.Len()-1))
7569
}
7670
}
7771

78-
// Len returns the number of declarations inside of this body.
79-
func (d DeclBody) Len() int {
80-
if d.IsZero() {
81-
return 0
82-
}
83-
84-
return len(d.raw.ptrs)
85-
}
86-
87-
// At returns the nth element of this body.
88-
func (d DeclBody) At(n int) DeclAny {
89-
return rawDecl{d.raw.ptrs[n], d.raw.kinds[n]}.With(d.Context())
90-
}
91-
92-
// Iter is an iterator over the nodes inside this body.
93-
func (d DeclBody) Iter(yield func(int, DeclAny) bool) {
72+
// Decls returns a [seq.Inserter] over the declarations in this body.
73+
func (d DeclBody) Decls() seq.Inserter[DeclAny] {
74+
type slice = seq.SliceInserter2[DeclAny, DeclKind, arena.Untyped]
9475
if d.IsZero() {
95-
return
76+
return slice{}
9677
}
9778

98-
for i := range d.raw.kinds {
99-
if !yield(i, d.At(i)) {
100-
break
101-
}
79+
return seq.SliceInserter2[DeclAny, DeclKind, arena.Untyped]{
80+
Slice1: &d.raw.kinds,
81+
Slice2: &d.raw.ptrs,
82+
Wrap: func(k DeclKind, p arena.Untyped) DeclAny {
83+
return rawDecl{p, k}.With(d.Context())
84+
},
85+
Unwrap: func(d DeclAny) (DeclKind, arena.Untyped) {
86+
d.Context().Nodes().panicIfNotOurs(d)
87+
return d.raw.kind, d.raw.ptr
88+
},
10289
}
10390
}
10491

105-
// Append appends a new declaration to this body.
106-
func (d DeclBody) Append(value DeclAny) {
107-
d.Insert(d.Len(), value)
108-
}
109-
110-
// Insert inserts a new declaration at the given index.
111-
func (d DeclBody) Insert(n int, value DeclAny) {
112-
d.Context().Nodes().panicIfNotOurs(value)
113-
114-
d.raw.kinds = slices.Insert(d.raw.kinds, n, value.Kind())
115-
d.raw.ptrs = slices.Insert(d.raw.ptrs, n, value.raw.ptr)
116-
}
117-
118-
// Delete deletes the declaration at the given index.
119-
func (d DeclBody) Delete(n int) {
120-
d.raw.kinds = slices.Delete(d.raw.kinds, n, n+1)
121-
d.raw.ptrs = slices.Delete(d.raw.ptrs, n, n+1)
122-
}
123-
12492
func wrapDeclBody(c Context, ptr arena.Pointer[rawDeclBody]) DeclBody {
12593
return DeclBody{wrapDecl(c, ptr)}
12694
}

experimental/ast/decl_file.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package ast
1616

1717
import (
1818
"github.com/bufbuild/protocompile/experimental/report"
19+
"github.com/bufbuild/protocompile/experimental/seq"
1920
"github.com/bufbuild/protocompile/experimental/token"
2021
"github.com/bufbuild/protocompile/internal/arena"
2122
"github.com/bufbuild/protocompile/internal/iter"
@@ -36,7 +37,7 @@ type File struct {
3637

3738
// Syntax returns this file's pragma, if it has one.
3839
func (f File) Syntax() (syntax DeclSyntax) {
39-
f.Iter(func(_ int, d DeclAny) bool {
40+
seq.Values(f.Decls())(func(d DeclAny) bool {
4041
if s := d.AsSyntax(); !s.IsZero() {
4142
syntax = s
4243
return false
@@ -48,7 +49,7 @@ func (f File) Syntax() (syntax DeclSyntax) {
4849

4950
// Package returns this file's package declaration, if it has one.
5051
func (f File) Package() (pkg DeclPackage) {
51-
f.Iter(func(_ int, d DeclAny) bool {
52+
seq.Values(f.Decls())(func(d DeclAny) bool {
5253
if p := d.AsPackage(); !p.IsZero() {
5354
pkg = p
5455
return false
@@ -62,7 +63,7 @@ func (f File) Package() (pkg DeclPackage) {
6263
func (f File) Imports() iter.Seq2[int, DeclImport] {
6364
return func(yield func(int, DeclImport) bool) {
6465
var i int
65-
f.Iter(func(_ int, d DeclAny) bool {
66+
seq.Values(f.Decls())(func(d DeclAny) bool {
6667
if imp := d.AsImport(); !imp.IsZero() {
6768
if !yield(i, imp) {
6869
return false

experimental/ast/decl_range.go

Lines changed: 22 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,15 @@
1515
package ast
1616

1717
import (
18-
"slices"
19-
2018
"github.com/bufbuild/protocompile/experimental/report"
19+
"github.com/bufbuild/protocompile/experimental/seq"
2120
"github.com/bufbuild/protocompile/experimental/token"
2221
"github.com/bufbuild/protocompile/internal/arena"
2322
)
2423

2524
// DeclRange represents an extension or reserved range declaration. They are almost identical
2625
// syntactically so they use the same AST node.
2726
//
28-
// In the Protocompile AST, ranges can contain arbitrary expressions. Thus, DeclRange
29-
// implements [Comma[ExprAny]].
30-
//
3127
// # Grammar
3228
//
3329
// DeclRange := (`extensions` | `reserved`) (Expr `,`)* Expr? CompactOptions? `;`?
@@ -47,10 +43,6 @@ type DeclRangeArgs struct {
4743
Semicolon token.Token
4844
}
4945

50-
var (
51-
_ Commas[ExprAny] = DeclRange{}
52-
)
53-
5446
// Keyword returns the keyword for this range.
5547
func (d DeclRange) Keyword() token.Token {
5648
if d.IsZero() {
@@ -70,64 +62,28 @@ func (d DeclRange) IsReserved() bool {
7062
return d.Keyword().Text() == "reserved"
7163
}
7264

73-
// Len implements [Slice].
74-
func (d DeclRange) Len() int {
75-
if d.IsZero() {
76-
return 0
77-
}
78-
79-
return len(d.raw.args)
80-
}
81-
82-
// At implements [Slice].
83-
func (d DeclRange) At(n int) ExprAny {
84-
return newExprAny(d.Context(), d.raw.args[n].Value)
85-
}
86-
87-
// Iter implements [Slice].
88-
func (d DeclRange) Iter(yield func(int, ExprAny) bool) {
65+
// Ranges returns the sequence of expressions denoting the ranges in this
66+
// range declaration.
67+
func (d DeclRange) Ranges() Commas[ExprAny] {
68+
type slice = commas[ExprAny, rawExpr]
8969
if d.IsZero() {
90-
return
70+
return slice{}
9171
}
92-
for i, arg := range d.raw.args {
93-
if !yield(i, newExprAny(d.Context(), arg.Value)) {
94-
break
95-
}
72+
return slice{
73+
ctx: d.Context(),
74+
SliceInserter: seq.SliceInserter[ExprAny, withComma[rawExpr]]{
75+
Slice: &d.raw.args,
76+
Wrap: func(c withComma[rawExpr]) ExprAny {
77+
return newExprAny(d.Context(), c.Value)
78+
},
79+
Unwrap: func(e ExprAny) withComma[rawExpr] {
80+
d.Context().Nodes().panicIfNotOurs(e)
81+
return withComma[rawExpr]{Value: e.raw}
82+
},
83+
},
9684
}
9785
}
9886

99-
// Append implements [Inserter].
100-
func (d DeclRange) Append(expr ExprAny) {
101-
d.InsertComma(d.Len(), expr, token.Zero)
102-
}
103-
104-
// Insert implements [Inserter].
105-
func (d DeclRange) Insert(n int, expr ExprAny) {
106-
d.InsertComma(n, expr, token.Zero)
107-
}
108-
109-
// Delete implements [Inserter].
110-
func (d DeclRange) Delete(n int) {
111-
d.raw.args = slices.Delete(d.raw.args, n, n+1)
112-
}
113-
114-
// Comma implements [Commas].
115-
func (d DeclRange) Comma(n int) token.Token {
116-
return d.raw.args[n].Comma.In(d.Context())
117-
}
118-
119-
// AppendComma implements [Commas].
120-
func (d DeclRange) AppendComma(expr ExprAny, comma token.Token) {
121-
d.InsertComma(d.Len(), expr, comma)
122-
}
123-
124-
// InsertComma implements [Commas].
125-
func (d DeclRange) InsertComma(n int, expr ExprAny, comma token.Token) {
126-
d.Context().Nodes().panicIfNotOurs(expr, comma)
127-
128-
d.raw.args = slices.Insert(d.raw.args, n, withComma[rawExpr]{expr.raw, comma.ID()})
129-
}
130-
13187
// Options returns the compact options list for this range.
13288
func (d DeclRange) Options() CompactOptions {
13389
if d.IsZero() {
@@ -157,16 +113,17 @@ func (d DeclRange) Semicolon() token.Token {
157113

158114
// Span implements [report.Spanner].
159115
func (d DeclRange) Span() report.Span {
116+
r := d.Ranges()
160117
switch {
161118
case d.IsZero():
162119
return report.Span{}
163-
case d.Len() == 0:
120+
case r.Len() == 0:
164121
return report.Join(d.Keyword(), d.Semicolon(), d.Options())
165122
default:
166123
return report.Join(
167124
d.Keyword(), d.Semicolon(), d.Options(),
168-
d.At(0),
169-
d.At(d.Len()-1),
125+
r.At(0),
126+
r.At(r.Len()-1),
170127
)
171128
}
172129
}

0 commit comments

Comments
 (0)