mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
173 lines
5.2 KiB
Go
173 lines
5.2 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zip
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
// Where a field sits, derived once for every language.
|
|
//
|
|
// A ZAP field IS an offset and a width, and the rule that assigns them is the
|
|
// same rule wherever the type was declared: walk the fields in declaration
|
|
// order, align each to its own width, and round the fixed section up to eight.
|
|
// Go's encoder derives it at run time from a reflect.Type (internal/zapenc), and
|
|
// it has to, because a Go value is encoded reflectively. Nothing else needs to:
|
|
// a description already says what each field is, so the offsets follow from the
|
|
// description alone.
|
|
//
|
|
// So the rule lives HERE, once, and the .zap a Rust service publishes is laid
|
|
// out by the same code that lays out a Go one. That the two agree with the Go
|
|
// ENCODER is not assumed either — TestProjectZAPMatchesRegistry compares this
|
|
// against the layout zapenc computes over the same types, which is the only way
|
|
// a schema and the bytes it describes stay one thing.
|
|
|
|
// slot is one field's place in the fixed section.
|
|
type slot struct {
|
|
at int
|
|
repr string // the IDL's spelling: bool, u32, text, bytes, list<...>, bytes_fixed[N]
|
|
elem string // a list's element spelling
|
|
}
|
|
|
|
// layout is a struct's fixed section: one slot per field, in declaration order.
|
|
type layoutOut struct {
|
|
slots []slot
|
|
size int
|
|
}
|
|
|
|
// layout assigns td's fields their slots, or names the first field that has no
|
|
// fixed form at all.
|
|
func (p *projector) layout(td *TypeDesc, inside map[string]bool) (layoutOut, bool) {
|
|
if inside[td.ID] {
|
|
return layoutOut{}, false // a field is an offset and a width; a recursive type has none.
|
|
}
|
|
inside[td.ID] = true
|
|
defer delete(inside, td.ID)
|
|
|
|
out := layoutOut{}
|
|
at := 0
|
|
for _, f := range td.Fields {
|
|
s, ok := p.slotOf(f.Type, inside)
|
|
if !ok {
|
|
return layoutOut{}, false
|
|
}
|
|
w := slotWidth(s.repr)
|
|
a := w
|
|
if n, fixed := fixedLen(s.repr); fixed {
|
|
a, w = 1, n
|
|
}
|
|
at = (at + a - 1) &^ (a - 1)
|
|
s.at = at
|
|
at += w
|
|
out.slots = append(out.slots, s)
|
|
}
|
|
out.size = (at + 7) &^ 7
|
|
return out, true
|
|
}
|
|
|
|
// slotOf is one field's spelling and whether it has a fixed form at all.
|
|
func (p *projector) slotOf(r TypeRef, inside map[string]bool) (slot, bool) {
|
|
switch {
|
|
case r.Map != nil:
|
|
return slot{}, false // no map in the IDL: a key set is not a layout.
|
|
case r.List != nil:
|
|
if r.Len > 0 {
|
|
// A fixed-length list of anything but bytes has no wire form: an
|
|
// element is one value at one width, and a run of them inline is not
|
|
// a slot this layout has.
|
|
return slot{}, false
|
|
}
|
|
e, ok := p.elemOf(*r.List, inside)
|
|
if !ok {
|
|
return slot{}, false
|
|
}
|
|
return slot{repr: "list<" + e + ">", elem: e}, true
|
|
case r.Ref != "":
|
|
td := p.types[r.Ref]
|
|
if td == nil {
|
|
return slot{}, false
|
|
}
|
|
switch td.Kind {
|
|
case "struct":
|
|
// A nested value crosses as a complete message in an {offset,length}
|
|
// slot, so it is `bytes` here — and only if it lays out at all.
|
|
if _, ok := p.layout(td, inside); !ok {
|
|
return slot{}, false
|
|
}
|
|
return slot{repr: "bytes"}, true
|
|
case "list":
|
|
if td.Len > 0 {
|
|
return slot{}, false
|
|
}
|
|
e, ok := p.elemOf(*td.Elem, inside)
|
|
if !ok {
|
|
return slot{}, false
|
|
}
|
|
return slot{repr: "list<" + e + ">", elem: e}, true
|
|
case "map":
|
|
return slot{}, false
|
|
}
|
|
s, ok := primSlot(td.Repr)
|
|
return optional(s, ok, r.Opt)
|
|
}
|
|
s, ok := primSlot(r.Prim)
|
|
return optional(s, ok, r.Opt)
|
|
}
|
|
|
|
// optional is a primitive that may be absent: a list of at most one element,
|
|
// which is how the Go encoder writes a pointer to one, so a present zero and an
|
|
// absent value are different bytes. A fixed array is inline and has no list.
|
|
func optional(s slot, ok bool, opt bool) (slot, bool) {
|
|
if !ok || !opt {
|
|
return s, ok
|
|
}
|
|
if _, fixed := fixedLen(s.repr); fixed {
|
|
return s, ok
|
|
}
|
|
return slot{repr: "list<" + s.repr + ">", elem: s.repr}, true
|
|
}
|
|
|
|
// elemOf is a list element's spelling. A list of lists has no form: the element
|
|
// of a list is one value at one width, and a list is neither.
|
|
func (p *projector) elemOf(r TypeRef, inside map[string]bool) (string, bool) {
|
|
r.Opt = false // a list element has no absent form: a nil one is written as its zero
|
|
s, ok := p.slotOf(r, inside)
|
|
if !ok || strings.HasPrefix(s.repr, "list<") {
|
|
return "", false
|
|
}
|
|
return s.repr, true
|
|
}
|
|
|
|
// primSlot is a primitive's spelling, or a refusal for one that has none.
|
|
func primSlot(prim string) (slot, bool) {
|
|
switch prim {
|
|
case "bool", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "bytes", "text":
|
|
return slot{repr: prim}, true
|
|
case "string":
|
|
return slot{repr: "text"}, true
|
|
}
|
|
if _, ok := fixedLen(prim); ok {
|
|
return slot{repr: prim}, true
|
|
}
|
|
return slot{}, false
|
|
}
|
|
|
|
// slotWidth is the fixed-area size of one spelling. Everything variable — text,
|
|
// bytes, a list, a nested value — carries an offset and a length.
|
|
func slotWidth(repr string) int {
|
|
switch {
|
|
case repr == "bool" || repr == "i8" || repr == "u8":
|
|
return 1
|
|
case repr == "i16" || repr == "u16":
|
|
return 2
|
|
case repr == "i32" || repr == "u32" || repr == "f32":
|
|
return 4
|
|
}
|
|
if n, ok := fixedLen(repr); ok {
|
|
return n
|
|
}
|
|
return 8
|
|
}
|