2026-07-06 09:49:50 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package lint
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
2026-09-20 11:40:39 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
2026-09-19 19:17:07 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
2026-07-06 09:49:50 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func lintSrc(t *testing.T, src string) []Diagnostic {
|
|
|
|
|
t.Helper()
|
|
|
|
|
f, errs := parser.Parse("test_amd64.s", src)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
return File(f, Config{Arch: arch.AMD64})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 11:40:39 +02:00
|
|
|
// 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})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
// lintSrcArch lints src under the architecture inferred from filename.
|
|
|
|
|
func lintSrcArch(t *testing.T, filename, src string) []Diagnostic {
|
|
|
|
|
t.Helper()
|
|
|
|
|
f, errs := parser.Parse(filename, src)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
return File(f, Config{Arch: arch.FromFilename(filename)})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func codes(diags []Diagnostic) map[string]int {
|
|
|
|
|
m := map[string]int{}
|
|
|
|
|
for _, d := range diags {
|
|
|
|
|
m[d.Code]++
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFixtureIsClean(t *testing.T) {
|
|
|
|
|
src, err := os.ReadFile("../testdata/sample_amd64.s")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f, errs := parser.Parse("sample_amd64.s", string(src))
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
2026-07-11 17:36:52 +02:00
|
|
|
// The fixture mirrors the go-flac kernels, which write the Go ABI0
|
2026-09-16 23:12:31 +02:00
|
|
|
// scratch registers (BX, R13) without saving them; legal under Go's
|
2026-07-11 17:36:52 +02:00
|
|
|
// stack-based ABI, so the register-clobber audit stays silent and the
|
|
|
|
|
// fixture must lint entirely clean.
|
|
|
|
|
diags := File(f, Config{Arch: arch.AMD64})
|
2026-07-06 09:49:50 +02:00
|
|
|
if len(diags) != 0 {
|
|
|
|
|
t.Fatalf("expected no diagnostics on the fixture, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUnknownInstruction(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
FOOBAR AX, BX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 1 {
|
|
|
|
|
t.Fatalf("want one unknown-instruction, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUndefinedLabel(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
JMP nowhere
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUndefinedLabel] != 1 {
|
|
|
|
|
t.Fatalf("want one undefined-label, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDuplicateLabel(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
loop:
|
|
|
|
|
ADDQ $1, AX
|
|
|
|
|
loop:
|
|
|
|
|
SUBQ $1, AX
|
|
|
|
|
JMP loop
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeDuplicateLabel] != 1 {
|
|
|
|
|
t.Fatalf("want one duplicate-label, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMissingRet(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
ADDQ $1, AX
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeMissingRet] != 1 {
|
|
|
|
|
t.Fatalf("want one missing-ret, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOperandCount(t *testing.T) {
|
|
|
|
|
// RET takes zero operands; JMP takes exactly one.
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
RET AX
|
|
|
|
|
JMP
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
c := codes(diags)
|
|
|
|
|
if c[CodeOperandCount] != 2 {
|
|
|
|
|
t.Fatalf("want two operand-count findings, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMissingTextflag(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeMissingTextflag] != 1 {
|
|
|
|
|
t.Fatalf("want one missing-textflag-include, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDisableRule(t *testing.T) {
|
|
|
|
|
f, _ := parser.Parse("t_amd64.s", `
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
diags := File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeMissingTextflag: true}})
|
|
|
|
|
if len(diags) != 0 {
|
|
|
|
|
t.Fatalf("disabling the rule should silence it, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMacroInvocationSkipped(t *testing.T) {
|
|
|
|
|
// DISPATCH is defined in-file; get_tls carries an underscore. Neither is a
|
|
|
|
|
// machine instruction, so both must be ignored by the unknown-instruction
|
|
|
|
|
// rule rather than flagged.
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
#define DISPATCH CALL ·x(SB)
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
DISPATCH
|
|
|
|
|
get_tls CX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 0 {
|
|
|
|
|
t.Fatalf("macro invocations must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUndefIsTerminal(t *testing.T) {
|
|
|
|
|
// A function whose body is UNDEF traps and never returns; it needs no RET.
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
UNDEF
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
|
|
|
t.Fatalf("UNDEF should count as terminal: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestArm64BranchAlias(t *testing.T) {
|
|
|
|
|
diags := lintSrcArch(t, "f_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
B done
|
|
|
|
|
done:
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if len(diags) != 0 {
|
|
|
|
|
t.Fatalf("arm64 B to a defined label should be clean: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 23:12:31 +02:00
|
|
|
// TestEvexMaskingRecognised checks that masked EVEX forms; the .Z suffix and
|
|
|
|
|
// an explicit K operand; are recognised and exempt from operand-count
|
2026-07-14 21:03:26 +02:00
|
|
|
// checks.
|
|
|
|
|
func TestEvexMaskingRecognised(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
VPADDD.Z Z1, Z2, K2, Z3
|
|
|
|
|
VPMINSD Z1, Z2, K5, Z3
|
|
|
|
|
VMOVDQU8 Z1, K3, (SI)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 0 {
|
|
|
|
|
t.Fatalf("masked EVEX must be recognised: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
if codes(diags)[CodeOperandCount] != 0 {
|
|
|
|
|
t.Fatalf("masked operand counts must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
func TestArm64AddressingSuffix(t *testing.T) {
|
|
|
|
|
// .W (pre-index) and .P (post-index) suffixes must resolve to the base
|
|
|
|
|
// instruction.
|
|
|
|
|
diags := lintSrcArch(t, "f_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
LDP.W (R0), (R1, R2)
|
|
|
|
|
VST1.P (R3), (R4)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 0 {
|
|
|
|
|
t.Fatalf("suffixed load/store should be recognised: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMacrosInPlaySuppressesLabelRules(t *testing.T) {
|
|
|
|
|
// Including a non-textflag header means macros may define labels and supply
|
|
|
|
|
// the RET, so undefined-label and missing-ret are suppressed.
|
|
|
|
|
diags := lintSrcArch(t, "f_arm64.s", `
|
|
|
|
|
#include "go_asm.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
JMP RARG0
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUndefinedLabel] != 0 || codes(diags)[CodeMissingRet] != 0 {
|
|
|
|
|
t.Fatalf("label rules should be suppressed in macro files: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-20 23:24:39 +02:00
|
|
|
|
|
|
|
|
func TestUnusedLabel(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
unused:
|
|
|
|
|
ADDQ $1, AX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnusedLabel] != 1 {
|
|
|
|
|
t.Fatalf("want one unused-label, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUsedLabelNotFlagged(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
loop:
|
|
|
|
|
ADDQ $1, AX
|
|
|
|
|
JMP loop
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnusedLabel] != 0 {
|
|
|
|
|
t.Fatalf("used label must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 11:40:39 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 23:24:39 +02:00
|
|
|
func TestInvalidTextflag(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT|BOGUS, $0
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeInvalidFlag] != 1 {
|
|
|
|
|
t.Fatalf("want one invalid-textflag, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidTextflags(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT|NOFRAME|DUPOK, $0
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeInvalidFlag] != 0 {
|
|
|
|
|
t.Fatalf("valid flags must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-21 00:17:38 +02:00
|
|
|
|
|
|
|
|
func TestStackImbalance(t *testing.T) {
|
2026-09-16 23:12:31 +02:00
|
|
|
// Function with frame size 16 but only SUB 8, SP; imbalance.
|
2026-08-21 00:17:38 +02:00
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $16-0
|
|
|
|
|
SUBQ $8, SP
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeStackImbalance] != 1 {
|
|
|
|
|
t.Fatalf("want one stack-imbalance, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestStackBalanced(t *testing.T) {
|
2026-09-16 23:12:31 +02:00
|
|
|
// Function with frame size 16 and matching SUB/ADD; balanced.
|
2026-08-21 00:17:38 +02:00
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $16-0
|
|
|
|
|
SUBQ $16, SP
|
|
|
|
|
ADDQ $16, SP
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeStackImbalance] != 0 {
|
|
|
|
|
t.Fatalf("balanced stack must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-21 01:16:18 +02:00
|
|
|
|
|
|
|
|
func TestRegisterWidthMismatch(t *testing.T) {
|
2026-09-16 23:12:31 +02:00
|
|
|
// MOVQ with 32-bit register; mismatch.
|
2026-08-21 01:16:18 +02:00
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ EAX, BX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeRegisterWidthMismatch] != 1 {
|
|
|
|
|
t.Fatalf("want one register-width-mismatch, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterWidthCorrect(t *testing.T) {
|
2026-09-16 23:12:31 +02:00
|
|
|
// MOVQ with 64-bit registers; correct.
|
2026-08-21 01:16:18 +02:00
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ RAX, RBX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeRegisterWidthMismatch] != 0 {
|
|
|
|
|
t.Fatalf("correct width must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-29 13:36:28 +02:00
|
|
|
|
|
|
|
|
func TestABI0RegisterArgs(t *testing.T) {
|
|
|
|
|
// A kernel with a // func signature whose parameters are never read from
|
|
|
|
|
// the FP frame: the classic register-args port bug.
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
// func kernel(text *byte, n int)
|
|
|
|
|
TEXT ·kernel(SB), NOSPLIT, $0-16
|
|
|
|
|
MOVQ DI, R10
|
|
|
|
|
MOVQ R9, AX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeABI0RegisterArgs] != 1 {
|
|
|
|
|
t.Fatalf("want one abi0-register-args, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reading every parameter from the frame is correct.
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
// func kernel(text *byte, n int)
|
|
|
|
|
TEXT ·kernel(SB), NOSPLIT, $0-16
|
|
|
|
|
MOVQ text+0(FP), AX
|
|
|
|
|
MOVQ n+8(FP), BX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeABI0RegisterArgs] != 0 {
|
|
|
|
|
t.Fatalf("frame-reading kernel must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only a result write to FP, no parameter read: still flagged.
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
// func top(sa []int32) int
|
|
|
|
|
TEXT ·top(SB), NOSPLIT, $0-24
|
|
|
|
|
MOVQ SI, R10
|
|
|
|
|
MOVQ R10, ret+16(FP)
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeABI0RegisterArgs] != 1 {
|
|
|
|
|
t.Fatalf("result-only FP write must still be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No signature: stay silent.
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
TEXT ·bare(SB), NOSPLIT, $0-16
|
|
|
|
|
MOVQ DI, AX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeABI0RegisterArgs] != 0 {
|
|
|
|
|
t.Fatalf("signature-less function must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterWidthCanonicalNames(t *testing.T) {
|
|
|
|
|
// Canonical Go asm names with an L operation: the correct spelling for a
|
|
|
|
|
// 32-bit operation, never a width mismatch.
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVL (BX)(R9*4), SI
|
|
|
|
|
XORL R9, R9
|
|
|
|
|
MOVL 128(BX)(R9*4), R12
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeRegisterWidthMismatch] != 0 {
|
|
|
|
|
t.Fatalf("canonical 64-bit names with L ops must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A byte register in an L operation stays a mismatch.
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVL AL, DX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeRegisterWidthMismatch] != 1 {
|
|
|
|
|
t.Fatalf("byte register in L op must be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-29 13:39:22 +02:00
|
|
|
|
2026-09-20 11:40:39 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 13:39:22 +02:00
|
|
|
func TestNonportableRegisterName(t *testing.T) {
|
|
|
|
|
diags := lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ RAX, RBX
|
|
|
|
|
MOVQ EAX, R9
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
// RAX and RBX (and EAX) are gasm aliases of AX/BX; R9 is canonical.
|
|
|
|
|
if got := codes(diags)[CodeNonportableRegister]; got != 3 {
|
|
|
|
|
t.Fatalf("want 3 nonportable-register-name, got %d: %+v", got, diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ AX, BX
|
|
|
|
|
MOVQ (BX)(R9*4), R12
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeNonportableRegister] != 0 {
|
|
|
|
|
t.Fatalf("canonical names must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-30 21:07:45 +02:00
|
|
|
|
|
|
|
|
func TestReservedRegisterWrite(t *testing.T) {
|
|
|
|
|
// A write to R18, the arm64 platform register, is flagged.
|
|
|
|
|
diags := lintSrcArch(t, "k_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVD $1, R18
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeReservedRegister] != 1 {
|
|
|
|
|
t.Fatalf("want one reserved-register-write, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reading R18 (as a base address) is legitimate.
|
|
|
|
|
diags = lintSrcArch(t, "k_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVD (R18), R0
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeReservedRegister] != 0 {
|
|
|
|
|
t.Fatalf("reads must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Other registers are unaffected.
|
|
|
|
|
diags = lintSrcArch(t, "k_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVD $1, R19
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeReservedRegister] != 0 {
|
|
|
|
|
t.Fatalf("R19 must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Macro-using files are exempt: an opaque macro may save and restore R18.
|
|
|
|
|
diags = lintSrcArch(t, "k_arm64.s", `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
#define SAVE_R18 MOVD R18, R4
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
SAVE_R18
|
|
|
|
|
MOVD $1, R18
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeReservedRegister] != 0 {
|
|
|
|
|
t.Fatalf("macro-using files must be exempt: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The rule is arm64-only: an amd64 file has no reserved register.
|
|
|
|
|
diags = lintSrc(t, `
|
|
|
|
|
#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
MOVQ $1, AX
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeReservedRegister] != 0 {
|
|
|
|
|
t.Fatalf("amd64 must not be flagged: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 19:17:07 +02:00
|
|
|
|
|
|
|
|
// TestHasIndirectBranchShape checks that a JMP/CALL through a register or
|
|
|
|
|
// memory suppresses reachability analysis, while a same-named label does not.
|
|
|
|
|
func TestHasIndirectBranchShape(t *testing.T) {
|
|
|
|
|
tab := arch.ForArch(arch.AMD64)
|
|
|
|
|
indirect := `TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
JMP AX
|
|
|
|
|
RET
|
|
|
|
|
`
|
|
|
|
|
f, errs := parser.Parse("t_amd64.s", indirect)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
if !hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
|
|
|
|
t.Error("JMP AX: indirect branch not detected")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
label := `TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
loop:
|
|
|
|
|
JMP loop
|
|
|
|
|
RET
|
|
|
|
|
`
|
|
|
|
|
f, errs = parser.Parse("t_amd64.s", label)
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
t.Fatalf("parse: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
if hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
|
|
|
|
t.Error("JMP loop: label treated as an indirect branch")
|
|
|
|
|
}
|
|
|
|
|
}
|