fix(lint): exempt shift counts, SETcc and ABIInternal from false positives
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
@@ -21,6 +22,21 @@ func lintSrc(t *testing.T, src string) []Diagnostic {
|
||||
return File(f, Config{Arch: arch.AMD64})
|
||||
}
|
||||
|
||||
// lintArchFile parses and lints src under a, then hands the same file to
|
||||
// assemble so the assertion is pinned against the encoder: a kernel the
|
||||
// linter reasons about must also be one the encoder accepts.
|
||||
func lintArchFile(t *testing.T, filename, src string, a arch.Arch, assemble func(*ast.File) (*asm.Image, error)) []Diagnostic {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse(filename, src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if _, err := assemble(f); err != nil {
|
||||
t.Fatalf("encoder rejects the kernel: %v", err)
|
||||
}
|
||||
return File(f, Config{Arch: a})
|
||||
}
|
||||
|
||||
// lintSrcArch lints src under the architecture inferred from filename.
|
||||
func lintSrcArch(t *testing.T, filename, src string) []Diagnostic {
|
||||
t.Helper()
|
||||
@@ -262,6 +278,138 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
func TestRiscvBranchFamilyRegistersLabels(t *testing.T) {
|
||||
// Every riscv64 pseudo-branch that references a label must register that
|
||||
// reference: the reversed branches BGT/BGTU/BLE/BLEU (GOROOT's
|
||||
// memmove_riscv64 branches with BGTU) and a label named like the ZERO
|
||||
// register alias (GOROOT's memclr_riscv64 carries a label named zero;
|
||||
// ZERO is the ABI name of X0) must not be reported unused.
|
||||
diags := lintSrcArch(t, "f_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
BGTU X10, X11, backward
|
||||
BGT X10, X11, zero
|
||||
BLE X10, X11, one
|
||||
BLEU X10, X11, two
|
||||
BEQZ X10, zero
|
||||
BNEZ X10, one
|
||||
JMP two
|
||||
backward:
|
||||
RET
|
||||
zero:
|
||||
RET
|
||||
one:
|
||||
RET
|
||||
two:
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeUnusedLabel] != 0 {
|
||||
t.Fatalf("branch-referenced labels must not be flagged unused: %+v", diags)
|
||||
}
|
||||
if codes(diags)[CodeUndefinedLabel] != 0 {
|
||||
t.Fatalf("defined labels must resolve: %+v", diags)
|
||||
}
|
||||
|
||||
// A branch to a truly undefined label still reports.
|
||||
diags = lintSrcArch(t, "f_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
BGT X10, X11, nowhere
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeUndefinedLabel] != 1 {
|
||||
t.Fatalf("undefined branch target must be flagged: %+v", diags)
|
||||
}
|
||||
|
||||
// A register-indirect JALR is not a label reference.
|
||||
diags = lintSrcArch(t, "f_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
JALR X1
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeUndefinedLabel] != 0 {
|
||||
t.Fatalf("register operand of JALR is not a label: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoong64BranchFamilyRegistersLabels(t *testing.T) {
|
||||
// The loong64 jumps and single-register branches (JAL, B, BL, BEQZ/BNEZ,
|
||||
// BFPT/BFPF) all reference their label from the last operand; GOROOT's
|
||||
// own basic kernels tail-call with JAL, so the reference must register.
|
||||
diags := lintSrcArch(t, "f_loong64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
BEQZ R4, fin
|
||||
BNEZ R4, fin
|
||||
BLTZ R4, fin
|
||||
JAL fin
|
||||
BL fin
|
||||
B fin
|
||||
RET
|
||||
fin:
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeUnusedLabel] != 0 {
|
||||
t.Fatalf("branch-referenced labels must not be flagged unused: %+v", diags)
|
||||
}
|
||||
if codes(diags)[CodeUndefinedLabel] != 0 {
|
||||
t.Fatalf("defined labels must resolve: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBranchFamiliesAssemble pins the lint branch sets to the encoder: every
|
||||
// mnemonic the linter classifies as a riscv64 or loong64 label branch must be
|
||||
// a branch the encoder actually assembles, with the label in the last
|
||||
// operand. If the encoder gains or renames a branch, this test fails and the
|
||||
// set follows it.
|
||||
func TestBranchFamiliesAssemble(t *testing.T) {
|
||||
riscvForms := map[string]string{}
|
||||
for m := range riscvBranches {
|
||||
riscvForms[m] = m + " X10, X11, tgt"
|
||||
}
|
||||
for _, m := range []string{"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ"} {
|
||||
riscvForms[m] = m + " X10, tgt"
|
||||
}
|
||||
riscvForms["JMP"] = "JMP tgt"
|
||||
riscvForms["JAL"] = "JAL tgt"
|
||||
|
||||
loongForms := map[string]string{}
|
||||
for _, m := range []string{"BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU"} {
|
||||
loongForms[m] = m + " R4, R5, tgt"
|
||||
}
|
||||
for _, m := range []string{"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ", "BFPT", "BFPF"} {
|
||||
loongForms[m] = m + " R4, tgt"
|
||||
}
|
||||
loongForms["JMP"] = "JMP tgt"
|
||||
loongForms["B"] = "B tgt"
|
||||
loongForms["JAL"] = "JAL tgt"
|
||||
loongForms["BL"] = "BL tgt"
|
||||
|
||||
for m, form := range riscvForms {
|
||||
src := "#include \"textflag.h\"\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||
"\t" + form + "\n" +
|
||||
"tgt:\n" +
|
||||
"\tRET\n"
|
||||
diags := lintArchFile(t, "f_riscv64.s", src, arch.RISCV, asm.AssembleFileRISCV)
|
||||
if codes(diags)[CodeUnusedLabel] != 0 || codes(diags)[CodeUndefinedLabel] != 0 {
|
||||
t.Errorf("riscv64 %s: label reference not registered: %+v", m, diags)
|
||||
}
|
||||
}
|
||||
for m, form := range loongForms {
|
||||
src := "#include \"textflag.h\"\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||
"\t" + form + "\n" +
|
||||
"tgt:\n" +
|
||||
"\tRET\n"
|
||||
diags := lintArchFile(t, "f_loong64.s", src, arch.LOONG64, asm.AssembleFileLOONG64)
|
||||
if codes(diags)[CodeUnusedLabel] != 0 || codes(diags)[CodeUndefinedLabel] != 0 {
|
||||
t.Errorf("loong64 %s: label reference not registered: %+v", m, diags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidTextflag(t *testing.T) {
|
||||
diags := lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
@@ -413,6 +561,72 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWidthShiftCount(t *testing.T) {
|
||||
// The shift and rotate count lives in CL by ISA definition (the D2/D3
|
||||
// group encodes the count outside the ModRM register field), so the count
|
||||
// operand is 8-bit no matter how wide the data is: SHLQ CL, AX is the
|
||||
// normal spelling of a 64-bit shift. The data operand keeps its check.
|
||||
diags := lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
SHLQ CL, AX
|
||||
SHRL CL, BX
|
||||
SARQ CL, CX
|
||||
ROLL CL, DX
|
||||
RORQ CL, R8
|
||||
RCLL CL, R9
|
||||
RCRQ CL, R10
|
||||
MOVQ CL, R10
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeRegisterWidthMismatch] != 1 {
|
||||
t.Fatalf("only the MOVQ CL data move must be flagged, got %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWidthSetcc(t *testing.T) {
|
||||
// A SETcc stores one byte whichever condition it tests (0F 90+cc), so
|
||||
// SETNE AL is always right and the trailing letters of SETEQ, SETPL and
|
||||
// SETLS are condition codes, not width suffixes.
|
||||
diags := lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
CMPQ AX, BX
|
||||
SETNE AL
|
||||
SETEQ AL
|
||||
SETPL AL
|
||||
SETLS AL
|
||||
SETCC (BX)
|
||||
SETGE (R8)
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeRegisterWidthMismatch] != 0 {
|
||||
t.Fatalf("SETcc destinations are 8-bit by definition: %+v", diags)
|
||||
}
|
||||
if codes(diags)[CodeUnknownInstr] != 0 {
|
||||
t.Fatalf("every SETcc spelling must be known: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWidthFrameNames(t *testing.T) {
|
||||
// GOROOT's BSD syscall stubs carry frame parameters whose names collide
|
||||
// with byte register names (kevent's ch and nch): MOVQ ch+8(FP), SI is a
|
||||
// frame reference, not the CH register.
|
||||
diags := lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·kevent(SB), NOSPLIT, $0-36
|
||||
MOVL kq+0(FP), DI
|
||||
MOVQ ch+8(FP), SI
|
||||
MOVL nch+16(FP), DX
|
||||
MOVQ ev+24(FP), R10
|
||||
MOVQ AX, ret+32(FP)
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeRegisterWidthMismatch] != 0 {
|
||||
t.Fatalf("frame and static symbol names are not registers: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonportableRegisterName(t *testing.T) {
|
||||
diags := lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
|
||||
Reference in New Issue
Block a user