diff --git a/experimental/ast/decl_def.go b/experimental/ast/decl_def.go index df168442..e976f040 100644 --- a/experimental/ast/decl_def.go +++ b/experimental/ast/decl_def.go @@ -17,6 +17,7 @@ package ast import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/arena" ) @@ -87,7 +88,7 @@ type DeclDefArgs struct { // introduced by a special keyword, this will be a [TypePath] whose single // identifier is that keyword. // -// See [DeclDef.Keyword]. +// See [DeclDef.KeywordToken]. func (d DeclDef) Type() TypeAny { if d.IsZero() { return TypeAny{} @@ -101,19 +102,28 @@ func (d DeclDef) SetType(ty TypeAny) { d.raw.ty = ty.raw } -// Keyword returns the introducing keyword for this definition, if +// KeywordToken returns the introducing keyword for this definition, if // there is one. // // See [DeclDef.Type] for details on where this keyword comes from. -func (d DeclDef) Keyword() token.Token { +func (d DeclDef) Keyword() keyword.Keyword { + return d.KeywordToken().Keyword() +} + +// KeywordToken returns the introducing keyword token for this definition, if +// there is one. +// +// See [DeclDef.Type] for details on where this keyword comes from. +func (d DeclDef) KeywordToken() token.Token { path := d.Type().AsPath() if path.IsZero() { return token.Zero } ident := path.Path.AsIdent() - switch ident.Text() { - case "message", "enum", "service", "extend", "oneof", "group", "rpc", "option": + switch ident.Keyword() { + case keyword.Message, keyword.Enum, keyword.Service, keyword.Extend, + keyword.Oneof, keyword.Group, keyword.RPC, keyword.Option: return ident default: return token.Zero @@ -234,7 +244,7 @@ func (d DeclDef) Semicolon() token.Token { // See [DeclDef.Classify]. func (d DeclDef) AsMessage() DefMessage { return DefMessage{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Body: d.Body(), Decl: d, @@ -250,7 +260,7 @@ func (d DeclDef) AsMessage() DefMessage { // See [DeclDef.Classify]. func (d DeclDef) AsEnum() DefEnum { return DefEnum{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Body: d.Body(), Decl: d, @@ -266,7 +276,7 @@ func (d DeclDef) AsEnum() DefEnum { // See [DeclDef.Classify]. func (d DeclDef) AsService() DefService { return DefService{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Body: d.Body(), Decl: d, @@ -281,7 +291,7 @@ func (d DeclDef) AsService() DefService { // See [DeclDef.Classify]. func (d DeclDef) AsExtend() DefExtend { return DefExtend{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Extendee: d.Name(), Body: d.Body(), Decl: d, @@ -316,7 +326,7 @@ func (d DeclDef) AsField() DefField { // See [DeclDef.Classify]. func (d DeclDef) AsOneof() DefOneof { return DefOneof{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Body: d.Body(), Decl: d, @@ -332,7 +342,7 @@ func (d DeclDef) AsOneof() DefOneof { // See [DeclDef.Classify]. func (d DeclDef) AsGroup() DefGroup { return DefGroup{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Equals: d.Equals(), Tag: d.Value(), @@ -368,7 +378,7 @@ func (d DeclDef) AsEnumValue() DefEnumValue { // See [DeclDef.Classify]. func (d DeclDef) AsMethod() DefMethod { return DefMethod{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Name: d.Name().AsIdent(), Signature: d.Signature(), Body: d.Body(), @@ -384,7 +394,7 @@ func (d DeclDef) AsMethod() DefMethod { // See [DeclDef.Classify]. func (d DeclDef) AsOption() DefOption { return DefOption{ - Keyword: d.Keyword(), + Keyword: d.KeywordToken(), Option: Option{ Path: d.Name(), Equals: d.Equals(), @@ -399,7 +409,7 @@ func (d DeclDef) AsOption() DefOption { // definition it's supposed to represent. // // To select which definition this probably is, this function looks at -// [DeclDef.Keyword]. If there is no keyword or it isn't something that it +// [DeclDef.KeywordToken]. If there is no keyword or it isn't something that it // recognizes, it is classified as either an enum value or a field, depending on // whether this definition has a type. // @@ -411,7 +421,7 @@ func (d DeclDef) Classify() DefKind { return DefKindInvalid } - switch d.Keyword().Text() { + switch d.KeywordToken().Text() { case "message": if !d.Body().IsZero() { return DefKindMessage diff --git a/experimental/ast/decl_file.go b/experimental/ast/decl_file.go index 5bd72871..baea1244 100644 --- a/experimental/ast/decl_file.go +++ b/experimental/ast/decl_file.go @@ -18,6 +18,7 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/arena" "github.com/bufbuild/protocompile/internal/iter" ) @@ -35,7 +36,7 @@ type File struct { DeclBody } -// Syntax returns this file's pragma, if it has one. +// Syntax returns this file's declaration, if it has one. func (f File) Syntax() (syntax DeclSyntax) { seq.Values(f.Decls())(func(d DeclAny) bool { if s := d.AsSyntax(); !s.IsZero() { @@ -76,7 +77,7 @@ func (f File) Imports() iter.Seq2[int, DeclImport] { } } -// DeclSyntax represents a language pragma, such as the syntax or edition +// DeclSyntax represents a language declaration, such as the syntax or edition // keywords. // // # Grammar @@ -103,8 +104,13 @@ type DeclSyntaxArgs struct { Semicolon token.Token } -// Keyword returns the keyword for this pragma. -func (d DeclSyntax) Keyword() token.Token { +// Keyword returns the keyword for this declaration. +func (d DeclSyntax) Keyword() keyword.Keyword { + return d.KeywordToken().Keyword() +} + +// KeywordToken returns the keyword token for this declaration. +func (d DeclSyntax) KeywordToken() token.Token { if d.IsZero() { return token.Zero } @@ -112,14 +118,14 @@ func (d DeclSyntax) Keyword() token.Token { return d.raw.keyword.In(d.Context()) } -// IsSyntax checks whether this is an OG syntax pragma. +// IsSyntax checks whether this is an OG syntax declaration. func (d DeclSyntax) IsSyntax() bool { - return d.Keyword().Text() == "syntax" + return d.Keyword() == keyword.Syntax } -// IsEdition checks whether this is a new-style edition pragma. +// IsEdition checks whether this is a new-style edition declaration. func (d DeclSyntax) IsEdition() bool { - return d.Keyword().Text() == "edition" + return d.Keyword() == keyword.Edition } // Equals returns the equals sign after the keyword. @@ -133,7 +139,7 @@ func (d DeclSyntax) Equals() token.Token { return d.raw.equals.In(d.Context()) } -// Value returns the value expression of this pragma. +// Value returns the value expression of this declaration. // // May be zero, if the user wrote something like syntax;. It can also be // a number or an identifier, for cases like edition = 2024; or syntax = proto2;. @@ -145,7 +151,7 @@ func (d DeclSyntax) Value() ExprAny { return newExprAny(d.Context(), d.raw.value) } -// SetValue sets the expression for this pragma's value. +// SetValue sets the expression for this declaration's value. // // If passed zero, this clears the value (e.g., for syntax = ;). func (d DeclSyntax) SetValue(expr ExprAny) { @@ -170,7 +176,7 @@ func (d DeclSyntax) SetOptions(opts CompactOptions) { d.raw.options = d.Context().Nodes().options.Compress(opts.raw) } -// Semicolon returns this pragma's ending semicolon. +// Semicolon returns this declaration's ending semicolon. // // May be zero, if the user forgot it. func (d DeclSyntax) Semicolon() token.Token { @@ -187,7 +193,7 @@ func (d DeclSyntax) Span() report.Span { return report.Span{} } - return report.Join(d.Keyword(), d.Equals(), d.Value(), d.Semicolon()) + return report.Join(d.KeywordToken(), d.Equals(), d.Value(), d.Semicolon()) } func wrapDeclSyntax(c Context, ptr arena.Pointer[rawDeclSyntax]) DeclSyntax { @@ -219,8 +225,13 @@ type DeclPackageArgs struct { Semicolon token.Token } -// Keyword returns the "package" keyword for this declaration. -func (d DeclPackage) Keyword() token.Token { +// Keyword returns the keyword for this declaration. +func (d DeclPackage) Keyword() keyword.Keyword { + return d.KeywordToken().Keyword() +} + +// KeywordToken returns the "package" token for this declaration. +func (d DeclPackage) KeywordToken() token.Token { if d.IsZero() { return token.Zero } @@ -274,7 +285,7 @@ func (d DeclPackage) Span() report.Span { return report.Span{} } - return report.Join(d.Keyword(), d.Path(), d.Semicolon()) + return report.Join(d.KeywordToken(), d.Path(), d.Semicolon()) } func wrapDeclPackage(c Context, ptr arena.Pointer[rawDeclPackage]) DeclPackage { @@ -306,8 +317,13 @@ type DeclImportArgs struct { Semicolon token.Token } -// Keyword returns the "import" keyword for this pragma. -func (d DeclImport) Keyword() token.Token { +// Keyword returns the keyword for this declaration. +func (d DeclImport) Keyword() keyword.Keyword { + return d.KeywordToken().Keyword() +} + +// KeywordToken returns the "import" keyword for this declaration. +func (d DeclImport) KeywordToken() token.Token { if d.IsZero() { return token.Zero } @@ -315,10 +331,15 @@ func (d DeclImport) Keyword() token.Token { return d.raw.keyword.In(d.Context()) } -// Keyword returns the modifier keyword for this pragma. +// Modifier returns the modifier keyword for this declaration. +func (d DeclImport) Modifier() keyword.Keyword { + return d.ModifierToken().Keyword() +} + +// ModifierToken returns the modifier token for this declaration. // // May be zero if there is no modifier. -func (d DeclImport) Modifier() token.Token { +func (d DeclImport) ModifierToken() token.Token { if d.IsZero() { return token.Zero } @@ -328,12 +349,12 @@ func (d DeclImport) Modifier() token.Token { // IsSyntax checks whether this is an "import public". func (d DeclImport) IsPublic() bool { - return d.Modifier().Text() == "public" + return d.Modifier() == keyword.Public } // IsEdition checks whether this is an "import weak". func (d DeclImport) IsWeak() bool { - return d.Modifier().Text() == "weak" + return d.Modifier() == keyword.Weak } // ImportPath returns the file path for this import as a string. @@ -389,7 +410,7 @@ func (d DeclImport) Span() report.Span { return report.Span{} } - return report.Join(d.Keyword(), d.Modifier(), d.ImportPath(), d.Semicolon()) + return report.Join(d.KeywordToken(), d.ModifierToken(), d.ImportPath(), d.Semicolon()) } func wrapDeclImport(c Context, ptr arena.Pointer[rawDeclImport]) DeclImport { diff --git a/experimental/ast/decl_range.go b/experimental/ast/decl_range.go index ee45e1b3..ee669f8e 100644 --- a/experimental/ast/decl_range.go +++ b/experimental/ast/decl_range.go @@ -18,6 +18,7 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" "github.com/bufbuild/protocompile/internal/arena" ) @@ -44,7 +45,12 @@ type DeclRangeArgs struct { } // Keyword returns the keyword for this range. -func (d DeclRange) Keyword() token.Token { +func (d DeclRange) Keyword() keyword.Keyword { + return d.KeywordToken().Keyword() +} + +// KeywordToken returns the keyword token for this range. +func (d DeclRange) KeywordToken() token.Token { if d.IsZero() { return token.Zero } @@ -54,12 +60,12 @@ func (d DeclRange) Keyword() token.Token { // IsExtensions checks whether this is an extension range. func (d DeclRange) IsExtensions() bool { - return d.Keyword().Text() == "extensions" + return d.Keyword() == keyword.Extensions } // IsReserved checks whether this is a reserved range. func (d DeclRange) IsReserved() bool { - return d.Keyword().Text() == "reserved" + return d.Keyword() == keyword.Reserved } // Ranges returns the sequence of expressions denoting the ranges in this @@ -118,10 +124,10 @@ func (d DeclRange) Span() report.Span { case d.IsZero(): return report.Span{} case r.Len() == 0: - return report.Join(d.Keyword(), d.Semicolon(), d.Options()) + return report.Join(d.KeywordToken(), d.Semicolon(), d.Options()) default: return report.Join( - d.Keyword(), d.Semicolon(), d.Options(), + d.KeywordToken(), d.Semicolon(), d.Options(), r.At(0), r.At(r.Len()-1), ) diff --git a/experimental/ast/enums.go b/experimental/ast/enums.go index 158aeb5e..a2d689a1 100644 --- a/experimental/ast/enums.go +++ b/experimental/ast/enums.go @@ -148,77 +148,6 @@ func (v TypeKind) GoString() string { return _table_TypeKind_GoString[int(v)] } -// TypePrefix is a prefix for an expression, such as a minus sign. -type ExprPrefix int8 - -const ( - ExprPrefixUnknown ExprPrefix = iota - ExprPrefixMinus -) - -// String implements [fmt.Stringer]. -func (v ExprPrefix) String() string { - if int(v) < 0 || int(v) > len(_table_ExprPrefix_String) { - return fmt.Sprintf("ExprPrefix(%v)", int(v)) - } - return _table_ExprPrefix_String[int(v)] -} - -// GoString implements [fmt.GoStringer]. -func (v ExprPrefix) GoString() string { - if int(v) < 0 || int(v) > len(_table_ExprPrefix_GoString) { - return fmt.Sprintf("astExprPrefix(%v)", int(v)) - } - return _table_ExprPrefix_GoString[int(v)] -} - -// ExprPrefixByName looks up a prefix kind by name. -// -// If name is not a known prefix, returns [ExprPrefixUnknown]. -func ExprPrefixByName(s string) ExprPrefix { - return _table_ExprPrefix_ExprPrefixByName[s] -} - -// TypeKind is a kind of type. There is one value of TypeKind for each -// Type* type in this package. -// -// TypePrefix is a prefix for a type, such as required, optional, or repeated. -type TypePrefix int8 - -const ( - TypePrefixUnknown TypePrefix = iota - TypePrefixOptional - TypePrefixRepeated - TypePrefixRequired - - // This is the "stream Foo.bar" syntax of RPC methods. It is also treated as - // a prefix. - TypePrefixStream -) - -// String implements [fmt.Stringer]. -func (v TypePrefix) String() string { - if int(v) < 0 || int(v) > len(_table_TypePrefix_String) { - return fmt.Sprintf("TypePrefix(%v)", int(v)) - } - return _table_TypePrefix_String[int(v)] -} - -// GoString implements [fmt.GoStringer]. -func (v TypePrefix) GoString() string { - if int(v) < 0 || int(v) > len(_table_TypePrefix_GoString) { - return fmt.Sprintf("astTypePrefix(%v)", int(v)) - } - return _table_TypePrefix_GoString[int(v)] -} - -// TypePrefixByName looks up a prefix kind by name. -// -// If name is not a known prefix, returns [TypePrefixUnknown]. -func TypePrefixByName(s string) TypePrefix { - return _table_TypePrefix_TypePrefixByName[s] -} - var _table_DeclKind_String = [...]string{ DeclKindInvalid: "DeclKindInvalid", DeclKindEmpty: "DeclKindEmpty", @@ -308,41 +237,4 @@ var _table_TypeKind_GoString = [...]string{ TypeKindPrefixed: "TypeKindPrefixed", TypeKindGeneric: "TypeKindGeneric", } - -var _table_ExprPrefix_String = [...]string{ - ExprPrefixUnknown: "unknown", - ExprPrefixMinus: "-", -} - -var _table_ExprPrefix_GoString = [...]string{ - ExprPrefixUnknown: "ExprPrefixUnknown", - ExprPrefixMinus: "ExprPrefixMinus", -} - -var _table_ExprPrefix_ExprPrefixByName = map[string]ExprPrefix{ - "-": ExprPrefixMinus, -} - -var _table_TypePrefix_String = [...]string{ - TypePrefixUnknown: "unknown", - TypePrefixOptional: "optional", - TypePrefixRepeated: "repeated", - TypePrefixRequired: "required", - TypePrefixStream: "stream", -} - -var _table_TypePrefix_GoString = [...]string{ - TypePrefixUnknown: "TypePrefixUnknown", - TypePrefixOptional: "TypePrefixOptional", - TypePrefixRepeated: "TypePrefixRepeated", - TypePrefixRequired: "TypePrefixRequired", - TypePrefixStream: "TypePrefixStream", -} - -var _table_TypePrefix_TypePrefixByName = map[string]TypePrefix{ - "optional": TypePrefixOptional, - "repeated": TypePrefixRepeated, - "required": TypePrefixRequired, - "stream": TypePrefixStream, -} var _ iter.Seq[int] // Mark iter as used. diff --git a/experimental/ast/enums.yaml b/experimental/ast/enums.yaml index cd476d42..fa7ffa1c 100644 --- a/experimental/ast/enums.yaml +++ b/experimental/ast/enums.yaml @@ -85,49 +85,3 @@ - name: TypeKindPath - name: TypeKindPrefixed - name: TypeKindGeneric - -- name: ExprPrefix - type: int8 - docs: | - TypePrefix is a prefix for an expression, such as a minus sign. - methods: - - kind: string - - kind: go-string - - kind: from-string - name: ExprPrefixByName - docs: | - ExprPrefixByName looks up a prefix kind by name. - - If name is not a known prefix, returns [ExprPrefixUnknown]. - skip: [ExprPrefixUnknown] - values: - - {name: ExprPrefixUnknown, string: unknown} - - {name: ExprPrefixMinus, string: "-"} - -- name: TypePrefix - type: int8 - docs: | - TypeKind is a kind of type. There is one value of TypeKind for each - Type* type in this package. - - TypePrefix is a prefix for a type, such as required, optional, or repeated. - methods: - - kind: string - - kind: go-string - - kind: from-string - name: TypePrefixByName - docs: | - TypePrefixByName looks up a prefix kind by name. - - If name is not a known prefix, returns [TypePrefixUnknown]. - skip: [TypePrefixUnknown] - values: - - {name: TypePrefixUnknown, string: unknown} - - {name: TypePrefixOptional, string: optional} - - {name: TypePrefixRepeated, string: repeated} - - {name: TypePrefixRequired, string: required} - - name: TypePrefixStream - string: stream - docs: | - This is the "stream Foo.bar" syntax of RPC methods. It is also treated as - a prefix. \ No newline at end of file diff --git a/experimental/ast/expr_prefixed.go b/experimental/ast/expr_prefixed.go index 8c965ac6..4d27014f 100644 --- a/experimental/ast/expr_prefixed.go +++ b/experimental/ast/expr_prefixed.go @@ -17,6 +17,7 @@ package ast import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" ) // ExprPrefixed is an expression prefixed with an operator. @@ -38,12 +39,11 @@ type ExprPrefixedArgs struct { } // Prefix returns this expression's prefix. -func (e ExprPrefixed) Prefix() ExprPrefix { - if e.IsZero() { - return 0 - } - - return ExprPrefixByName(e.PrefixToken().Text()) +// +// Returns [keyword.Unknown] if [TypePrefixed.PrefixToken] does not contain +// a known prefix. +func (e ExprPrefixed) Prefix() keyword.Keyword { + return e.PrefixToken().Keyword() } // Prefix returns the token representing this expression's prefix. diff --git a/experimental/ast/path.go b/experimental/ast/path.go index fd100bed..54dd9efb 100644 --- a/experimental/ast/path.go +++ b/experimental/ast/path.go @@ -22,7 +22,6 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/ext/iterx" - "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // Path represents a multi-part identifier. @@ -70,12 +69,11 @@ func (p Path) ToRelative() Path { // AsIdent returns the single identifier that comprises this path, or // the zero token. func (p Path) AsIdent() token.Token { - var buf [2]PathComponent - prefix := slicesx.AppendSeq(buf[:0], iterx.Limit(2, p.Components)) - if len(prefix) != 1 || !prefix[0].Separator().IsZero() { + first, _ := iterx.OnlyOne(p.Components) + if !first.Separator().IsZero() { return token.Zero } - return prefix[0].AsIdent() + return first.AsIdent() } // AsPredeclared returns the [predeclared.Name] that this path represents. diff --git a/experimental/ast/type_prefixed.go b/experimental/ast/type_prefixed.go index 4766f8c9..b4f9226b 100644 --- a/experimental/ast/type_prefixed.go +++ b/experimental/ast/type_prefixed.go @@ -17,6 +17,7 @@ package ast import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" ) // TypePrefixed is a type with a [TypePrefix]. @@ -48,14 +49,10 @@ type TypePrefixedArgs struct { // Prefix extracts the modifier out of this type. // -// Returns [TypePrefixUnknown] if [TypePrefixed.PrefixToken] does not contain -// a known modifier. -func (t TypePrefixed) Prefix() TypePrefix { - if t.IsZero() { - return 0 - } - - return TypePrefixByName(t.PrefixToken().Text()) +// Returns [keyword.Unknown] if [TypePrefixed.PrefixToken] does not contain +// a known prefix. +func (t TypePrefixed) Prefix() keyword.Keyword { + return t.PrefixToken().Keyword() } // PrefixToken returns the token representing this type's prefix. diff --git a/experimental/internal/astx/encode.go b/experimental/internal/astx/encode.go index 2f7e77e4..8684ee1e 100644 --- a/experimental/internal/astx/encode.go +++ b/experimental/internal/astx/encode.go @@ -24,6 +24,7 @@ import ( "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/experimental/token/keyword" compilerpb "github.com/bufbuild/protocompile/internal/gen/buf/compiler/v1alpha1" ) @@ -214,7 +215,7 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { Value: c.expr(decl.Value()), Options: c.options(decl.Options()), Span: c.span(decl), - KeywordSpan: c.span(decl.Keyword()), + KeywordSpan: c.span(decl.KeywordToken()), EqualsSpan: c.span(decl.Equals()), SemicolonSpan: c.span(decl.Semicolon()), }}} @@ -226,7 +227,7 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { Path: c.path(decl.Path()), Options: c.options(decl.Options()), Span: c.span(decl), - KeywordSpan: c.span(decl.Keyword()), + KeywordSpan: c.span(decl.KeywordToken()), SemicolonSpan: c.span(decl.Semicolon()), }}} @@ -245,8 +246,8 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { ImportPath: c.expr(decl.ImportPath()), Options: c.options(decl.Options()), Span: c.span(decl), - KeywordSpan: c.span(decl.Keyword()), - ModifierSpan: c.span(decl.Modifier()), + KeywordSpan: c.span(decl.KeywordToken()), + ModifierSpan: c.span(decl.ModifierToken()), ImportPathSpan: c.span(decl.ImportPath()), SemicolonSpan: c.span(decl.Semicolon()), }}} @@ -277,7 +278,7 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { Kind: kind, Options: c.options(decl.Options()), Span: c.span(decl), - KeywordSpan: c.span(decl.Keyword()), + KeywordSpan: c.span(decl.KeywordToken()), SemicolonSpan: c.span(decl.Semicolon()), } @@ -321,7 +322,7 @@ func (c *protoEncoder) decl(decl ast.DeclAny) *compilerpb.Decl { Value: c.expr(decl.Value()), Options: c.options(decl.Options()), Span: c.span(decl), - KeywordSpan: c.span(decl.Keyword()), + KeywordSpan: c.span(decl.KeywordToken()), EqualsSpan: c.span(decl.Equals()), SemicolonSpan: c.span(decl.Semicolon()), } @@ -418,8 +419,13 @@ func (c *protoEncoder) expr(expr ast.ExprAny) *compilerpb.Expr { case ast.ExprKindPrefixed: expr := expr.AsPrefixed() + var prefix compilerpb.Expr_Prefixed_Prefix + if expr.Prefix() == keyword.Minus { + prefix = compilerpb.Expr_Prefixed_PREFIX_MINUS + } + return &compilerpb.Expr{Expr: &compilerpb.Expr_Prefixed_{Prefixed: &compilerpb.Expr_Prefixed{ - Prefix: compilerpb.Expr_Prefixed_Prefix(expr.Prefix()), + Prefix: prefix, Expr: c.expr(expr.Expr()), Span: c.span(expr), PrefixSpan: c.span(expr.PrefixToken()), @@ -504,8 +510,21 @@ func (c *protoEncoder) type_(ty ast.TypeAny) *compilerpb.Type { case ast.TypeKindPrefixed: ty := ty.AsPrefixed() + + var prefix compilerpb.Type_Prefixed_Prefix + switch ty.Prefix() { + case keyword.Optional: + prefix = compilerpb.Type_Prefixed_PREFIX_OPTIONAL + case keyword.Repeated: + prefix = compilerpb.Type_Prefixed_PREFIX_REPEATED + case keyword.Required: + prefix = compilerpb.Type_Prefixed_PREFIX_REQUIRED + case keyword.Stream: + prefix = compilerpb.Type_Prefixed_PREFIX_STREAM + } + return &compilerpb.Type{Type: &compilerpb.Type_Prefixed_{Prefixed: &compilerpb.Type_Prefixed{ - Prefix: compilerpb.Type_Prefixed_Prefix(ty.Prefix()), + Prefix: prefix, Type: c.type_(ty.Type()), Span: c.span(ty), PrefixSpan: c.span(ty.PrefixToken()), diff --git a/experimental/parser/parse_expr.go b/experimental/parser/parse_expr.go index 1ea5ecfd..fee5fd79 100644 --- a/experimental/parser/parse_expr.go +++ b/experimental/parser/parse_expr.go @@ -108,8 +108,8 @@ func parseExprInfix(p *parser, c *token.Cursor, where taxa.Place, lhs ast.ExprAn case 1: //nolint:gocritic // This is a switch for consistency with the rest of the file. - switch next.Text() { - case "to": + switch next.Keyword() { + case keyword.To: return p.NewExprRange(ast.ExprRangeArgs{ Start: lhs, To: c.Next(), @@ -134,7 +134,7 @@ func parseExprPrefix(p *parser, c *token.Cursor, where taxa.Place) ast.ExprAny { case next.IsZero(): return ast.ExprAny{} - case next.Text() == "-": + case next.Keyword() == keyword.Minus: c.Next() inner := parseExprPrefix(p, c, taxa.Minus.After()) return p.NewExprPrefixed(ast.ExprPrefixedArgs{ diff --git a/experimental/parser/parse_type.go b/experimental/parser/parse_type.go index 57baf437..ecce4b84 100644 --- a/experimental/parser/parse_type.go +++ b/experimental/parser/parse_type.go @@ -132,8 +132,8 @@ func parseTypeImpl(p *parser, c *token.Cursor, where taxa.Place, pathAfter bool) // based on what comes after it. var isMod bool _, rest := tyPath.Split(1) - switch ast.TypePrefixByName(ident.Name()) { - case ast.TypePrefixOptional, ast.TypePrefixRepeated, ast.TypePrefixRequired: + switch ident.Keyword() { + case keyword.Optional, keyword.Repeated, keyword.Required: // NOTE: We do not need to look at isInMethod here, because it // implies isList (sort of: in the case of writing something // like `returns optional.Foo`, this will be parsed as @@ -141,7 +141,7 @@ func parseTypeImpl(p *parser, c *token.Cursor, where taxa.Place, pathAfter bool) // invalid, because of the missing parentheses, so we don't need to // legalize it. isMod = !isList || rest.IsZero() - case ast.TypePrefixStream: + case keyword.Stream: isMod = isInMethod || rest.IsZero() } diff --git a/experimental/report/diff.go b/experimental/report/diff.go index 9b8da82b..fbaca36f 100644 --- a/experimental/report/diff.go +++ b/experimental/report/diff.go @@ -87,24 +87,19 @@ func unifiedDiff(span Span, edits []Edit) (Span, []hunk) { // Partition offsets into overlapping lines. That is, this connects together // all edit spans whose end and start are not separated by a newline. - prev := 0 - parts := slicesx.SplitFunc(edits, func(i int, next Edit) bool { - if i == prev { - return false - } - - chunk := src[edits[i-1].End:next.Start] - if !strings.Contains(chunk, "\n") { - return false - } - - prev = i - return true + parts := slicesx.SplitAfterFunc(edits, func(i int, edit Edit) bool { + next, ok := slicesx.Get(edits, i+1) + return ok && edit.End < next.Start && // Go treats str[x:y] for x > y as an error. + strings.Contains(src[edit.End:next.Start], "\n") }) var out []hunk var prevHunk int parts(func(edits []Edit) bool { + if len(edits) == 0 { + return true + } + // First, figure out the start and end of the modified region. start, end := edits[0].Start, edits[0].End for _, edit := range edits[1:] { diff --git a/internal/ext/iterx/consume.go b/internal/ext/iterx/consume.go new file mode 100644 index 00000000..ecf6329a --- /dev/null +++ b/internal/ext/iterx/consume.go @@ -0,0 +1,63 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package iterx contains extensions to Go's package iter. +package iterx + +import ( + "fmt" + "strings" + + "github.com/bufbuild/protocompile/internal/iter" +) + +// Count counts the number of elements in seq that match the given predicate. +// +// If p is nil, it is treated as func(_ T) bool { return true }. +func Count[T any](seq iter.Seq[T]) int { + var total int + seq(func(_ T) bool { + total++ + return true + }) + return total +} + +// Join is like [strings.Join], but works on an iterator. Elements are +// stringified as if by [fmt.Print]. +func Join[T any](seq iter.Seq[T], sep string) string { + var out strings.Builder + first := true + seq(func(v T) bool { + if !first { + out.WriteString(sep) + } + first = false + + fmt.Fprint(&out, v) + return true + }) + return out.String() +} + +// Every returns whether every element of an iterator satisfies the given +// predicate. Returns true if seq yields no values. +func Every[T any](seq iter.Seq[T], p func(T) bool) bool { + all := true + seq(func(v T) bool { + all = p(v) + return all + }) + return all +} diff --git a/internal/ext/iterx/filtermap.go b/internal/ext/iterx/filtermap.go new file mode 100644 index 00000000..a805959f --- /dev/null +++ b/internal/ext/iterx/filtermap.go @@ -0,0 +1,104 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iterx + +import ( + "github.com/bufbuild/protocompile/internal/iter" +) + +// This file contains the matrix of {Map, Filter, FilterMap} x {1, 2, 1to2, 2to1}, +// except that Filter1to2 and Filter2to1 don't really make sense. + +// Map returns a new iterator applying f to each element of seq. +func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] { + return FilterMap(seq, func(v T) (U, bool) { return f(v), true }) +} + +// Filter returns a new iterator that only includes values satisfying p. +func Filter[T any](seq iter.Seq[T], p func(T) bool) iter.Seq[T] { + return FilterMap(seq, func(v T) (T, bool) { return v, p(v) }) +} + +// FilterMap combines the operations of [Map] and [Filter]. +func FilterMap[T, U any](seq iter.Seq[T], f func(T) (U, bool)) iter.Seq[U] { + return func(yield func(U) bool) { + seq(func(v T) bool { + v2, ok := f(v) + return !ok || yield(v2) + }) + } +} + +// Map2 returns a new iterator applying f to each element of seq. +func Map2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W)) iter.Seq2[V, W] { + return FilterMap2(seq, func(v1 T, v2 U) (V, W, bool) { + x1, x2 := f(v1, v2) + return x1, x2, true + }) +} + +// Filter2 returns a new iterator that only includes values satisfying p. +func Filter2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) iter.Seq2[T, U] { + return FilterMap2(seq, func(v1 T, v2 U) (T, U, bool) { return v1, v2, p(v1, v2) }) +} + +// FilterMap2 combines the operations of [Map] and [Filter]. +func FilterMap2[T, U, V, W any](seq iter.Seq2[T, U], f func(T, U) (V, W, bool)) iter.Seq2[V, W] { + return func(yield func(V, W) bool) { + seq(func(v1 T, v2 U) bool { + x1, x2, ok := f(v1, v2) + return !ok || yield(x1, x2) + }) + } +} + +// Map2to1 is like [Map], but it also acts a Y pipe for converting a two-element +// iterator into a one-element iterator. +func Map2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) V) iter.Seq[V] { + return FilterMap2to1(seq, func(v1 T, v2 U) (V, bool) { + return f(v1, v2), true + }) +} + +// FilterMap2to1 is like [FilterMap], but it also acts a Y pipe for converting +// a two-element iterator into a one-element iterator. +func FilterMap2to1[T, U, V any](seq iter.Seq2[T, U], f func(T, U) (V, bool)) iter.Seq[V] { + return func(yield func(V) bool) { + seq(func(v1 T, v2 U) bool { + v, ok := f(v1, v2) + return !ok || yield(v) + }) + } +} + +// Map1To2 is like [Map], but it also acts a Y pipe for converting a one-element +// iterator into a two-element iterator. +func Map1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V)) iter.Seq2[U, V] { + return FilterMap1To2(seq, func(v T) (U, V, bool) { + x1, x2 := f(v) + return x1, x2, true + }) +} + +// FilterMap1To2 is like [FilterMap], but it also acts a Y pipe for converting +// a one-element iterator into a two-element iterator. +func FilterMap1To2[T, U, V any](seq iter.Seq[T], f func(T) (U, V, bool)) iter.Seq2[U, V] { + return func(yield func(U, V) bool) { + seq(func(v T) bool { + x1, x2, ok := f(v) + return !ok || yield(x1, x2) + }) + } +} diff --git a/internal/ext/iterx/get.go b/internal/ext/iterx/get.go new file mode 100644 index 00000000..e817aee7 --- /dev/null +++ b/internal/ext/iterx/get.go @@ -0,0 +1,105 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iterx + +import ( + "github.com/bufbuild/protocompile/internal/iter" +) + +// First retrieves the first element of an iterator. +func First[T any](seq iter.Seq[T]) (v T, ok bool) { + seq(func(x T) bool { + v = x + ok = true + return false + }) + return v, ok +} + +// OnlyOne retrieves the only element of an iterator. +func OnlyOne[T any](seq iter.Seq[T]) (v T, ok bool) { + var found T + seq(func(x T) bool { + if !ok { + found = x + } + ok = !ok + return ok + }) + if ok { + // Ensure we return the zero value if there is more + // than one element. + v = found + } + return v, ok +} + +// Find returns the first element that matches a predicate. +// +// Returns the value and the index at which it was found, or -1 if it wasn't +// found. +func Find[T any](seq iter.Seq[T], p func(T) bool) (int, T) { + var v T + var idx int + var found bool + seq(func(x T) bool { + if p(x) { + v = x + found = true + return false + } + idx++ + return true + }) + if !found { + idx = -1 + } + return idx, v +} + +// Find2 is like [Find] but for two-element iterators. +func Find2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) (int, T, U) { + var v1 T + var v2 U + var idx int + var found bool + seq(func(x1 T, x2 U) bool { + if p(x1, x2) { + v1, v2 = x1, x2 + found = true + return false + } + idx++ + return true + }) + if !found { + idx = -1 + } + return idx, v1, v2 +} + +// Index returns the index of the first element of seq that satisfies p. +// +// if not found, returns -1. +func Index[T any](seq iter.Seq[T], p func(T) bool) int { + idx, _ := Find(seq, p) + return idx +} + +// Index2 is like [Index], but for two-element iterators. +func Index2[T, U any](seq iter.Seq2[T, U], p func(T, U) bool) int { + idx, _, _ := Find2(seq, p) + return idx +} diff --git a/internal/ext/iterx/iterx.go b/internal/ext/iterx/iterx.go index 02b2677c..46b2f3ee 100644 --- a/internal/ext/iterx/iterx.go +++ b/internal/ext/iterx/iterx.go @@ -15,49 +15,43 @@ // Package iterx contains extensions to Go's package iter. package iterx -import "github.com/bufbuild/protocompile/internal/iter" +import ( + "fmt" -// Limit limits a sequence to only yield at most limit times. -func Limit[T any](limit uint, seq iter.Seq[T]) iter.Seq[T] { + "github.com/bufbuild/protocompile/internal/iter" +) + +// Take returns an iterator over the first n elements of a sequence. +func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] { return func(yield func(T) bool) { - seq(func(value T) bool { - if limit == 0 || !yield(value) { - return false + seq(func(v T) bool { + if n > 0 && yield(v) { + n-- + return true } - limit-- - return true + return false }) } } -// First retrieves the first element of an iterator. -func First[T any](seq iter.Seq[T]) (v T, ok bool) { - seq(func(x T) bool { - v = x - ok = true - return false +// Enumerate adapts an iterator to yield an incrementing index each iteration +// step. +func Enumerate[T any](seq iter.Seq[T]) iter.Seq2[int, T] { + var i int + return Map1To2(seq, func(v T) (int, T) { + i++ + return i - 1, v }) - return v, ok } -// All returns whether every element of an iterator satisfies the given -// predicate. Returns true if seq yields no values. -func All[T any](seq iter.Seq[T], p func(T) bool) bool { - all := true - seq(func(v T) bool { - all = p(v) - return all +// Strings maps an iterator with [fmt.Sprint], yielding an iterator of strings. +func Strings[T any](seq iter.Seq[T]) iter.Seq[string] { + return Map(seq, func(v T) string { + if s, ok := any(v).(string); ok { + return s // Avoid dumb copies. + } + return fmt.Sprint(v) }) - return all -} - -// Map returns a new iterator applying f to each element of seq. -func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] { - return func(yield func(U) bool) { - seq(func(value T) bool { - return yield(f(value)) - }) - } } // Chain returns an iterator that calls a sequence of iterators in sequence. diff --git a/internal/ext/iterx/kv.go b/internal/ext/iterx/kv.go new file mode 100644 index 00000000..38873e42 --- /dev/null +++ b/internal/ext/iterx/kv.go @@ -0,0 +1,29 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iterx + +import ( + "github.com/bufbuild/protocompile/internal/iter" +) + +// Left returns a new iterator that drops the right value of a [iter.Seq2]. +func Left[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] { + return Map2to1(seq, func(k K, _ V) K { return k }) +} + +// Right returns a new iterator that drops the left value of a [iter.Seq2]. +func Right[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] { + return Map2to1(seq, func(_ K, v V) V { return v }) +} diff --git a/internal/ext/mapsx/collect.go b/internal/ext/mapsx/collect.go new file mode 100644 index 00000000..78bf5004 --- /dev/null +++ b/internal/ext/mapsx/collect.go @@ -0,0 +1,48 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapsx + +import "github.com/bufbuild/protocompile/internal/iter" + +// Collect polyfills [maps.Collect]. +func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]V { + return Insert(make(map[K]V), seq) +} + +// Insert polyfills [maps.Insert]. +func Insert[M ~map[K]V, K comparable, V any](m M, seq iter.Seq2[K, V]) M { + seq(func(k K, v V) bool { + m[k] = v + return true + }) + return m +} + +// CollectSet is like [Collect], but it implicitly fills in each map value +// with a struct{} value. +func CollectSet[K comparable](seq iter.Seq[K]) map[K]struct{} { + return InsertKeys(make(map[K]struct{}), seq) +} + +// InsertKeys is like [Insert], but it implicitly fills in each map value +// with the zero value. +func InsertKeys[M ~map[K]V, K comparable, V any](m M, seq iter.Seq[K]) M { + seq(func(k K) bool { + var zero V + m[k] = zero + return true + }) + return m +} diff --git a/internal/ext/mapsx/mapsx.go b/internal/ext/mapsx/mapsx.go new file mode 100644 index 00000000..6ef19cc0 --- /dev/null +++ b/internal/ext/mapsx/mapsx.go @@ -0,0 +1,41 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package mapsx contains extensions to Go's package maps. +package mapsx + +import "github.com/bufbuild/protocompile/internal/iter" + +// Keys polyfills [maps.Keys]. +func Keys[M ~map[K]V, K comparable, V any](m M) iter.Seq[K] { + return func(yield func(k K) bool) { + for k := range m { + if !yield(k) { + return + } + } + } +} + +// KeySet returns a copy of m, with its values replaced with empty structs. +func KeySet[M ~map[K]V, K comparable, V any](m M) map[K]struct{} { + // return CollectSet(Keys(m)) + // Instead of going through an iterator, inline the loop so that + // we can preallocate and avoid rehashes. + keys := make(map[K]struct{}, len(m)) + for k := range m { + keys[k] = struct{}{} + } + return keys +} diff --git a/internal/ext/slicesx/index.go b/internal/ext/slicesx/index.go index 70976731..996a2eac 100644 --- a/internal/ext/slicesx/index.go +++ b/internal/ext/slicesx/index.go @@ -17,9 +17,16 @@ package slicesx import ( "unsafe" + "github.com/bufbuild/protocompile/internal/ext/iterx" "github.com/bufbuild/protocompile/internal/ext/unsafex" ) +// IndexFunc is like [slices.IndexFunc], but also takes the index of the element +// being examined as an input to the predicate. +func IndexFunc[S ~[]E, E any](s S, p func(int, E) bool) int { + return iterx.Index2(All(s), p) +} + // PointerIndex returns an integer n such that p == &s[n], or -1 if there is // no such integer. // diff --git a/internal/ext/slicesx/iter.go b/internal/ext/slicesx/iter.go index 6d90660e..63e61325 100644 --- a/internal/ext/slicesx/iter.go +++ b/internal/ext/slicesx/iter.go @@ -19,6 +19,39 @@ import ( "github.com/bufbuild/protocompile/internal/iter" ) +// All is a polyfill for [slices.All]. +func All[S ~[]E, E any](s S) iter.Seq2[int, E] { + return func(yield func(int, E) bool) { + for i, v := range s { + if !yield(i, v) { + return + } + } + } +} + +// Backward is a polyfill for [slices.Backward]. +func Backward[S ~[]E, E any](s S) iter.Seq2[int, E] { + return func(yield func(int, E) bool) { + for i := len(s) - 1; i >= 0; i-- { + if !yield(i, s[i]) { + return + } + } + } +} + +// Values is a polyfill for [slices.Values]. +func Values[S ~[]E, E any](s S) iter.Seq[E] { + return func(yield func(E) bool) { + for _, v := range s { + if !yield(v) { + return + } + } + } +} + // Collect polyfills [slices.Collect]. func Collect[E any](seq iter.Seq[E]) []E { return AppendSeq[[]E](nil, seq) @@ -86,36 +119,3 @@ func PartitionKey[S ~[]E, E any, K comparable](s S, key func(E) K) iter.Seq2[int } } } - -// SplitFunc splits a slice according to the given predicate. -// -// Whenever p returns true, this function will yield all prior elements not -// yet yielded. -func SplitFunc[S ~[]E, E any](s S, p func(int, E) bool) iter.Seq[S] { - return func(yield func(S) bool) { - var start int - for i, r := range s { - if !p(i, r) { - continue - } - if !yield(s[start:i]) { - return - } - start = i - } - if start < len(s) { - yield(s[start:]) - } - } -} - -// Values is a polyfill for [slices.Values]. -func Values[S ~[]E, E any](s S) iter.Seq[E] { - return func(yield func(E) bool) { - for _, v := range s { - if !yield(v) { - return - } - } - } -} diff --git a/internal/ext/slicesx/split.go b/internal/ext/slicesx/split.go new file mode 100644 index 00000000..165465c9 --- /dev/null +++ b/internal/ext/slicesx/split.go @@ -0,0 +1,121 @@ +// Copyright 2020-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slicesx + +import ( + "slices" + + "github.com/bufbuild/protocompile/internal/iter" +) + +// Cut is like [strings.Cut], but for slices. +func Cut[S ~[]E, E comparable](s S, needle E) (before, after S, found bool) { + idx := slices.Index(s, needle) + if idx == -1 { + return s, s[len(s):], false + } + return s[:idx], s[idx+1:], true +} + +// CutAfter is like [Cut], but includes the needle in before. +func CutAfter[S ~[]E, E comparable](s S, needle E) (before, after S, found bool) { + idx := slices.Index(s, needle) + if idx == -1 { + return s, s[len(s):], false + } + return s[:idx+1], s[idx+1:], true +} + +// CutFunc is like [Cut], but uses a function to select the cut-point. +func CutFunc[S ~[]E, E any](s S, p func(int, E) bool) (before, after S, found bool) { + idx := IndexFunc(s, p) + if idx == -1 { + return s, s[len(s):], false + } + return s[:idx], s[idx+1:], true +} + +// CutAfterFunc is like [CutFunc], but includes the needle in before. +func CutAfterFunc[S ~[]E, E any](s S, p func(int, E) bool) (before, after S, found bool) { + idx := IndexFunc(s, p) + if idx == -1 { + return s, s[len(s):], false + } + return s[:idx+1], s[idx+1:], true +} + +// Split is like [strings.Split], but for slices. +func Split[S ~[]E, E comparable](s S, sep E) iter.Seq[S] { + return func(yield func(S) bool) { + for { + before, after, found := Cut(s, sep) + if !yield(before) { + return + } + if !found { + break + } + s = after + } + } +} + +// SplitAfter is like [strings.SplitAfter], but for slices. +func SplitAfter[S ~[]E, E comparable](s S, sep E) iter.Seq[S] { + return func(yield func(S) bool) { + for { + before, after, found := CutAfter(s, sep) + if !yield(before) { + return + } + if !found { + break + } + s = after + } + } +} + +// SplitFunc is like [Split], but uses a function to select the cut-point. +func SplitFunc[S ~[]E, E any](s S, sep func(int, E) bool) iter.Seq[S] { + return func(yield func(S) bool) { + for { + before, after, found := CutFunc(s, sep) + if !yield(before) { + return + } + if !found { + break + } + s = after + } + } +} + +// SplitAfterFunc is like [SplitAfter], but uses a function to select the cut-point. +func SplitAfterFunc[S ~[]E, E any](s S, sep func(int, E) bool) iter.Seq[S] { + return func(yield func(S) bool) { + for { + before, after, found := CutAfterFunc(s, sep) + if !yield(before) { + return + } + if !found { + break + } + s = after + } + } +} diff --git a/internal/ext/stringsx/stringsx.go b/internal/ext/stringsx/stringsx.go index b3e36d54..cc856707 100644 --- a/internal/ext/stringsx/stringsx.go +++ b/internal/ext/stringsx/stringsx.go @@ -26,7 +26,7 @@ import ( "github.com/bufbuild/protocompile/internal/iter" ) -// Rune returns the rune at the given index. +// Rune returns the rune at the given byte index. // // Returns 0, false if out of bounds. Returns U+FFFD, false if rune decoding fails. func Rune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { @@ -37,7 +37,7 @@ func Rune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { return r, r != utf8.RuneError } -// Rune returns the previous rune at the given index. +// Rune returns the previous rune at the given byte index. // // Returns 0, false if out of bounds. Returns U+FFFD, false if rune decoding fails. func PrevRune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { @@ -49,9 +49,14 @@ func PrevRune[I slicesx.SliceIndex](s string, idx I) (rune, bool) { return r, r != utf8.RuneError } +// Byte returns the rune at the given index. +func Byte[I slicesx.SliceIndex](s string, idx I) (byte, bool) { + return slicesx.Get(unsafex.BytesAlias[[]byte](s), idx) +} + // EveryFunc verifies that all runes in the string satisfy the given predicate. func EveryFunc(s string, p func(rune) bool) bool { - return iterx.All(Runes(s), p) + return iterx.Every(Runes(s), p) } // Runes returns an iterator over the runes in a string. diff --git a/internal/ext/unsafex/unsafex.go b/internal/ext/unsafex/unsafex.go index 799f9870..eae2865b 100644 --- a/internal/ext/unsafex/unsafex.go +++ b/internal/ext/unsafex/unsafex.go @@ -134,3 +134,15 @@ func StringAlias[S ~[]E, E any](data S) string { len(data)*Size[E](), ) } + +// BytesAlias is the inverse of [StringAlias]. +// +// The same caveats apply as with [StringAlias] around mutating `data`. +// +//go:nosplit +func BytesAlias[S ~[]B, B ~byte](data string) []B { + return unsafe.Slice( + Bitcast[*B](unsafe.StringData(data)), + len(data), + ) +}