diff --git a/asm/riscv_toolchain_test.go b/asm/riscv_toolchain_test.go new file mode 100644 index 0000000..faa8cf1 --- /dev/null +++ b/asm/riscv_toolchain_test.go @@ -0,0 +1,267 @@ +// Copyright (c) 2026 Petr Balvín (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) + } +}