Files
zeekay 03451a9a1b The C++ backend on the parser package
The parser moved to idl and the C++ backend was still holding the old one,
so the two lines of work could not both be had. They meet here: emitcpp.go
imports idl like emit.go does, and structSize moves to idl beside the
Struct whose width it reports — a stated `@N` or where the last field ends
is a property of the declaration, not of whoever is printing it.

The grammar tests move with the grammar. Emission tests stay with the
emitter. Every golden, Go and C++, is unchanged: the merge is a move, not
a change of what the generator prints.
2026-09-06 13:15:57 -07:00

925 lines
34 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"bytes"
"fmt"
"github.com/zap-proto/go/idl"
"go/format"
"strings"
)
// Emit emits one .go file per struct AND one per interface in f. Returns a
// map of basename → gofmt-clean bytes, ready to write. The basename is the
// lowercase snake-cased struct/interface name plus "_zap.go" (matches the
// spec convention).
func Emit(f *idl.File) (map[string][]byte, error) {
out := make(map[string][]byte, len(f.Structs)+len(f.Interfaces))
for _, s := range f.Structs {
buf, err := emitStruct(f, s)
if err != nil {
return nil, fmt.Errorf("emit %s: %w", s.Name, err)
}
clean, err := format.Source(buf)
if err != nil {
return nil, fmt.Errorf("gofmt %s: %w\n--- raw ---\n%s\n--- end ---", s.Name, err, buf)
}
out[snakeCase(s.Name)+"_zap.go"] = clean
}
for _, iface := range f.Interfaces {
buf, err := emitInterfaceFile(f, iface)
if err != nil {
return nil, fmt.Errorf("emit %s: %w", iface.Name, err)
}
clean, err := format.Source(buf)
if err != nil {
return nil, fmt.Errorf("gofmt %s: %w\n--- raw ---\n%s\n--- end ---", iface.Name, err, buf)
}
out[snakeCase(iface.Name)+"_zap.go"] = clean
}
return out, nil
}
// EmitSingle emits one combined .go file containing all structs and
// interfaces from f. Returns the basename (derived from the schema source
// filename) and the gofmt-clean bytes. Used by callers that prefer one
// file per schema over one file per struct.
func EmitSingle(f *idl.File) (string, []byte, error) {
if len(f.Structs) == 0 && len(f.Interfaces) == 0 {
return "", nil, fmt.Errorf("no structs or interfaces to emit")
}
source := f.Source
if source == "" {
source = f.Package + ".zap"
}
// Output filename: strip the .zap extension, snake_case, append _zap.go.
base := source
if i := strings.LastIndex(base, "."); i >= 0 {
base = base[:i]
}
outName := base + "_zap.go"
var w bytes.Buffer
writeFileHeader(&w, f, f.Package, source, f.Structs, len(f.Interfaces) > 0)
for _, s := range f.Structs {
if err := validate(f, s); err != nil {
return "", nil, err
}
emitOffsets(&w, s)
emitReader(&w, f, s)
emitBuilder(&w, f, s)
w.WriteString("\n")
}
for _, iface := range f.Interfaces {
if err := validateInterface(f, iface); err != nil {
return "", nil, err
}
emitInterface(&w, iface)
}
clean, err := format.Source(w.Bytes())
if err != nil {
return "", nil, fmt.Errorf("gofmt: %w\n--- raw ---\n%s\n--- end ---", err, w.Bytes())
}
return outName, clean, nil
}
// writeFileHeader writes the generated-file banner, package clause, and
// import block. Only the imports actually used by the emitted code are
// written: structs reference zap; interfaces reference fmt + rpc. A
// combined file with both gets all three. Go rejects unused imports, so
// these flags must match what the body emits.
func writeFileHeader(w *bytes.Buffer, file *idl.File, pkg, source string, structs []*idl.Struct, needsRPC bool) {
n := goNeeds(file, structs)
w.WriteString("// Code generated by zapgen; DO NOT EDIT.\n")
fmt.Fprintf(w, "// source: %s\n\n", source)
// The last segment of the package path: a Go package name is one word.
if i := strings.LastIndex(pkg, "."); i >= 0 {
pkg = pkg[i+1:]
}
fmt.Fprintf(w, "package %s\n\n", pkg)
w.WriteString("import (\n")
if n.binary {
// An inline list element is laid out by hand, in an array.
w.WriteString("\t\"encoding/binary\"\n")
}
if needsRPC {
// The generated Client reports a non-OK status via fmt.Errorf.
w.WriteString("\t\"fmt\"\n")
}
if n.floats {
w.WriteString("\t\"math\"\n")
}
if len(structs) > 0 {
w.WriteString("\n\tzap \"github.com/zap-proto/go\"\n")
}
if needsRPC {
w.WriteString("\t\"github.com/zap-proto/go/rpc\"\n")
}
w.WriteString(")\n\n")
}
// goNeeds is what the emitted Go will reach for beyond the runtime: the
// little-endian writer an inline payload is laid out with, and the float bit
// casts. Naming exactly what the file uses is what keeps the import block
// honest.
type goNeed struct {
binary bool
floats bool
}
func goNeeds(file *idl.File, structs []*idl.Struct) goNeed {
var n goNeed
for _, s := range structs {
inline := !file.Tail(s)
for _, f := range s.Fields {
switch f.Type.Kind {
case idl.KindU16, idl.KindU32, idl.KindU64, idl.KindI16, idl.KindI32, idl.KindI64:
n.binary = n.binary || inline
case idl.KindF32, idl.KindF64:
n.binary = n.binary || inline
n.floats = n.floats || inline
case idl.KindList:
elem := *f.Type.ListElem
if elem.Kind == idl.KindF32 || elem.Kind == idl.KindF64 {
n.floats = true
}
}
}
}
return n
}
func emitStruct(f *idl.File, s *idl.Struct) ([]byte, error) {
if err := validate(f, s); err != nil {
return nil, err
}
var w bytes.Buffer
writeFileHeader(&w, f, f.Package, sourceName(f), []*idl.Struct{s}, false)
emitOffsets(&w, s)
emitReader(&w, f, s)
emitBuilder(&w, f, s)
return w.Bytes(), nil
}
// emitInterfaceFile emits one standalone .go file for a single interface
// (the per-interface output of Emit). It imports rpc for the call envelope.
func emitInterfaceFile(f *idl.File, iface *idl.Interface) ([]byte, error) {
if err := validateInterface(f, iface); err != nil {
return nil, err
}
var w bytes.Buffer
writeFileHeader(&w, f, f.Package, sourceName(f), nil, true)
emitInterface(&w, iface)
return w.Bytes(), nil
}
// sourceName returns the recorded source basename, or a fallback derived
// from the package for callers that build a idl.File directly without idl.Parse.
func sourceName(f *idl.File) string {
if f.Source != "" {
return f.Source
}
return f.Package + ".zap"
}
// validateInterface checks that every method param references a struct
// declared in the same file (method payloads are ZAP structs) and that
// method names are unique within the interface. Fail fast and loud.
func validateInterface(f *idl.File, iface *idl.Interface) error {
if len(iface.Methods) == 0 {
return fmt.Errorf("interface %s: no methods", iface.Name)
}
known := make(map[string]bool, len(f.Structs))
for _, s := range f.Structs {
known[s.Name] = true
}
seen := make(map[string]bool, len(iface.Methods))
for _, m := range iface.Methods {
if seen[m.Name] {
return fmt.Errorf("interface %s: duplicate method %s", iface.Name, m.Name)
}
seen[m.Name] = true
for _, p := range []*idl.Param{m.Request, m.Response} {
if p == nil {
continue
}
if !known[p.StructName] {
return fmt.Errorf("interface %s method %s: unknown struct %q in param %q",
iface.Name, m.Name, p.StructName, p.Name)
}
}
}
return nil
}
// validate checks for offset overlap and bad field types. Fail fast and
// loud — the schema author is responsible for the layout, but a typo
// shouldn't silently emit broken code.
func validate(file *idl.File, s *idl.Struct) error {
size := idl.StructSize(s)
if size == 0 {
return fmt.Errorf("struct %s: no fields", s.Name)
}
if s.Size > 0 {
end := 0
for _, f := range s.Fields {
if e := f.Offset + f.Type.SlotSize(); e > end {
end = e
}
}
if s.Size < end {
return fmt.Errorf("struct %s: stated width %d is narrower than its fields, which end at %d",
s.Name, s.Size, end)
}
}
owner := make([]string, size)
for _, f := range s.Fields {
sz := f.Type.SlotSize()
if sz == 0 {
return fmt.Errorf("struct %s field %s: unsupported type %s", s.Name, f.Name, f.Type.Kind)
}
if err := validateField(file, s, f); err != nil {
return err
}
if f.Offset < 0 {
return fmt.Errorf("struct %s field %s: negative offset %d", s.Name, f.Name, f.Offset)
}
for i := 0; i < sz; i++ {
pos := f.Offset + i
if owner[pos] != "" {
return fmt.Errorf("struct %s: field %s at offset %d overlaps field %s",
s.Name, f.Name, pos, owner[pos])
}
owner[pos] = f.Name
}
}
return nil
}
func emitOffsets(w *bytes.Buffer, s *idl.Struct) {
w.WriteString("const (\n")
lower := lowerFirst(s.Name)
for _, f := range s.Fields {
fmt.Fprintf(w, "\t%s%sOff = %d\n", lower, f.Name, f.Offset)
}
fmt.Fprintf(w, "\t%sSize = %d\n", lower, idl.StructSize(s))
w.WriteString(")\n\n")
}
func emitReader(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
fmt.Fprintf(w, "// %s is a zero-copy view into a ZAP-encoded %s message.\n", s.Name, s.Name)
fmt.Fprintf(w, "type %s struct{ o zap.Object }\n\n", s.Name)
fmt.Fprintf(w, "// Wrap%s parses b and returns a typed view. Returns an error if the\n", s.Name)
w.WriteString("// wire-level checks (magic, version, size) fail.\n")
fmt.Fprintf(w, "func Wrap%s(b []byte) (%s, error) {\n", s.Name, s.Name)
w.WriteString("\tm, err := zap.Parse(b)\n")
w.WriteString("\tif err != nil {\n")
fmt.Fprintf(w, "\t\treturn %s{}, err\n", s.Name)
w.WriteString("\t}\n")
fmt.Fprintf(w, "\treturn %s{o: m.Root()}, nil\n", s.Name)
w.WriteString("}\n\n")
lower := lowerFirst(s.Name)
for _, f := range s.Fields {
emitFieldReader(w, file, s.Name, lower, f)
}
}
func emitFieldReader(w *bytes.Buffer, file *idl.File, structName, lower string, f *idl.Field) {
offsetConst := fmt.Sprintf("%s%sOff", lower, f.Name)
switch f.Type.Kind {
case idl.KindBool:
fmt.Fprintf(w, "func (t %s) %s() bool { return t.o.Bool(%s) }\n", structName, f.Name, offsetConst)
case idl.KindU8:
fmt.Fprintf(w, "func (t %s) %s() uint8 { return t.o.Uint8(%s) }\n", structName, f.Name, offsetConst)
case idl.KindU16:
fmt.Fprintf(w, "func (t %s) %s() uint16 { return t.o.Uint16(%s) }\n", structName, f.Name, offsetConst)
case idl.KindU32:
fmt.Fprintf(w, "func (t %s) %s() uint32 { return t.o.Uint32(%s) }\n", structName, f.Name, offsetConst)
case idl.KindU64:
fmt.Fprintf(w, "func (t %s) %s() uint64 { return t.o.Uint64(%s) }\n", structName, f.Name, offsetConst)
case idl.KindI8:
fmt.Fprintf(w, "func (t %s) %s() int8 { return t.o.Int8(%s) }\n", structName, f.Name, offsetConst)
case idl.KindI16:
fmt.Fprintf(w, "func (t %s) %s() int16 { return t.o.Int16(%s) }\n", structName, f.Name, offsetConst)
case idl.KindI32:
fmt.Fprintf(w, "func (t %s) %s() int32 { return t.o.Int32(%s) }\n", structName, f.Name, offsetConst)
case idl.KindI64:
fmt.Fprintf(w, "func (t %s) %s() int64 { return t.o.Int64(%s) }\n", structName, f.Name, offsetConst)
case idl.KindF32:
fmt.Fprintf(w, "func (t %s) %s() float32 { return t.o.Float32(%s) }\n", structName, f.Name, offsetConst)
case idl.KindF64:
fmt.Fprintf(w, "func (t %s) %s() float64 { return t.o.Float64(%s) }\n", structName, f.Name, offsetConst)
case idl.KindText:
fmt.Fprintf(w, "func (t %s) %s() string { return t.o.Text(%s) }\n", structName, f.Name, offsetConst)
case idl.KindBytes:
fmt.Fprintf(w, "func (t %s) %s() []byte { return t.o.Bytes(%s) }\n", structName, f.Name, offsetConst)
case idl.KindBytesFixed:
fmt.Fprintf(w, "func (t %s) %s() [%d]byte {\n", structName, f.Name, f.Type.FixedSize)
fmt.Fprintf(w, "\tvar out [%d]byte\n", f.Type.FixedSize)
fmt.Fprintf(w, "\tcopy(out[:], t.o.BytesFixed(%s, %d))\n", offsetConst, f.Type.FixedSize)
w.WriteString("\treturn out\n")
w.WriteString("}\n")
case idl.KindList:
// ListStride, not List: the schema knows the element width, so the
// reader gets the clamp for free — length * stride has to fit what is
// left of the buffer, and a lying length word dies once here instead
// of at every element.
elem := *f.Type.ListElem
fmt.Fprintf(w, "func (t %s) %s() zap.List { return t.o.ListStride(%s, %d) }\n",
structName, f.Name, offsetConst, file.Stride(elem))
emitElement(w, file, structName, f, elem)
case idl.KindStruct:
fmt.Fprintf(w, "func (t %s) %s() %s { return %s{o: t.o.Object(%s)} }\n",
structName, f.Name, f.Type.StructName, f.Type.StructName, offsetConst)
}
}
// --- builders ---------------------------------------------------------------
//
// Three entry points per struct, because the wire has three places a struct
// can sit: a message of its own (New), one object inside a message somebody
// else is writing (Append), and one element of an inline run (Encode). Encode
// exists only for a struct with no tail — a struct with a tail has nowhere to
// put it between two neighbours in a run.
//
// Every payload a struct points at is written BEFORE the struct's own object,
// so its pointer fields are set from offsets that already exist. That is the
// order the Lux reference writes, and writing the object first would move
// every byte after it.
func emitBuilder(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
emitInput(w, file, s)
if !file.Tail(s) {
emitEncode(w, s)
}
emitAppend(w, file, s)
emitNew(w, file, s)
}
func emitInput(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
fmt.Fprintf(w, "\n// %sInput collects the field values for New%s.\n", s.Name, s.Name)
fmt.Fprintf(w, "type %sInput struct {\n", s.Name)
for _, f := range s.Fields {
fmt.Fprintf(w, "\t%s %s\n", f.Name, goInputType(f.Type))
}
w.WriteString("}\n\n")
}
// emitEncode writes one inline list element: the fixed payload alone,
// unaligned, exactly as wide as the stride.
func emitEncode(w *bytes.Buffer, s *idl.Struct) {
lower := lowerFirst(s.Name)
fmt.Fprintf(w, "// Encode%s lays one %s payload out as an inline list element.\n", s.Name, s.Name)
fmt.Fprintf(w, "func Encode%s(in %sInput) [%sSize]byte {\n", s.Name, s.Name, lower)
fmt.Fprintf(w, "\tvar e [%sSize]byte\n", lower)
for _, f := range s.Fields {
emitEncodeField(w, lower, f)
}
w.WriteString("\treturn e\n}\n\n")
}
func emitEncodeField(w *bytes.Buffer, lower string, f *idl.Field) {
off := fmt.Sprintf("%s%sOff", lower, f.Name)
switch f.Type.Kind {
case idl.KindBool:
fmt.Fprintf(w, "\tif in.%s {\n\t\te[%s] = 1\n\t}\n", f.Name, off)
case idl.KindU8:
fmt.Fprintf(w, "\te[%s] = in.%s\n", off, f.Name)
case idl.KindI8:
fmt.Fprintf(w, "\te[%s] = uint8(in.%s)\n", off, f.Name)
case idl.KindU16:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint16(e[%s:], in.%s)\n", off, f.Name)
case idl.KindI16:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint16(e[%s:], uint16(in.%s))\n", off, f.Name)
case idl.KindU32:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint32(e[%s:], in.%s)\n", off, f.Name)
case idl.KindI32:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint32(e[%s:], uint32(in.%s))\n", off, f.Name)
case idl.KindF32:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint32(e[%s:], math.Float32bits(in.%s))\n", off, f.Name)
case idl.KindU64:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint64(e[%s:], in.%s)\n", off, f.Name)
case idl.KindI64:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint64(e[%s:], uint64(in.%s))\n", off, f.Name)
case idl.KindF64:
fmt.Fprintf(w, "\tbinary.LittleEndian.PutUint64(e[%s:], math.Float64bits(in.%s))\n", off, f.Name)
case idl.KindBytesFixed:
fmt.Fprintf(w, "\tcopy(e[%s:], in.%s[:])\n", off, f.Name)
}
}
func emitAppend(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
fmt.Fprintf(w, "// Append%s writes one %s object into b — every payload it points at first,\n", s.Name, s.Name)
w.WriteString("// then the object itself — and returns the object's offset.\n")
fmt.Fprintf(w, "func Append%s(b *zap.Builder, in %sInput) int {\n", s.Name, s.Name)
emitBody(w, file, s)
w.WriteString("\treturn ob.Finish()\n}\n\n")
}
func emitNew(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
fmt.Fprintf(w, "// New%s builds a ZAP-encoded %s message from in and returns the bytes.\n", s.Name, s.Name)
fmt.Fprintf(w, "func New%s(in %sInput) []byte {\n", s.Name, s.Name)
// Version 2 in both languages: it is the header the Lux chains carry and
// the one the hardened runtime writes by default. NewBuilder here still
// defaults to version 1, so the generated builder says which it means.
w.WriteString("\tb := zap.NewBuilderV2(256)\n")
emitBody(w, file, s)
w.WriteString("\tob.FinishAsRoot()\n")
w.WriteString("\treturn b.Finish()\n}\n")
}
func emitBody(w *bytes.Buffer, file *idl.File, s *idl.Struct) {
lower := lowerFirst(s.Name)
// Objects first, runs second, each in field order. That is the order the
// Lux reference writes in — every element of every pointer list, then the
// pointer runs over them — and the order decides the bytes.
for _, f := range s.Fields {
emitPayload(w, file, lower, f, payloadObjects)
}
for _, f := range s.Fields {
emitPayload(w, file, lower, f, payloadRuns)
}
fmt.Fprintf(w, "\tob := b.StartObject(%sSize)\n", lower)
for _, f := range s.Fields {
emitFieldWriter(w, lower, f)
}
}
func payloadVar(f *idl.Field) string { return lowerFirst(f.Name) + "Off" }
// emitPayload writes whatever a field points at, before the object exists. An
// empty list writes nothing at all: the reference leaves the pointer pair null
// and does not align, and an alignment pad nobody asked for would move every
// byte after it.
func emitPayload(w *bytes.Buffer, file *idl.File, lower string, f *idl.Field, p pass) {
switch f.Type.Kind {
case idl.KindStruct:
if p != payloadObjects {
return
}
v := payloadVar(f)
fmt.Fprintf(w, "\t%s := 0\n", v)
fmt.Fprintf(w, "\tif in.%s != nil {\n", f.Name)
fmt.Fprintf(w, "\t\t%s = Append%s(b, *in.%s)\n", v, f.Type.StructName, f.Name)
w.WriteString("\t}\n")
case idl.KindList:
elem := *f.Type.ListElem
v := payloadVar(f)
stride := file.Stride(elem)
if file.Shape(elem) == idl.ShapePointer {
ptrs := lowerFirst(f.Name) + "Ptrs"
if p == payloadObjects {
fmt.Fprintf(w, "\t%s := make([]int, 0, len(in.%s))\n", ptrs, f.Name)
fmt.Fprintf(w, "\tfor _, elem := range in.%s {\n", f.Name)
fmt.Fprintf(w, "\t\t%s = append(%s, Append%s(b, elem))\n", ptrs, ptrs, elem.StructName)
w.WriteString("\t}\n")
return
}
fmt.Fprintf(w, "\t%s := 0\n", v)
fmt.Fprintf(w, "\tif len(in.%s) > 0 {\n", f.Name)
fmt.Fprintf(w, "\t\tlb := b.StartList(%d)\n", stride)
fmt.Fprintf(w, "\t\tfor _, at := range %s {\n\t\t\tlb.AddObjectPtr(at)\n\t\t}\n", ptrs)
fmt.Fprintf(w, "\t\t%s = lb.FinishOffset()\n", v)
w.WriteString("\t}\n")
return
}
if p != payloadRuns {
return
}
fmt.Fprintf(w, "\t%s := 0\n", v)
fmt.Fprintf(w, "\tif len(in.%s) > 0 {\n", f.Name)
fmt.Fprintf(w, "\t\tlb := b.StartList(%d)\n", stride)
fmt.Fprintf(w, "\t\tfor _, elem := range in.%s {\n", f.Name)
switch file.Shape(elem) {
case idl.ShapeFixed:
w.WriteString("\t\t\tlb.AddBytes(elem[:])\n")
case idl.ShapeInline:
fmt.Fprintf(w, "\t\t\tpayload := Encode%s(elem)\n\t\t\tlb.AddBytes(payload[:])\n", elem.StructName)
case idl.ShapeNumber:
if elem.Kind == idl.KindBool {
w.WriteString("\t\t\tvar v uint8\n\t\t\tif elem {\n\t\t\t\tv = 1\n\t\t\t}\n\t\t\tlb.AddUint8(v)\n")
break
}
fmt.Fprintf(w, "\t\t\tlb.%s(%s)\n", goListAdd(elem), goListValue(elem))
}
w.WriteString("\t\t}\n")
fmt.Fprintf(w, "\t\t%s = lb.FinishOffset()\n", v)
w.WriteString("\t}\n")
}
}
func goListAdd(elem idl.Type) string {
switch elem.SlotSize() {
case 1:
return "AddUint8"
case 4:
return "AddUint32"
case 8:
return "AddUint64"
}
return "AddUint32"
}
func goListValue(elem idl.Type) string {
switch elem.Kind {
case idl.KindI8:
return "uint8(elem)"
case idl.KindI32:
return "uint32(elem)"
case idl.KindI64:
return "uint64(elem)"
case idl.KindF32:
return "math.Float32bits(elem)"
case idl.KindF64:
return "math.Float64bits(elem)"
}
return "elem"
}
// goInputType returns the Go type used in the Input struct: a value for a
// scalar, a fixed array for bytes_fixed[N], a slice for a byte run the caller
// already holds, and the nested type's own Input wherever the wire holds a
// struct — so a whole tree is one value and the caller never hands over
// pre-built bytes it cannot check.
func goInputType(t idl.Type) string {
switch t.Kind {
case idl.KindBool:
return "bool"
case idl.KindU8:
return "uint8"
case idl.KindU16:
return "uint16"
case idl.KindU32:
return "uint32"
case idl.KindU64:
return "uint64"
case idl.KindI8:
return "int8"
case idl.KindI16:
return "int16"
case idl.KindI32:
return "int32"
case idl.KindI64:
return "int64"
case idl.KindF32:
return "float32"
case idl.KindF64:
return "float64"
case idl.KindText:
return "string"
case idl.KindBytes:
return "[]byte"
case idl.KindBytesFixed:
return fmt.Sprintf("[%d]byte", t.FixedSize)
case idl.KindList:
// A list element is held by value. Only a nested FIELD is a pointer,
// and only because a field can be absent; an absent list element is
// just a shorter list.
if t.ListElem.Kind == idl.KindStruct {
return "[]" + t.ListElem.StructName + "Input"
}
return "[]" + goInputType(*t.ListElem)
case idl.KindStruct:
return "*" + t.StructName + "Input"
}
return "any"
}
func emitFieldWriter(w *bytes.Buffer, lower string, f *idl.Field) {
offsetConst := fmt.Sprintf("%s%sOff", lower, f.Name)
switch f.Type.Kind {
case idl.KindBool:
fmt.Fprintf(w, "\tob.SetBool(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindU8:
fmt.Fprintf(w, "\tob.SetUint8(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindU16:
fmt.Fprintf(w, "\tob.SetUint16(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindU32:
fmt.Fprintf(w, "\tob.SetUint32(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindU64:
fmt.Fprintf(w, "\tob.SetUint64(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindI8:
fmt.Fprintf(w, "\tob.SetUint8(%s, uint8(in.%s))\n", offsetConst, f.Name)
case idl.KindI16:
fmt.Fprintf(w, "\tob.SetUint16(%s, uint16(in.%s))\n", offsetConst, f.Name)
case idl.KindI32:
fmt.Fprintf(w, "\tob.SetUint32(%s, uint32(in.%s))\n", offsetConst, f.Name)
case idl.KindI64:
fmt.Fprintf(w, "\tob.SetUint64(%s, uint64(in.%s))\n", offsetConst, f.Name)
case idl.KindF32:
fmt.Fprintf(w, "\tob.SetFloat32(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindF64:
fmt.Fprintf(w, "\tob.SetFloat64(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindText:
fmt.Fprintf(w, "\tob.SetText(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindBytes:
fmt.Fprintf(w, "\tob.SetBytes(%s, in.%s)\n", offsetConst, f.Name)
case idl.KindBytesFixed:
fmt.Fprintf(w, "\tob.SetBytesFixed(%s, in.%s[:])\n", offsetConst, f.Name)
case idl.KindStruct:
fmt.Fprintf(w, "\tob.SetObject(%s, %s)\n", offsetConst, payloadVar(f))
case idl.KindList:
fmt.Fprintf(w, "\tob.SetList(%s, %s, len(in.%s))\n", offsetConst, payloadVar(f), f.Name)
}
}
// --- service emission (interface → ordinal table + Client + Server) ---------
// emitInterface emits, for one interface:
// - a method-ordinal const table (<Iface><idl.Method>Ordinal)
// - a Channel interface (the transport seam the Client ships calls over)
// - a typed Client (one method per declared method)
// - a Handler interface (the abstract server contract) plus a
// Dispatch<Iface> function that decodes the ordinal and routes to it.
//
// Ordinals are 1, 2, 3, … by declaration order, baked from the AST so the
// wire ordinal never shifts when a method is appended. The wire envelope
// is the runtime rpc package, shared with every other language runtime.
func emitInterface(w *bytes.Buffer, iface *idl.Interface) {
emitOrdinals(w, iface)
emitClient(w, iface)
emitServer(w, iface)
}
func emitOrdinals(w *bytes.Buffer, iface *idl.Interface) {
fmt.Fprintf(w, "// Method ordinals for the %s service (stable 1-based wire ids).\n", iface.Name)
w.WriteString("const (\n")
for _, m := range iface.Methods {
fmt.Fprintf(w, "\t%s uint32 = %d\n", ordinalName(iface, m), m.Ordinal)
}
w.WriteString(")\n\n")
}
// ordinalName is the exported const holding a method's wire ordinal,
// e.g. interface Echo + method ping -> EchoPingOrdinal.
func ordinalName(iface *idl.Interface, m *idl.Method) string {
return iface.Name + exportIdent(m.Name) + "Ordinal"
}
// exportIdent upper-cases the first letter of a (possibly lowercase)
// schema method name so the generated Go identifier is exported. Schema
// method names are written lowercase (`ping`) per the spec; the generated
// client/handler methods and ordinal consts must be exported so consumers
// in other packages can reach them.
func exportIdent(s string) string {
if s == "" {
return s
}
r := []rune(s)
if r[0] >= 'a' && r[0] <= 'z' {
r[0] -= 'a' - 'A'
}
return string(r)
}
func emitClient(w *bytes.Buffer, iface *idl.Interface) {
// Channel: the seam the Client ships an envelope over and awaits a
// correlated response. A transport (e.g. a TCP RPC connection) supplies it.
fmt.Fprintf(w, "// %sChannel ships one Call envelope and awaits its correlated Response.\n", iface.Name)
fmt.Fprintf(w, "type %sChannel interface {\n", iface.Name)
w.WriteString("\tCall(envelope []byte) (rpc.Response, error)\n")
w.WriteString("}\n\n")
// Client.
fmt.Fprintf(w, "// %sClient is a typed RPC client for the %s service over a ZAP call channel.\n", iface.Name, iface.Name)
fmt.Fprintf(w, "// Each call takes a fresh PromiseID from sess; the pipelined \"On\" form of a\n")
fmt.Fprintf(w, "// method sets Target to a prior call's Promise so the server chains them.\n")
fmt.Fprintf(w, "type %sClient struct {\n", iface.Name)
fmt.Fprintf(w, "\tch %sChannel\n", iface.Name)
w.WriteString("\tcap []byte\n")
w.WriteString("\tsess *rpc.Session\n")
w.WriteString("}\n\n")
fmt.Fprintf(w, "// New%sClient returns a client that issues calls over ch, attaching cap\n", iface.Name)
w.WriteString("// (which may be nil) to every request.\n")
fmt.Fprintf(w, "func New%sClient(ch %sChannel, capability []byte) *%sClient {\n", iface.Name, iface.Name, iface.Name)
fmt.Fprintf(w, "\treturn &%sClient{ch: ch, cap: capability, sess: rpc.NewSession()}\n", iface.Name)
w.WriteString("}\n\n")
for _, m := range iface.Methods {
emitClientMethod(w, iface, m)
}
}
// emitClientMethod emits two forms per method, both over one shared
// envelope path (no duplicated wire construction):
//
// - M(req) — the originating call: a fresh PromiseID, Target = NoTarget.
// - MOn(on rpc.Promise, req) — the dependent call: Target = on.ID, so the
// server substitutes on's resolved answer for this call's payload before
// dispatch (promise pipelining).
//
// Both return the call's own rpc.Promise (so a chain of length > 2 can target
// the previous link) alongside the method's natural result.
func emitClientMethod(w *bytes.Buffer, iface *idl.Interface, m *idl.Method) {
mname := exportIdent(m.Name)
arg, payload := methodArg(m)
// Result tuple: the originating Promise is always returned first so calls
// chain; then ([]byte,)error per the method's response shape.
var ret, okReturn, errReturn string
if m.Response != nil {
ret = "(rpc.Promise, []byte, error)"
okReturn = "\treturn p, resp.Body, nil\n"
errReturn = "\t\treturn p, nil, "
} else {
ret = "(rpc.Promise, error)"
okReturn = "\treturn p, nil\n"
errReturn = "\t\treturn p, "
}
// Plain form: originating call (Target = NoTarget, own payload).
plainArgs := arg
fmt.Fprintf(w, "func (c *%sClient) %s(%s) %s {\n", iface.Name, mname, plainArgs, ret)
fmt.Fprintf(w, "\treturn c.invoke%s(rpc.NoTarget, %s)\n", mname, payload)
w.WriteString("}\n\n")
// Pipelined form: dependent call (Target = on.ID). Its payload is supplied
// server-side by substituting on's resolved answer, so this form takes NO
// request param — the upstream result IS the input.
fmt.Fprintf(w, "// %sOn issues %s as a dependent call pipelined on the answer of on:\n", mname, mname)
fmt.Fprintf(w, "// the server substitutes on's resolved result for this call's payload\n")
w.WriteString("// before dispatch, so it ships without waiting for on to round-trip.\n")
fmt.Fprintf(w, "func (c *%sClient) %sOn(on rpc.Promise) %s {\n", iface.Name, mname, ret)
fmt.Fprintf(w, "\treturn c.invoke%s(on.ID, nil)\n", mname)
w.WriteString("}\n\n")
// Shared envelope path: one place that builds+ships the request. The
// payload is nil for a dependent call (target != NoTarget) since the
// server substitutes the resolved answer.
fmt.Fprintf(w, "func (c *%sClient) invoke%s(target uint32, payload []byte) %s {\n", iface.Name, mname, ret)
w.WriteString("\tp := c.sess.Next()\n")
w.WriteString("\tresp, err := c.ch.Call(rpc.BuildRequest(rpc.Call{\n")
fmt.Fprintf(w, "\t\tMethod: %s,\n", ordinalName(iface, m))
w.WriteString("\t\tPromiseID: p.ID,\n")
w.WriteString("\t\tTarget: target,\n")
w.WriteString("\t\tCap: c.cap,\n")
w.WriteString("\t\tPayload: payload,\n")
w.WriteString("\t}))\n")
w.WriteString("\tif err != nil {\n")
if m.Response != nil {
w.WriteString("\t\treturn p, nil, err\n")
} else {
w.WriteString("\t\treturn p, err\n")
}
w.WriteString("\t}\n")
w.WriteString("\tif resp.Status != rpc.StatusOK {\n")
fmt.Fprintf(w, "%sfmt.Errorf(\"%s.%s: status %%d\", resp.Status)\n", errReturn, iface.Name, mname)
w.WriteString("\t}\n")
w.WriteString(okReturn)
w.WriteString("}\n\n")
}
// methodArg returns the Go parameter list and the payload expression for a
// method's request: ("req []byte", "req") when it has a request param, or
// ("", "nil") for a void request.
func methodArg(m *idl.Method) (arg, payload string) {
if m.Request == nil {
return "", "nil"
}
return m.Request.Name + " []byte", m.Request.Name
}
func emitServer(w *bytes.Buffer, iface *idl.Interface) {
// Handler: the abstract server contract — one method per RPC method.
fmt.Fprintf(w, "// %sHandler is the server contract for the %s service. Implement each\n", iface.Name, iface.Name)
fmt.Fprintf(w, "// method, then route requests to it with Dispatch%s.\n", iface.Name)
fmt.Fprintf(w, "type %sHandler interface {\n", iface.Name)
for _, m := range iface.Methods {
arg, _ := methodArg(m)
ret := "error"
if m.Response != nil {
ret = "([]byte, error)"
}
fmt.Fprintf(w, "\t%s(%s) %s\n", exportIdent(m.Name), arg, ret)
}
w.WriteString("}\n\n")
// Dispatch: decode the request envelope, route by ordinal, build the
// response envelope.
fmt.Fprintf(w, "// Dispatch%s decodes a Call envelope, routes it by method ordinal to h,\n", iface.Name)
w.WriteString("// and returns the response envelope. An unknown ordinal yields a\n")
w.WriteString("// StatusNotFound response; a handler error yields StatusInternal.\n")
fmt.Fprintf(w, "func Dispatch%s(h %sHandler, envelope []byte) ([]byte, error) {\n", iface.Name, iface.Name)
w.WriteString("\tcall, err := rpc.ParseRequest(envelope)\n")
w.WriteString("\tif err != nil {\n\t\treturn nil, err\n\t}\n")
w.WriteString("\tswitch call.Method {\n")
for _, m := range iface.Methods {
fmt.Fprintf(w, "\tcase %s:\n", ordinalName(iface, m))
emitDispatchCase(w, m)
}
w.WriteString("\tdefault:\n")
w.WriteString("\t\treturn rpc.BuildResponse(rpc.StatusNotFound, call.PromiseID, nil), nil\n")
w.WriteString("\t}\n")
w.WriteString("}\n\n")
}
// emitDispatchCase emits the body of one ordinal case in Dispatch<Iface>.
func emitDispatchCase(w *bytes.Buffer, m *idl.Method) {
mname := exportIdent(m.Name)
callArg := ""
if m.Request != nil {
callArg = "call.Payload"
}
if m.Response != nil {
fmt.Fprintf(w, "\t\tbody, err := h.%s(%s)\n", mname, callArg)
w.WriteString("\t\tif err != nil {\n")
w.WriteString("\t\t\treturn rpc.BuildResponse(rpc.StatusInternal, call.PromiseID, nil), nil\n")
w.WriteString("\t\t}\n")
w.WriteString("\t\treturn rpc.BuildResponse(rpc.StatusOK, call.PromiseID, body), nil\n")
} else {
fmt.Fprintf(w, "\t\tif err := h.%s(%s); err != nil {\n", mname, callArg)
w.WriteString("\t\t\treturn rpc.BuildResponse(rpc.StatusInternal, call.PromiseID, nil), nil\n")
w.WriteString("\t\t}\n")
w.WriteString("\t\treturn rpc.BuildResponse(rpc.StatusOK, call.PromiseID, nil), nil\n")
}
}
// snakeCase converts CamelCase / PascalCase to snake_case. ASCII only —
// our schema identifiers are Go identifiers.
func snakeCase(s string) string {
var b strings.Builder
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
b.WriteByte('_')
}
if r >= 'A' && r <= 'Z' {
b.WriteRune(r + ('a' - 'A'))
} else {
b.WriteRune(r)
}
}
return b.String()
}
// lowerFirst returns s with the first byte lowercased (ASCII). Used to
// build "baseTxFooOff" constants from struct name "BaseTx" + field
// "Foo".
func lowerFirst(s string) string {
if s == "" {
return s
}
r := []rune(s)
if r[0] >= 'A' && r[0] <= 'Z' {
r[0] = r[0] + ('a' - 'A')
}
return string(r)
}
// validateField checks the things a field's TYPE has to satisfy before any
// backend can name a wire shape for it: a struct must exist, and a list
// element must be something the wire has a run of. bytes, text and a list of
// lists have no stride, so a list of them has no encoding — saying so here
// is what keeps every backend from inventing one.
func validateField(file *idl.File, s *idl.Struct, f *idl.Field) error {
switch f.Type.Kind {
case idl.KindStruct:
if file.Struct(f.Type.StructName) == nil {
return fmt.Errorf("struct %s field %s: unknown struct %q",
s.Name, f.Name, f.Type.StructName)
}
case idl.KindList:
elem := f.Type.ListElem
if elem == nil {
return fmt.Errorf("struct %s field %s: list with no element type", s.Name, f.Name)
}
switch elem.Kind {
case idl.KindBytes, idl.KindText, idl.KindList:
return fmt.Errorf("struct %s field %s: list<%s> has no element stride",
s.Name, f.Name, elem.Kind)
case idl.KindStruct:
if file.Struct(elem.StructName) == nil {
return fmt.Errorf("struct %s field %s: unknown struct %q",
s.Name, f.Name, elem.StructName)
}
}
}
return nil
}
// emitElement gives a list its typed element accessor. A run of numbers
// already has one on zap.List, so only the two struct shapes and the fixed
// byte run need naming here — and naming them is what keeps the stride out of
// the caller's hands.
func emitElement(w *bytes.Buffer, file *idl.File, structName string, f *idl.Field, elem idl.Type) {
switch file.Shape(elem) {
case idl.ShapeFixed:
fmt.Fprintf(w, "func (t %s) %sAt(i int) [%d]byte {\n", structName, f.Name, elem.FixedSize)
fmt.Fprintf(w, "\tvar out [%d]byte\n", elem.FixedSize)
fmt.Fprintf(w, "\tcopy(out[:], t.%s().Object(i, %d).BytesFixed(0, %d))\n",
f.Name, elem.FixedSize, elem.FixedSize)
w.WriteString("\treturn out\n}\n")
case idl.ShapeInline:
fmt.Fprintf(w, "func (t %s) %sAt(i int) %s { return %s{o: t.%s().Object(i, %d)} }\n",
structName, f.Name, elem.StructName, elem.StructName, f.Name, file.Stride(elem))
case idl.ShapePointer:
fmt.Fprintf(w, "func (t %s) %sAt(i int) %s { return %s{o: t.%s().ObjectPtr(i)} }\n",
structName, f.Name, elem.StructName, elem.StructName, f.Name)
}
}