Compare commits

...
6 Commits
Author SHA1 Message Date
petrbalvin 382efe538a feat(verify): add universal --ground-truth verification against go tool asm
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f8d28a42ba feat(verify): complete the analyze family and add stereo16 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 51a2854d7f feat(verify): add analyzeO2/Res and decodeMono24 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin c234c3dd5b feat(verify): add analyzeO1Range and fastStereoSums differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 9262990ce5 feat(verify): extend differential tests to go-flac and AVX-512, add --abi/--profile CLI flags
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin d9f6167a4d feat(verify): add basic-block enumeration and path-diversity profiling
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
9 changed files with 1264 additions and 4 deletions
+91 -3
View File
@@ -8,6 +8,7 @@
package main package main
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@@ -29,7 +30,7 @@ import (
// version is the release version, stamped at build time via // version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release). // -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.19.0" var version = "0.25.0"
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
@@ -472,7 +473,7 @@ requires -p, the package path, and the installed Go toolchain).
} }
func cmdVerify(args []string) int { func cmdVerify(args []string) int {
fs := newCommand("verify", "gasm verify <file.s>", ` fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic unresolved external symbols) and executable — the prerequisite for dynamic
@@ -481,11 +482,19 @@ testing.
With -smoke, each NOSPLIT function is called with a zeroed argument block to 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 confirm the JIT trampoline works end-to-end. This is safe only for functions
that tolerate nil pointers and zero lengths in their arguments. 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") 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")
groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm")
fs.Parse(args) fs.Parse(args)
if fs.NArg() != 1 { if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] <file.s>") fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
return 2 return 2
} }
path := fs.Arg(0) path := fs.Arg(0)
@@ -504,6 +513,61 @@ that tolerate nil pointers and zero lengths in their arguments.
names := k.FuncNames() names := k.FuncNames()
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names)) fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
rc := 0 rc := 0
// Ground-truth comparison: assemble with go tool asm and compare bytes.
if *groundTruth {
gt, err := verify.GroundTruth(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
for _, name := range names {
fl, _ := k.Func(name)
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
goCode, ok := gt[name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue
}
total++
// Compare, masking relocation sites (disp32 fields that the
// Go linker fills at link time — gasm resolves them internally).
gasmCmp := make([]byte, len(gasmCode))
goCmp := make([]byte, len(goCode))
copy(gasmCmp, gasmCode)
copy(goCmp, goCode)
for _, r := range fl.Relocs {
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
gasmCmp[j] = 0
}
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
goCmp[j] = 0
}
}
if bytes.Equal(gasmCmp, goCmp) {
matched++
if len(fl.Relocs) > 0 {
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
} else {
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
}
} else {
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
if gasmCmp[i] != goCmp[i] {
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
break
}
}
rc = 1
}
}
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
if matched < total {
rc = 1
}
}
for _, name := range names { for _, name := range names {
fl, _ := k.Func(name) fl, _ := k.Func(name)
flags := "" flags := ""
@@ -511,6 +575,16 @@ that tolerate nil pointers and zero lengths in their arguments.
flags = " NOSPLIT" flags = " NOSPLIT"
} }
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags) 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 { if *smoke && fl.NoSplit {
args := make([]byte, fl.Args) args := make([]byte, fl.Args)
_, err := k.CallFunc(name, args) _, err := k.CallFunc(name, args)
@@ -521,6 +595,20 @@ that tolerate nil pointers and zero lengths in their arguments.
fmt.Printf(" smoke: OK\n") 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 return rc
} }
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.19.0" version := "0.25.0"
default: default:
@just --list @just --list
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
}
}
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"fmt"
"sort"
)
// Block describes one basic block within a function: a maximal sequence of
// instructions with a single entry point (a label or the function start) and
// a single exit (a jump, conditional jump or RET).
type Block struct {
Offset int // byte offset within the function
Label string // label name ("" for the entry block)
}
// Blocks identifies the basic blocks of a function from its local labels.
// Each label is a potential jump target and therefore a block boundary; the
// function entry (offset 0) is always a block. The blocks are returned in
// ascending offset order.
func (k *Kernel) Blocks(name string) ([]Block, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
blocks := []Block{{Offset: 0, Label: "(entry)"}}
// Build a reverse map: offset → label name.
offToLabel := make(map[int]string, len(fl.Labels))
for label, off := range fl.Labels {
if off > 0 && off < fl.Size {
offToLabel[off] = label
}
}
// Collect and sort offsets.
offsets := make([]int, 0, len(offToLabel))
for off := range offToLabel {
offsets = append(offsets, off)
}
sort.Ints(offsets)
for _, off := range offsets {
blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]})
}
return blocks, nil
}
// BlockCount returns the number of identified basic blocks for the function.
func (k *Kernel) BlockCount(name string) (int, error) {
blocks, err := k.Blocks(name)
if err != nil {
return 0, err
}
return len(blocks), nil
}
// PathFingerprint is the observable output of one function execution: the
// values written back into the result slots of the argument block. Two
// executions that produce the same fingerprint took observationally
// equivalent paths (though they may differ internally).
type PathFingerprint struct {
Results []uint64 // the result words from the arg block
}
// ProfilePaths runs the function with each of the given argument blocks and
// collects the distinct output fingerprints. This measures path diversity:
// how many observationally different execution paths the input corpus
// exercises. Combined with Blocks (the static block count), it gives a
// lower bound on code coverage.
func (k *Kernel) ProfilePaths(name string, argSets [][]byte, resultOffsets []int) ([]PathFingerprint, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
seen := map[string]bool{}
var paths []PathFingerprint
for _, args := range argSets {
if len(args) < fl.Args {
return nil, fmt.Errorf("verify: %s: arg block too small", name)
}
out, err := k.CallFunc(name, args)
if err != nil {
return nil, err
}
fp := PathFingerprint{}
key := ""
for _, off := range resultOffsets {
v := GetUint64(out, off)
fp.Results = append(fp.Results, v)
key += fmt.Sprintf("%016x", v)
}
if !seen[key] {
seen[key] = true
paths = append(paths, fp)
}
}
return paths, nil
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
"unsafe"
)
func TestBlocks(t *testing.T) {
k := loadBasic(t)
// The "sum" function has labels: sum_done, sum_loop.
blocks, err := k.Blocks("sum")
if err != nil {
t.Fatalf("Blocks(sum): %v", err)
}
if len(blocks) < 3 {
t.Errorf("sum: expected at least 3 blocks (entry + 2 labels), got %d", len(blocks))
}
if blocks[0].Offset != 0 {
t.Errorf("first block offset = %d, want 0", blocks[0].Offset)
}
t.Logf("sum blocks: %v", blocks)
}
func TestBlockCount(t *testing.T) {
k := loadLZ4Kernel(t)
n, err := k.BlockCount("decodeBlockAVX2")
if err != nil {
t.Fatalf("BlockCount: %v", err)
}
// The decoder has many labels (dec_loop, dec_malformed, etc.).
if n < 10 {
t.Errorf("decodeBlockAVX2: expected at least 10 blocks, got %d", n)
}
t.Logf("decodeBlockAVX2: %d basic blocks", n)
}
func TestProfilePaths(t *testing.T) {
k := loadLZ4Kernel(t)
// Build a corpus of varied LZ4 blocks.
var argSets [][]byte
blocks := []struct {
src []byte
dstSize int
}{
{[]byte{0x00}, 16}, // empty
{[]byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 16}, // literals only
{[]byte{0x54, 'A', 'A', 'A', 'A', 'A', 5, 0, 0x30, 'B', 'B', 'B'}, 32}, // match
{[]byte{0x14, 'X', 1, 0, 0x10, 'Y'}, 16}, // overlapping
{[]byte{0x50, 'H'}, 16}, // malformed
{[]byte{0x14, 'X', 0, 0}, 16}, // zero offset
}
for _, b := range blocks {
args := make([]byte, 64)
if len(b.src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&b.src[0]))
}
PutUint64(args, 8, uint64(len(b.src)))
PutUint64(args, 16, uint64(cap(b.src)))
dst := make([]byte, b.dstSize)
if len(dst) > 0 {
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
}
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
argSets = append(argSets, args)
}
// Result offsets: n+48 and code+56.
paths, err := k.ProfilePaths("decodeBlockAVX2", argSets, []int{48, 56})
if err != nil {
t.Fatalf("ProfilePaths: %v", err)
}
// We expect at least 3 distinct paths: success (various n), malformed, zero offset.
if len(paths) < 3 {
t.Errorf("expected at least 3 distinct paths, got %d", len(paths))
}
t.Logf("decodeBlockAVX2: %d distinct output paths from %d inputs", len(paths), len(argSets))
}
+585
View File
@@ -0,0 +1,585 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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]
}
}
func analyzeO1RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
swin = swin[:len(dstP)+1]
for j := 0; j+1 < len(swin); j++ {
r := swin[j+1] - swin[j]
if r == -2147483648 { // math.MinInt32
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func analyzeO2RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
swin = swin[:len(dstP)+2]
for j := 0; j+2 < len(swin); j++ {
r := swin[j+2] - 2*swin[j+1] + swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func analyzeResRangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
for j := 0; j < len(swin); j++ {
r := swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func decodeMono24Go(src []byte, dst []int32) {
for i := 0; i < len(dst); i++ {
off := 3 * i
u := uint32(src[off]) | uint32(src[off+1])<<8 | uint32(src[off+2])<<16
dst[i] = int32(u<<8) >> 8
}
}
func decodeStereo16Go(src []byte, left, right []int32) {
for i := 0; i < len(left); i++ {
left[i] = int32(int16(uint16(src[4*i]) | uint16(src[4*i+1])<<8))
right[i] = int32(int16(uint16(src[4*i+2]) | uint16(src[4*i+3])<<8))
}
}
// --- 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])
}
}
}
})
}
}
func TestFLACAnalyzeO1Range(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(33))
for iter := 0; iter < 300; iter++ {
n := 1 + rng.Intn(128) // partition size
swin := make([]int32, n+1)
for i := range swin {
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
}
goDstP := make([]uint32, n)
var goHist [32]uint16
goSum, goOvf := analyzeO1RangeGo(swin, goDstP, &goHist)
jitDstP := make([]uint32, n)
var jitHist [32]uint16
args := make([]byte, 72) // 65 rounded up
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
PutUint64(args, 8, uint64(len(swin)))
PutUint64(args, 16, uint64(cap(swin)))
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDstP)))
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
out, err := k.CallFunc("analyzeO1RangeAVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
jitSum := GetUint64(out, 56)
jitOvf := out[64] != 0
if jitSum != goSum {
t.Fatalf("iter %d: partSum mismatch: JIT=%d Go=%d", iter, jitSum, goSum)
}
if jitOvf != goOvf {
t.Fatalf("iter %d: overflow mismatch: JIT=%v Go=%v", iter, jitOvf, goOvf)
}
for i := range goDstP {
if jitDstP[i] != goDstP[i] {
t.Fatalf("iter %d: dstP[%d] mismatch: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
}
}
if jitHist != goHist {
t.Fatalf("iter %d: hist mismatch: JIT=%v Go=%v", iter, jitHist, goHist)
}
}
}
func TestFLACFastStereoSums(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(44))
for iter := 0; iter < 300; iter++ {
n := 1 + rng.Intn(256)
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)
}
// Go reference: compute the four sums.
var goSums [4]uint64
for i := 0; i < n; i++ {
l := left[i]
r := right[i]
side := l - r
mid := (l + r) >> 1
goSums[0] += foldAbs(l) + foldAbs(r)
goSums[1] += foldAbs(l) + foldAbs(side)
goSums[2] += foldAbs(side) + foldAbs(r)
goSums[3] += foldAbs(mid) + foldAbs(side)
}
var jitSums [4]uint64
args := make([]byte, 56)
PutPtr(args, 0, unsafe.Pointer(&left[0]))
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(cap(left)))
PutPtr(args, 24, unsafe.Pointer(&right[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(right)))
PutPtr(args, 48, unsafe.Pointer(&jitSums[0]))
_, err := k.CallFunc("fastStereoSumsAVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
if jitSums != goSums {
t.Fatalf("iter %d: sums mismatch:\n JIT=%v\n Go =%v", iter, jitSums, goSums)
}
}
}
func foldAbs(v int32) uint64 {
return uint64(uint32(v<<1) ^ uint32(v>>31))
}
// runAnalyzeTest is the shared harness for the analyzeO*Range family.
func runAnalyzeTest(t *testing.T, k *Kernel, name string, order int, ref func([]int32, []uint32, *[32]uint16) (uint64, bool)) {
t.Helper()
rng := rand.New(rand.NewSource(int64(order)*100 + 7))
for iter := 0; iter < 200; iter++ {
n := 1 + rng.Intn(128)
swin := make([]int32, n+order)
for i := range swin {
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
}
goDstP := make([]uint32, n)
var goHist [32]uint16
goSum, goOvf := ref(swin, goDstP, &goHist)
jitDstP := make([]uint32, n)
var jitHist [32]uint16
args := make([]byte, 72)
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
PutUint64(args, 8, uint64(len(swin)))
PutUint64(args, 16, uint64(cap(swin)))
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDstP)))
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
out, err := k.CallFunc(name, args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
jitSum := GetUint64(out, 56)
jitOvf := out[64] != 0
if jitSum != goSum {
t.Fatalf("iter %d: partSum: JIT=%d Go=%d", iter, jitSum, goSum)
}
if jitOvf != goOvf {
t.Fatalf("iter %d: overflow: JIT=%v Go=%v", iter, jitOvf, goOvf)
}
for i := range goDstP {
if jitDstP[i] != goDstP[i] {
t.Fatalf("iter %d: dstP[%d]: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
}
}
if jitHist != goHist {
t.Fatalf("iter %d: hist mismatch", iter)
}
}
}
func TestFLACAnalyzeO2Range(t *testing.T) {
k := loadFLACKernel(t)
runAnalyzeTest(t, k, "analyzeO2RangeAVX2", 2, analyzeO2RangeGo)
}
func TestFLACAnalyzeResRange(t *testing.T) {
k := loadFLACKernel(t)
// analyzeResRange has order 0: swin IS the residual (no prediction).
runAnalyzeTest(t, k, "analyzeResRangeAVX2", 0, analyzeResRangeGo)
}
func TestFLACAnalyzeO3Range(t *testing.T) {
k := loadFLACKernel(t)
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
swin = swin[:len(dstP)+3]
var partSum uint64
var overflow bool
for j := 0; j+3 < len(swin); j++ {
r := swin[j+3] - 3*swin[j+2] + 3*swin[j+1] - swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return partSum, overflow
}
runAnalyzeTest(t, k, "analyzeO3RangeAVX2", 3, ref)
}
func TestFLACAnalyzeO4Range(t *testing.T) {
k := loadFLACKernel(t)
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
swin = swin[:len(dstP)+4]
var partSum uint64
var overflow bool
for j := 0; j+4 < len(swin); j++ {
r := swin[j+4] - 4*swin[j+3] + 6*swin[j+2] - 4*swin[j+1] + swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return partSum, overflow
}
runAnalyzeTest(t, k, "analyzeO4RangeAVX2", 4, ref)
}
func TestFLACDecodeMono24(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(55))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]byte, 3*n)
rng.Read(src)
goDst := make([]int32, n)
decodeMono24Go(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("decodeMono24AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := range goDst {
if jitDst[i] != goDst[i] {
t.Fatalf("iter %d: dst[%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
}
}
}
}
func TestFLACDecodeStereo16(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(66))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]byte, 4*n) // [L0,R0,L1,R1,...]
rng.Read(src)
goLeft := make([]int32, n)
goRight := make([]int32, n)
decodeStereo16Go(src, goLeft, goRight)
jitLeft := make([]int32, n)
jitRight := make([]int32, n)
args := make([]byte, 72)
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(&jitLeft[0]))
PutPtr(args, 48, unsafe.Pointer(&jitRight[0]))
}
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitLeft)))
PutUint64(args, 56, uint64(n))
PutUint64(args, 64, uint64(cap(jitRight)))
_, err := k.CallFunc("decodeStereo16AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := 0; i < n; i++ {
if jitLeft[i] != goLeft[i] || jitRight[i] != goRight[i] {
t.Fatalf("iter %d: [%d] L: JIT=%d Go=%d; R: JIT=%d Go=%d",
iter, i, jitLeft[i], goLeft[i], jitRight[i], goRight[i])
}
}
}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// GroundTruth assembles the given .s file with the Go toolchain's own
// assembler and returns the machine code bytes for each TEXT function,
// keyed by the function's short name (the part after the middle dot).
// This is the universal oracle: any file that `go tool asm` accepts can
// be verified, with no hand-written reference.
func GroundTruth(path string) (map[string][]byte, error) {
goroot := runtime.GOROOT()
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
if _, err := os.Stat(asmBin); err != nil {
return nil, fmt.Errorf("verify: go tool asm not found at %s: %w", asmBin, err)
}
includeDir := filepath.Join(goroot, "pkg", "include")
// Create a temp file for the object output.
tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
if err != nil {
return nil, fmt.Errorf("verify: tempdir: %w", err)
}
defer os.RemoveAll(tmpDir)
objPath := filepath.Join(tmpDir, "out.o")
// Derive a package name from the file name (the assembler needs -p).
base := filepath.Base(path)
pkg := strings.TrimSuffix(base, ".s")
pkg = strings.TrimSuffix(pkg, "_amd64")
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("verify: go tool asm: %w\n%s", err, out)
}
objData, err := os.ReadFile(objPath)
if err != nil {
return nil, fmt.Errorf("verify: read object: %w", err)
}
return extractGOOBJCode(objData)
}
// GOOBJ block indices (cmd/internal/goobj).
const (
blkAutolib = iota
blkPkgIdx
blkFile
blkSymdef
blkHashed64def
blkHasheddef
blkNonpkgdef
blkNonpkgref
blkRefFlags
blkHash64
blkHash
blkRelocIdx
blkAuxIdx
blkDataIdx
blkReloc
blkAux
blkData
blkRefName
blkEnd
)
const goobjMagic = "\x00go120ld"
// extractGOOBJCode parses a GOOBJ payload and returns the code bytes for
// each non-package STEXT symbol (the functions).
func extractGOOBJCode(data []byte) (map[string][]byte, error) {
// Find the GOOBJ header (after the "go object ..." preamble).
i := bytes.Index(data, []byte(goobjMagic))
if i < 0 {
return nil, fmt.Errorf("verify: no GOOBJ magic in object file")
}
b := data[i:]
le := binary.LittleEndian
// Read block offsets (20 bytes into the header: 4 magic + 8 go version
// + 8 experiment = 20, then blkEnd+1 uint32 offsets).
var offs [blkEnd + 1]uint32
for j := 0; j <= blkEnd; j++ {
offs[j] = le.Uint32(b[20+4*j:])
}
blk := func(idx int) []byte { return b[offs[idx]:offs[idx+1]] }
// Parse non-package symbol definitions (blkNonpkgdef): each entry is
// 21 bytes: [nameLen:4][nameOff:4][abi:2][type:1][flag:1][flag2:1][size:4][align:4].
const symSize = 21
nonpkg := blk(blkNonpkgdef)
nSyms := len(nonpkg) / symSize
// Data index (blkDataIdx): one uint32 per defined symbol across ALL
// definition blocks (blkSymdef + blkHashed64def + blkHasheddef +
// blkNonpkgdef), in that order. We need the offset for the nonpkg
// symbols, which come last.
dataIdx := blk(blkDataIdx)
dataBlk := blk(blkData)
// Count symbols in the preceding definition blocks.
preceding := 0
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
preceding += len(blk(bi)) / symSize
}
// Symbol name offsets in the GOOBJ symbol table are absolute byte
// offsets from the start of the GOOBJ payload (the magic).
readStr := func(off, ln uint32) string {
if int(off+ln) > len(b) {
return ""
}
return string(b[off : off+ln])
}
result := make(map[string][]byte)
const kindSTEXT = 1
for s := 0; s < nSyms; s++ {
x := nonpkg[s*symSize:]
nameLen := le.Uint32(x[0:])
nameOff := le.Uint32(x[4:])
typ := x[10]
size := le.Uint32(x[13:])
if typ != kindSTEXT || size == 0 {
continue
}
name := readStr(nameOff, nameLen)
// Strip the package prefix (everything up to and including the
// last middle dot or period-dot).
name = stripPkg(name)
// Data offset from the index (nonpkg symbols follow the preceding blocks).
diIdx := preceding + s
if (diIdx+1)*4 > len(dataIdx) {
continue
}
dOff := le.Uint32(dataIdx[diIdx*4:])
if int(dOff+size) > len(dataBlk) {
continue
}
code := make([]byte, size)
copy(code, dataBlk[dOff:dOff+size])
result[name] = code
}
return result, nil
}
// stripPkg removes the package path prefix from a symbol name, leaving
// just the function name. "pkg/path·FuncName" → "FuncName".
func stripPkg(name string) string {
if i := strings.LastIndex(name, "\u00B7"); i >= 0 {
return name[i+len("\u00B7"):]
}
if i := strings.LastIndex(name, "\"."); i >= 0 {
return name[i+2:]
}
if i := strings.LastIndex(name, "."); i >= 0 {
return name[i+1:]
}
return name
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
)
func TestGroundTruthBasic(t *testing.T) {
// Use the simple test kernel — it assembles with go tool asm.
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("GroundTruth: %v", err)
}
if len(gt) == 0 {
t.Fatal("no functions extracted from ground truth")
}
// The "add" function should be present and non-empty.
code, ok := gt["add"]
if !ok {
t.Fatalf("function 'add' not found in ground truth; got: %v", keys(gt))
}
if len(code) == 0 {
t.Fatal("add: zero-length code")
}
t.Logf("ground truth functions: %v", keys(gt))
}
func TestGroundTruthComparison(t *testing.T) {
// Assemble with gasm and compare against go tool asm.
k, err := Load("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("Load: %v", err)
}
defer k.Close()
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("GroundTruth: %v", err)
}
for _, name := range k.FuncNames() {
fl, _ := k.Func(name)
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
goCode, ok := gt[name]
if !ok {
t.Errorf("%s: not in ground truth", name)
continue
}
if len(gasmCode) != len(goCode) {
t.Errorf("%s: size mismatch: gasm=%d go=%d", name, len(gasmCode), len(goCode))
continue
}
for i := range gasmCode {
if gasmCode[i] != goCode[i] {
t.Errorf("%s: byte %d differs: gasm=%02x go=%02x", name, i, gasmCode[i], goCode[i])
break
}
}
}
}
func TestGroundTruthBadFile(t *testing.T) {
_, err := GroundTruth("/nonexistent/file_amd64.s")
if err == nil {
t.Fatal("expected error for nonexistent file")
}
}
func keys(m map[string][]byte) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
+5
View File
@@ -120,3 +120,8 @@ func (k *Kernel) Close() {
k.exec.Unmap() k.exec.Unmap()
} }
} }
// Image returns the assembled image (code + data + metadata).
func (k *Kernel) Image() *asm.Image {
return k.img
}