// 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: "invalid FENCE predecessor operand", }, { name: "fence successor", src: "\tFENCE W, Q\n", want: "invalid FENCE 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) } }