builder: value-typed ListBuilder + fix latent zap/v1 breakage

StartList returns ListBuilder by value (mirrors the value-typed ObjectBuilder):
Add*/Finish keep pointer receivers, every caller binds to an addressable local so
Go auto-addresses it. Dropped the dead elemSize field (never read — stride is
implicit in which Add* is called). Byte-identical wire (golden+fuzz+all round-trips
green). NOTE: not a measured alloc win — escape analysis already stack-allocated
the &ListBuilder{} at all 9 call sites (composite stays 583ns/5allocs, the 5 being
mandatory writeEnvelopePrefix pool-egress copies). Value is code-cleanliness +
consistency.

Also fixes zap/v1 subpackage: v1.2.4's ObjectBuilder value change left v1/*.go
referencing *zap.ObjectBuilder → the subpackage didn't build (latent; only the
root package is imported downstream so it went unnoticed). Converted to the value
type. Full module (root + v1) builds + tests green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-14 14:06:56 -07:00
co-authored by Hanzo Dev
parent f9909a847a
commit ea1506e5c2
5 changed files with 20 additions and 15 deletions
+10 -5
View File
@@ -330,17 +330,22 @@ func (b *Builder) WriteText(s string) int {
type ListBuilder struct {
b *Builder
startPos int
elemSize int
count int
}
// StartList starts building a list.
func (b *Builder) StartList(elemSize int) *ListBuilder {
// StartList starts building a list. Returns a VALUE (not a heap pointer): the
// Add*/Finish methods take pointer receivers, and every caller binds the result
// to an addressable local (`lb := b.StartList(...)`), so Go auto-addresses the
// local and count mutations accumulate on it — no per-list heap allocation.
// Mirrors the value-typed StartObject. elemSize is unused (the stride is
// implicit in which Add* the caller invokes) and kept only as a documenting
// param.
func (b *Builder) StartList(elemSize int) ListBuilder {
_ = elemSize
b.align(Alignment)
return &ListBuilder{
return ListBuilder{
b: b,
startPos: b.pos,
elemSize: elemSize,
}
}
+3 -3
View File
@@ -12,13 +12,13 @@ import "github.com/luxfi/zap"
// straight-line `b.StartObject` construction that inlines at the call
// site. The (ob, b) must be the pair returned by the same StartObject —
// passing a mismatched pair is a programming error.
func SetterFrom[S Schema](ob *zap.ObjectBuilder, b *zap.Builder) Setter[S] {
func SetterFrom[S Schema](ob zap.ObjectBuilder, b *zap.Builder) Setter[S] {
return Setter[S]{ob: ob, b: b}
}
// WriteString writes a variable-length UTF-8 string field to the object
// tail and stores its {relOffset, length} pointer at byteOffset in the
// fixed payload. The typed peer of [*zap.ObjectBuilder.SetText] for the
// fixed payload. The typed peer of [zap.ObjectBuilder.SetText] for the
// Setter-based build path. Read it back with the generated string
// accessor (which calls v1's zero-copy Object.Text).
func WriteString[S Schema](s Setter[S], byteOffset uint32, v string) {
@@ -28,7 +28,7 @@ func WriteString[S Schema](s Setter[S], byteOffset uint32, v string) {
// WriteBytes writes a variable-length byte slice to the object tail and
// stores its {relOffset, length} pointer at byteOffset. The typed peer
// of [*zap.ObjectBuilder.SetBytes]. Zero-copy on read via Object.Bytes.
// of [zap.ObjectBuilder.SetBytes]. Zero-copy on read via Object.Bytes.
func WriteBytes[S Schema](s Setter[S], byteOffset uint32, v []byte) {
ob, _ := s.raw()
ob.SetBytes(int(byteOffset), v)
+4 -4
View File
@@ -34,7 +34,7 @@ import (
// passed by value.
type Builder[S Schema] struct {
b *zap.Builder
ob *zap.ObjectBuilder
ob zap.ObjectBuilder
}
// NewBuilderFor returns a fresh Builder[S] sized for S's fixed
@@ -55,7 +55,7 @@ func NewBuilderFor[S Schema]() Builder[S] {
// startBuild is the non-generic, shape-free counterpart of
// [NewBuilderFor]: allocate a v1 [*zap.Builder] sized for the fixed
// payload, anchor an [*zap.ObjectBuilder] at the root, and write the
// payload, anchor an [zap.ObjectBuilder] at the root, and write the
// kind discriminator. The caller (per-schema shim, codegen, or
// generic [NewBuilderFor]) supplies size and kind as constants — the
// Go compiler folds them at compile time when called from a concrete
@@ -64,7 +64,7 @@ func NewBuilderFor[S Schema]() Builder[S] {
// Centralizing the allocation here keeps the generic [NewBuilderFor]
// body tiny and lets the inliner fold it into the caller — same
// trick as [WrapRaw] / [parseHeaderImpl].
func startBuild(size int, kind uint8) (*zap.Builder, *zap.ObjectBuilder) {
func startBuild(size int, kind uint8) (*zap.Builder, zap.ObjectBuilder) {
b := zap.NewBuilder(zap.HeaderSize + size)
ob := b.StartObject(size)
ob.SetUint8(0, kind)
@@ -93,7 +93,7 @@ func (bb Builder[S]) Finish() (View[S], []byte) {
// The Raw's payload is the fixed-section slice; the caller wraps it
// as [View[S]] via [AsView]. The buffer is returned for callers that
// need the on-wire bytes.
func finishBuild(b *zap.Builder, ob *zap.ObjectBuilder, size int) (Raw, []byte) {
func finishBuild(b *zap.Builder, ob zap.ObjectBuilder, size int) (Raw, []byte) {
rootOff := ob.FinishAsRoot()
buf := b.Finish()
end := rootOff + size
+1 -1
View File
@@ -202,7 +202,7 @@ func WriteB[S Schema, T FieldKind](bb Builder[S], f Field[S, T], val T) {
// against unsafe.Sizeof on the type parameter, each branch calling
// one v1 ObjectBuilder.Set* method, folds to one MOV per concrete T
// after generic instantiation and inlining.
func writeAt[T FieldKind](ob *zap.ObjectBuilder, offset uint32, val T) {
func writeAt[T FieldKind](ob zap.ObjectBuilder, offset uint32, val T) {
off := int(offset)
switch unsafe.Sizeof(val) {
case 1:
+2 -2
View File
@@ -388,13 +388,13 @@ func Build[S Schema](init func(Setter[S])) (View[S], []byte) {
// them and is itself reachable from the caller's stack). Setter values
// are NOT safe to retain after the builder's Finish returns.
type Setter[S Schema] struct {
ob *zap.ObjectBuilder
ob zap.ObjectBuilder
b *zap.Builder
}
// raw exposes the underlying builder for advanced primitives that
// need to write variable-length tails (Text, Bytes, Lists). It is
// unexported; the variable-length writers in [list.go] use it.
func (s Setter[S]) raw() (*zap.ObjectBuilder, *zap.Builder) {
func (s Setter[S]) raw() (zap.ObjectBuilder, *zap.Builder) {
return s.ob, s.b
}