diff --git a/experimental/ast/commas.go b/experimental/ast/commas.go index c3f7db70..fc88b69d 100644 --- a/experimental/ast/commas.go +++ b/experimental/ast/commas.go @@ -15,10 +15,10 @@ package ast import ( - "slices" - "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" + "github.com/bufbuild/protocompile/internal/ext/slicesx" + "github.com/bufbuild/protocompile/internal/ext/unsafex" ) // Commas is like [Slice], but it's for a comma-delimited list of some kind. @@ -48,13 +48,13 @@ type withComma[T any] struct { Comma token.ID } -type commas[T, E any] struct { - seq.SliceInserter[T, withComma[E]] +type commas[T any, Raw unsafex.Int] struct { + seq.InserterWrapper2[T, Raw, token.ID, *slicesx.Inline[Raw], *slicesx.Inline[token.ID]] ctx Context } func (c commas[T, _]) Comma(n int) token.Token { - return (*c.SliceInserter.Slice)[n].Comma.In(c.ctx) + return c.InserterWrapper2.Slice2.At(n).In(c.ctx) } func (c commas[T, _]) AppendComma(value T, comma token.Token) { @@ -63,8 +63,7 @@ func (c commas[T, _]) AppendComma(value T, comma token.Token) { func (c commas[T, _]) InsertComma(n int, value T, comma token.Token) { c.ctx.Nodes().panicIfNotOurs(comma) - v := c.SliceInserter.Unwrap(value) - v.Comma = comma.ID() - - *c.Slice = slices.Insert(*c.Slice, n, v) + e1, _ := c.InserterWrapper2.Unwrap(value) + c.InserterWrapper2.Slice1.Insert(n, e1) + c.InserterWrapper2.Slice2.Insert(n, comma.ID()) } diff --git a/experimental/ast/decl_body.go b/experimental/ast/decl_body.go index 0422ee88..d87774cc 100644 --- a/experimental/ast/decl_body.go +++ b/experimental/ast/decl_body.go @@ -19,6 +19,7 @@ import ( "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/arena" + "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // DeclBody is the body of a [DeclBody], or the whole contents of a [File]. The @@ -41,8 +42,8 @@ type rawDeclBody struct { // These slices are co-indexed; they are parallelizes to save // three bytes per decl (declKind is 1 byte, but decl is 4; if // they're stored in AOS format, we waste 3 bytes of padding). - kinds []DeclKind - ptrs []arena.Untyped + kinds slicesx.Inline[DeclKind] + ptrs slicesx.Inline[arena.Untyped] } // Braces returns this body's surrounding braces, if it has any. @@ -71,22 +72,27 @@ func (d DeclBody) Span() report.Span { // Decls returns a [seq.Inserter] over the declarations in this body. func (d DeclBody) Decls() seq.Inserter[DeclAny] { - type slice = seq.SliceInserter2[DeclAny, DeclKind, arena.Untyped] - if d.IsZero() { - return slice{} + var ( + kinds *slicesx.Inline[DeclKind] + ptrs *slicesx.Inline[arena.Untyped] + ) + if !d.IsZero() { + kinds = &d.raw.kinds + ptrs = &d.raw.ptrs } - return seq.SliceInserter2[DeclAny, DeclKind, arena.Untyped]{ - Slice1: &d.raw.kinds, - Slice2: &d.raw.ptrs, - Wrap: func(k DeclKind, p arena.Untyped) DeclAny { + // A single return here promotes devirtualization of both the interface + // and the funcvals within. + return seq.WrapInserter2( + kinds, ptrs, + func(k DeclKind, p arena.Untyped) DeclAny { return rawDecl{p, k}.With(d.Context()) }, - Unwrap: func(d DeclAny) (DeclKind, arena.Untyped) { + func(d DeclAny) (DeclKind, arena.Untyped) { d.Context().Nodes().panicIfNotOurs(d) return d.raw.kind, d.raw.ptr }, - } + ) } func wrapDeclBody(c Context, ptr arena.Pointer[rawDeclBody]) DeclBody { diff --git a/experimental/ast/decl_def.go b/experimental/ast/decl_def.go index df168442..72216e86 100644 --- a/experimental/ast/decl_def.go +++ b/experimental/ast/decl_def.go @@ -45,17 +45,14 @@ import ( type DeclDef struct{ declImpl[rawDeclDef] } type rawDeclDef struct { - ty rawType // Not present for enum fields. - name rawPath - signature *rawSignature - - equals token.ID - value rawExpr - - options arena.Pointer[rawCompactOptions] - body arena.Pointer[rawDeclBody] - semi token.ID + ty rawType + value rawExpr + name rawPath + equals token.ID + options arena.Pointer[rawCompactOptions] + body arena.Pointer[rawDeclBody] + semi token.ID } // DeclDefArgs is arguments for creating a [DeclDef] with [Context.NewDeclDef]. @@ -198,7 +195,7 @@ func (d DeclDef) Options() CompactOptions { // // Setting it to a zero Options clears it. func (d DeclDef) SetOptions(opts CompactOptions) { - d.raw.options = d.Context().Nodes().options.Compress(opts.raw) + d.raw.options = d.Context().Nodes().compactOptions.Compress(opts.raw) } // Body returns this definition's body, if it has one. @@ -386,9 +383,12 @@ func (d DeclDef) AsOption() DefOption { return DefOption{ Keyword: d.Keyword(), Option: Option{ - Path: d.Name(), - Equals: d.Equals(), - Value: d.Value(), + d.withContext, + &rawOption{ + path: d.Name().raw, + equals: d.Equals().ID(), + value: d.Value().raw, + }, }, Semicolon: d.Semicolon(), Decl: d, diff --git a/experimental/ast/decl_file.go b/experimental/ast/decl_file.go index 5bd72871..662a9a49 100644 --- a/experimental/ast/decl_file.go +++ b/experimental/ast/decl_file.go @@ -88,9 +88,11 @@ func (f File) Imports() iter.Seq2[int, DeclImport] { type DeclSyntax struct{ declImpl[rawDeclSyntax] } type rawDeclSyntax struct { - keyword, equals, semi token.ID - value rawExpr - options arena.Pointer[rawCompactOptions] + value rawExpr + keyword token.ID + equals token.ID + semi token.ID + options arena.Pointer[rawCompactOptions] } // DeclSyntaxArgs is arguments for [Context.NewDeclSyntax]. @@ -167,7 +169,7 @@ func (d DeclSyntax) Options() CompactOptions { // // Setting it to a zero Options clears it. func (d DeclSyntax) SetOptions(opts CompactOptions) { - d.raw.options = d.Context().Nodes().options.Compress(opts.raw) + d.raw.options = d.Context().Nodes().compactOptions.Compress(opts.raw) } // Semicolon returns this pragma's ending semicolon. @@ -254,7 +256,7 @@ func (d DeclPackage) Options() CompactOptions { // // Setting it to a zero Options clears it. func (d DeclPackage) SetOptions(opts CompactOptions) { - d.raw.options = d.Context().Nodes().options.Compress(opts.raw) + d.raw.options = d.Context().Nodes().compactOptions.Compress(opts.raw) } // Semicolon returns this package's ending semicolon. @@ -292,9 +294,11 @@ func wrapDeclPackage(c Context, ptr arena.Pointer[rawDeclPackage]) DeclPackage { type DeclImport struct{ declImpl[rawDeclImport] } type rawDeclImport struct { - keyword, modifier, semi token.ID - importPath rawExpr - options arena.Pointer[rawCompactOptions] + importPath rawExpr + keyword token.ID + modifier token.ID + semi token.ID + options arena.Pointer[rawCompactOptions] } // DeclImportArgs is arguments for [Context.NewDeclImport]. @@ -369,7 +373,7 @@ func (d DeclImport) Options() CompactOptions { // // Setting it to a zero Options clears it. func (d DeclImport) SetOptions(opts CompactOptions) { - d.raw.options = d.Context().Nodes().options.Compress(opts.raw) + d.raw.options = d.Context().Nodes().compactOptions.Compress(opts.raw) } // Semicolon returns this import's ending semicolon. diff --git a/experimental/ast/decl_range.go b/experimental/ast/decl_range.go index 9cea4247..c4034e6a 100644 --- a/experimental/ast/decl_range.go +++ b/experimental/ast/decl_range.go @@ -19,6 +19,7 @@ import ( "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/arena" + "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // DeclRange represents an extension or reserved range declaration. They are almost identical @@ -31,7 +32,8 @@ type DeclRange struct{ declImpl[rawDeclRange] } type rawDeclRange struct { keyword token.ID - args []withComma[rawExpr] + args slicesx.Inline[rawExpr] + commas slicesx.Inline[token.ID] options arena.Pointer[rawCompactOptions] semi token.ID } @@ -65,22 +67,29 @@ func (d DeclRange) IsReserved() bool { // Ranges returns the sequence of expressions denoting the ranges in this // range declaration. func (d DeclRange) Ranges() Commas[ExprAny] { - type slice = commas[ExprAny, rawExpr] - if d.IsZero() { - return slice{} + var ( + args *slicesx.Inline[rawExpr] + toks *slicesx.Inline[token.ID] + ) + if !d.IsZero() { + args = &d.raw.args + toks = &d.raw.commas } - return slice{ + + // A single return here promotes devirtualization of both the interface + // and the funcvals within. + return commas[ExprAny, rawExpr]{ ctx: d.Context(), - SliceInserter: seq.SliceInserter[ExprAny, withComma[rawExpr]]{ - Slice: &d.raw.args, - Wrap: func(c withComma[rawExpr]) ExprAny { - return newExprAny(d.Context(), c.Value) + InserterWrapper2: seq.WrapInserter2( + args, toks, + func(e rawExpr, _ token.ID) ExprAny { + return newExprAny(d.Context(), e) }, - Unwrap: func(e ExprAny) withComma[rawExpr] { + func(e ExprAny) (rawExpr, token.ID) { d.Context().Nodes().panicIfNotOurs(e) - return withComma[rawExpr]{Value: e.raw} + return e.raw, 0 }, - }, + ), } } @@ -97,7 +106,7 @@ func (d DeclRange) Options() CompactOptions { // // Setting it to a nil Options clears it. func (d DeclRange) SetOptions(opts CompactOptions) { - d.raw.options = d.Context().Nodes().options.Compress(opts.raw) + d.raw.options = d.Context().Nodes().compactOptions.Compress(opts.raw) } // Semicolon returns this range's ending semicolon. diff --git a/experimental/ast/expr.go b/experimental/ast/expr.go index 154fd961..b09d7a68 100644 --- a/experimental/ast/expr.go +++ b/experimental/ast/expr.go @@ -61,7 +61,7 @@ type ExprAny struct { type rawExpr = pathLike[ExprKind] func newExprAny(c Context, e rawExpr) ExprAny { - if c == nil || (e == rawExpr{}) { + if c == nil || e.isZero() { return ExprAny{} } diff --git a/experimental/ast/expr_array.go b/experimental/ast/expr_array.go index 50bc24fe..3a19cb84 100644 --- a/experimental/ast/expr_array.go +++ b/experimental/ast/expr_array.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/internal/ext/slicesx" ) // ExprArray represents an array of expressions between square brackets. @@ -29,7 +30,8 @@ type ExprArray struct{ exprImpl[rawExprArray] } type rawExprArray struct { brackets token.ID - args []withComma[rawExpr] + args slicesx.Inline[rawExpr] + commas slicesx.Inline[token.ID] } // Brackets returns the token tree corresponding to the whole [...]. @@ -45,22 +47,29 @@ func (e ExprArray) Brackets() token.Token { // Elements returns the sequence of expressions in this array. func (e ExprArray) Elements() Commas[ExprAny] { - type slice = commas[ExprAny, rawExpr] - if e.IsZero() { - return slice{} + var ( + args *slicesx.Inline[rawExpr] + toks *slicesx.Inline[token.ID] + ) + if !e.IsZero() { + args = &e.raw.args + toks = &e.raw.commas } - return slice{ + + // A single return here promotes devirtualization of both the interface + // and the funcvals within. + return commas[ExprAny, rawExpr]{ ctx: e.Context(), - SliceInserter: seq.SliceInserter[ExprAny, withComma[rawExpr]]{ - Slice: &e.raw.args, - Wrap: func(c withComma[rawExpr]) ExprAny { - return newExprAny(e.Context(), c.Value) + InserterWrapper2: seq.WrapInserter2( + args, toks, + func(r rawExpr, _ token.ID) ExprAny { + return newExprAny(e.Context(), r) }, - Unwrap: func(e ExprAny) withComma[rawExpr] { - e.Context().Nodes().panicIfNotOurs(e) - return withComma[rawExpr]{Value: e.raw} + func(r ExprAny) (rawExpr, token.ID) { + e.Context().Nodes().panicIfNotOurs(r) + return r.raw, 0 }, - }, + ), } } diff --git a/experimental/ast/expr_dict.go b/experimental/ast/expr_dict.go index 255e2755..19f2bb89 100644 --- a/experimental/ast/expr_dict.go +++ b/experimental/ast/expr_dict.go @@ -15,12 +15,11 @@ package ast import ( - "slices" - "github.com/bufbuild/protocompile/experimental/report" "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/arena" + "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // ExprDict represents a an array of message fields between curly braces. @@ -36,7 +35,8 @@ type ExprDict struct{ exprImpl[rawExprDict] } type rawExprDict struct { braces token.ID - fields []withComma[arena.Pointer[rawExprField]] + fields slicesx.Inline[arena.Pointer[rawExprField]] + commas slicesx.Inline[token.ID] } // Braces returns the token tree corresponding to the whole {...}. @@ -52,100 +52,35 @@ func (e ExprDict) Braces() token.Token { // Elements returns the sequence of expressions in this array. func (e ExprDict) Elements() Commas[ExprField] { - type slice = commas[ExprField, arena.Pointer[rawExprField]] - if e.IsZero() { - return slice{} + var ( + args *slicesx.Inline[arena.Pointer[rawExprField]] + toks *slicesx.Inline[token.ID] + ) + if !e.IsZero() { + args = &e.raw.fields + toks = &e.raw.commas } - return slice{ + + // A single return here promotes devirtualization of both the interface + // and the funcvals within. + return commas[ExprField, arena.Pointer[rawExprField]]{ ctx: e.Context(), - SliceInserter: seq.SliceInserter[ExprField, withComma[arena.Pointer[rawExprField]]]{ - Slice: &e.raw.fields, - Wrap: func(c withComma[arena.Pointer[rawExprField]]) ExprField { + InserterWrapper2: seq.WrapInserter2( + args, toks, + func(r arena.Pointer[rawExprField], _ token.ID) ExprField { return ExprField{exprImpl[rawExprField]{ e.withContext, - e.Context().Nodes().exprs.fields.Deref(c.Value), + e.Context().Nodes().exprs.fields.Deref(r), }} }, - Unwrap: func(e ExprField) withComma[arena.Pointer[rawExprField]] { + func(r ExprField) (arena.Pointer[rawExprField], token.ID) { e.Context().Nodes().panicIfNotOurs(e) - ptr := e.Context().Nodes().exprs.fields.Compress(e.raw) - return withComma[arena.Pointer[rawExprField]]{ptr, 0} + return e.Context().Nodes().exprs.fields.Compress(r.raw), 0 }, - }, - } -} - -// Len implements [Slice]. -func (e ExprDict) Len() int { - if e.IsZero() { - return 0 - } - - return len(e.raw.fields) -} - -// At implements [Slice]. -func (e ExprDict) At(n int) ExprField { - ptr := e.raw.fields[n].Value - return ExprField{exprImpl[rawExprField]{ - e.withContext, - e.Context().Nodes().exprs.fields.Deref(ptr), - }} -} - -// Iter implements [Slice]. -func (e ExprDict) Iter(yield func(int, ExprField) bool) { - if e.IsZero() { - return - } - - for i, f := range e.raw.fields { - e := ExprField{exprImpl[rawExprField]{ - e.withContext, - e.Context().Nodes().exprs.fields.Deref(f.Value), - }} - if !yield(i, e) { - break - } + ), } } -// Append implements [Inserter]. -func (e ExprDict) Append(expr ExprField) { - e.InsertComma(e.Len(), expr, token.Zero) -} - -// Insert implements [Inserter]. -func (e ExprDict) Insert(n int, expr ExprField) { - e.InsertComma(n, expr, token.Zero) -} - -// Delete implements [Inserter]. -func (e ExprDict) Delete(n int) { - e.raw.fields = slices.Delete(e.raw.fields, n, n+1) -} - -// Comma implements [Commas]. -func (e ExprDict) Comma(n int) token.Token { - return e.raw.fields[n].Comma.In(e.Context()) -} - -// AppendComma implements [Commas]. -func (e ExprDict) AppendComma(expr ExprField, comma token.Token) { - e.InsertComma(e.Len(), expr, comma) -} - -// InsertComma implements [Commas]. -func (e ExprDict) InsertComma(n int, expr ExprField, comma token.Token) { - e.Context().Nodes().panicIfNotOurs(expr, comma) - if expr.IsZero() { - panic("protocompile/ast: cannot append zero ExprField to ExprMessage") - } - - ptr := e.Context().Nodes().exprs.fields.Compress(expr.raw) - e.raw.fields = slices.Insert(e.raw.fields, n, withComma[arena.Pointer[rawExprField]]{ptr, comma.ID()}) -} - // Span implements [report.Spanner]. func (e ExprDict) Span() report.Span { if e.IsZero() { diff --git a/experimental/ast/nodes.go b/experimental/ast/nodes.go index ef5c7652..0141447b 100644 --- a/experimental/ast/nodes.go +++ b/experimental/ast/nodes.go @@ -28,10 +28,12 @@ type Nodes struct { // The context for these nodes. Context Context - decls decls - types types - exprs exprs - options arena.Arena[rawCompactOptions] + decls decls + types types + exprs exprs + + compactOptions arena.Arena[rawCompactOptions] + options arena.Arena[rawOption] } // Root returns the root AST node for this context. @@ -62,7 +64,7 @@ func (n *Nodes) NewDeclSyntax(args DeclSyntaxArgs) DeclSyntax { keyword: args.Keyword.ID(), equals: args.Equals.ID(), value: args.Value.raw, - options: n.options.Compress(args.Options.raw), + options: n.compactOptions.Compress(args.Options.raw), semi: args.Semicolon.ID(), })) } @@ -74,7 +76,7 @@ func (n *Nodes) NewDeclPackage(args DeclPackageArgs) DeclPackage { return wrapDeclPackage(n.Context, n.decls.packages.NewCompressed(rawDeclPackage{ keyword: args.Keyword.ID(), path: args.Path.raw, - options: n.options.Compress(args.Options.raw), + options: n.compactOptions.Compress(args.Options.raw), semi: args.Semicolon.ID(), })) } @@ -87,7 +89,7 @@ func (n *Nodes) NewDeclImport(args DeclImportArgs) DeclImport { keyword: args.Keyword.ID(), modifier: args.Modifier.ID(), importPath: args.ImportPath.raw, - options: n.options.Compress(args.Options.raw), + options: n.compactOptions.Compress(args.Options.raw), semi: args.Semicolon.ID(), })) } @@ -102,7 +104,7 @@ func (n *Nodes) NewDeclDef(args DeclDefArgs) DeclDef { name: args.Name.raw, equals: args.Equals.ID(), value: args.Value.raw, - options: n.options.Compress(args.Options.raw), + options: n.compactOptions.Compress(args.Options.raw), body: n.decls.bodies.Compress(args.Body.raw), semi: args.Semicolon.ID(), } @@ -140,7 +142,7 @@ func (n *Nodes) NewDeclRange(args DeclRangeArgs) DeclRange { return wrapDeclRange(n.Context, n.decls.ranges.NewCompressed(rawDeclRange{ keyword: args.Keyword.ID(), - options: n.options.Compress(args.Options.raw), + options: n.compactOptions.Compress(args.Options.raw), semi: args.Semicolon.ID(), })) } @@ -249,11 +251,22 @@ func (n *Nodes) NewTypeGeneric(args TypeGenericArgs) TypeGeneric { }} } +// NewOption creates a new Option node. +func (n *Nodes) NewOption(args OptionArgs) Option { + n.panicIfNotOurs(args.Path, args.Equals, args.Value) + + return wrapOption(n.Context, n.options.NewCompressed(rawOption{ + path: args.Path.raw, + equals: args.Equals.ID(), + value: args.Value.raw, + })) +} + // NewCompactOptions creates a new CompactOptions node. func (n *Nodes) NewCompactOptions(brackets token.Token) CompactOptions { n.panicIfNotOurs(brackets) - return wrapOptions(n.Context, n.options.NewCompressed(rawCompactOptions{ + return wrapOptions(n.Context, n.compactOptions.NewCompressed(rawCompactOptions{ brackets: brackets.ID(), })) } diff --git a/experimental/ast/options.go b/experimental/ast/options.go index 6f040401..d37f0a28 100644 --- a/experimental/ast/options.go +++ b/experimental/ast/options.go @@ -20,6 +20,7 @@ import ( "github.com/bufbuild/protocompile/experimental/seq" "github.com/bufbuild/protocompile/experimental/token" "github.com/bufbuild/protocompile/internal/arena" + "github.com/bufbuild/protocompile/internal/ext/slicesx" ) // CompactOptions represents the collection of options attached to a [DeclAny], @@ -36,16 +37,47 @@ type CompactOptions struct { type rawCompactOptions struct { brackets token.ID - options []withComma[rawOption] + options slicesx.Inline[arena.Pointer[rawOption]] + commas slicesx.Inline[token.ID] } // Option is a key-value pair inside of a [CompactOptions] or a [DefOption]. type Option struct { + withContext + raw *rawOption +} + +// OptionArgs is arguments for [Node.NewOption]. +type OptionArgs struct { Path Path Equals token.Token Value ExprAny } +// Path returns the path (i.e., the key) for this option. +func (o Option) Path() Path { + if o.IsZero() { + return Path{} + } + return o.raw.path.With(o.Context()) +} + +// Equals returns the equals sign for this option, if present. +func (o Option) Equals() token.Token { + if o.IsZero() { + return token.Zero + } + return o.raw.equals.In(o.Context()) +} + +// Value returns the expression for the value this option is set to, if present. +func (o Option) Value() ExprAny { + if o.IsZero() { + return ExprAny{} + } + return newExprAny(o.Context(), o.raw.value) +} + type rawOption struct { path rawPath equals token.ID @@ -63,27 +95,33 @@ func (o CompactOptions) Brackets() token.Token { // Entries returns the sequence of options in this CompactOptions. func (o CompactOptions) Entries() Commas[Option] { - type slice = commas[Option, rawOption] - if o.IsZero() { - return slice{} + var ( + opts *slicesx.Inline[arena.Pointer[rawOption]] + toks *slicesx.Inline[token.ID] + ) + if !o.IsZero() { + opts = &o.raw.options + toks = &o.raw.commas } - return slice{ + return commas[Option, arena.Pointer[rawOption]]{ ctx: o.Context(), - SliceInserter: seq.SliceInserter[Option, withComma[rawOption]]{ - Slice: &o.raw.options, - Wrap: func(c withComma[rawOption]) Option { - return c.Value.With(o.Context()) + InserterWrapper2: seq.WrapInserter2( + opts, toks, + func(r arena.Pointer[rawOption], _ token.ID) Option { + return wrapOption(o.Context(), r) }, - Unwrap: func(v Option) withComma[rawOption] { - o.Context().Nodes().panicIfNotOurs(v.Path, v.Equals, v.Value) - return withComma[rawOption]{Value: rawOption{ - path: v.Path.raw, - equals: v.Equals.ID(), - value: v.Value.raw, - }} + func(v Option) (arena.Pointer[rawOption], token.ID) { + o.Context().Nodes().panicIfNotOurs(v) + + ptr := v.Context().Nodes().options.Compress(v.raw) + if ptr.Nil() { + ptr = o.Context().Nodes().options.NewCompressed(*v.raw) + } + + return ptr, 0 }, - }, + ), } } @@ -102,17 +140,16 @@ func wrapOptions(c Context, ptr arena.Pointer[rawCompactOptions]) CompactOptions } return CompactOptions{ internal.NewWith(c), - c.Nodes().options.Deref(ptr), + c.Nodes().compactOptions.Deref(ptr), } } -func (o *rawOption) With(c Context) Option { - if o == nil { +func wrapOption(c Context, ptr arena.Pointer[rawOption]) Option { + if ptr.Nil() { return Option{} } return Option{ - Path: o.path.With(c), - Equals: o.equals.In(c), - Value: newExprAny(c, o.value), + internal.NewWith(c), + c.Nodes().options.Deref(ptr), } } diff --git a/experimental/ast/pathlike.go b/experimental/ast/pathlike.go index 1e2314d1..39e1ea31 100644 --- a/experimental/ast/pathlike.go +++ b/experimental/ast/pathlike.go @@ -31,12 +31,35 @@ import ( // interpreted depending on kind, such as an arena pointer. // // This logic is implemented in the functions below. -type pathLike[Kind ~int8] struct { +type pathLike[Kind ~int8] uint64 /*struct { + // This is implemented as a uint64 so that it conforms to unsafex.Int. + // The below is the intended layout, which is implemented by + // packPathLike/unpack + // Can't use Kind for the type because this must be wide // enough to accommodate token.ID in the event this // pathLike actually represents a path. StartOrKind int32 EndOrValue int32 +}*/ + +func packPathLike[Kind ~int8](startOrKind, endOrValue int32) pathLike[Kind] { + return pathLike[Kind]( + // Cast to uint32 first to avoid sign extension. + uint64(uint32(startOrKind)) | + uint64(endOrValue)<<32, // Sign extension is ok here. + ) +} + +// isZero exists to make callsites a bit more obvious than "ty == 0", for +// consistency with other IsZero() functions that actually don't just compare +// to a literal zero. +func (p pathLike[Kind]) isZero() bool { + return p == 0 +} + +func (p pathLike[Kind]) unpack() (startOrKind, endOrValue int32) { + return int32(p), int32(p >> 32) } // wrapInPathLike wraps a integer-like value in a pathLike. @@ -47,10 +70,7 @@ func wrapPathLike[Value ~int32 | ~uint32, Kind ~int8](kind Kind, value Value) pa panic(fmt.Sprintf("protocompile/ast: invalid pathLike representation: %v, %v", kind, value)) } - return pathLike[Kind]{ - StartOrKind: ^int32(kind), - EndOrValue: int32(value), - } + return packPathLike[Kind](^int32(kind), int32(value)) } // unwrapPathLike unwraps a pointer previously wrapped with wrapPathLike. @@ -62,21 +82,20 @@ func unwrapPathLike[Value ~int32 | ~uint32, Kind ~int8](want Kind, p pathLike[Ki return 0 } - return Value(p.EndOrValue) + _, value := p.unpack() + return Value(value) } // wrapPath wraps a path in a pathLike. func wrapPath[Kind ~int8](path rawPath) pathLike[Kind] { - return pathLike[Kind]{ - StartOrKind: int32(path.Start), - EndOrValue: int32(path.End), - } + return packPathLike[Kind](int32(path.Start), int32(path.End)) } // kind returns the kind within this pathLike, if it is not a path. func (p pathLike[Kind]) kind() (Kind, bool) { - if p.StartOrKind < 0 && p.EndOrValue != 0 { - return Kind(^p.StartOrKind), true + kind, value := p.unpack() + if kind < 0 && value != 0 { + return Kind(^kind), true } return 0, false } @@ -87,5 +106,9 @@ func (p pathLike[Kind]) path(c Context) (Path, bool) { return Path{}, false } - return rawPath{Start: token.ID(p.StartOrKind), End: token.ID(p.EndOrValue)}.With(c), true + start, end := p.unpack() + return rawPath{ + Start: token.ID(start), + End: token.ID(end), + }.With(c), true } diff --git a/experimental/ast/type.go b/experimental/ast/type.go index 815b85af..555af50d 100644 --- a/experimental/ast/type.go +++ b/experimental/ast/type.go @@ -59,7 +59,7 @@ type TypeAny struct { type rawType = pathLike[TypeKind] func newTypeAny(ctx Context, t rawType) TypeAny { - if ctx == nil || (t == rawType{}) { + if ctx == nil || t.isZero() { return TypeAny{} } return TypeAny{internal.NewWith(ctx), t} diff --git a/experimental/ast/zero_test.go b/experimental/ast/zero_test.go index a8e8f93c..bdecc40e 100644 --- a/experimental/ast/zero_test.go +++ b/experimental/ast/zero_test.go @@ -100,7 +100,17 @@ func testZero[Node report.Spanner](t *testing.T) { continue } - assert.Zero(t, r.Interface(), "non-zero return #%d %#v of %T.%s", i, r, z, m.Name) + got := r.Interface() + if r.Type().Kind() != reflect.Pointer { + switch v := got.(type) { + case interface{ IsZero() bool }: + got = !v.IsZero() + case interface{ Len() int }: + got = v.Len() + } + } + + assert.Zero(t, got, "non-zero return #%d %#v of %T.%s", i, r, z, m.Name) } } }) diff --git a/experimental/internal/astx/encode.go b/experimental/internal/astx/encode.go index 2f7e77e4..5ccc910a 100644 --- a/experimental/internal/astx/encode.go +++ b/experimental/internal/astx/encode.go @@ -377,9 +377,9 @@ func (c *protoEncoder) options(options ast.CompactOptions) *compilerpb.Options { seq.Values(options.Entries())(func(o ast.Option) bool { proto.Entries = append(proto.Entries, &compilerpb.Options_Entry{ - Path: c.path(o.Path), - Value: c.expr(o.Value), - EqualsSpan: c.span(o.Equals), + Path: c.path(o.Path()), + Value: c.expr(o.Value()), + EqualsSpan: c.span(o.Equals()), }) return true }) @@ -460,7 +460,7 @@ func (c *protoEncoder) expr(expr ast.ExprAny) *compilerpb.Expr { Span: c.span(expr), OpenSpan: c.span(a.LeafSpan()), CloseSpan: c.span(b.LeafSpan()), - CommaSpans: c.commas(expr), + CommaSpans: c.commas(expr.Elements()), } seq.Values(expr.Elements())(func(e ast.ExprField) bool { proto.Entries = append(proto.Entries, c.exprField(e)) diff --git a/experimental/internal/taxa/classify.go b/experimental/internal/taxa/classify.go index f816461b..d1dbeec7 100644 --- a/experimental/internal/taxa/classify.go +++ b/experimental/internal/taxa/classify.go @@ -127,7 +127,7 @@ func Classify(node report.Spanner) Noun { return Extend case ast.DefOption: var first ast.PathComponent - node.Path.Components(func(pc ast.PathComponent) bool { + node.Path().Components(func(pc ast.PathComponent) bool { first = pc return false }) diff --git a/experimental/parser/parse_decl.go b/experimental/parser/parse_decl.go index 366c0cc7..4f083bb8 100644 --- a/experimental/parser/parse_decl.go +++ b/experimental/parser/parse_decl.go @@ -391,12 +391,12 @@ func parseOptions(p *parser, brackets token.Token, _ taxa.Noun) ast.CompactOptio eq = token.Zero } - option := ast.Option{ + option := p.NewOption(ast.OptionArgs{ Path: path, Equals: eq, Value: parseExpr(p, c, taxa.CompactOptions.In()), - } - return option, !option.Value.IsZero() + }) + return option, !option.Value().IsZero() }, }.appendTo(options.Entries()) diff --git a/experimental/parser/parse_expr.go b/experimental/parser/parse_expr.go index 0e65a6b2..9a6be2e2 100644 --- a/experimental/parser/parse_expr.go +++ b/experimental/parser/parse_expr.go @@ -189,7 +189,7 @@ func parseExprSolo(p *parser, c *token.Cursor, where taxa.Place) ast.ExprAny { field = p.NewExprField(ast.ExprFieldArgs{Value: expr}) } - dict.AppendComma(field, comma) + dict.Elements().AppendComma(field, comma) return true }) return dict.AsAny() diff --git a/experimental/seq/seq.go b/experimental/seq/seq.go index 7aa736ae..7602c364 100644 --- a/experimental/seq/seq.go +++ b/experimental/seq/seq.go @@ -20,7 +20,11 @@ // and thus must use proxy types that implement the interfaces in this package. package seq -import "github.com/bufbuild/protocompile/internal/iter" +import ( + "slices" + + "github.com/bufbuild/protocompile/internal/iter" +) // Indexer is a type that can be indexed like a slice. type Indexer[T any] interface { @@ -89,3 +93,31 @@ func Append[T any](seq Inserter[T], values ...T) { seq.Insert(seq.Len(), v) } } + +// Slice makes a Go slice into a [Setter][T]. +type Slice[T any] []T + +// Len implements [Indexer]. +func (s Slice[T]) Len() int { + return len(s) +} + +// At implements [Indexer]. +func (s Slice[T]) At(n int) T { + return s[n] +} + +// SetAt implements [Setter]. +func (s Slice[T]) SetAt(n int, v T) { + s[n] = v +} + +// Insert implements [Inserter]. +func (s *Slice[T]) Insert(n int, v T) { + *s = slices.Insert(*s, n, v) +} + +// Delete implements [Inserter]. +func (s *Slice[T]) Delete(n int) { + *s = slices.Insert(*s, n) +} diff --git a/experimental/seq/slice.go b/experimental/seq/slice.go deleted file mode 100644 index f606ef8d..00000000 --- a/experimental/seq/slice.go +++ /dev/null @@ -1,152 +0,0 @@ -// 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 seq - -import "slices" - -// TODO: Would this optimize better if this was a single type parameter -// constrained by interface { Wrap(E) T; Unwrap(T) E }? Indexer values are -// ephemera, so the size of this struct is not crucial, but it would save on -// having to allocate two [runtime.funcval]s when returning an Indexer. - -// Slice implements [Indexer][T] using an ordinary slice as the backing storage, -// and using the given functions to perform the conversion to and from the -// underlying raw values. -type Slice[T, E any] struct { - Slice []E - Wrap func(E) T - Unwrap func(T) E -} - -// Len implements [Indexer]. -func (s Slice[T, _]) Len() int { - return len(s.Slice) -} - -// At implements [Indexer]. -func (s Slice[T, _]) At(idx int) T { - return s.Wrap(s.Slice[idx]) -} - -// SetAt implements [Setter]. -func (s Slice[T, _]) SetAt(idx int, value T) { - s.Slice[idx] = s.Unwrap(value) -} - -// SliceInserter is like [Slice], but also implements [Inserter][T]. -type SliceInserter[T, E any] struct { - Slice *[]E - Wrap func(E) T - Unwrap func(T) E -} - -// Len implements [Indexer]. -func (s SliceInserter[T, _]) Len() int { - if s.Slice == nil { - return 0 - } - return len(*s.Slice) -} - -// At implements [Indexer]. -func (s SliceInserter[T, _]) At(idx int) T { - return s.Wrap((*s.Slice)[idx]) -} - -// SetAt implements [Setter]. -func (s SliceInserter[T, _]) SetAt(idx int, value T) { - (*s.Slice)[idx] = s.Unwrap(value) -} - -// Insert implements [Inserter]. -func (s SliceInserter[T, _]) Insert(idx int, value T) { - *s.Slice = slices.Insert(*s.Slice, idx, s.Unwrap(value)) -} - -// Delete implements [Inserter]. -func (s SliceInserter[T, _]) Delete(idx int) { - *s.Slice = slices.Delete(*s.Slice, idx, idx+1) -} - -// Slice2 is like Slice, but it uses a pair of slices instead of one. -// -// This is useful for cases where the raw data is represented in -// a struct-of-arrays format for memory efficiency. -type Slice2[T, E1, E2 any] struct { - // All functions on Slice2 assume that these two slices are - // always of equal length. - Slice1 []E1 - Slice2 []E2 - - Wrap func(E1, E2) T - Unwrap func(T) (E1, E2) -} - -// Len implements [Indexer]. -func (s Slice2[T, _, _]) Len() int { - return len(s.Slice1) -} - -// At implements [Indexer]. -func (s Slice2[T, _, _]) At(idx int) T { - return s.Wrap(s.Slice1[idx], s.Slice2[idx]) -} - -// SetAt implements [Setter]. -func (s Slice2[T, _, _]) SetAt(idx int, value T) { - s.Slice1[idx], s.Slice2[idx] = s.Unwrap(value) -} - -// SliceInserter2 is like Slice2, but also implements [Inserter][T]. -type SliceInserter2[T, E1, E2 any] struct { - // All functions on SliceInserter2 assume that these two slices are - // always of equal length. - Slice1 *[]E1 - Slice2 *[]E2 - - Wrap func(E1, E2) T - Unwrap func(T) (E1, E2) -} - -// Len implements [Indexer]. -func (s SliceInserter2[T, _, _]) Len() int { - if s.Slice1 == nil { - return 0 - } - return len(*s.Slice1) -} - -// At implements [Indexer]. -func (s SliceInserter2[T, _, _]) At(idx int) T { - return s.Wrap((*s.Slice1)[idx], (*s.Slice2)[idx]) -} - -// SetAt implements [Setter]. -func (s SliceInserter2[T, _, _]) SetAt(idx int, value T) { - (*s.Slice1)[idx], (*s.Slice2)[idx] = s.Unwrap(value) -} - -// Insert implements [Inserter]. -func (s SliceInserter2[T, _, _]) Insert(idx int, value T) { - r1, r2 := s.Unwrap(value) - *s.Slice1 = slices.Insert(*s.Slice1, idx, r1) - *s.Slice2 = slices.Insert(*s.Slice2, idx, r2) -} - -// Delete implements [Inserter]. -func (s SliceInserter2[T, _, _]) Delete(idx int) { - *s.Slice1 = slices.Delete(*s.Slice1, idx, idx+1) - *s.Slice2 = slices.Delete(*s.Slice2, idx, idx+1) -} diff --git a/experimental/seq/wrapper.go b/experimental/seq/wrapper.go new file mode 100644 index 00000000..bd9296dc --- /dev/null +++ b/experimental/seq/wrapper.go @@ -0,0 +1,132 @@ +// 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 seq + +// TODO: Would this optimize better if this was a single type parameter +// constrained by interface { Wrap(E) T; Unwrap(T) E }? Indexer values are +// ephemera, so the size of this struct is not crucial, but it would save on +// having to allocate two [runtime.funcval]s when returning an Indexer. + +// Wrapper implements [Setter][T] using another Setter as the backing storage, +// and using the given functions to perform the conversion to and from the +// underlying raw values. +type Wrapper[T, E any, S Setter[E]] struct { + Slice S + Wrap func(E) T + Unwrap func(T) E +} + +// Wrap is a helper for constructing a wrapper without needing to spell out the +// whole type. +func Wrap[T, E any, S Setter[E]](seq S, wrap func(E) T, unwrap func(T) E) Wrapper[T, E, S] { + return Wrapper[T, E, S]{seq, wrap, unwrap} +} + +// Len implements [Indexer]. +func (s Wrapper[T, _, _]) Len() int { + return s.Slice.Len() +} + +// At implements [Indexer]. +func (s Wrapper[T, _, _]) At(idx int) T { + return s.Wrap(s.Slice.At(idx)) +} + +// SetAt implements [Setter]. +func (s Wrapper[T, _, _]) SetAt(idx int, value T) { + s.Slice.SetAt(idx, s.Unwrap(value)) +} + +// InserterWrapper is like [Wrapper], but also implements [Inserter][T]. +type InserterWrapper[T, E any, S Inserter[E]] struct { + Wrapper[T, E, S] +} + +// WrapInserter is a helper for constructing a wrapper without needing to spell out the +// whole type. +func WrapInserter[T, E any, S Inserter[E]](seq S, wrap func(E) T, unwrap func(T) E) InserterWrapper[T, E, S] { + return InserterWrapper[T, E, S]{Wrap(seq, wrap, unwrap)} +} + +// Insert implements [Inserter]. +func (s InserterWrapper[T, _, _]) Insert(idx int, value T) { + s.Slice.Insert(idx, s.Unwrap(value)) +} + +// Delete implements [Inserter]. +func (s InserterWrapper[T, _, _]) Delete(idx int) { + s.Slice.Delete(idx) +} + +// Wrapper2 is like Slice, but it uses a pair of slices instead of one. +// +// This is useful for cases where the raw data is represented in +// a struct-of-arrays format for memory efficiency. +type Wrapper2[T, E1, E2 any, S1 Setter[E1], S2 Setter[E2]] struct { + // All functions on Slice2 assume that these two slices are + // always of equal length. + Slice1 S1 + Slice2 S2 + + Wrap func(E1, E2) T + Unwrap func(T) (E1, E2) +} + +// Wrap2 is a helper for constructing a wrapper without needing to spell out the +// whole type. +func Wrap2[T, E1, E2 any, S1 Setter[E1], S2 Setter[E2]](seq1 S1, seq2 S2, wrap func(E1, E2) T, unwrap func(T) (E1, E2)) Wrapper2[T, E1, E2, S1, S2] { + return Wrapper2[T, E1, E2, S1, S2]{seq1, seq2, wrap, unwrap} +} + +// Len implements [Indexer]. +func (s Wrapper2[T, _, _, _, _]) Len() int { + return s.Slice1.Len() +} + +// At implements [Indexer]. +func (s Wrapper2[T, _, _, _, _]) At(idx int) T { + return s.Wrap(s.Slice1.At(idx), s.Slice2.At(idx)) +} + +// SetAt implements [Setter]. +func (s Wrapper2[T, _, _, _, _]) SetAt(idx int, value T) { + e1, e2 := s.Unwrap(value) + s.Slice1.SetAt(idx, e1) + s.Slice2.SetAt(idx, e2) +} + +// InserterWrapper2 is like Slice2, but also implements [Inserter][T]. +type InserterWrapper2[T, E1, E2 any, S1 Inserter[E1], S2 Inserter[E2]] struct { + Wrapper2[T, E1, E2, S1, S2] +} + +// WrapInserter2 is a helper for constructing a wrapper without needing to spell out the +// whole type. +func WrapInserter2[T, E1, E2 any, S1 Inserter[E1], S2 Inserter[E2]](seq1 S1, seq2 S2, wrap func(E1, E2) T, unwrap func(T) (E1, E2)) InserterWrapper2[T, E1, E2, S1, S2] { + return InserterWrapper2[T, E1, E2, S1, S2]{Wrap2(seq1, seq2, wrap, unwrap)} +} + +// Insert implements [Inserter]. +func (s InserterWrapper2[T, _, _, _, _]) Insert(idx int, value T) { + r1, r2 := s.Unwrap(value) + s.Slice1.Insert(idx, r1) + s.Slice2.Insert(idx, r2) +} + +// Delete implements [Inserter]. +func (s InserterWrapper2[T, _, _, _, _]) Delete(idx int) { + s.Slice1.Delete(idx) + s.Slice2.Delete(idx) +} diff --git a/internal/ext/slicesx/inline.go b/internal/ext/slicesx/inline.go new file mode 100644 index 00000000..d5ffe65c --- /dev/null +++ b/internal/ext/slicesx/inline.go @@ -0,0 +1,225 @@ +// 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" + "unsafe" + + "github.com/bufbuild/protocompile/internal/ext/unsafex" +) + +// Inline is a slice of scalar values, which does not allocate a separate +// underlying array for small slices. It *cannot* hold pointers. +// +// The zero value is empty and ready to use. +type Inline[T unsafex.Int] struct { + _ [0]chan int // Make the type incomparable. + + // This is either nil, a pointer into inlineSentinels, or a pointer to + // an [N]uint32. + data unsafe.Pointer + + // These fields are only a length and capacity when data is not one of the + // five sentinel values (i.e., when IsInlined is true). In that case, these + // fields are reinterpreted as a [N]T. This is safe, because neither + // [2]uint64 and [N]T are both pointer-free types. + inline struct { + len, cap uint64 + } +} + +// NewInline wraps a slice in an Inlined32. +// +// Wrapping nil returns the zero. +func NewInline[T unsafex.Int](s []T) Inline[T] { + return Inline[T]{ + data: unsafe.Pointer(unsafe.SliceData(s)), + inline: struct{ len, cap uint64 }{ + len: uint64(len(s)), + cap: uint64(cap(s)), + }, + } +} + +// Len returns the length of the slice. +func (s *Inline[T]) Len() int { + if s == nil { + return 0 + } + return len(s.unsafeSlice()) +} + +// Cap returns the capacity of the slice. +// +// Inlined slices always have a capacity equal to 16 / Sizeof(T). +func (s *Inline[T]) Cap() int { + if s == nil { + return 0 + } + return cap(s.unsafeSlice()) +} + +// At returns the value at index n. +// +// Panics if the index is out of range. +func (s *Inline[T]) At(n int) T { + return s.unsafeSlice()[n] +} + +// SetAt sets the value at index n. +// +// Panics if the index is out of range. +func (s *Inline[T]) SetAt(n int, v T) { + s.unsafeSlice()[n] = v +} + +// Insert inserts a value at index n. +// +// Panics if the index is out of range. +func (s *Inline[T]) Insert(n int, v T) { + s.set(slices.Insert(s.unsafeSlice(), n, v)) +} + +// Delete deletes the value at index n. +// +// Panics if the index is out of range. +func (s *Inline[T]) Delete(n int) { + s.set(slices.Delete(s.unsafeSlice(), n, n+1)) +} + +// Slice returns the underlying slice. +// +// If the slice inlined, this will return a copy, which cannot be mutated +// through. +func (s *Inline[T]) Slice() []T { + if s == nil { + return nil + } + + if !s.IsInlined() { + // We can safely return this slice, because it is not inlined. + return s.unsafeSlice() + } + + // Make a copy of the slice. This makes sure that the user cannot mutate + // *s though the slice we return, and copied will be thrown away after the + // function returns. + copied := *s + return copied.unsafeSlice() +} + +// IsInlined returns whether this slice is currently in inlined mode. +func (s *Inline[T]) IsInlined() bool { + if s == nil { + return true + } + + _, ok := inlineLen(s.data) + return ok +} + +// Compact converts this into an inlined slice if possible. +func (s *Inline[T]) Compact() { + if s.IsInlined() { + // Nothing to do. + return + } + + if int(s.inline.len) > s.maxInlineLen() { + // Compacting it won't fit. + return + } + + old := s.unsafeSlice() + if s.inline.len == 0 { + *s = NewInline[T](nil) + } else { + s.data = inlineSentinel(len(old)) + copy(s.unsafeSlice(), old) + } +} + +// Format implements fmt.Formatter. +// func (s *Inline[T]) Format(state fmt.State, verb rune) { +// fmt.Fprintf(state, fmt.FormatString(state, verb), s.unsafeSlice()) +// } + +// maxInlineLen returns the maximum number of inline elements. +func (*Inline[T]) maxInlineLen() int { + return len(inlineSentinels) / unsafex.Size[T]() +} + +// set sets the value of this slice to slice. +// +// This function has a special case for when slice is an alias of this slice's +// inline region. +func (s *Inline[T]) set(slice []T) { + if unsafe.Pointer(unsafe.SliceData(slice)) == unsafe.Pointer(&s.inline) { + s.data = inlineSentinel(len(slice)) + return + } + + *s = NewInline(slice) +} + +// unsafeSlice returns the slice this Inline represents. +// +// This slice MUST NOT be escaped into public API! If it does, users may write +// this code: +// +// var s Inline[int32] +// s.Append(1) +// s1 := s.unsafeSlice() +// for range 4 { +// s.Append(2) +// } +// +// s2 := s.unsafeSlice() +// s1[0] = 10000 +// s2[16] = 42 // Out-of-bounds write. +// +// Upon pushing to the point that the slice spills to the heap, s1 still holds +// a pointer back into the slice, so writing to it will overwrite the length +// and capacity of the slice! +// +// Similarly, users MUST NOT be given access to pointers into this slice. +func (s *Inline[T]) unsafeSlice() []T { + if idx, ok := inlineLen(s.data); ok { + return unsafe.Slice(unsafex.Bitcast[*T](&s.inline), s.maxInlineLen())[:idx] + } + + return unsafe.Slice((*T)(s.data), s.inline.cap)[:s.inline.len] +} + +// Need to use unsafe.Sizeof here to make this into a constant. +var inlineSentinels [unsafe.Sizeof(Inline[byte]{}.inline)]byte + +func inlineSentinel(n int) unsafe.Pointer { + if n == 0 { + return nil + } + return unsafe.Pointer(&inlineSentinels[n-1]) +} + +func inlineLen(p unsafe.Pointer) (int, bool) { + if p == nil { + return 0, true + } + if idx := PointerIndex(inlineSentinels[:], (*byte)(p)); idx >= 0 { + return idx + 1, true + } + return 0, false +} diff --git a/internal/ext/slicesx/inline_test.go b/internal/ext/slicesx/inline_test.go new file mode 100644 index 00000000..4c70360c --- /dev/null +++ b/internal/ext/slicesx/inline_test.go @@ -0,0 +1,112 @@ +// 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_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bufbuild/protocompile/experimental/seq" + "github.com/bufbuild/protocompile/internal/ext/slicesx" +) + +func TestInline8(t *testing.T) { + t.Parallel() + + var s slicesx.Inline[int8] + assert.Equal(t, []int8{}, s.Slice()) + assert.Equal(t, 16, s.Cap()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 1, 2, 3) + assert.Equal(t, []int8{1, 2, 3}, s.Slice()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7) + assert.Equal(t, []int8{1, 2, 3, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Delete(0) + s.Delete(0) + assert.Equal(t, []int8{3, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Compact() + assert.Equal(t, []int8{3, 2, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7}, s.Slice()) + assert.True(t, s.IsInlined()) + + s.Delete(1) + assert.Equal(t, []int8{3, 3, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7}, s.Slice()) + assert.True(t, s.IsInlined()) +} + +func TestInline32(t *testing.T) { + t.Parallel() + + var s slicesx.Inline[int32] + assert.Equal(t, []int32{}, s.Slice()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 1, 2, 3) + assert.Equal(t, []int32{1, 2, 3}, s.Slice()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 2, 3) + assert.Equal(t, []int32{1, 2, 3, 2, 3}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Delete(0) + s.Delete(0) + assert.Equal(t, []int32{3, 2, 3}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Compact() + assert.Equal(t, []int32{3, 2, 3}, s.Slice()) + assert.True(t, s.IsInlined()) + + s.Delete(1) + assert.Equal(t, []int32{3, 3}, s.Slice()) + assert.True(t, s.IsInlined()) +} + +func TestInline64(t *testing.T) { + t.Parallel() + + var s slicesx.Inline[int64] + assert.Equal(t, []int64{}, s.Slice()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 1, 2) + assert.Equal(t, []int64{1, 2}, s.Slice()) + assert.True(t, s.IsInlined()) + + seq.Append(&s, 3) + assert.Equal(t, []int64{1, 2, 3}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Delete(0) + s.Delete(0) + assert.Equal(t, []int64{3}, s.Slice()) + assert.False(t, s.IsInlined()) + + s.Compact() + assert.Equal(t, []int64{3}, s.Slice()) + assert.True(t, s.IsInlined()) + + s.Delete(0) + assert.Equal(t, []int64{}, s.Slice()) + assert.True(t, s.IsInlined()) +} diff --git a/internal/ext/slicesx/small.go b/internal/ext/slicesx/small.go new file mode 100644 index 00000000..4275126c --- /dev/null +++ b/internal/ext/slicesx/small.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 slicesx + +import ( + "fmt" + "unsafe" +) + +// Small is a slice that's only two words wide. +// +// Instead of storing both length and capacity, this type only stores a pointer +// and length. This representation is ideal for slices that will not be +// appended to. +type Small[T any] struct { + _ [0]chan int // Make the type incomparable. + + ptr unsafe.Pointer + len int +} + +// NewSmall wraps a slice as a small slice. This function forgets the capacity, +// so it is equivalent to call NewSmall(s[:len(s):len(s)]). +// +// Beware: because the capacity is discarded the following code will result in +// quadratic runtime. +// +// var x Small[int] +// for _, y := range s { +// // Allocates a fresh backing array each iteration. +// x = NewSmall(append(x.Slice(), y)) +// } +// +// Instead, it is better to convert back into a Go slice, perform modifications +// in a batch, and then convert back into a Small[T]. +func NewSmall[T any](s []T) Small[T] { + return Small[T]{ + ptr: unsafe.Pointer(unsafe.SliceData(s)), + len: len(s), + } +} + +// Slice returns a Go slice view into this small slice. +func (s Small[T]) Slice() []T { + return unsafe.Slice((*T)(s.ptr), s.len) +} + +// Format implements fmt.Formatter. +func (s Small[T]) Format(state fmt.State, verb rune) { + fmt.Fprintf(state, fmt.FormatString(state, verb), s.Slice()) +} diff --git a/internal/ext/slicesx/small_test.go b/internal/ext/slicesx/small_test.go new file mode 100644 index 00000000..36074321 --- /dev/null +++ b/internal/ext/slicesx/small_test.go @@ -0,0 +1,33 @@ +// 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_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bufbuild/protocompile/internal/ext/slicesx" +) + +func TestSmall(t *testing.T) { + t.Parallel() + + s := slicesx.NewSmall[int32](nil) + assert.Nil(t, s.Slice()) + + s = slicesx.NewSmall[int32]([]int32{1, 2, 3, 4}) + assert.Equal(t, []int32{1, 2, 3, 4}, s.Slice()) +}