Files
gasm-sdk/asm/riscv_csr_test.go
T
petrbalvin bffe408afa feat(asm): resolve every CSR name the toolchain knows
The riscv64 assembler carried fourteen hand-picked CSR names where the
toolchain resolves three hundred and twenty-nine: a CSRR/CSRW family
instruction naming any privileged register beyond the few base ones came
out as unknown CSR.  The table now carries the RISC-V privileged
specification's register set exactly as go tool asm spells it, and a
differential test assembles every name through both assemblers and
requires the words to agree byte for byte.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00

111 lines
3.3 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
)
// TestRISCVCSRDifferential proves the whole CSR name table against the
// toolchain at once: one TEXT whose body reads every register the table
// carries (CSRRW X0, NAME, X5), assembled by gasm and by go tool asm, must
// agree byte for byte. A single wrong address names its register through
// the first differing word: the CSR address occupies the instruction's top
// twelve bits, so 329 names spread over distinct words identify themselves.
func TestRISCVCSRDifferential(t *testing.T) {
names := make([]string, 0, len(riscvCSRNames))
for name := range riscvCSRNames {
names = append(names, name)
}
slices.Sort(names)
var body strings.Builder
for _, name := range names {
body.WriteString(fmt.Sprintf("\tCSRRW X0, %s, X5\n", name))
}
src := "#include \"textflag.h\"\n\nTEXT ·csrs(SB), NOSPLIT, $0\n" + body.String() + "\tRET\n"
dir := t.TempDir()
path := filepath.Join(dir, "csrs_riscv64.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
assertRISCVDifferential(t, path, src, "csrs")
}
// TestRISCVCSRPseudosDifferential pins the CSR pseudo spellings against the
// oracle: the write-only forms (source first, CSR second), their immediate
// variants and the CSRR read pseudo, one word each.
func TestRISCVCSRPseudosDifferential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·csrps(SB), NOSPLIT, $0
CSRS X5, TIME
CSRC X5, CYCLE
CSRW X5, INSTRET
CSRSI $1, TIME
CSRCI $2, CYCLE
CSRWI $3, INSTRET
CSRR VL, X10
CSRRW X0, TIME, X11
CSRRS X0, CYCLE, X12
CSRRC X0, INSTRET, X13
CSRRWI $4, TIME, X14
CSRRSI $5, CYCLE, X15
CSRRCI $6, INSTRET, X16
RET
`
dir := t.TempDir()
path := filepath.Join(dir, "csrps_riscv64.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
assertRISCVDifferential(t, path, src, "csrps")
}
// assertRISCVDifferential assembles the same source with gasm and with the
// toolchain for riscv64 and requires the named function's code bytes to
// agree. The live oracle is a deliberate-run comparison, so -short skips it
// (the push pipeline's mode); the golden bytes of the individual encoders
// are pinned separately in every mode.
func assertRISCVDifferential(t *testing.T, path, src, fn string) {
t.Helper()
oracle := oracleFuncCode(t, toolAsmObject(t, path, "riscv64"))
// The oracle keys its functions by the qualified object name
// (pkg.name); match on the local part.
want := map[string][]byte{}
for name, code := range oracle {
if _, after, ok := strings.Cut(name, "."); ok {
want[after] = code
} else {
want[name] = code
}
}
if want[fn] == nil {
t.Fatalf("the oracle object carries no function %q (has %v)", fn, keysOf(want))
}
f, perrs := parser.Parse(path, src)
if len(perrs) > 0 {
t.Fatalf("parse: %v", perrs[0])
}
img, err := AssembleFileRISCV(f)
if err != nil {
t.Fatalf("AssembleFileRISCV: %v", err)
}
got := trimTrailingZeroWords(img.Code)
wantB := trimTrailingZeroWords(want[fn])
if !slices.Equal(got, wantB) {
t.Errorf("%s: gasm and go tool asm disagree:\n gasm % x\n go % x", fn, got, wantB)
}
}