Files
gasm-sdk/lint/abi0.go
T

99 lines
3.1 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-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 <ABIInternal> TEXT reads its arguments from the register
// file by declaration (runtime·memmove<ABIInternal> 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
}