test(asm): pin the riscv64 tail against the toolchain byte for byte

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:51:02 +02:00
1 parent 9d50212a71
commit e9510e8a68
1 file changed
+267
+267
View File
@@ -0,0 +1,267 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// writeRISCVSrc materialises a source in the test's temp dir under a name the
// architecture scanners recognise, returning its path.
func writeRISCVSrc(t *testing.T, name, src string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
return path
}
// TestRISCVToolchainFile_Differential assembles the toolchain's own riscv64
// testdata (cmd/asm's 2000-line riscv64.s, every instruction family the
// assembler knows) with gasm and with go tool asm and requires the function's
// bytes to agree word for word: the whole corpus in one gate.
func TestRISCVToolchainFile_Differential(t *testing.T) {
if testing.Short() {
t.Skip("live go tool asm oracle: skipped in -short mode")
}
out, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
t.Skipf("no GOROOT: %v", err)
}
path := filepath.Join(strings.TrimSpace(string(out)),
"src", "cmd", "asm", "internal", "asm", "testdata", "riscv64.s")
data, err := os.ReadFile(path)
if err != nil {
t.Skipf("toolchain testdata not found: %v", err)
}
// The file reaches textflag.h through a repository-relative include,
// which the temp copy cannot resolve; the toolchain's include path
// carries the same header under its canonical name.
src := strings.ReplaceAll(string(data),
"#include \"../../../../../runtime/textflag.h\"",
"#include \"textflag.h\"")
local := writeRISCVSrc(t, "corpus_riscv64.s", src)
assertRISCVDifferential(t, local, src, "asmtest")
}
// TestRISCVRVCTailCompress_Differential proves the compress pass's tail
// against the toolchain: the two-operand arithmetic forms, the negated
// SUBW immediate on its compressed ADDIW, and the C.ADD that closes the
// large-immediate expansion when rd == rs1.
func TestRISCVRVCTailCompress_Differential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·tail(SB), NOSPLIT, $0
ADD X5, X6 // c.add
ADD X6, X5 // c.add
ADD X0, X6 // uncompressed: rd prime to rs1 zero
ADD X5, X0 // uncompressed: rs2 zero
SUB X9, X8 // c.sub
SUBW X9, X8 // c.subw
ADDW X9, X8 // c.addw
AND X9, X8 // c.and
OR X9, X8 // c.or
XOR X9, X8 // c.xor
AND X5, X6 // uncompressed: outside the prime registers
SUB $1, X6 // c.addi -1
SUBW $1, X6 // c.addiw -1
ADDI $4095, X5 // expansion closing on c.add
ADDI $-4097, X5 // expansion closing on c.add
MOV X0, X6 // c.li $0
MOV X5, X6 // c.mv
RET
`
path := writeRISCVSrc(t, "tail_riscv64.s", src)
assertRISCVDifferential(t, path, src, "tail")
}
// TestRISCVMovWidths_Differential proves the register-to-register width
// moves: MOVW as ADDIW, MOVBU as ANDI $255, the sign-extending MOVB and MOVH
// and zero-extending MOVHU and MOVWU pairs, with the per-half compression
// the toolchain's compress pass picks on the prime registers.
func TestRISCVMovWidths_Differential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·widths(SB), NOSPLIT, $0
MOVB X5, X6 // slli 56 + srai 56
MOVH X5, X6 // slli 48 + srai 48
MOVW X5, X6 // addiw $0
MOVBU X5, X6 // andi $255
MOVHU X5, X6 // slli 48 + srli 48
MOVWU X5, X6 // slli 32 + srli 32
MOVB X8, X9 // slli + compressed c.srai
MOVHU X8, X9 // slli + compressed c.srli
MOVB X6, X6 // compressed c.slli first half
MOVW X9, X8 // addiw $0, uncompressed
RET
`
path := writeRISCVSrc(t, "widths_riscv64.s", src)
assertRISCVDifferential(t, path, src, "widths")
}
// TestRISCVMovBanks_Differential proves the floating-point move routing: the
// cross-bank FMV forms in both directions and widths, the in-bank FSGNJ
// moves, the zero constant through FMV from X0 and the pooled $f32/$f64
// constants through AUIPC + FLW/FLD.
func TestRISCVMovBanks_Differential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·banks(SB), NOSPLIT, $0
MOVF X1, F3 // fmv.w.x
MOVF F3, X1 // fmv.x.w
MOVD X1, F3 // fmv.d.x
MOVD F3, X1 // fmv.x.d
MOVF F0, F1 // fsgnj.s
MOVD F1, F2 // fsgnj.d
MOVF $(0.0), F3 // fmv.w.x from X0
MOVD $(0.0), F3 // fmv.d.x from X0
MOVF $(709.78271289338397), F3 // auipc + flw, pooled $f32
MOVD $(709.78271289338397), F3 // auipc + fld, pooled $f64
RET
`
path := writeRISCVSrc(t, "banks_riscv64.s", src)
assertRISCVDifferential(t, path, src, "banks")
}
// TestRISCVFenceFlags_Differential proves the FENCE flag operands: the
// sixteen IORW spellings pack into the predecessor and successor nibbles,
// the bare form keeps iorw, iorw, and FENCE.TSO keeps its mode.
func TestRISCVFenceFlags_Differential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·fences(SB), NOSPLIT, $0
FENCE
FENCE W, W
FENCE I, O
FENCE IORW, IORW
FENCE RW, IR
FENCE OW, IO
FENCE.TSO
RET
`
path := writeRISCVSrc(t, "fences_riscv64.s", src)
assertRISCVDifferential(t, path, src, "fences")
}
// TestRISCVCSRImmediate_Differential proves the immediate CSR pseudos: the
// write-only spellings pick the immediate opcode from the source's kind, so
// CSRW $2 assembles byte-identically to CSRRWI, in both the two-operand
// pseudo form and the three-operand full form.
func TestRISCVCSRImmediate_Differential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·csrs(SB), NOSPLIT, $0
CSRW $2, VSTART
CSRS $2, VSTART
CSRC $2, VSTART
CSRWI $2, VSTART
CSRSI $2, VSTART
CSRCI $2, VSTART
CSRW X5, VSTART
CSRRW $2, TIME, X5
CSRRS $2, TIME, X5
CSRRC $2, TIME, X5
CSRRWI $2, TIME, X5
CSRRSI $2, TIME, X5
CSRRCI $2, TIME, X5
CSRRW X10, TIME, X5
RET
`
path := writeRISCVSrc(t, "csrs_riscv64.s", src)
assertRISCVDifferential(t, path, src, "csrs")
}
// TestRISCVTLSDifferential proves the local-exec TLS sequence: a symbol the
// file declares TLSBSS resolves through LUI + ADDIW (one R_RISCV_TLS_LE
// field), the ADD of the thread pointer, and the access through TMP, for
// every width in both directions.
func TestRISCVTLSDifferential(t *testing.T) {
src := `#include "textflag.h"
GLOBL tls(SB), TLSBSS, $8
TEXT ·tlsle(SB), NOSPLIT, $0
MOV tls(SB), X5
MOVB tls(SB), X5
MOV X5, tls(SB)
MOVB X5, tls(SB)
RET
`
path := writeRISCVSrc(t, "tlsle_riscv64.s", src)
assertRISCVDifferential(t, path, src, "tlsle")
}
// TestRISCVToolchainRejections pins the error parity of the new forms: the
// toolchain refuses an unknown FENCE flag and a register where the immediate
// CSR pseudos want a five-bit unsigned immediate, and so does the encoder.
func TestRISCVToolchainRejections(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{
name: "fence predecessor",
src: "\tFENCE X, W\n",
want: "FENCE: invalid predecessor operand",
},
{
name: "fence successor",
src: "\tFENCE W, Q\n",
want: "FENCE: invalid successor operand",
},
{
name: "immediate CSR pseudo with register",
src: "\tCSRWI X5, VSTART\n",
want: "CSRWI expects an immediate source",
},
{
name: "MOV width between banks",
src: "\tMOVB X5, F3\n",
want: "MOVB: expected integer register in rd position",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fn := firstTextRISCV(t, "#include \"textflag.h\"\n\nTEXT ·r(SB), NOSPLIT, $0\n"+tc.src+"\tRET\n")
_, _, _, _, _, _, err := assembleRISCV(fn, nil)
if err == nil {
t.Fatalf("source assembled, want rejection %q", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("error %q does not carry %q", err.Error(), tc.want)
}
})
}
}
// TestRISCVTLSRelocKind pins the relocation the TLS sequence records: one
// R_RISCV_TLS_LE field spanning the LUI + ADDIW pair, the symbol named and
// no addend for a plain tls(SB) reference.
func TestRISCVTLSRelocKind(t *testing.T) {
fn := firstTextRISCV(t, `#include "textflag.h"
GLOBL tls(SB), TLSBSS, $8
TEXT ·t(SB), NOSPLIT, $0
MOV tls(SB), X5
RET
`)
_, _, relocs, _, _, _, err := assembleRISCV(fn, map[string]bool{"tls": true})
if err != nil {
t.Fatal(err)
}
if len(relocs) != 1 {
t.Fatalf("got %d relocs, want 1", len(relocs))
}
r := relocs[0]
if r.Kind != RelRISCVTLSLE || r.Name != "tls" || r.Off != 0 || r.After != 8 {
t.Errorf("reloc = %+v, want TLS_LE tls [0, 8)", r)
}
}