mirror of
https://github.com/luxfi/zap.git
synced 2026-10-30 15:04:16 +00:00
The repeated-message wire shape: a list whose elements carry their own
variable-length tails (string/bytes/nested), which the fixed-stride inline
List cannot hold. Completes proto field-shape coverage in the runtime —
map<k,v> reduces to a repeated {key,value} entry, so this is its keystone too.
Composition, not a new wire concept: ListNested = List ∘ NestedAt. The list
slots are a contiguous stride-4 object-pointer array; each pointer is
followed exactly as a singular nested pointer to an out-of-line tailed View.
Runtime:
- builder.go: ListBuilder.AddObjectPtr(targetPos) — appends a signed 4-byte
relative object pointer (the repeated-message element kind).
- v1/list_nested.go: ListNested[N] (Len/At/All/IsZero), ListNestedAt[S,N]
(read), WriteListNested[S,N] + NestedElemSetter[N] (write). Same audited
bounds discipline as Object.Object / ListStride (RED-HIGH-1/2 inherited).
Codegen:
- elemIsTailed: a list element with a string/bytes/nested/list sub-field
auto-routes to ListNested/WriteListNested/ListNestedAt; scalar-only
elements stay in the tighter inline List/WriteList/ListAt. One Elem
declaration, the generator picks the correct encoding.
Proven end-to-end: runtime roundtrip (3 elements each with its OWN string
tail, via At and All, + empty), emit routing test (tailed->ListNested,
scalar-only->List), and a generated-code wire roundtrip (testpkg/repeatwire).
Root zap + full v1+codegen suite green; scalar listwire unchanged (no regression).
676 lines
23 KiB
Go
676 lines
23 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package codegen
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// typeToSize maps a [zapv1.FieldKind] Go type name to its byte width.
|
|
// Used by the field-write code to pick the right ObjectBuilder.Set*
|
|
// method.
|
|
var typeToSize = map[string]int{
|
|
"bool": 1,
|
|
"int8": 1,
|
|
"uint8": 1,
|
|
"int16": 2,
|
|
"uint16": 2,
|
|
"int32": 4,
|
|
"uint32": 4,
|
|
"float32": 4,
|
|
"int64": 8,
|
|
"uint64": 8,
|
|
"float64": 8,
|
|
}
|
|
|
|
// typeToSetMethod maps a Go type name to the v1 ObjectBuilder method
|
|
// that writes it.
|
|
var typeToSetMethod = map[string]string{
|
|
"bool": "SetBool",
|
|
"int8": "SetInt8",
|
|
"uint8": "SetUint8",
|
|
"int16": "SetInt16",
|
|
"uint16": "SetUint16",
|
|
"int32": "SetInt32",
|
|
"uint32": "SetUint32",
|
|
"float32": "SetFloat32",
|
|
"int64": "SetInt64",
|
|
"uint64": "SetUint64",
|
|
"float64": "SetFloat64",
|
|
}
|
|
|
|
// fieldByteSize returns the size in bytes of a schema field. Handles
|
|
// both scalar FieldKind types and "bytes<N>" byte-array types.
|
|
// Returns (0, false) for unknown types.
|
|
func fieldByteSize(f Field) (int, bool) {
|
|
if _, ok := f.IsList(); ok {
|
|
return 8, true // list pointer {relOffset uint32, length uint32}
|
|
}
|
|
if _, ok := f.IsNested(); ok {
|
|
return 4, true // object pointer {relOffset uint32} — singular, no length
|
|
}
|
|
if n, ok := f.IsBytes(); ok {
|
|
return n, true
|
|
}
|
|
if f.IsVariable() {
|
|
return 8, true // tail pointer {relOffset uint32, length uint32}
|
|
}
|
|
if sz, ok := typeToSize[f.Type]; ok {
|
|
return sz, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Emit writes the per-schema *.go file to w. Returns an error if the
|
|
// schema is invalid (unknown field type, offset overlap, fields past
|
|
// declared Size, etc.).
|
|
//
|
|
// The emitted code follows the v1 hand-rolled pattern: every step
|
|
// (Parse, Root, Uint reads, kind check, View compose) uses v1
|
|
// primitives that inline into the caller's frame. The result matches
|
|
// v1 hand-rolled performance to within compiler noise — verified by
|
|
// the bench suite.
|
|
//
|
|
// Scalar fields go through the [zapv1.Field][S, T] generic handle
|
|
// (declared in the per-schema <GoName>Fields var). Fixed-width
|
|
// byte-array fields ("bytes<N>") are emitted as standalone typed
|
|
// accessor functions that call v1's SetBytesFixed / BytesFixedSlice
|
|
// — they do NOT appear in the Fields struct because [N]byte is not a
|
|
// [zapv1.FieldKind] member. Both forms produce the same wire layout.
|
|
func Emit(w io.Writer, s Schema) error {
|
|
// Validate field types + offsets.
|
|
for _, f := range s.Fields {
|
|
fsz, ok := fieldByteSize(f)
|
|
if !ok {
|
|
return fmt.Errorf("codegen: field %q has unsupported type %q",
|
|
f.Name, f.Type)
|
|
}
|
|
if int(f.Offset)+fsz > s.Size {
|
|
return fmt.Errorf("codegen: field %q at offset %d size %d extends past declared payload size %d",
|
|
f.Name, f.Offset, fsz, s.Size)
|
|
}
|
|
}
|
|
// Detect offset overlap (each byte may belong to at most one field).
|
|
if s.Size > 0 {
|
|
owner := make([]string, s.Size)
|
|
// Reserve offset 0 for the kind discriminator — UNLESS this is a
|
|
// flat list-element schema, whose slot carries no kind byte (fields
|
|
// may legitimately start at offset 0).
|
|
if !s.Element {
|
|
owner[0] = "<kind>"
|
|
}
|
|
for _, f := range s.Fields {
|
|
fsz, _ := fieldByteSize(f)
|
|
for i := 0; i < fsz; i++ {
|
|
p := int(f.Offset) + i
|
|
if owner[p] != "" {
|
|
return fmt.Errorf("codegen: field %q at offset %d overlaps field %q",
|
|
f.Name, p, owner[p])
|
|
}
|
|
owner[p] = f.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
// Header.
|
|
fmt.Fprintf(w, `// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
//
|
|
// Code generated by github.com/luxfi/zap/v1/codegen — DO NOT EDIT.
|
|
// Source schema: %s (kind=0x%02x, size=%d).
|
|
|
|
package %s
|
|
|
|
import (
|
|
"github.com/luxfi/zap"
|
|
zapv1 "github.com/luxfi/zap/v1"
|
|
)
|
|
|
|
`, s.WireName, s.Kind, s.Size, s.Package)
|
|
|
|
// Constants.
|
|
fmt.Fprintf(w, `// Kind%s is the wire discriminator for %s.
|
|
const Kind%s zapv1.KindByte = 0x%02x
|
|
|
|
// Size%s is the fixed object payload size in bytes.
|
|
const Size%s = %d
|
|
|
|
`, s.WireName, s.WireName, s.WireName, s.Kind,
|
|
s.WireName, s.WireName, s.Size)
|
|
|
|
// Schema marker type.
|
|
fmt.Fprintf(w, `// %s is the v1 schema marker for %s. Methods are constant-
|
|
// returning so concrete-typed [zapv1.Wrap[%s]] calls fold to
|
|
// literals at the call site.
|
|
type %s struct{}
|
|
|
|
func (%s) Kind() zapv1.KindByte { return Kind%s }
|
|
func (%s) Size() int { return Size%s }
|
|
func (%s) Name() string { return %q }
|
|
|
|
`, s.GoName, s.WireName, s.GoName, s.GoName,
|
|
s.GoName, s.WireName,
|
|
s.GoName, s.WireName,
|
|
s.GoName, s.WireName)
|
|
|
|
// Field offset constants — all fields, scalar and byte-array.
|
|
for _, f := range s.Fields {
|
|
fmt.Fprintf(w, "// Offset%s_%s is the byte offset of the %s field.\nconst Offset%s_%s = %d\n\n",
|
|
s.WireName, f.Name, f.Name,
|
|
s.WireName, f.Name, f.Offset)
|
|
}
|
|
|
|
// <SchemaGoName>Fields struct — scalar fields only.
|
|
// Byte-array fields are handled by standalone accessor functions
|
|
// (emitted after the constructor) because [N]byte is not a
|
|
// FieldKind union member.
|
|
// Scalar fields go in the Fields struct (zapv1.Field handles).
|
|
// Fixed byte-arrays and variable-length tails (string/bytes) get
|
|
// standalone typed accessors instead — neither [N]byte nor string/
|
|
// []byte is a zapv1.FieldKind member.
|
|
var scalarFields []Field
|
|
var accessorFields []Field
|
|
var listFields []Field
|
|
var nestedFields []Field
|
|
for _, f := range s.Fields {
|
|
switch {
|
|
case isList(f):
|
|
listFields = append(listFields, f)
|
|
case isNested(f):
|
|
nestedFields = append(nestedFields, f)
|
|
default:
|
|
if _, isBytes := f.IsBytes(); isBytes || f.IsVariable() {
|
|
accessorFields = append(accessorFields, f)
|
|
} else {
|
|
scalarFields = append(scalarFields, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(scalarFields) > 0 {
|
|
fmt.Fprintf(w, "// %sFields is the namespace of every scalar field accessor for %s.\n",
|
|
s.GoName, s.WireName)
|
|
fmt.Fprintf(w, "// Use with [zapv1.Read] / [zapv1.Write] for type-safe access.\n")
|
|
fmt.Fprintf(w, "// Byte-array fields are emitted as standalone functions, see below.\nvar %sFields = struct {\n",
|
|
s.GoName)
|
|
for _, f := range scalarFields {
|
|
fmt.Fprintf(w, "\t%s zapv1.Field[%s, %s]\n", f.Name, s.GoName, f.Type)
|
|
}
|
|
fmt.Fprintf(w, "}{\n")
|
|
for _, f := range scalarFields {
|
|
fmt.Fprintf(w, "\t%s: zapv1.At[%s, %s](%d),\n",
|
|
f.Name, s.GoName, f.Type, f.Offset)
|
|
}
|
|
fmt.Fprintf(w, "}\n\n")
|
|
} else {
|
|
// Still emit the empty struct so callers can rely on the
|
|
// <GoName>Fields symbol existing.
|
|
fmt.Fprintf(w, "// %sFields is empty: %s has no scalar fields.\n",
|
|
s.GoName, s.WireName)
|
|
fmt.Fprintf(w, "var %sFields = struct{}{}\n\n", s.GoName)
|
|
}
|
|
|
|
// New constructor.
|
|
emitNewConstructor(w, s, scalarFields, accessorFields, listFields, nestedFields)
|
|
emitWrapFunction(w, s)
|
|
emitAccessorFields(w, s, accessorFields)
|
|
emitListSupport(w, s, listFields)
|
|
emitNestedSupport(w, s, nestedFields)
|
|
emitRegisterInit(w, s)
|
|
|
|
return nil
|
|
}
|
|
|
|
// isList reports whether f is a list field (has an element descriptor).
|
|
func isList(f Field) bool { _, ok := f.IsList(); return ok }
|
|
|
|
// isNested reports whether f is a singular nested-object field.
|
|
func isNested(f Field) bool { _, ok := f.IsNested(); return ok }
|
|
|
|
// elemIsTailed reports whether a list element carries its own
|
|
// variable-length tail (a string/bytes field, or a nested/list sub-field).
|
|
// Such elements cannot live in a fixed-stride [zapv1.List] — they need the
|
|
// out-of-line pointer-array [zapv1.ListNested] (the "repeated message"
|
|
// encoding) so each element's tail has somewhere to go. Scalar-only (and
|
|
// fixed-byte-only) elements stay in the tighter inline List.
|
|
func elemIsTailed(elem *ListElem) bool {
|
|
for _, ef := range elem.Fields {
|
|
if ef.IsVariable() || isList(ef) || isNested(ef) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// emitElemWrites writes one builder statement per element/nested field,
|
|
// reading from srcExpr (e.g. "it" for a list element or "inner" for a
|
|
// nested value) into the element Setter named setVar (e.g. "e"). Shared by
|
|
// the list-element and nested-object build bodies so the two stay
|
|
// orthogonal: scalars go through the typed [zapv1.Write] handle, while
|
|
// string/bytes tails use [zapv1.WriteString]/[zapv1.WriteBytes] against the
|
|
// element schema's Offset<Wire>_<Field> constants. wire is the element
|
|
// schema's WireName (for those Offset constants); goName is its marker
|
|
// GoName (for the <GoName>Fields handle). indent prefixes each line.
|
|
func emitElemWrites(w io.Writer, goName, wire, setVar, srcExpr, indent string, fields []Field) {
|
|
for _, f := range fields {
|
|
switch {
|
|
case f.IsVarString():
|
|
fmt.Fprintf(w, "%szapv1.WriteString(%s, Offset%s_%s, %s.%s)\n",
|
|
indent, setVar, wire, f.Name, srcExpr, f.Name)
|
|
case f.IsVarBytes():
|
|
fmt.Fprintf(w, "%szapv1.WriteBytes(%s, Offset%s_%s, %s.%s)\n",
|
|
indent, setVar, wire, f.Name, srcExpr, f.Name)
|
|
default:
|
|
fmt.Fprintf(w, "%szapv1.Write(%s, %sFields.%s, %s.%s)\n",
|
|
indent, setVar, goName, f.Name, srcExpr, f.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// emitNewConstructor writes the New%s function that builds a fresh
|
|
// ZAP buffer with all fields set from parameters. The body uses v1
|
|
// primitives (zap.NewBuilder, b.StartObject, ob.Set*, b.Finish) so
|
|
// every step inlines into the caller.
|
|
func emitNewConstructor(w io.Writer, s Schema, scalar, accessor, list, nested []Field) {
|
|
// Function signature — scalar fields first, then accessor fields
|
|
// (fixed byte arrays as [N]byte, variable string as string, variable
|
|
// bytes as []byte), then list fields ([]ElementValue), then nested
|
|
// fields (*NestedValue — pointer so nil encodes unset), in declared
|
|
// order.
|
|
var args []string
|
|
for _, f := range scalar {
|
|
args = append(args, fmt.Sprintf("%s %s", lowerFirst(f.Name), f.Type))
|
|
}
|
|
for _, f := range accessor {
|
|
switch {
|
|
case f.IsVarString():
|
|
args = append(args, fmt.Sprintf("%s string", lowerFirst(f.Name)))
|
|
case f.IsVarBytes():
|
|
args = append(args, fmt.Sprintf("%s []byte", lowerFirst(f.Name)))
|
|
default:
|
|
n, _ := f.IsBytes()
|
|
args = append(args, fmt.Sprintf("%s [%d]byte", lowerFirst(f.Name), n))
|
|
}
|
|
}
|
|
for _, f := range list {
|
|
elem, _ := f.IsList()
|
|
args = append(args, fmt.Sprintf("%s []%s", lowerFirst(f.Name), elem.Value))
|
|
}
|
|
for _, f := range nested {
|
|
nm, _ := f.IsNested()
|
|
args = append(args, fmt.Sprintf("%s *%s", lowerFirst(f.Name), nm.Value))
|
|
}
|
|
|
|
// Buffer capacity: fixed payload plus the byte length of every
|
|
// variable-length tail value, so the common case is a single alloc.
|
|
// (The Builder still grows if the estimate is short.)
|
|
capExpr := fmt.Sprintf("zap.HeaderSize + Size%s", s.WireName)
|
|
for _, f := range accessor {
|
|
if f.IsVariable() {
|
|
capExpr += fmt.Sprintf(" + len(%s)", lowerFirst(f.Name))
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, `// New%s builds a fresh %s in a ZAP buffer.
|
|
//
|
|
// Hand-rolled fast path: inlines v1 primitives ([zap.NewBuilder],
|
|
// [*Builder.StartObject], [*ObjectBuilder.Set*], [*Builder.Finish])
|
|
// so the entire build expands at the call site. Scalars and fixed byte
|
|
// arrays live in the fixed payload; variable-length string/bytes go in
|
|
// the object tail (the fixed payload holds their 8-byte pointers).
|
|
func New%s(%s) (zapv1.View[%s], []byte) {
|
|
b := zap.NewBuilder(%s)
|
|
ob := b.StartObject(Size%s)
|
|
`, s.WireName, s.WireName, s.WireName, strings.Join(args, ", "), s.GoName,
|
|
capExpr, s.WireName)
|
|
// Flat list-element slots carry no kind byte; top-level messages stamp
|
|
// the discriminator at offset 0.
|
|
if !s.Element {
|
|
fmt.Fprintf(w, "\tob.SetUint8(0, uint8(Kind%s))\n", s.WireName)
|
|
}
|
|
|
|
for _, f := range scalar {
|
|
fmt.Fprintf(w, "\tob.%s(Offset%s_%s, %s)\n",
|
|
typeToSetMethod[f.Type], s.WireName, f.Name, lowerFirst(f.Name))
|
|
}
|
|
for _, f := range accessor {
|
|
switch {
|
|
case f.IsVarString():
|
|
fmt.Fprintf(w, "\tob.SetText(Offset%s_%s, %s)\n",
|
|
s.WireName, f.Name, lowerFirst(f.Name))
|
|
case f.IsVarBytes():
|
|
fmt.Fprintf(w, "\tob.SetBytes(Offset%s_%s, %s)\n",
|
|
s.WireName, f.Name, lowerFirst(f.Name))
|
|
default:
|
|
// Fixed array passed as a slice via [:] so SetBytesFixed gets []byte.
|
|
fmt.Fprintf(w, "\tob.SetBytesFixed(Offset%s_%s, %s[:])\n",
|
|
s.WireName, f.Name, lowerFirst(f.Name))
|
|
}
|
|
}
|
|
|
|
// List and nested fields write into the object tail AFTER every
|
|
// fixed/string/bytes field, via the typed [zapv1.WriteList] /
|
|
// [zapv1.WriteNested] machinery bridged onto the hand-rolled
|
|
// ObjectBuilder by [zapv1.SetterFrom]. Each sub-object is built inline
|
|
// (one write per sub-field) so variable-length parents and tail objects
|
|
// never interleave.
|
|
if len(list) > 0 || len(nested) > 0 {
|
|
fmt.Fprintf(w, "\tls := zapv1.SetterFrom[%s](ob, b)\n", s.GoName)
|
|
for _, f := range list {
|
|
elem, _ := f.IsList()
|
|
// Tailed elements (string/bytes/nested sub-fields) need the
|
|
// out-of-line pointer array; scalar-only elements stay inline.
|
|
writeFn, setterT := "WriteList", "ElemSetter"
|
|
if elemIsTailed(elem) {
|
|
writeFn, setterT = "WriteListNested", "NestedElemSetter"
|
|
}
|
|
fmt.Fprintf(w, "\tzapv1.%s[%s, %s](ls, Offset%s_%s, func(es *zapv1.%s[%s]) {\n",
|
|
writeFn, s.GoName, elem.Schema, s.WireName, f.Name, setterT, elem.Schema)
|
|
fmt.Fprintf(w, "\t\tfor _, it := range %s {\n", lowerFirst(f.Name))
|
|
fmt.Fprintf(w, "\t\t\tes.Append(func(e zapv1.Setter[%s]) {\n", elem.Schema)
|
|
emitElemWrites(w, elem.Schema, elem.Wire, "e", "it", "\t\t\t\t", elem.Fields)
|
|
fmt.Fprintf(w, "\t\t\t})\n\t\t}\n\t})\n")
|
|
}
|
|
// Nested: a singular out-of-line object, written only when present
|
|
// (nil pointer => unset => null object pointer).
|
|
for _, f := range nested {
|
|
nm, _ := f.IsNested()
|
|
pname := lowerFirst(f.Name)
|
|
fmt.Fprintf(w, "\tif %s != nil {\n", pname)
|
|
fmt.Fprintf(w, "\t\tzapv1.WriteNested[%s, %s](ls, Offset%s_%s, func(e zapv1.Setter[%s]) {\n",
|
|
s.GoName, nm.Schema, s.WireName, f.Name, nm.Schema)
|
|
emitElemWrites(w, nm.Schema, nm.Wire, "e", pname, "\t\t\t", nm.Fields)
|
|
fmt.Fprintf(w, "\t\t})\n\t}\n")
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, ` rootOff := ob.FinishAsRoot()
|
|
buf := b.Finish()
|
|
end := rootOff + Size%s
|
|
return zapv1.AsView[%s](zapv1.RawFromSlices(buf, rootOff, end)), buf
|
|
}
|
|
|
|
`, s.WireName, s.GoName)
|
|
}
|
|
|
|
// emitWrapFunction writes the Wrap%s function that parses and
|
|
// type-checks a buffer. The body uses v1 primitives so it inlines.
|
|
func emitWrapFunction(w io.Writer, s Schema) {
|
|
fmt.Fprintf(w, `// Wrap%s parses a ZAP buffer into a typed
|
|
// [zapv1.View[%s]]. Validates the wire frame and the kind
|
|
// discriminator at offset 0; returns [*zapv1.SchemaError] on kind
|
|
// mismatch.
|
|
//
|
|
// Hand-rolled fast path: inlines [zap.Parse] (which is itself
|
|
// inlineable). Zero heap allocs on the happy path; matches v1
|
|
// hand-rolled performance.
|
|
func Wrap%s(b []byte) (zapv1.View[%s], error) {
|
|
msg, err := zap.Parse(b)
|
|
if err != nil {
|
|
return zapv1.View[%s]{}, err
|
|
}
|
|
root := msg.Root()
|
|
`, s.WireName, s.WireName, s.WireName, s.GoName, s.GoName)
|
|
|
|
// Top-level messages validate the kind discriminator at offset 0; flat
|
|
// list-element schemas carry no kind byte, so they skip the check.
|
|
if !s.Element {
|
|
fmt.Fprintf(w, ` if got := root.Uint8(0); got != uint8(Kind%s) {
|
|
return zapv1.View[%s]{}, zapv1.NewSchemaError(
|
|
Kind%s, zapv1.KindByte(got), %q)
|
|
}
|
|
`, s.WireName, s.GoName, s.WireName, s.WireName)
|
|
}
|
|
|
|
fmt.Fprintf(w, ` data := msg.Bytes()
|
|
rootOff := root.Offset()
|
|
end := rootOff + Size%s
|
|
if end > len(data) {
|
|
end = len(data) // Read returns zero on OOB; v1 graceful degradation
|
|
}
|
|
return zapv1.AsView[%s](zapv1.RawFromSlices(data, rootOff, end)), nil
|
|
}
|
|
|
|
`, s.WireName, s.GoName)
|
|
}
|
|
|
|
// emitByteFieldAccessors emits per-byte-array-field typed accessor
|
|
// functions. Each field gets a Get<Field>(view) [N]byte function that
|
|
// reads the inline byte array from the view's payload via v1's
|
|
// Object.BytesFixedSlice; zero copy on the read path, one [N]byte
|
|
// copy into the returned value (necessary because the slice is alive
|
|
// only as long as the View's underlying buffer is, and the typed
|
|
// accessor must hand the caller an owned value).
|
|
func emitAccessorFields(w io.Writer, s Schema, fields []Field) {
|
|
if len(fields) == 0 {
|
|
return
|
|
}
|
|
for _, f := range fields {
|
|
switch {
|
|
case f.IsVarString():
|
|
fmt.Fprintf(w, `// %s%s reads the variable-length %s string field of %s.
|
|
//
|
|
// Zero-copy: v1's Object.Text returns a string over a sub-slice of the
|
|
// underlying buffer. Valid only while the View's buffer is alive.
|
|
func %s%s(v zapv1.View[%s]) string {
|
|
msg := zap.WrapBuffer(v.Bytes())
|
|
obj := msg.RootObjectAt(int(zapv1.RootOff(v)))
|
|
return obj.Text(Offset%s_%s)
|
|
}
|
|
|
|
`, s.WireName, f.Name, f.Name, s.WireName,
|
|
s.WireName, f.Name, s.GoName,
|
|
s.WireName, f.Name)
|
|
case f.IsVarBytes():
|
|
fmt.Fprintf(w, `// %s%s reads the variable-length %s bytes field of %s.
|
|
//
|
|
// Zero-copy: v1's Object.Bytes returns a sub-slice of the underlying
|
|
// buffer. Valid only while the View's buffer is alive; copy to retain.
|
|
func %s%s(v zapv1.View[%s]) []byte {
|
|
msg := zap.WrapBuffer(v.Bytes())
|
|
obj := msg.RootObjectAt(int(zapv1.RootOff(v)))
|
|
return obj.Bytes(Offset%s_%s)
|
|
}
|
|
|
|
`, s.WireName, f.Name, f.Name, s.WireName,
|
|
s.WireName, f.Name, s.GoName,
|
|
s.WireName, f.Name)
|
|
default:
|
|
n, _ := f.IsBytes()
|
|
fmt.Fprintf(w, `// %s%s reads the %d-byte inline %s field of %s.
|
|
//
|
|
// Zero-copy on the read side (v1's Object.BytesFixedSlice returns
|
|
// a sub-slice of the underlying buffer); the returned [%d]byte is a
|
|
// value copy so the caller's slot owns the bytes after the View's
|
|
// buffer is freed.
|
|
func %s%s(v zapv1.View[%s]) [%d]byte {
|
|
msg := zap.WrapBuffer(v.Bytes())
|
|
obj := msg.RootObjectAt(int(zapv1.RootOff(v)))
|
|
slice := obj.BytesFixedSlice(Offset%s_%s, %d)
|
|
var out [%d]byte
|
|
if len(slice) == %d {
|
|
copy(out[:], slice)
|
|
}
|
|
return out
|
|
}
|
|
|
|
`, s.WireName, f.Name, n, f.Name, s.WireName,
|
|
n,
|
|
s.WireName, f.Name, s.GoName, n,
|
|
s.WireName, f.Name, n,
|
|
n,
|
|
n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// emitListSupport writes, for each list field: the per-element value
|
|
// input struct the constructor accepts, and the typed read accessor that
|
|
// returns a [zapv1.List][E] over the object tail (range-over-func ready,
|
|
// zero-copy). The element schema's own Fields/Wrap live in its emitted
|
|
// file; here we only need its marker GoName and field shape.
|
|
func emitListSupport(w io.Writer, s Schema, list []Field) {
|
|
for _, f := range list {
|
|
elem, _ := f.IsList()
|
|
|
|
// Per-element value-input struct (e.g. `type Item struct{ ID,
|
|
// Value uint64 }`). The constructor takes []<Value>; WriteList
|
|
// builds each element from it.
|
|
fmt.Fprintf(w, "// %s is the per-element input for the %s.%s list.\ntype %s struct {\n",
|
|
elem.Value, s.WireName, f.Name, elem.Value)
|
|
for _, ef := range elem.Fields {
|
|
fmt.Fprintf(w, "\t%s %s\n", ef.Name, nestedFieldGoType(ef))
|
|
}
|
|
fmt.Fprintf(w, "}\n\n")
|
|
|
|
// Read accessor: a typed, range-over-func-ready list view. Tailed
|
|
// elements use the out-of-line ListNested[E]; scalar-only elements
|
|
// use the inline List[E]. Both are zero-copy and range-over-func.
|
|
listT, atFn := "List", "ListAt"
|
|
if elemIsTailed(elem) {
|
|
listT, atFn = "ListNested", "ListNestedAt"
|
|
}
|
|
fmt.Fprintf(w, `// %s%s returns the variable-length %s list of %s as a
|
|
// range-over-func-ready [zapv1.%s][%s]. Zero-copy: each element view
|
|
// indexes straight into the underlying buffer.
|
|
//
|
|
// for it := range %s%s(view).All() {
|
|
// // zapv1.Read(it, %sFields.X)
|
|
// }
|
|
func %s%s(v zapv1.View[%s]) zapv1.%s[%s] {
|
|
return zapv1.%s[%s, %s](v, Offset%s_%s)
|
|
}
|
|
|
|
`, s.WireName, f.Name, f.Name, s.WireName,
|
|
listT, elem.Schema,
|
|
s.WireName, f.Name,
|
|
elem.Schema,
|
|
s.WireName, f.Name, s.GoName, listT, elem.Schema,
|
|
atFn, s.GoName, elem.Schema, s.WireName, f.Name)
|
|
}
|
|
}
|
|
|
|
// emitNestedSupport writes, for each nested-object field: the value-input
|
|
// struct the constructor accepts (by pointer), and the typed read accessor
|
|
// returning a [zapv1.View][N] over the nested object in the tail (zero-copy;
|
|
// the zero View when the field is unset/null). The singular peer of
|
|
// [emitListSupport]. The nested schema's own Fields/Wrap live in its own
|
|
// emitted file; here we need only its marker GoName and field shape.
|
|
func emitNestedSupport(w io.Writer, s Schema, nested []Field) {
|
|
for _, f := range nested {
|
|
nm, _ := f.IsNested()
|
|
|
|
// Per-nested value-input struct (e.g. `type Inner struct{ ID
|
|
// uint64; Label string }`). The constructor takes *<Value>;
|
|
// WriteNested builds the object from it when non-nil.
|
|
fmt.Fprintf(w, "// %s is the value input for the singular %s.%s nested object.\ntype %s struct {\n",
|
|
nm.Value, s.WireName, f.Name, nm.Value)
|
|
for _, ef := range nm.Fields {
|
|
fmt.Fprintf(w, "\t%s %s\n", ef.Name, nestedFieldGoType(ef))
|
|
}
|
|
fmt.Fprintf(w, "}\n\n")
|
|
|
|
// Read accessor: a typed View over the nested object. Test the
|
|
// result with View.IsZero for the unset case.
|
|
fmt.Fprintf(w, `// %s%s returns the singular %s nested object of %s as a
|
|
// zero-copy [zapv1.View][%s]. An unset (null) nested field yields the zero
|
|
// View — test it with [zapv1.View.IsZero].
|
|
func %s%s(v zapv1.View[%s]) zapv1.View[%s] {
|
|
return zapv1.NestedAt[%s, %s](v, Offset%s_%s)
|
|
}
|
|
|
|
`, s.WireName, f.Name, f.Name, s.WireName,
|
|
nm.Schema,
|
|
s.WireName, f.Name, s.GoName, nm.Schema,
|
|
s.GoName, nm.Schema, s.WireName, f.Name)
|
|
}
|
|
}
|
|
|
|
// nestedFieldGoType returns the Go type to use in a value-input struct
|
|
// field: the declared scalar type, "string"/"[]byte" for variable tails,
|
|
// or "[N]byte" for fixed byte arrays.
|
|
func nestedFieldGoType(f Field) string {
|
|
switch {
|
|
case f.IsVarString():
|
|
return "string"
|
|
case f.IsVarBytes():
|
|
return "[]byte"
|
|
default:
|
|
if n, ok := f.IsBytes(); ok {
|
|
return fmt.Sprintf("[%d]byte", n)
|
|
}
|
|
return f.Type
|
|
}
|
|
}
|
|
|
|
// emitRegisterInit writes an init function that registers the schema
|
|
// with the [zapv1.DefaultRegistry]. Skipped when [Schema.SkipRegistry]
|
|
// is true, used for schemas whose Kind byte is local to a per-package
|
|
// (private) registry rather than globally unique.
|
|
func emitRegisterInit(w io.Writer, s Schema) {
|
|
if s.SkipRegistry {
|
|
return
|
|
}
|
|
fmt.Fprintf(w, `func init() {
|
|
zapv1.Register[%s](zapv1.DefaultRegistry)
|
|
}
|
|
`, s.GoName)
|
|
}
|
|
|
|
// lowerFirst returns s with its first character lower-cased. Used to
|
|
// turn an exported field name ("Time") into a parameter name ("time").
|
|
//
|
|
// If the lower-cased result collides with a Go keyword (e.g. "Type" →
|
|
// "type"), the result is suffixed with "_" ("type_") to avoid a parse
|
|
// error in the emitted function signature. Identifier semantics are
|
|
// preserved: the parameter name is local to the constructor, and the
|
|
// schema field name (exported) is untouched.
|
|
func lowerFirst(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
var out string
|
|
if s[0] >= 'A' && s[0] <= 'Z' {
|
|
out = string(s[0]+32) + s[1:]
|
|
} else {
|
|
out = s
|
|
}
|
|
if goKeywords[out] {
|
|
return out + "_"
|
|
}
|
|
return out
|
|
}
|
|
|
|
// goKeywords is the set of reserved identifiers in Go 1.x. Listed from
|
|
// the Go spec §"Keywords" (https://go.dev/ref/spec#Keywords).
|
|
var goKeywords = map[string]bool{
|
|
"break": true,
|
|
"case": true,
|
|
"chan": true,
|
|
"const": true,
|
|
"continue": true,
|
|
"default": true,
|
|
"defer": true,
|
|
"else": true,
|
|
"fallthrough": true,
|
|
"for": true,
|
|
"func": true,
|
|
"go": true,
|
|
"goto": true,
|
|
"if": true,
|
|
"import": true,
|
|
"interface": true,
|
|
"map": true,
|
|
"package": true,
|
|
"range": true,
|
|
"return": true,
|
|
"select": true,
|
|
"struct": true,
|
|
"switch": true,
|
|
"type": true,
|
|
"var": true,
|
|
}
|