mirror of
https://github.com/zap-proto/go.git
synced 2026-10-30 15:05:00 +00:00
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.
925 lines
34 KiB
Go
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)
|
|
}
|
|
}
|