diff --git a/justfile b/justfile index f3784be..48fada1 100644 --- a/justfile +++ b/justfile @@ -82,7 +82,7 @@ fuzz target pkg fuzztime="60s": # The cmd/link GOOBJ parity gate, opt-in: it costs minutes no pipeline can afford. link-parity: - systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 0 -run 'TestGOOBJLinkRegression' ./verify/ + systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 0 -run 'TestGOOBJLink' ./verify/ # Benchmarks. On an idle machine only, and deliberately unfenced. bench pkgs=packages: diff --git a/verify/goobj_link_vector_test.go b/verify/goobj_link_vector_test.go new file mode 100644 index 0000000..8063fb2 --- /dev/null +++ b/verify/goobj_link_vector_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "os" + "os/exec" + "testing" +) + +// goobjVectorMainSrc is the Go side of the vector regression. The kernel +// computes a signed byte checksum through the RVV load/store and arithmetic +// families; main derives the same value in Go and panics on any divergence +// before printing the deterministic line the baseline and the gasm-linked +// binaries must agree on. +const goobjVectorMainSrc = `package main + +func checksum(buf []byte, seed int64) int64 + +func main() { + buf := []byte("gasm link parity vector kernel!") + total := int64(0) + for _, b := range buf { + total += int64(int8(b)) + } + want := total*0x0101010101010101 ^ 0x5eed + if got := checksum(buf, 0x5eed); got != want { + panic("checksum") + } + println("ok", checksum(buf, 0x5eed)) +} +` + +// TestGOOBJLinkRVVKernel proves the riscv64 vector families merged into the +// encoder corpus are proven end to end: the kernel assembles by gasm into a +// GOOBJ object, substitutes byte-wise into a real go build archive, relinks +// with cmd/link and the linked binary computes the same checksum as the +// toolchain-built one under qemu-user. The reduction reads the live vl, so +// the checksum holds for any hardware VLEN. +func TestGOOBJLinkRVVKernel(t *testing.T) { + if testing.Short() { + t.Skip("builds the gasm binary and links Go programs") + } + if os.Getenv("GASM_LINK_PARITY") == "" { + t.Skip("deliberate verification: set GASM_LINK_PARITY=1 (just link-parity)") + } + goBin, err := exec.LookPath("go") + if err != nil { + t.Skip("no Go toolchain available") + } + gasmBin := buildLinkParityGasm(t, goBin) + t.Run("riscv64", func(t *testing.T) { + goobjLinkKernel(t, goBin, gasmBin, "riscv64", "linkvec_riscv64.s", goobjVectorMainSrc) + }) +} diff --git a/verify/testdata/linkvec_riscv64.s b/verify/testdata/linkvec_riscv64.s new file mode 100644 index 0000000..5079d2d --- /dev/null +++ b/verify/testdata/linkvec_riscv64.s @@ -0,0 +1,65 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +#include "textflag.h" + +// The vector link-regression kernel for riscv64: the RVV load/store and +// arithmetic families computing a checksum end to end. bytetotal +// strip-mines a byte buffer with VSETVLI, loads each chunk with VLE8V, +// widens it to E64 lanes with VSEXTVF8 and reduces the chunk with +// VREDSUMVS, so the result never depends on the hardware VLEN. checksum +// passes the slice to bytetotal through the ABI0 stack slots and mixes the +// reduction with the seed through the intra-file mix call. mix multiplies +// by the odd golden-ratio constant and XORs the seed, the same arithmetic +// the Go side of the regression mirrors bit for bit. + +// func bytetotal(buf []byte) int64 +TEXT ·bytetotal(SB), NOSPLIT, $0-32 + MOV buf+0(FP), X10 + MOV buf_len+8(FP), X12 + MOV $0, X13 + BEQZ X12, empty + +loop: + VSETVLI X12, E64, M1, TA, MA, X11 + VLE8V (X10), V16 + VSEXTVF8 V16, V9 + VMVVI $0, V10 + VREDSUMVS V10, V9, V11 + VMVXS V11, X14 + ADD X14, X13, X13 + ADD X11, X10, X10 + SUB X11, X12, X12 + BNE X12, X0, loop + +empty: + MOV X13, ret+24(FP) + RET + +// func mix(x, seed int64) int64 +TEXT ·mix(SB), NOSPLIT, $0-24 + MOV x+0(FP), X10 + MOV seed+8(FP), X11 + MOV $0x0101010101010101, X12 + MUL X12, X10, X10 + XOR X11, X10, X10 + MOV X10, ret+16(FP) + RET + +// func checksum(buf []byte, seed int64) int64 +TEXT ·checksum(SB), NOSPLIT, $40-40 + MOV buf+0(FP), X10 + MOV buf+8(FP), X11 + MOV buf+16(FP), X12 + MOV X10, 8(SP) + MOV X11, 16(SP) + MOV X12, 24(SP) + CALL ·bytetotal(SB) + MOV 32(SP), X10 + MOV seed+24(FP), X11 + MOV X10, 8(SP) + MOV X11, 16(SP) + CALL ·mix(SB) + MOV 24(SP), X10 + MOV X10, ret+32(FP) + RET