Files
zeekay 6439541c91 Prove the two backends agree, on bytes neither of them wrote
Generating C++ is a claim. This runs it.

Both probes build the same cases from the same inputs — every type the dialect
has, plus the request and response envelopes the generated client and dispatch
produce — and print the hex; the two outputs are the same file. Then each
probe reads both probes' bytes with its own generated accessors, four
crossings, all agreeing. Then both read a corpus of real P-chain transactions
produced by the Go chain implementation, 134 vectors of which 44 are truncated
or malformed on purpose, and answer the same fields in the same places.

Neither probe decides anything: every value printed comes from a generated
accessor or a generated builder, so a disagreement between the probes is a
disagreement between the backends and nowhere else.

It earned its keep immediately. Five corpus vectors separated a Go [N]byte
accessor answering zeros from a C++ span answering empty, and the round trip
found the zero Object panicking in the Go runtime where C++ answered zero.
Both are fixed; neither was visible by reading.

list<T> carries a pointer and a count and nothing about elements — the dialect
cannot state a stride — so both languages agree on the count, which is all
either can say. That gap is in the dialect, in both backends at once.
2026-09-06 08:35:14 -07:00

120 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: BSD-3-Clause-Eco
#
# The two-backend proof, end to end.
#
# 1. regenerate Go AND C++ from the same schemas with the same zapgen
# 2. build both probes
# 3. compare what each BUILT, byte for byte
# 4. read each side's bytes back with BOTH readers and compare the fields
# 5. compare what each READ from a corpus of real chain bytes
#
# A pass means the C++ backend writes the bytes the Go backend writes and
# reads the same fields out of bytes neither of them wrote.
#
# The C++ runtime comes from github.com/zap-proto/cpp, pinned below. Set
# ZAP_CPP to a checkout to use one you already have.
#
# The corpus is a vectors.tsv in node2's format. Pass a path, or set
# ZAP_CORPUS. Without one, only the build comparison runs, and the script
# says so rather than reporting a pass it did not earn.
set -euo pipefail
ZAP_CPP_REV=075e43b14878af486a98974759b57a5881df157a
here=$(cd "$(dirname "$0")" && pwd)
repo=$(cd "$here/.." && pwd)
work=${TMPDIR:-/tmp}/zapgen-conformance
corpus=${1:-${ZAP_CORPUS:-}}
mkdir -p "$work"
# ── the C++ runtime ────────────────────────────────────────────────────────
if [ -z "${ZAP_CPP:-}" ]; then
ZAP_CPP=$work/zap-cpp
if [ ! -d "$ZAP_CPP/.git" ]; then
git clone -q https://github.com/zap-proto/cpp "$ZAP_CPP"
fi
git -C "$ZAP_CPP" fetch -q origin
git -C "$ZAP_CPP" checkout -q $ZAP_CPP_REV
fi
echo "runtime: $ZAP_CPP @ $(git -C "$ZAP_CPP" rev-parse --short HEAD)"
# ── regenerate ─────────────────────────────────────────────────────────────
cd "$repo"
go build -o "$work/zapgen" ./cmd/zapgen
gen() { # gen SCHEMA GO_PKG_DIR
"$work/zapgen" -single -out "conformance/gen/$2" "$1"
"$work/zapgen" -lang cpp -single -out conformance/gen/hpp "$1"
}
gen conformance/schema/probe.zap probe
gen conformance/schema/pchain.zap pchain
gen cmd/zapgen/testdata/echo.zap echo
gen cmd/zapgen/testdata/basetx.zap xvm
# ── the per-struct headers compile too ─────────────────────────────────────
# -single is what the probes use; the default is one header per struct, with
# a sibling include per nested struct. Both have to build.
per=$work/per-struct
rm -rf "$per" && mkdir -p "$per"
"$work/zapgen" -lang cpp -out "$per" conformance/schema/probe.zap
"$work/zapgen" -lang cpp -out "$per" conformance/schema/pchain.zap
"$work/zapgen" -lang cpp -out "$per" cmd/zapgen/testdata/echo.zap
"$work/zapgen" -lang cpp -out "$per" cmd/zapgen/testdata/basetx.zap
{
echo "// Generated by conformance/run.sh: include every per-struct header."
for h in "$per"/*.hpp; do echo "#include \"$(basename "$h")\""; done
echo "int main() { return 0; }"
} > "$per/all.cpp"
${CXX:-g++} -std=c++23 -O1 -Wall -Wextra -I"$ZAP_CPP/include" -I"$per" \
-o "$work/per-struct-all" "$per/all.cpp" "$ZAP_CPP/src/rpc.cpp"
echo "per-struct headers: $(ls "$per"/*.hpp | wc -l) compiled together"
# ── build both probes ──────────────────────────────────────────────────────
go build -o "$work/probe-go" ./conformance/go
${CXX:-g++} -std=c++23 -O1 -Wall -Wextra \
-I"$ZAP_CPP/include" -Iconformance/gen/hpp \
-o "$work/probe-cpp" conformance/cpp/probe.cpp "$ZAP_CPP/src/rpc.cpp"
# ── what each BUILT ────────────────────────────────────────────────────────
"$work/probe-go" write > "$work/go.write"
"$work/probe-cpp" write > "$work/cpp.write"
if diff -u "$work/go.write" "$work/cpp.write"; then
echo "write parity: $(wc -l < "$work/go.write") cases identical"
else
echo "write parity: FAILED"
exit 1
fi
# ── each reader over the other language's bytes ────────────────────────────
# Four runs, so every combination is covered: each reader over its own bytes
# and over the other's. All four must print the same fields.
"$work/probe-go" back "$work/go.write" > "$work/go-reads-go"
"$work/probe-cpp" back "$work/go.write" > "$work/cpp-reads-go"
"$work/probe-go" back "$work/cpp.write" > "$work/go-reads-cpp"
"$work/probe-cpp" back "$work/cpp.write" > "$work/cpp-reads-cpp"
if diff -u "$work/go-reads-go" "$work/cpp-reads-go" &&
diff -u "$work/go-reads-go" "$work/go-reads-cpp" &&
diff -u "$work/go-reads-go" "$work/cpp-reads-cpp"; then
echo "read-back parity: $(wc -l < "$work/go-reads-go") cases, both readers, both writers"
else
echo "read-back parity: FAILED"
exit 1
fi
# ── what each READ ─────────────────────────────────────────────────────────
if [ -z "$corpus" ]; then
echo "read parity: SKIPPED (no corpus; pass a vectors.tsv path or set ZAP_CORPUS)"
exit 0
fi
"$work/probe-go" read "$corpus" > "$work/go.read"
"$work/probe-cpp" read "$corpus" > "$work/cpp.read"
if diff -u "$work/go.read" "$work/cpp.read"; then
n=$(wc -l < "$work/go.read")
ok=$(grep -c 'parse=ok' "$work/go.read" || true)
echo "read parity: $n corpus vectors identical ($ok parsed, $((n - ok)) refused by both)"
else
echo "read parity: FAILED"
exit 1
fi