feat(asm): pool the arm64 offsets the split bands cannot carry

Offsets beyond the split bands ride a per-function literal pool the way
the toolchain lays one out: a PC-relative literal load into REGTMP, then
the register-offset access (the pair family adds the base addition), the
pooled words appended after the last instruction behind the UNDEF guard,
deduplicated by value with the sign- and width-aware load selection.

The same differential pass against the corpus exposed three wrong-code
bugs and fixes them: the logical-immediate period marker rode the wrong
position for every element below 64 bits, so the 32-bit forms encoded a
different constant than written; the plain register operand of an
ADD/SUB against SP took the shifted-register form where the toolchain
uses the extended one with the identity extend, silently truncating
through UXTB; and the AUTIA1716 and AUTIB1716 hint constants were the
PACIA and PACIB encodings.  An offset sweep across every band boundary
now pins all three against the live oracle.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:36:24 +02:00
1 parent 71e8dd550d
commit a0fa7e802c
4 files changed
+381 -38

No files matched your search

+123
View File
@@ -5,7 +5,13 @@ package verify
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
@@ -30,6 +36,7 @@ func TestGroundTruthARM64(t *testing.T) {
"../testdata/verify/shifts_arm64.s",
"../testdata/verify/atomics_arm64.s",
"../testdata/verify/qmov_arm64.s",
"../testdata/verify/splits_arm64.s",
"../testdata/verify/crypto_arm64.s",
"../testdata/verify/integer_arm64.s",
"../testdata/verify/simd_arm64.s",
@@ -97,3 +104,119 @@ func TestGroundTruthARM64(t *testing.T) {
})
}
}
// 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))
}
}
}
})
}
}