2026-08-20 13:32:52 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package verify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-10-06 21:41:57 +02:00
|
|
|
"encoding/binary"
|
|
|
|
|
"fmt"
|
2026-08-20 13:32:52 +02:00
|
|
|
"os"
|
2026-10-06 21:41:57 +02:00
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"slices"
|
|
|
|
|
"strings"
|
2026-08-20 13:32:52 +02:00
|
|
|
"testing"
|
|
|
|
|
|
2026-09-26 11:08:43 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
2026-08-20 13:32:52 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestGroundTruthARM64 assembles the arm64 test kernels with gasm and
|
|
|
|
|
// compares them byte-for-byte against `go tool asm` (GOARCH=arm64). The
|
|
|
|
|
// relocation fields of static-symbol references are masked before the
|
|
|
|
|
// comparison, since the toolchain leaves them zero for the linker.
|
|
|
|
|
func TestGroundTruthARM64(t *testing.T) {
|
|
|
|
|
for _, path := range []string{
|
|
|
|
|
"../testdata/verify/basic_arm64.s",
|
2026-08-20 14:07:12 +02:00
|
|
|
"../testdata/verify/fp_arm64.s",
|
2026-08-20 14:31:15 +02:00
|
|
|
"../testdata/verify/movimm_arm64.s",
|
|
|
|
|
"../testdata/verify/branch_arm64.s",
|
|
|
|
|
"../testdata/verify/call_arm64.s",
|
2026-09-14 23:25:14 +02:00
|
|
|
"../testdata/verify/bigframe_arm64.s",
|
|
|
|
|
"../testdata/verify/guard_arm64.s",
|
2026-09-19 19:17:07 +02:00
|
|
|
"../testdata/verify/indirect_arm64.s",
|
2026-09-20 06:45:03 +02:00
|
|
|
"../testdata/verify/exclusive_arm64.s",
|
|
|
|
|
"../testdata/verify/shifts_arm64.s",
|
|
|
|
|
"../testdata/verify/atomics_arm64.s",
|
2026-10-06 20:21:04 +02:00
|
|
|
"../testdata/verify/qmov_arm64.s",
|
2026-10-06 21:41:57 +02:00
|
|
|
"../testdata/verify/splits_arm64.s",
|
2026-10-06 23:30:40 +02:00
|
|
|
"../testdata/verify/regoffset_arm64.s",
|
2026-10-07 00:21:27 +02:00
|
|
|
"../testdata/verify/simdarr_arm64.s",
|
2026-09-20 06:45:03 +02:00
|
|
|
"../testdata/verify/crypto_arm64.s",
|
|
|
|
|
"../testdata/verify/integer_arm64.s",
|
|
|
|
|
"../testdata/verify/simd_arm64.s",
|
2026-09-20 14:25:47 +02:00
|
|
|
"../testdata/verify/widenimm_arm64.s",
|
|
|
|
|
"../testdata/verify/carryshift_arm64.s",
|
2026-09-20 21:17:20 +02:00
|
|
|
"../testdata/verify/simdmove_arm64.s",
|
|
|
|
|
"../testdata/verify/bookkeep_arm64.s",
|
2026-09-20 06:45:03 +02:00
|
|
|
"../testdata/verify/system_arm64.s",
|
2026-08-20 13:32:52 +02:00
|
|
|
} {
|
|
|
|
|
t.Run(path, func(t *testing.T) {
|
|
|
|
|
src, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read: %v", err)
|
|
|
|
|
}
|
|
|
|
|
f, errs := parser.Parse(path, string(src))
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
img, err := asm.AssembleFileARM64(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("AssembleFileARM64: %v", err)
|
|
|
|
|
}
|
|
|
|
|
gt, err := GroundTruthARM64(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GroundTruthARM64: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matched := 0
|
|
|
|
|
for _, fn := range img.Funcs {
|
|
|
|
|
gasmCode := maskRelocs(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
|
|
|
|
goCode, ok := gt[fn.Name]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Errorf("%s: not in ground truth (%d functions)", fn.Name, len(gt))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
goCode = maskRelocs(goCode, fn.Relocs)
|
|
|
|
|
// The Go toolchain may add zero padding at the end of
|
|
|
|
|
// functions. Compare up to the shorter length, then
|
|
|
|
|
// verify any trailing bytes are zero.
|
2026-08-29 15:40:31 +02:00
|
|
|
cmpLen := min(len(goCode), len(gasmCode))
|
2026-08-20 13:32:52 +02:00
|
|
|
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
|
|
|
|
t.Errorf("%s: MISMATCH gasm=%d go=%d bytes\n%s", fn.Name, len(gasmCode), len(goCode), diffHex(gasmCode, goCode))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Check trailing padding is zero.
|
|
|
|
|
trailingOK := true
|
|
|
|
|
if len(goCode) > len(gasmCode) {
|
|
|
|
|
for _, b := range goCode[len(gasmCode):] {
|
|
|
|
|
if b != 0 {
|
|
|
|
|
trailingOK = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !trailingOK {
|
|
|
|
|
t.Errorf("%s: non-zero trailing bytes in go tool asm output", fn.Name)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
matched++
|
|
|
|
|
t.Logf("%s: MATCH (%d bytes, go=%d)", fn.Name, fn.Size, len(goCode))
|
|
|
|
|
}
|
|
|
|
|
if matched == 0 {
|
|
|
|
|
t.Fatal("no functions matched")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-06 21:41:57 +02:00
|
|
|
|
|
|
|
|
// TestGroundTruthARM64OffsetSweep sweeps load/store offsets across every
|
|
|
|
|
// band boundary the encoder branches on and compares the assembled bytes
|
|
|
|
|
// against the toolchain for the whole sweep in one go tool asm run. The
|
|
|
|
|
// offsets the toolchain itself rejects are dropped first, so the comparison
|
|
|
|
|
// pins the split and single-instruction encodings, not the error parity.
|
|
|
|
|
func TestGroundTruthARM64OffsetSweep(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("live go tool asm oracle: skipped in -short mode")
|
|
|
|
|
}
|
|
|
|
|
goBin, err := exec.LookPath("go")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Skip("no Go toolchain available")
|
|
|
|
|
}
|
|
|
|
|
out, err := exec.Command(goBin, "env", "GOROOT").Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("go env GOROOT: %v", err)
|
|
|
|
|
}
|
|
|
|
|
includeDir := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
|
|
|
|
|
|
|
|
|
bands := []int64{
|
|
|
|
|
0, 255, 256, 4094, 4095, 4096, 4097, 8190, 8191, 8192,
|
|
|
|
|
16380, 16384, 32760, 32768, 65504, 65520, 65536, 65552,
|
|
|
|
|
0xfff000, 0xfff000 + 8190, 0xfff000 + 16380, 0xfff000 + 32760,
|
|
|
|
|
0xfff000 + 65520, 0xfffff8, 0xffffff, 0x1000000, 0x1006ff8,
|
|
|
|
|
0xfff000 + 0xfff<<4, 0x1000ffe,
|
|
|
|
|
}
|
|
|
|
|
var offs []int64
|
|
|
|
|
for _, b := range bands {
|
|
|
|
|
for _, d := range []int64{-8, -2, -1, 0, 1, 2, 8} {
|
|
|
|
|
if v := b + d; v >= 0 {
|
|
|
|
|
offs = append(offs, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offs = append(offs, -b-1, -b, -b+1)
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(offs)
|
|
|
|
|
offs = slices.Compact(offs)
|
|
|
|
|
|
|
|
|
|
// One function per mnemonic; a line the toolchain rejects (an unaligned
|
|
|
|
|
// band edge, a pool case gasm rejects until the pool lands) would fail
|
|
|
|
|
// the whole oracle run, so the sweep keeps only the offsets both
|
|
|
|
|
// assemblers accept: build per-mnemonic files and compare the ones that
|
|
|
|
|
// assemble on both sides.
|
|
|
|
|
for _, mnem := range []string{"MOVB", "MOVH", "MOVW", "MOVD", "FMOVS", "FMOVD", "FMOVQ", "LDP", "STP"} {
|
|
|
|
|
t.Run(mnem, func(t *testing.T) {
|
|
|
|
|
var body strings.Builder
|
|
|
|
|
for _, off := range offs {
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "LDP":
|
|
|
|
|
fmt.Fprintf(&body, "\t%s\t%d(R2), (R1, R3)\n", mnem, off)
|
|
|
|
|
case "STP":
|
|
|
|
|
fmt.Fprintf(&body, "\t%s\t(R1, R3), %d(R2)\n", mnem, off)
|
|
|
|
|
case "FMOVS", "FMOVD", "FMOVQ":
|
|
|
|
|
fmt.Fprintf(&body, "\t%s\tF1, %d(R2)\n", mnem, off)
|
|
|
|
|
default:
|
|
|
|
|
fmt.Fprintf(&body, "\t%s\tR1, %d(R2)\n", mnem, off)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
src := "TEXT ·f(SB), 7, $0-0\n" + body.String() + "\tRET\n"
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
path := filepath.Join(dir, "sweep.s")
|
|
|
|
|
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f, errs := parser.Parse(path, src)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
gasmImg, gasmErr := asm.AssembleFileARM64(f)
|
|
|
|
|
objPath := filepath.Join(dir, "oracle.o")
|
|
|
|
|
cmd := exec.Command(goBin, "tool", "asm", "-I", includeDir, "-p", "sweep", "-o", objPath, path)
|
|
|
|
|
cmd.Env = append(os.Environ(), "GOARCH=arm64")
|
|
|
|
|
oracleOut, oracleErr := cmd.CombinedOutput()
|
|
|
|
|
if gasmErr != nil && oracleErr != nil {
|
|
|
|
|
return // both reject: agreement on the boundary
|
|
|
|
|
}
|
|
|
|
|
if gasmErr != nil {
|
|
|
|
|
t.Fatalf("gasm rejects what the toolchain accepts:\n%v", gasmErr)
|
|
|
|
|
}
|
|
|
|
|
if oracleErr != nil {
|
|
|
|
|
t.Fatalf("gasm accepts what the toolchain rejects:\n%s", oracleOut)
|
|
|
|
|
}
|
|
|
|
|
gt, err := GroundTruthARM64(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ground truth: %v", err)
|
|
|
|
|
}
|
|
|
|
|
goCode, ok := gt["f"]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("oracle function missing")
|
|
|
|
|
}
|
|
|
|
|
for _, fn := range gasmImg.Funcs {
|
|
|
|
|
if fn.Name != "f" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
gasmCode := gasmImg.Code[fn.Offset : fn.Offset+fn.Size]
|
|
|
|
|
cmpLen := min(len(goCode), len(gasmCode))
|
|
|
|
|
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
|
|
|
|
for w := 0; w < cmpLen/4; w++ {
|
|
|
|
|
g := binary.LittleEndian.Uint32(gasmCode[w*4 : w*4+4])
|
|
|
|
|
o := binary.LittleEndian.Uint32(goCode[w*4 : w*4+4])
|
|
|
|
|
if g != o {
|
|
|
|
|
t.Fatalf("word %d (offset %d) differs: gasm %#08x go %#08x", w, w*4, g, o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
t.Fatalf("prefixes equal but lengths differ (gasm %d, go %d)", len(gasmCode), len(goCode))
|
|
|
|
|
}
|
|
|
|
|
for _, b := range goCode[len(gasmCode):] {
|
|
|
|
|
if b != 0 {
|
|
|
|
|
t.Fatalf("non-zero trailing oracle bytes (gasm %d, go %d)", len(gasmCode), len(goCode))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|