Files
zeekay 0e63002876 Say which class a nested accessor returns
A field may carry the name of its own type — `Child Child` is a natural
schema — and in C++ the member then shadows the class inside the class body,
so `Child Child() const;` is ill-formed. The qualified name says which is
meant. Go has no such collision, which is why this only shows up on one side.

Per-struct headers now also forward-declare the classes they name, not just
their own, so declaration order is free there too. Two structs that point at
each other still need -single: two headers cannot each be complete for the
other, and the cycle compiles in one header. Both cases are checked by
compiling, not by reading.
2026-09-06 08:37:18 -07:00
..

Two backends, one wire

zapgen emits Go and C++ from the same schema. This directory is the evidence that the two agree, and it is run, not asserted:

conformance/run.sh [VECTORS.TSV]

The C++ runtime (github.com/zap-proto/cpp) is cloned at a pinned revision unless ZAP_CPP names a checkout. The corpus is any vectors.tsv in the node2 chain-differential format — pass a path or set ZAP_CORPUS. Without one, the script runs the build comparison and says the read comparison was skipped rather than reporting a pass it did not earn.

What it compares

  1. The per-struct headers compile. -single is what the probes use; the default is one header per struct with a sibling include per nested struct. Both are built.

  2. What each backend BUILT, byte for byte. Both probes construct the same cases from the same inputs — every type the dialect has, plus the RPC request and response envelopes the generated client and dispatch produce — and print the hex. The two outputs must be the same file.

  3. What each backend READS BACK. Each probe reads both probes' bytes with its own generated accessors and prints every field. Four runs: each reader over its own writer and over the other's. All four must agree, which is what "the C++ reader reads what the Go builder wrote" means concretely.

  4. What each backend reads out of bytes NEITHER of them wrote. The corpus is real P-chain transactions produced by the Go chain implementation, and it is mostly hostile: of 134 vectors, 44 are truncated or malformed on purpose. Both readers must answer the same fields, including answering zero in the same places.

The parts

schema/probe.zap    every type the dialect has, in one struct
schema/pchain.zap   the P-chain spending envelope the corpus carries
gen/                generated: gen/<pkg>/*.go for Go, gen/hpp/*.hpp for C++
go/probe.go         the Go probe:  write | back | read
cpp/probe.cpp       the C++ probe: write | back | read
run.sh              generate, build, compare

gen/ is regenerated by run.sh on every run; nothing in it is edited by hand. Both probes print in one format so the comparison is diff, and neither probe decides anything: every value they print comes from a generated accessor or a generated builder, so a disagreement between the probes is a disagreement between the backends and nowhere else.

What it found

Two defects, both caught by running rather than reading:

  • A fixed-width field disagreed on a short buffer. Go's [N]byte accessor answers N zero bytes when the buffer cannot hold the field; the C++ span answered empty. Five corpus vectors separated them. The C++ emitter now answers N zero bytes too.

  • The Go runtime faulted where C++ answered zero. A null nested-object pointer yields the zero Object, whose accessors dereferenced a nil *Message. The read side is documented as total and the C++ runtime is; Object.data() in zap.go makes the Go one total as well.

What it does not cover

list<T> carries a pointer and an element count and nothing about the elements: the dialect cannot state a stride, so neither backend can emit an element accessor. Both languages agree on the count, which is all either can say. The chains' own lists are fixed-stride records, so a schema cannot yet describe a P-chain output — a gap in the DIALECT, in both backends at once.