feat(lint): eleven new rules over directives, data and addressing

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:54 +02:00
parent f405cea5bc
commit 69dcbec8ef
12 changed files with 1431 additions and 19 deletions
+143 -10
View File
@@ -10,6 +10,7 @@ package lint
import (
"fmt"
"slices"
"strconv"
"strings"
"unicode/utf8"
@@ -84,6 +85,17 @@ const (
CodeNonportableRegister = "nonportable-register-name"
CodeUnencodable = "unencodable-instruction"
CodeReservedRegister = "reserved-register-write"
CodeMissingArgSize = "missing-argsize"
CodeNoFrameFrameSize = "noframe-frame-size"
CodeUnnamedFPRef = "unnamed-fp-reference"
CodeHardwareSP = "hardware-sp-addressing"
CodeVEXSSEMixing = "vex-sse-mixing"
CodeUnnamedResult = "unnamed-result"
CodeDataWidth = "data-width"
CodeDataValueOverflow = "data-value-overflow"
CodeDataStringWidth = "data-string-width"
CodeDataNoGlobl = "data-without-globl"
CodeDataExceedsGlobl = "data-exceeds-globl"
)
// knownTextFlags are the flags recognised by runtime/textflag.h, plus the
@@ -95,6 +107,19 @@ var knownTextFlags = map[string]bool{
"TOPFRAME": true, "ABIWRAPPER": true,
}
// textOnlyFlags and dataOnlyFlags carry the placement half of the textflag.h
// table: the names that exist on one directive but not the other. NOPROF and
// DUPOK apply to both, and every name outside these two sets is either
// shared or already reported by the unknown-flag check.
var textOnlyFlags = map[string]bool{
"NOSPLIT": true, "WRAPPER": true, "NEEDCTXT": true, "NOFRAME": true,
"REFLECTED": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true,
}
var dataOnlyFlags = map[string]bool{
"RODATA": true, "NOPTR": true, "TLSBSS": true,
}
// pseudoOps are assembler pseudo-operations that are valid instruction-position
// tokens but are not machine instructions and so absent from the arch tables.
var pseudoOps = map[string]bool{
@@ -165,12 +190,15 @@ func File(f *ast.File, cfg Config) []Diagnostic {
for _, d := range f.Decls {
var flags []string
var pos token.Position
var directive string
switch dd := d.(type) {
case *ast.Text:
flags = dd.Flags
pos = dd.Keyword.Pos
directive = "TEXT"
case *ast.Globl:
flags = dd.Flags
directive = "GLOBL"
if dd.Name != nil {
pos = dd.Name.Pos
}
@@ -188,11 +216,28 @@ func File(f *ast.File, cfg Config) []Diagnostic {
Code: CodeInvalidFlag,
Message: fmt.Sprintf("unknown TEXT/GLOBL flag %q", fl),
})
continue
}
// Placement: the textflag.h table binds half the names to
// one directive. Both assemblers accept a misplaced name
// silently, so the flag simply does nothing.
if (directive == "TEXT" && dataOnlyFlags[fl]) ||
(directive == "GLOBL" && textOnlyFlags[fl]) {
out = append(out, Diagnostic{
Pos: pos,
Severity: Warning,
Code: CodeInvalidFlag,
Message: fmt.Sprintf("flag %q does not apply to %s", fl, directive),
})
}
}
}
}
// DATA and GLOBL structure: widths, values that fit them, and the
// GLOBL declaration that sizes each initialised symbol.
out = append(out, checkDataDecls(f, cfg)...)
sortDiagnostics(out)
return out
}
@@ -376,17 +421,33 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
}
}
// Missing RET heuristic. Functions that invoke a macro are skipped: the
// macro body (opaque to us) may supply the RET. A TEXT whose symbol is
// missing (already reported by the parser) is skipped too.
// Missing RET. Two shapes of the same defect, a function that can run
// off the end of its body: one with no RET anywhere, and one that has a
// RET (or a terminator) somewhere but whose last instruction is not a
// terminator, so the tail falls through into whatever follows the TEXT.
// Functions that invoke a macro are skipped: the macro body (opaque to
// us) may supply the RET. A TEXT whose symbol is missing (already
// reported by the parser) is skipped too.
if doLabelChecks && !cfg.Disable[CodeMissingRet] && t.Name != nil &&
instrCount > 0 && !hasRet && !lastTerminal && !hasMacro {
out = append(out, Diagnostic{
Pos: t.Keyword.Pos,
Severity: Warning,
Code: CodeMissingRet,
Message: fmt.Sprintf("function %q has no RET", t.Name.Name),
})
instrCount > 0 && !hasMacro {
if !trailingTerminal(t, cfg.Arch) {
if !hasRet && !lastTerminal {
out = append(out, Diagnostic{
Pos: t.Keyword.Pos,
Severity: Warning,
Code: CodeMissingRet,
Message: fmt.Sprintf("function %q has no RET", t.Name.Name),
})
} else {
out = append(out, Diagnostic{
Pos: t.Keyword.Pos,
Severity: Warning,
Code: CodeMissingRet,
Message: fmt.Sprintf("function %q can fall off its end: the last instruction is neither "+
"RET nor an unconditional branch, so execution continues into the next TEXT", t.Name.Name),
})
}
}
}
// Stack imbalance: track SP changes and flag if the net delta at RET
@@ -485,9 +546,81 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
// FUNCDATA / PCDATA structural validation.
out = append(out, checkFuncdata(t, cfg)...)
// TEXT directive hygiene: the NOFRAME flag against a non-zero frame, and
// a missing argument area where the // func signature implies one.
out = append(out, checkTextDirectives(t, cfg)...)
// Addressing edges: unnamed FP references and hardware stack pointer
// offsets, the one-character spellings OPERANDS.md calls the sharpest
// edge in the language.
out = append(out, scanAddressing(t, cfg)...)
// Encoding mixing: VEX and legacy SSE in one kernel pay a transition
// penalty on every switch (amd64 only).
if cfg.Arch == arch.AMD64 && !hasMacro && !cfg.Disable[CodeVEXSSEMixing] {
out = append(out, checkVectorEncoding(t, tab, macros)...)
}
// Result naming: the // func signature names its results, but the body
// addresses a result slot with the generic ret spelling.
if !cfg.Disable[CodeUnnamedResult] {
out = append(out, checkResultNaming(t)...)
}
return out
}
// trailingTerminal reports whether the function's last real instruction ends
// control flow: RET, UNDEF, an arch-conditional unconditional branch, or an
// architectural trap. Trailing labels, GC annotations and code/data padding
// pseudo-ops are skipped, so a terminator followed by padding (the goexit
// traceback pattern) still counts. A body of nothing but skipped statements
// counts as terminal: there is no fall-through instruction to report.
func trailingTerminal(t *ast.Text, a arch.Arch) bool {
for _, s := range slices.Backward(t.Body) {
in, ok := s.(*ast.Instr)
if !ok {
continue // a trailing label
}
upper := strings.ToUpper(in.Mnemonic.Text)
switch upper {
case "FUNCDATA", "PCDATA", "GO_ARGS", "PCALIGN",
"BYTE", "WORD", "LONG", "QUAD", "FLOAT":
continue // annotations and padding carry no control flow
}
return upper == "RET" || upper == "UNDEF" ||
isUnconditionalJump(a, upper) || isTrap(a, upper)
}
return true
}
// isTrap reports whether the mnemonic is an architectural trap: an
// instruction whose execution cannot continue (a breakpoint or an
// unconditional fault). The runtime ends its abort paths with these.
func isTrap(a arch.Arch, upper string) bool {
switch a {
case arch.AMD64:
return upper == "INT"
case arch.ARM64:
return upper == "BRK"
case arch.RISCV:
return upper == "EBREAK"
case arch.LOONG64:
return upper == "BREAK"
}
return false
}
// hasTextFlag reports whether the TEXT carries the named flag.
func hasTextFlag(t *ast.Text, name string) bool {
for _, f := range t.Flags {
if strings.EqualFold(f, name) {
return true
}
}
return false
}
// reachesRuntime reports whether a function can reach the Go runtime: it is
// not NOSPLIT (so the stack-split and traceback machinery runs) or it makes a
// CALL. Goroutine-pointer registers must survive such functions; a NOSPLIT