// Copyright (c) 2026 Petr Balvín (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) } }