test(asm): pin the riscv64 tail against the toolchain byte for byte
Assisted-by: GLM 5.3
This commit is contained in:
1 parent
9d50212a71
commit
e9510e8a68
1 file changed
+267
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user