2026-10-07 13:21:35 +02:00
|
|
|
// 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",
|
2026-10-07 19:28:44 +02:00
|
|
|
want: "invalid FENCE predecessor operand",
|
2026-10-07 13:21:35 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fence successor",
|
|
|
|
|
src: "\tFENCE W, Q\n",
|
2026-10-07 19:28:44 +02:00
|
|
|
want: "invalid FENCE successor operand",
|
2026-10-07 13:21:35 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|