From 9262990ce5b521a926568081edc3272a52c6a2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sun, 26 Jul 2026 16:13:11 +0200 Subject: [PATCH] feat(verify): extend differential tests to go-flac and AVX-512, add --abi/--profile CLI flags Assisted-by: Qwen 3.8 Max Preview --- cmd/gasm/main.go | 37 +++++++- justfile | 2 +- verify/avx512_test.go | 144 +++++++++++++++++++++++++++++ verify/flac_test.go | 208 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 verify/avx512_test.go create mode 100644 verify/flac_test.go diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index d5d0277..440ba1b 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -29,7 +29,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.20.0" +var version = "0.21.0" func main() { if len(os.Args) < 2 { @@ -472,7 +472,7 @@ requires -p, the package path, and the installed Go toolchain). } func cmdVerify(args []string) int { - fs := newCommand("verify", "gasm verify ", ` + fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] ", ` Assemble FILE (amd64), map it into executable memory and report the available functions. This confirms the assembled image is self-consistent (no unresolved external symbols) and executable — the prerequisite for dynamic @@ -481,11 +481,18 @@ testing. With -smoke, each NOSPLIT function is called with a zeroed argument block to confirm the JIT trampoline works end-to-end. This is safe only for functions that tolerate nil pointers and zero lengths in their arguments. + +With -abi, each function is called with sentinel values in the callee-saved +registers (BP, R14) and a red-zone canary below SP; violations are reported. + +With -profile, the static basic-block structure is listed for each function. `) smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args") + abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)") + profile := fs.Bool("profile", false, "list basic-block structure per function") fs.Parse(args) if fs.NArg() != 1 { - fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] ") + fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] ") return 2 } path := fs.Arg(0) @@ -511,6 +518,16 @@ that tolerate nil pointers and zero lengths in their arguments. flags = " NOSPLIT" } fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags) + + if *profile { + blocks, err := k.Blocks(name) + if err != nil { + fmt.Printf(" profile: %v\n", err) + } else { + fmt.Printf(" blocks: %d\n", len(blocks)) + } + } + if *smoke && fl.NoSplit { args := make([]byte, fl.Args) _, err := k.CallFunc(name, args) @@ -521,6 +538,20 @@ that tolerate nil pointers and zero lengths in their arguments. fmt.Printf(" smoke: OK\n") } } + + if *abi && fl.NoSplit { + args := make([]byte, fl.Args) + _, report, err := k.CallFuncChecked(name, args) + if err != nil { + fmt.Printf(" abi: FAIL — %v\n", err) + rc = 1 + } else if !report.OK() { + fmt.Printf(" abi: %s\n", report) + rc = 1 + } else { + fmt.Printf(" abi: clean\n") + } + } } return rc } diff --git a/justfile b/justfile index 173d632..66264e7 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.20.0" +version := "0.21.0" default: @just --list diff --git a/verify/avx512_test.go b/verify/avx512_test.go new file mode 100644 index 0000000..bb1dd8b --- /dev/null +++ b/verify/avx512_test.go @@ -0,0 +1,144 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "bytes" + "math/rand" + "os" + "testing" + "unsafe" +) + +const lz4AVX512Path = "../../go-libraries/go-lz4/avx512_amd64.s" + +func loadLZ4AVX512Kernel(t *testing.T) *Kernel { + t.Helper() + if _, err := os.Stat(lz4AVX512Path); err != nil { + t.Skipf("sibling kernel not available: %v", err) + } + k, err := Load(lz4AVX512Path) + if err != nil { + t.Fatalf("Load(%s): %v", lz4AVX512Path, err) + } + t.Cleanup(k.Close) + return k +} + +func TestAVX512DecodeKnownAnswers(t *testing.T) { + k := loadLZ4AVX512Kernel(t) + + tests := []struct { + name string + src []byte + wantN int + wantCode int + }{ + {"literals_only", []byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 5, 0}, + {"literals_and_match", []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'}, 16, 0}, + {"overlapping", []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'}, 10, 0}, + {"malformed", []byte{0x50, 'H', 'e'}, 0, 1}, + {"zero_offset", []byte{0x14, 'X', 0x00, 0x00}, 0, 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dst := make([]byte, 64) + args := make([]byte, 64) + PutPtr(args, 0, unsafe.Pointer(&tt.src[0])) + PutUint64(args, 8, uint64(len(tt.src))) + PutUint64(args, 16, uint64(cap(tt.src))) + PutPtr(args, 24, unsafe.Pointer(&dst[0])) + PutUint64(args, 32, uint64(len(dst))) + PutUint64(args, 40, uint64(cap(dst))) + + out, err := k.CallFunc("decodeBlockAVX512", args) + if err != nil { + t.Fatalf("CallFunc: %v", err) + } + n := int(GetUint64(out, 48)) + code := int(GetUint64(out, 56)) + if n != tt.wantN || code != tt.wantCode { + t.Errorf("got (n=%d, code=%d), want (n=%d, code=%d)", n, code, tt.wantN, tt.wantCode) + } + }) + } +} + +func TestAVX512DifferentialFuzz(t *testing.T) { + k := loadLZ4AVX512Kernel(t) + rng := rand.New(rand.NewSource(77)) + + for i := 0; i < 3000; i++ { + wantSize := 1 + rng.Intn(4096) + src := genLZ4Block(rng, wantSize) + dstSize := wantSize + 64 + + goDst := make([]byte, dstSize) + goN, goCode := decodeBlockGo(src, goDst) + + jitDst := make([]byte, dstSize) + args := make([]byte, 64) + if len(src) > 0 { + PutPtr(args, 0, unsafe.Pointer(&src[0])) + } + PutUint64(args, 8, uint64(len(src))) + PutUint64(args, 16, uint64(cap(src))) + if dstSize > 0 { + PutPtr(args, 24, unsafe.Pointer(&jitDst[0])) + } + PutUint64(args, 32, uint64(dstSize)) + PutUint64(args, 40, uint64(cap(jitDst))) + + out, err := k.CallFunc("decodeBlockAVX512", args) + if err != nil { + t.Fatalf("iter %d: %v", i, err) + } + jitN := int(GetUint64(out, 48)) + jitCode := int(GetUint64(out, 56)) + + if jitCode != goCode { + t.Fatalf("iter %d: code mismatch: JIT=%d Go=%d", i, jitCode, goCode) + } + if jitCode != 0 { + continue + } + if jitN != goN { + t.Fatalf("iter %d: n mismatch: JIT=%d Go=%d", i, jitN, goN) + } + if !bytes.Equal(jitDst[:jitN], goDst[:goN]) { + t.Fatalf("iter %d: output mismatch (n=%d)", i, jitN) + } + } +} + +func TestAVX512WideCopy(t *testing.T) { + k := loadLZ4AVX512Kernel(t) + + sizes := []int{0, 1, 31, 32, 63, 64, 65, 127, 128, 256, 1024} + for _, n := range sizes { + src := make([]byte, n) + for i := range src { + src[i] = byte(i*11 + 3) + } + dst := make([]byte, n) + + args := make([]byte, 48) + if n > 0 { + PutPtr(args, 0, unsafe.Pointer(&dst[0])) + PutPtr(args, 24, unsafe.Pointer(&src[0])) + } + PutUint64(args, 8, uint64(n)) + PutUint64(args, 16, uint64(n)) + PutUint64(args, 32, uint64(n)) + PutUint64(args, 40, uint64(n)) + + _, err := k.CallFunc("wideCopyAVX512", args) + if err != nil { + t.Fatalf("wideCopyAVX512(n=%d): %v", n, err) + } + if !bytes.Equal(dst, src) { + t.Errorf("wideCopyAVX512(n=%d): mismatch", n) + } + } +} diff --git a/verify/flac_test.go b/verify/flac_test.go new file mode 100644 index 0000000..3a5c587 --- /dev/null +++ b/verify/flac_test.go @@ -0,0 +1,208 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package verify + +import ( + "bytes" + "math/rand" + "os" + "testing" + "unsafe" +) + +const flacKernelPath = "../../go-libraries/go-flac/avx2_amd64.s" + +func loadFLACKernel(t *testing.T) *Kernel { + t.Helper() + if _, err := os.Stat(flacKernelPath); err != nil { + t.Skipf("sibling kernel not available: %v", err) + } + k, err := Load(flacKernelPath) + if err != nil { + t.Fatalf("Load(%s): %v", flacKernelPath, err) + } + t.Cleanup(k.Close) + return k +} + +// --- Portable Go references (from go-flac/simd.go) --- + +func decodeMono16Go(src []byte, dst []int32) { + for i := 0; i < len(dst); i++ { + dst[i] = int32(int16(uint16(src[2*i]) | uint16(src[2*i+1])<<8)) + } +} + +func pack16Go(dst []byte, src []int32) { + for i, v := range src { + dst[2*i] = byte(v) + dst[2*i+1] = byte(v >> 8) + } +} + +func decorrelateLeftSideGo(left, right, out []int32) { + for i := range left { + l := left[i] + out[2*i] = l + out[2*i+1] = l - right[i] + } +} + +func decorrelateSideRightGo(left, right, out []int32) { + for i := range left { + side := left[i] + rch := right[i] + out[2*i] = rch + side + out[2*i+1] = rch + } +} + +func decorrelateMidSideGo(left, right, out []int32) { + for i := range left { + mid := left[i] + side := right[i] + mid2 := mid<<1 | (side & 1) + out[2*i] = (mid2 + side) >> 1 + out[2*i+1] = (mid2 - side) >> 1 + } +} + +func decorrelateInterleaveGo(left, right, out []int32) { + for i := range left { + out[2*i] = left[i] + out[2*i+1] = right[i] + } +} + +// --- Differential tests --- + +func TestFLACDecodeMono16(t *testing.T) { + k := loadFLACKernel(t) + rng := rand.New(rand.NewSource(7)) + + for iter := 0; iter < 500; iter++ { + n := rng.Intn(256) + src := make([]byte, 2*n) + rng.Read(src) + + goDst := make([]int32, n) + decodeMono16Go(src, goDst) + + jitDst := make([]int32, n) + args := make([]byte, 48) + if len(src) > 0 { + PutPtr(args, 0, unsafe.Pointer(&src[0])) + } + PutUint64(args, 8, uint64(len(src))) + PutUint64(args, 16, uint64(cap(src))) + if n > 0 { + PutPtr(args, 24, unsafe.Pointer(&jitDst[0])) + } + PutUint64(args, 32, uint64(n)) + PutUint64(args, 40, uint64(cap(jitDst))) + + _, err := k.CallFunc("decodeMono16AVX2", args) + if err != nil { + t.Fatalf("iter %d: %v", iter, err) + } + for i := range goDst { + if jitDst[i] != goDst[i] { + t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i]) + } + } + } +} + +func TestFLACPack16(t *testing.T) { + k := loadFLACKernel(t) + rng := rand.New(rand.NewSource(13)) + + for iter := 0; iter < 500; iter++ { + n := rng.Intn(256) + src := make([]int32, n) + for i := range src { + src[i] = int32(rng.Intn(65536) - 32768) + } + + goDst := make([]byte, 2*n) + pack16Go(goDst, src) + + jitDst := make([]byte, 2*n) + args := make([]byte, 48) + if len(jitDst) > 0 { + PutPtr(args, 0, unsafe.Pointer(&jitDst[0])) + } + PutUint64(args, 8, uint64(len(jitDst))) + PutUint64(args, 16, uint64(cap(jitDst))) + if n > 0 { + PutPtr(args, 24, unsafe.Pointer(&src[0])) + } + PutUint64(args, 32, uint64(n)) + PutUint64(args, 40, uint64(cap(src))) + + _, err := k.CallFunc("pack16AVX2", args) + if err != nil { + t.Fatalf("iter %d: %v", iter, err) + } + if !bytes.Equal(jitDst, goDst) { + t.Fatalf("iter %d: output mismatch (n=%d)", iter, n) + } + } +} + +func TestFLACDecorrelate(t *testing.T) { + k := loadFLACKernel(t) + rng := rand.New(rand.NewSource(21)) + + kernels := []struct { + name string + ref func(left, right, out []int32) + }{ + {"decorrelateLeftSideAVX2", decorrelateLeftSideGo}, + {"decorrelateSideRightAVX2", decorrelateSideRightGo}, + {"decorrelateMidSideAVX2", decorrelateMidSideGo}, + {"decorrelateInterleaveAVX2", decorrelateInterleaveGo}, + } + + for _, kk := range kernels { + t.Run(kk.name, func(t *testing.T) { + for iter := 0; iter < 200; iter++ { + n := rng.Intn(128) + left := make([]int32, n) + right := make([]int32, n) + for i := range left { + left[i] = int32(rng.Intn(1<<24) - 1<<23) + right[i] = int32(rng.Intn(1<<24) - 1<<23) + } + + goOut := make([]int32, 2*n) + kk.ref(left, right, goOut) + + jitOut := make([]int32, 2*n) + args := make([]byte, 72) + if n > 0 { + PutPtr(args, 0, unsafe.Pointer(&left[0])) + PutPtr(args, 24, unsafe.Pointer(&right[0])) + PutPtr(args, 48, unsafe.Pointer(&jitOut[0])) + } + PutUint64(args, 8, uint64(n)) + PutUint64(args, 16, uint64(cap(left))) + PutUint64(args, 32, uint64(n)) + PutUint64(args, 40, uint64(cap(right))) + PutUint64(args, 56, uint64(2*n)) + PutUint64(args, 64, uint64(cap(jitOut))) + + _, err := k.CallFunc(kk.name, args) + if err != nil { + t.Fatalf("iter %d: %v", iter, err) + } + for i := range goOut { + if jitOut[i] != goOut[i] { + t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitOut[i], goOut[i]) + } + } + } + }) + } +}