225 lines
7.5 KiB
Go
225 lines
7.5 KiB
Go
// 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"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||
|
|
)
|
||
|
|
|
||
|
|
// riscv64ErrorCatalogues are the toolchain's own negative-case files for
|
||
|
|
// riscv64, relative to cmd/asm's testdata: riscv64error.s carries the
|
||
|
|
// preprocess-stage rejections and riscv64validation.s the validate-stage
|
||
|
|
// ones (register banks, compressed constraints, vector shapes).
|
||
|
|
var riscv64ErrorCatalogues = []string{"riscv64error.s", "riscv64validation.s"}
|
||
|
|
|
||
|
|
// riscv64AcceptedErrorShapes lists the toolchain's catalogue spellings gasm
|
||
|
|
// still accepts, each an acceptance superset with a documented reason. The
|
||
|
|
// list only shrinks: every tightening of the encoder moves spellings out of
|
||
|
|
// it, and a spelling reappearing here means a regression.
|
||
|
|
var riscv64AcceptedErrorShapes = []string{}
|
||
|
|
|
||
|
|
// TestRISCVToolchainErrorParity walks the toolchain's riscv64error.s and
|
||
|
|
// riscv64validation.s (Go 1.27) and requires gasm to reject every case the
|
||
|
|
// toolchain rejects, with an equivalent diagnostic, the documented acceptance
|
||
|
|
// supersets above excepted. A live Go toolchain is needed for the source
|
||
|
|
// files; the test skips without one or in -short.
|
||
|
|
func TestRISCVToolchainErrorParity(t *testing.T) {
|
||
|
|
goroot := riscv64Goroot(t)
|
||
|
|
dir := filepath.Join(goroot, "src", "cmd", "asm", "internal", "asm", "testdata")
|
||
|
|
allowed := map[string]bool{}
|
||
|
|
for _, s := range riscv64AcceptedErrorShapes {
|
||
|
|
allowed[s] = true
|
||
|
|
}
|
||
|
|
for _, name := range riscv64ErrorCatalogues {
|
||
|
|
data, err := os.ReadFile(filepath.Join(dir, name))
|
||
|
|
if err != nil {
|
||
|
|
t.Skip(err)
|
||
|
|
}
|
||
|
|
for raw := range strings.SplitSeq(string(data), "\n") {
|
||
|
|
line := strings.TrimSpace(raw)
|
||
|
|
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "TEXT") || !strings.Contains(line, "ERROR") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
body := line
|
||
|
|
if i := strings.Index(body, "//"); i >= 0 {
|
||
|
|
body = strings.TrimSpace(body[:i])
|
||
|
|
}
|
||
|
|
want := ""
|
||
|
|
if m := regexp.MustCompile(`ERROR "([^"]*)"`).FindStringSubmatch(line); m != nil {
|
||
|
|
want = m[1]
|
||
|
|
}
|
||
|
|
body = strings.ReplaceAll(body, "\t", " ")
|
||
|
|
body = strings.Join(strings.Fields(body), " ")
|
||
|
|
// A symbol reference needs the file's own declarations, which a
|
||
|
|
// one-line probe cannot carry.
|
||
|
|
if strings.Contains(body, "(SB)") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||
|
|
f, perr := parser.Parse("errorparity.s", src)
|
||
|
|
if len(perr) > 0 {
|
||
|
|
continue // the parser already rejects the spelling
|
||
|
|
}
|
||
|
|
_, aerr := AssembleFileRISCV(f)
|
||
|
|
if aerr == nil {
|
||
|
|
if !allowed[body] {
|
||
|
|
t.Errorf("gasm accepts what the toolchain rejects: %s", body)
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if want != "" && !riscv64DiagEquivalent(want, aerr.Error()) {
|
||
|
|
t.Errorf("gasm rejects %s with an inequivalent diagnostic:\n toolchain: %s\n gasm: %s", body, want, aerr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64DiagEquivalent answers whether gasm's rejection carries the same
|
||
|
|
// information as the toolchain's expected message. The two word the same
|
||
|
|
// facts differently: the toolchain writes "immediate out of range 0 to 31"
|
||
|
|
// where gasm may spell the field's own name, and gasm prefixes the mnemonic.
|
||
|
|
// Canonicalisation drops the subjects and connectives and compares the
|
||
|
|
// remaining token sequence.
|
||
|
|
func riscv64DiagEquivalent(want, got string) bool {
|
||
|
|
return riscv64DiagSubseq(riscv64DiagTokens(want), riscv64DiagTokens(got))
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64DiagTokens lower-cases a diagnostic, strips punctuation and the
|
||
|
|
// connective tokens, and returns its words. "to" survives only between
|
||
|
|
// digits, where the range wording uses it.
|
||
|
|
func riscv64DiagTokens(msg string) []string {
|
||
|
|
msg = strings.ToLower(msg)
|
||
|
|
for _, r := range []string{"[", "]", ",", ".", ":", "\n", "(", ")"} {
|
||
|
|
msg = strings.ReplaceAll(msg, r, " ")
|
||
|
|
}
|
||
|
|
fields := strings.Fields(msg)
|
||
|
|
out := make([]string, 0, len(fields))
|
||
|
|
for i, w := range fields {
|
||
|
|
switch w {
|
||
|
|
case "the", "a", "operand", "immediate", "an", "for":
|
||
|
|
// subjects and articles carry no constraint
|
||
|
|
case "to":
|
||
|
|
if i > 0 && i+1 < len(fields) && riscv64IsNumeric(fields[i-1]) && riscv64IsNumeric(fields[i+1]) {
|
||
|
|
continue // the range connective
|
||
|
|
}
|
||
|
|
out = append(out, w)
|
||
|
|
default:
|
||
|
|
out = append(out, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64DiagSubseq answers whether want is a subsequence of got.
|
||
|
|
func riscv64DiagSubseq(want, got []string) bool {
|
||
|
|
i := 0
|
||
|
|
for _, w := range got {
|
||
|
|
if i < len(want) && w == want[i] {
|
||
|
|
i++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return i == len(want)
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64IsNumeric reports whether s parses as a signed decimal number, the
|
||
|
|
// token shape the range connective sits between.
|
||
|
|
func riscv64IsNumeric(s string) bool {
|
||
|
|
if s == "" {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if s[0] == '-' || s[0] == '+' {
|
||
|
|
s = s[1:]
|
||
|
|
}
|
||
|
|
if s == "" {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
for _, r := range s {
|
||
|
|
if r < '0' || r > '9' {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64Goroot resolves the live toolchain root, the environment's own value
|
||
|
|
// first, `go env GOROOT` as the fallback.
|
||
|
|
func riscv64Goroot(t *testing.T) string {
|
||
|
|
t.Helper()
|
||
|
|
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||
|
|
return goroot
|
||
|
|
}
|
||
|
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("no GOROOT: %v", err)
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(string(out))
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64RejectedNames pins the names the toolchain's riscv64 table carries
|
||
|
|
// but refuses to encode: each is a "no encoding for instruction" under every
|
||
|
|
// operand shape GOARCH=riscv64 go tool asm accepts. gasm rejects them too,
|
||
|
|
// and the pair of rejections is the parity this catalogue asserts.
|
||
|
|
var riscv64RejectedNames = []string{"DUFFCOPY", "DUFFZERO", "PCALIGNMAX"}
|
||
|
|
|
||
|
|
// TestRISCVBacklogNameParity walks the audit's known-but-unencodable
|
||
|
|
// backlog: the names go tool asm recognises on riscv64 yet refuses to encode
|
||
|
|
// must be refused by gasm as well.
|
||
|
|
func TestRISCVBacklogNameParity(t *testing.T) {
|
||
|
|
if testing.Short() {
|
||
|
|
t.Skip("live toolchain backlog: skipped in -short mode")
|
||
|
|
}
|
||
|
|
for _, name := range riscv64RejectedNames {
|
||
|
|
if riscv64ProbeAssembles(name + " X5") {
|
||
|
|
t.Errorf("gasm encodes %s, which the toolchain refuses under every shape", name)
|
||
|
|
}
|
||
|
|
if riscv64ProbeAssembles(name) {
|
||
|
|
t.Errorf("gasm encodes %s, which the toolchain refuses under every shape", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64ProbeAssembles answers whether a single statement wrapped in its
|
||
|
|
// own function parses and assembles on riscv64.
|
||
|
|
func riscv64ProbeAssembles(body string) bool {
|
||
|
|
src := "#include \"textflag.h\"\n\nTEXT ·probe(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||
|
|
f, perr := parser.Parse("backlogparity.s", src)
|
||
|
|
if len(perr) > 0 {
|
||
|
|
return false // the parser already rejects the line
|
||
|
|
}
|
||
|
|
_, aerr := AssembleFileRISCV(f)
|
||
|
|
return aerr == nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// riscv64CatalogueSelfCheck guards the catalogue walk itself: every ERROR
|
||
|
|
// line in the two files must yield a probe body, so a formatting change in
|
||
|
|
// the toolchain's files cannot silently empty the parity set.
|
||
|
|
func TestRISCVErrorCatalogueSelfCheck(t *testing.T) {
|
||
|
|
if testing.Short() {
|
||
|
|
t.Skip("live toolchain catalogue: skipped in -short mode")
|
||
|
|
}
|
||
|
|
goroot := riscv64Goroot(t)
|
||
|
|
dir := filepath.Join(goroot, "src", "cmd", "asm", "internal", "asm", "testdata")
|
||
|
|
total := 0
|
||
|
|
for _, name := range riscv64ErrorCatalogues {
|
||
|
|
data, err := os.ReadFile(filepath.Join(dir, name))
|
||
|
|
if err != nil {
|
||
|
|
t.Skip(err)
|
||
|
|
}
|
||
|
|
n := strings.Count(string(data), "ERROR")
|
||
|
|
total += n
|
||
|
|
if n == 0 {
|
||
|
|
t.Errorf("%s carries no ERROR lines; the catalogue walk is empty", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if total < 1000 {
|
||
|
|
t.Errorf("catalogue carries %d ERROR lines, want at least 1000", total)
|
||
|
|
}
|
||
|
|
}
|