// Copyright (c) 2026 Petr BalvĂ­n (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 }