Call the generator Layouts, not Codecs

A codec is a runtime encode/decode step, which is the thing ZAP removes: a
field IS its offset, and a read is a pointer into the bytes that arrived.
Nothing here runs on the served path — Layouts reflects at BUILD time and emits
constant offsets. Naming it for the layer it exists to delete told readers the
opposite of what it does.

LayoutOf already names one type's shape; this emits those as source.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-10 17:34:08 -07:00
co-authored by Hanzo Dev
parent 1175f459b7
commit cda55906cf
16 changed files with 85 additions and 80 deletions
+2
View File
@@ -0,0 +1,2 @@
Co-authored-by: Hanzo Dev <[email protected]>
+2 -2
View File
@@ -553,7 +553,7 @@ and points `ZIP_RUNTIME_DIR` at it.
## Stating a type's wire — `codec.go` (v1.36.26)
`zip.Codecs(roots...)` renders the `MarshalZAP`/`UnmarshalZAP` pair for each
`zip.Layouts(roots...)` renders the `MarshalZAP`/`UnmarshalZAP` pair for each
root and for every struct nested below it, grouped by the package that owns the
type. `LayoutOf`'s doc named three readers of the one derivation — the plane
encodes against it, a `.zap` schema states it, a generator emits it as constants
@@ -587,7 +587,7 @@ block-local for the same reason. The assertion is what makes a deleted codec a
build failure rather than a silent return to reflection.
**The wire does not move**, and that is held, not asserted:
`internal/codec.TestTheCodecKeepsTheWire` requires the generated bytes to equal
`internal/codec.TestTheLayoutKeepsTheWire` requires the generated bytes to equal
`zapenc.Marshal`'s byte for byte over a fixture carrying one field of every form.
That is what lets a codec roll out one pod at a time. `TestTheCodecIsCheckedIn`
regenerates and diffs, so a type that moved without a regeneration is a failing
+1 -1
View File
@@ -68,7 +68,7 @@ const CallPath = "/.well-known/zip/op/"
// CallContentType marks a body on the op-call plane. It names ZAP explicitly so
// a request that arrives with anything else is a caller that has not been
// updated, rather than a body silently read under the wrong codec.
// updated, rather than a body silently read under the wrong layout.
const CallContentType = "application/zap"
// callFault is how a refusal crosses the plane: the status the op chose, its
+1 -1
View File
@@ -191,7 +191,7 @@ func TestCall_WireIsZAPFramesNotHTTP(t *testing.T) {
req.URI().SetPath(zip.CallPath + "flags_bool")
// The body is ZAP, because this plane carries nothing else — a JSON body
// here is a caller that has not been updated, and is refused rather than
// read under the wrong codec.
// read under the wrong layout.
inBody, err := zapenc.Marshal(&boolIn{Flag: "beta"})
if err != nil {
t.Fatalf("marshal ZAP body: %v", err)
+4 -5
View File
@@ -27,10 +27,10 @@ func crossApp() *zip.App {
zip.Describe("POST /v1/account", zip.Doc{
Description: "GetAccount retrieves account details by ID.\n\nReturns the balance and active status.",
Fields: map[string]string{
"AccountIn.id": "Unique account identifier",
"AccountOut.id": "Account identifier",
"AccountOut.balance": "Current balance in cents",
"AccountOut.active": "Whether account is currently active",
"AccountIn.id": "Unique account identifier",
"AccountOut.id": "Account identifier",
"AccountOut.balance": "Current balance in cents",
"AccountOut.active": "Whether account is currently active",
},
Example: []byte(`{"id": "acc_1"}`),
Response: []byte(`{"id": "acc_1", "balance": 1000, "active": true}`),
@@ -205,4 +205,3 @@ func TestCppCLI_GeneratesCppCLIBindings(t *testing.T) {
t.Errorf("C++ CLI missing run_cli dispatcher")
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ type optionalID struct {
}
func TestAnOptionalIDIsReadBack(t *testing.T) {
written, err := Codecs(reflect.TypeOf(optionalID{}))
written, err := Layouts(reflect.TypeOf(optionalID{}))
if err != nil {
t.Fatal(err)
}
@@ -1,4 +1,4 @@
package codec
package layout
import (
"bytes"
@@ -93,14 +93,14 @@ func trunks() []Trunk {
Text: "hello", Raw: []byte{1, 2, 3},
Leaf: Leaf{N: 1, S: "leaf"},
PtrU: &u, PtrS: &s, PtrB: &b, PtrL: &Leaf{N: 2, S: "ptr"},
Nums: []uint32{1, 2, 3},
Sigs: []int32{-1, 0, 1},
Bigs: []float64{1.5, -2.5},
Strs: []string{"a", "bb"},
Bufs: [][]byte{{9}, {8, 7}},
Bits: []bool{true, false, true},
Kids: []Leaf{{N: 3, S: "x"}, {N: 4, S: "y"}},
Ptrs: []*Leaf{{N: 5, S: "p"}, {N: 6, S: "q"}},
Nums: []uint32{1, 2, 3},
Sigs: []int32{-1, 0, 1},
Bigs: []float64{1.5, -2.5},
Strs: []string{"a", "bb"},
Bufs: [][]byte{{9}, {8, 7}},
Bits: []bool{true, false, true},
Kids: []Leaf{{N: 3, S: "x"}, {N: 4, S: "y"}},
Ptrs: []*Leaf{{N: 5, S: "p"}, {N: 6, S: "q"}},
Seals: []Sealed{{}, {}},
PKids: &[]Leaf{{N: 7, S: "pk"}},
PNums: &[]uint32{11, 12},
@@ -1,4 +1,4 @@
// Package codec holds the types that [zip.Codecs] is proved against.
// Package layout holds the types that [zip.Layouts] is proved against.
//
// The proof it exists for is byte equality: for a type the REFLECTIVE encoder
// can write, the generated codec must write the same bytes and read back the
@@ -10,7 +10,7 @@
// The fixtures cover every slot form the emitter has a branch for, plus the one
// the reflective encoder refuses outright: bytes_fixed[N], where there are no
// bytes to compare against because there are none at all.
package codec
package layout
// Count is a defined scalar, which needs a conversion on the way in and out.
type Count uint64
@@ -31,33 +31,33 @@ type Sealed struct {
// Trunk carries one field of every form the emitter writes.
type Trunk struct {
B bool
I8 int8
I16 int16
I32 int32
I64 int64
U8 uint8
U16 uint16
U32 uint32
U64 uint64
F32 float32
F64 float64
C Count
Text string
Raw []byte
Leaf Leaf
PtrU *uint64
PtrS *string
PtrB *bool
PtrL *Leaf
Nums []uint32
Sigs []int32
Bigs []float64
Strs []string
Bufs [][]byte
Bits []bool
Kids []Leaf
Ptrs []*Leaf
B bool
I8 int8
I16 int16
I32 int32
I64 int64
U8 uint8
U16 uint16
U32 uint32
U64 uint64
F32 float32
F64 float64
C Count
Text string
Raw []byte
Leaf Leaf
PtrU *uint64
PtrS *string
PtrB *bool
PtrL *Leaf
Nums []uint32
Sigs []int32
Bigs []float64
Strs []string
Bufs [][]byte
Bits []bool
Kids []Leaf
Ptrs []*Leaf
Seal Sealed
Seals []Sealed
PKids *[]Leaf
@@ -1,6 +1,6 @@
// Code generated by zip; DO NOT EDIT.
package codec
package layout
import (
"encoding/binary"
+12 -12
View File
@@ -1,7 +1,7 @@
package zip
// The Go codec — the source that STATES a type's ZAP wire instead of having one
// derived from it at every call.
// Layouts — the source that STATES a type's ZAP wire instead of deriving one at
// every call. Nothing here runs on the served path; this is a generator.
//
// [LayoutOf] names three readers of the one derivation: the plane encodes
// against it, a .zap schema states it as `Name type @Offset`, and a generator
@@ -32,7 +32,7 @@ package zip
// for field and in the same order, for every field the reflective encoder can
// write at all. That is not a promise about intent — it falls out of reading the
// offsets from [LayoutOf] rather than deriving them a second time, and
// TestTheCodecKeepsTheWire holds it against the reflective encoder directly.
// TestTheLayoutKeepsTheWire holds it against the reflective encoder directly.
//
// The sequence it reproduces: every list is written FIRST, because SetList takes
// an offset that must already exist; then the object is started at the declared
@@ -58,9 +58,9 @@ import (
"strings"
)
// Codec is one emitted file: the Go source stating the wire of every type in one
// Layout is one emitted file: the Go source stating the wire of every type in one
// package, and the names of the types it states.
type Codec struct {
type Layout struct {
Path string // the package's import path
Package string // its package clause
Types []string // the types declared, alphabetically
@@ -74,7 +74,7 @@ type Codec struct {
Stated bool
}
// Codecs renders a [Wire] implementation for each of roots and for every struct
// Layouts renders a [Wire] implementation for each of roots and for every struct
// nested below them, grouped by the package that owns the type.
//
// A nested value is reached BY ITS METHOD, so the set is closed downward: a
@@ -87,7 +87,7 @@ type Codec struct {
// is no layout to state and a codec that guessed one would speak a wire nobody
// reads. Answering that is a change to the TYPE, which is not a generator's to
// make.
func Codecs(roots ...reflect.Type) ([]Codec, error) {
func Layouts(roots ...reflect.Type) ([]Layout, error) {
want := map[reflect.Type]bool{}
var refused []string
for _, r := range roots {
@@ -104,7 +104,7 @@ func Codecs(roots ...reflect.Type) ([]Codec, error) {
for t := range want {
byPath[t.PkgPath()] = append(byPath[t.PkgPath()], t)
}
out := make([]Codec, 0, len(byPath))
out := make([]Layout, 0, len(byPath))
for path, ts := range byPath {
sort.Slice(ts, func(i, j int) bool { return ts[i].Name() < ts[j].Name() })
c, err := one(path, ts, want)
@@ -308,7 +308,7 @@ func (p *pkg) spell(t reflect.Type) string {
return t.String()
}
func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, error) {
func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Layout, error) {
p := &pkg{
self: path,
alias: map[string]string{},
@@ -319,7 +319,7 @@ func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, err
names := make([]string, 0, len(ts))
for _, t := range ts {
if err := declare(&body, t, p); err != nil {
return Codec{}, err
return Layout{}, err
}
names = append(names, t.Name())
}
@@ -352,9 +352,9 @@ func one(path string, ts []reflect.Type, want map[reflect.Type]bool) (Codec, err
src, err := format.Source(out.Bytes())
if err != nil {
return Codec{}, fmt.Errorf("zip: emitted source for %s does not parse: %w\n%s", path, err, out.String())
return Layout{}, fmt.Errorf("zip: emitted source for %s does not parse: %w\n%s", path, err, out.String())
}
return Codec{Path: path, Package: clause, Types: names, Source: src}, nil
return Layout{Path: path, Package: clause, Types: names, Source: src}, nil
}
// stated is the compile-time assertion that a type answers for its own bytes.
+8 -8
View File
@@ -8,22 +8,22 @@ import (
"testing"
"github.com/zap-proto/zip"
"github.com/zap-proto/zip/internal/codec"
"github.com/zap-proto/zip/internal/layout"
)
// TestTheCodecIsCheckedIn regenerates internal/codec/zap_gen.go and requires the
// TestTheLayoutIsCheckedIn regenerates internal/layout/zap_gen.go and requires the
// file on disk to match. The generated codec is the wire those types speak, so a
// change to the types that nobody regenerated is a wire that moved under its
// readers — and this is where that is noticed, not in a rolling deploy.
func TestTheCodecIsCheckedIn(t *testing.T) {
got, err := zip.Codecs(reflect.TypeOf(codec.Trunk{}), reflect.TypeOf(codec.Ided{}))
func TestTheLayoutIsCheckedIn(t *testing.T) {
got, err := zip.Layouts(reflect.TypeOf(layout.Trunk{}), reflect.TypeOf(layout.Ided{}))
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("got %d packages, want 1", len(got))
}
path := filepath.Join("internal", "codec", "zap_gen.go")
path := filepath.Join("internal", "layout", "zap_gen.go")
want, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
@@ -35,7 +35,7 @@ func TestTheCodecIsCheckedIn(t *testing.T) {
}
t.Fatal("wrote " + path + "; run again")
}
t.Errorf("%s is stale; regenerate with WRITE=1 go test -run TestTheCodecIsCheckedIn ./...", path)
t.Errorf("%s is stale; regenerate with WRITE=1 go test -run TestTheLayoutIsCheckedIn ./...", path)
}
}
@@ -43,8 +43,8 @@ func TestTheCodecIsCheckedIn(t *testing.T) {
// only proves it PARSES; a codec that names a field that moved, or converts a
// value the type will not take, parses and does not build.
func TestTheEmittedSourceCompiles(t *testing.T) {
out, err := exec.Command("go", "build", "./internal/codec").CombinedOutput()
out, err := exec.Command("go", "build", "./internal/layout").CombinedOutput()
if err != nil {
t.Fatalf("go build ./internal/codec: %v\n%s", err, out)
t.Fatalf("go build ./internal/layout: %v\n%s", err, out)
}
}
+2
View File
@@ -0,0 +1,2 @@
Co-authored-by: Hanzo Dev <[email protected]>
+2
View File
@@ -0,0 +1,2 @@
Co-authored-by: Hanzo Dev <[email protected]>
+1 -1
View File
@@ -455,7 +455,7 @@ func TestSDK_AWholeAppStillGeneratesAroundAGap(t *testing.T) {
// wire, which is the failure this projection exists to remove. It is also not
// fixable by generating harder: the declared type's MarshalZAP lives in the
// service's package, and restating the field as [32]uint8 restates the bytes and
// not the codec.
// not the layout.
func TestSDK_AnIdCrossesOnTheCodecTheSDKWrites(t *testing.T) {
type ID [32]byte
type Tx struct {
+8 -8
View File
@@ -21,7 +21,7 @@ import (
// The fixture these tests share.
//
// Declared at package scope, because reflect gives a type declared inside a
// function no package path — and [zip.Codecs] groups by the package that owns
// function no package path — and [zip.Layouts] groups by the package that owns
// the type, which is the question here: the SDK restates these types in a
// package of its own, and the codec it writes beside them has to be the codec
// zip writes here.
@@ -63,7 +63,7 @@ type Ided struct {
Name string `json:"name"`
}
// TestSDK_TheCodecIsTheOneZipEmits is the wire claim stated as source. A Go
// TestSDK_TheLayoutIsTheOneZipEmits is the wire claim stated as source. A Go
// client and the Rust and C++ clients speak to the same service, so what the
// SDK writes for an id has to be what zip writes for it — not something that
// also happens to work.
@@ -72,7 +72,7 @@ type Ided struct {
// codec" is literal: the section is byte-identical, because it is the same
// emitter reading the same [LayoutOf] offsets. Anything less than identical
// here is a second wire.
func TestSDK_TheCodecIsTheOneZipEmits(t *testing.T) {
func TestSDK_TheLayoutIsTheOneZipEmits(t *testing.T) {
app := zip.New(zip.Config{AppName: "p", DisableStartupMessage: true})
zip.Post(app, "/v1/tx", func(_ context.Context, in *Tx) (*Tx, error) { return in, nil },
zip.WithOperationID("get_tx"))
@@ -81,9 +81,9 @@ func TestSDK_TheCodecIsTheOneZipEmits(t *testing.T) {
if err != nil {
t.Fatalf("SDK: %v", err)
}
native, err := zip.Codecs(reflect.TypeOf(Tx{}))
native, err := zip.Layouts(reflect.TypeOf(Tx{}))
if err != nil {
t.Fatalf("Codecs: %v", err)
t.Fatalf("Layouts: %v", err)
}
if len(native) != 1 {
t.Fatalf("got %d packages, want 1", len(native))
@@ -197,9 +197,9 @@ func TestSDK_TheTwoCodecsWriteTheSameBytes(t *testing.T) {
if len(sdk.Gaps) != 0 {
t.Fatalf("gaps: %+v", sdk.Gaps)
}
native, err := zip.Codecs(reflect.TypeOf(Tx{}), reflect.TypeOf(Ided{}))
native, err := zip.Layouts(reflect.TypeOf(Tx{}), reflect.TypeOf(Ided{}))
if err != nil {
t.Fatalf("Codecs: %v", err)
t.Fatalf("Layouts: %v", err)
}
// The emitter names the package the TYPES live in, and in the probe they
// live in svc. That one line is the only edit; the codec below it is the
@@ -481,7 +481,7 @@ func fail(format string, a ...any) {
// "reached by its method". A type whose fields are all unexported — a time.Time,
// a netip.AddrPort — has no slots, so it crosses as a complete and EMPTY object
// that its parent writes inline. There is nothing to state and no method to
// reach for, and [Codecs] answers the same way. A codec here that called
// reach for, and [Layouts] answers the same way. A codec here that called
// MarshalZAP on it would be a method that does not exist.
func TestSDK_AValueWithNoSlotsIsStillWrittenInline(t *testing.T) {
type stamped struct {
+2 -2
View File
@@ -39,7 +39,7 @@ import (
// bytes_fixed[32] of a .zap schema — crosses the plane on a codec or not at all.
// A type implementing Wire writes it inline (zap.ObjectBuilder.SetBytesFixed) and
// reads it back as a slice of the buffer that arrived (zap.Object.BytesFixed).
// [Codecs] writes that pair for a service's own types, and [App.SDK] writes it
// [Layouts] writes that pair for a service's own types, and [App.SDK] writes it
// for the restatement a generated client holds.
//
// The compatibility rule does not change: the layout is still the type, so
@@ -77,7 +77,7 @@ type Shape = zapenc.Shape
// relative offset. Packing, or giving a nested value four bytes, moves every
// field after it.
//
// bytes_fixed[N] is laid out here and is NOT carried by the reflective codec. The
// bytes_fixed[N] is laid out here and is NOT carried by the reflective layout. The
// layout is what a schema and a generator need; refusing to encode it is what
// makes an id — an ids.ID is [32]byte — force a type to declare its own wire
// rather than quietly acquiring a reflective one.