// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package lint import ( "fmt" "strings" "sourcedock.dev/petrbalvin/gasm-devkit/ast" ) // checkABI0Args flags TEXT functions whose // func signature declares // parameters that are never read from the FP frame. Under ABI0 every // argument lives on the stack and is addressed as name+offset(FP); a kernel // that never references a parameter's frame slot is almost certainly // consuming the caller's register contents instead, which produces // value-dependent garbage that only a direct-call differential test (not a // pipeline-level fuzz) can observe. // // The check requires a parseable signature; functions without one, and // functions whose parameters are all covered by frame reads, stay silent. func checkABI0Args(t *ast.Text) []Diagnostic { // An explicit TEXT reads its arguments from the register // file by declaration (runtime·memmove is the canonical // example), so the ABI0 frame contract does not apply to it. if t.Name != nil && t.Name.ABI != "" { return nil } params, ok := abiParamNames(t.Doc) if !ok || len(params) == 0 { return nil } // Collect every FP frame slot the body references, by symbol name (e.g. // "text" for text+0(FP)). Frame reads and writes both count: writing a // parameter slot is as wrong as ignoring it, but reads dominate, and the // distinction is not worth the precision here. frameRefs := make(map[string]bool) for _, s := range t.Body { in, ok := s.(*ast.Instr) if !ok { continue } for _, op := range in.Operands { if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "FP" { continue } frameRefs[strings.ToUpper(op.Addr.Sym.Name)] = true } } if len(frameRefs) == 0 { // No FP reference at all: the function ignores the frame entirely. return []Diagnostic{{ Pos: t.Keyword.Pos, Severity: Warning, Code: CodeABI0RegisterArgs, Message: fmt.Sprintf("function %q declares %d parameter(s) but never reads the FP frame; "+ "ABI0 passes arguments as name+offset(FP), not in registers", t.Name.Name, len(params)), }} } var unread []string for _, p := range params { if p == "" { continue } if !frameSlotCovers(frameRefs, p) { unread = append(unread, p) } } if len(unread) == 0 { return nil } return []Diagnostic{{ Pos: t.Keyword.Pos, Severity: Warning, Code: CodeABI0RegisterArgs, Message: fmt.Sprintf("function %q never reads parameter slot(s) %s from the FP frame; "+ "suspected register-args port bug (ABI0 arguments arrive as name+offset(FP))", t.Name.Name, strings.Join(unread, ", ")), }} } // frameSlotCovers reports whether any referenced FP slot belongs to the // parameter: the slot either spells the parameter name itself or the // compound form Go's ABI0 uses for multi-word types (swin_base, swin_len, // swin_cap for a parameter named swin). func frameSlotCovers(frameRefs map[string]bool, param string) bool { p := strings.ToUpper(param) if frameRefs[p] { return true } for slot := range frameRefs { if strings.HasPrefix(slot, p+"_") { return true } } return false }