Files
gasm-sdk/lint/directives.go
T

114 lines
3.9 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package lint
import (
"fmt"
"strings"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
)
// checkTextDirectives holds the two directive-hygiene checks that read the
// TEXT line against the layer it lives in: the NOFRAME flag against the
// declared frame, and a missing argument area where the // func signature
// implies one.
func checkTextDirectives(t *ast.Text, cfg Config) []Diagnostic {
var out []Diagnostic
if t.Frame == nil || !t.Frame.Imm.HasVal {
return out
}
// NOFRAME is only valid with a frame size of zero (textflag.h); the
// toolchain accepts a non-zero combination silently and still allocates
// the frame, so the flag states an arrangement the directive does not
// describe. Negative frames are the ABI-wrapper spelling and are left
// alone: the arm64 BSD syscall stubs deliberately pair NOFRAME with $-8.
if !cfg.Disable[CodeNoFrameFrameSize] && hasTextFlag(t, "NOFRAME") && t.Frame.Imm.Val > 0 {
out = append(out, Diagnostic{
Pos: t.Keyword.Pos,
Severity: Warning,
Code: CodeNoFrameFrameSize,
Message: fmt.Sprintf("NOFRAME suppresses frame setup and is only valid with a zero frame size, "+
"but this TEXT declares $%d", t.Frame.Imm.Val),
})
}
// Without NOSPLIT the stack-growth preamble runs, and the argument area
// the wrapper laid out should be stated explicitly: an omitted area
// records ArgsSizeUnknown (funcdata.h) in the object and go vet then
// skips the size check against the prototype. Only functions whose
// // func signature implies a non-zero area are flagged; with no
// signature or a zero area, $frame alone is the normal spelling.
if !cfg.Disable[CodeMissingArgSize] && t.Args == nil && !hasTextFlag(t, "NOSPLIT") {
if want, ok := abiExpectedArgSize(t.Doc); ok && want > 0 {
out = append(out, Diagnostic{
Pos: t.Keyword.Pos,
Severity: Warning,
Code: CodeMissingArgSize,
Message: fmt.Sprintf("TEXT omits the argument area; the // func signature implies %d bytes, "+
"so the frame should read $%d-%d (an omitted area records ArgsSizeUnknown)", want, t.Frame.Imm.Val, want),
})
}
}
return out
}
// checkResultNaming flags result slots addressed with the generic ret
// spelling although the // func signature names its results. The name is
// documentation the toolchain enforces for FP references, and go vet checks
// it against the prototype, so a signature that names a result and a body
// that writes ret+N(FP) disagree about what the bytes mean.
func checkResultNaming(t *ast.Text) []Diagnostic {
results, ok := abiResultSlots(t.Doc)
if !ok || len(results) == 0 {
return nil
}
// A parameter named ret makes the spelling a parameter reference, not
// the generic result name; the rule cannot tell them apart and stays
// silent.
params, pok := abiParamNames(t.Doc)
if pok {
for _, p := range params {
if strings.EqualFold(p, "ret") {
return nil
}
}
}
var out []Diagnostic
for _, s := range t.Body {
in, ok := s.(*ast.Instr)
if !ok {
continue
}
for _, op := range in.Operands {
sym := op.Addr.Sym
if op.Kind != ast.OpAddr || sym == nil || sym.Pseudo != "FP" {
continue
}
if !strings.EqualFold(sym.Name, "ret") || !sym.HasOff {
continue
}
for _, r := range results {
// A result the signature itself names ret coincides with
// the generic spelling; there is no better name to suggest.
if strings.EqualFold(r.name, "ret") {
break
}
if sym.Offset >= r.off && sym.Offset < r.off+r.size {
out = append(out, Diagnostic{
Pos: op.Pos,
Severity: Hint,
Code: CodeUnnamedResult,
Message: fmt.Sprintf("the // func signature names this result %q; "+
"reference it as %q+%d(FP) rather than ret+%d(FP)", r.name, r.name, r.off, sym.Offset),
})
break
}
}
}
}
return out
}