Files
gasm-sdk/lint/abi.go
T

220 lines
6.6 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package lint
import (
"go/ast"
"go/parser"
"go/token"
"strconv"
"strings"
)
// ExpectedArgSize computes the argument-area size (parameters plus results,
// laid out with Go's alignment rules on a 64-bit target) implied by the
// `// func …` signature in a doc comment. It returns ok=false when there is
// no parseable signature or it uses a type whose size cannot be determined
// (a named type), so the caller can skip the fix rather than guess.
func ExpectedArgSize(doc string) (int64, bool) {
return abiExpectedArgSize(doc)
}
// abiExpectedArgSize computes the argument-area size (parameters plus results,
// laid out with Go's alignment rules on a 64-bit target) from the `// func …`
// signature in a TEXT function's doc comment. It returns ok=false when there
// is no parseable signature or it uses a type whose size cannot be determined
// (a named type), so the caller can skip the check rather than guess.
//
// The signature is parsed with the standard library's Go parser, so every
// legal signature form (shared names such as `left, right []int32`, nested
// pointers, arrays, structs) is handled correctly.
func abiExpectedArgSize(doc string) (int64, bool) {
sig := signatureLine(doc)
if sig == "" {
return 0, false
}
fn, ok := parseSignature(sig)
if !ok || fn == nil {
return 0, false
}
return signatureSize(fn.Type.Params, fn.Type.Results)
}
// abiParamNames returns the parameter names declared by the `// func …`
// signature in a doc comment, in declaration order. Shared names
// (`left, right []int32`) expand to one entry per name; a nameless parameter
// (`[]int32`) contributes an empty placeholder so offsets stay aligned.
func abiParamNames(doc string) ([]string, bool) {
sig := signatureLine(doc)
if sig == "" {
return nil, false
}
fn, ok := parseSignature(sig)
if !ok || fn == nil || fn.Type.Params == nil {
return nil, false
}
var names []string
for _, field := range fn.Type.Params.List {
if len(field.Names) == 0 {
names = append(names, "")
continue
}
for _, n := range field.Names {
names = append(names, n.Name)
}
}
return names, true
}
// parseSignature parses a `func …` line into a Go FuncDecl.
func parseSignature(sig string) (*ast.FuncDecl, bool) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sig.go", "package p\n"+sig+" {}\n", 0)
if err != nil || len(f.Decls) == 0 {
return nil, false
}
fn, ok := f.Decls[0].(*ast.FuncDecl)
if !ok || fn.Type == nil {
return nil, false
}
return fn, true
}
// signatureLine returns the first `func …` line from a doc comment, trimmed.
func signatureLine(doc string) string {
for line := range strings.SplitSeq(doc, "\n") {
if t := strings.TrimSpace(line); strings.HasPrefix(t, "func ") {
return t
}
}
return ""
}
// signatureSize lays out the parameters and results and returns the total byte
// size of the argument area, matching Go's ABI0 stack layout: parameters are
// laid out first, then the result area begins on a word (8-byte) boundary.
func signatureSize(params, results *ast.FieldList) (int64, bool) {
paramsSize, _, ok := fieldsSizeAlign(params)
if !ok {
return 0, false
}
resultsSize, _, ok := fieldsSizeAlign(results)
if !ok {
return 0, false
}
// With no results the argument area is exactly the parameter size. When
// there are results, the result area begins on a word (8-byte) boundary
// after the parameters (Go's ABI0 stack layout).
if resultsSize == 0 {
return int64(paramsSize), true
}
return int64(alignUp(paramsSize, 8) + resultsSize), true
}
// fieldsSizeAlign lays out a field list sequentially (each field aligned to its
// own alignment) and returns the total size and the maximum field alignment.
func fieldsSizeAlign(list *ast.FieldList) (size, align int, ok bool) {
if list == nil {
return 0, 1, true
}
offset, maxAlign := 0, 1
for _, field := range list.List {
es, ea, fieldOK := typeSizeAlign(field.Type)
if !fieldOK {
return 0, 0, false
}
n := len(field.Names)
if n == 0 {
n = 1
}
for range n {
offset = alignUp(offset, ea)
offset += es
}
if ea > maxAlign {
maxAlign = ea
}
}
return offset, maxAlign, true
}
// basicSizes maps built-in type names to {size, align} on a 64-bit target.
var basicSizes = map[string][2]int{
"bool": {1, 1}, "byte": {1, 1}, "int8": {1, 1}, "uint8": {1, 1},
"int16": {2, 2}, "uint16": {2, 2},
"int32": {4, 4}, "uint32": {4, 4}, "float32": {4, 4},
"int": {8, 8}, "int64": {8, 8}, "uint": {8, 8}, "uint64": {8, 8},
"uintptr": {8, 8}, "float64": {8, 8},
"complex64": {8, 4}, "complex128": {16, 8},
"string": {16, 8}, "any": {16, 8}, "error": {16, 8},
}
// typeSizeAlign returns the size and alignment in bytes of a type expression,
// or ok=false when the size cannot be determined (an unknown named type).
func typeSizeAlign(e ast.Expr) (size, align int, ok bool) {
switch t := e.(type) {
case *ast.Ident:
if sa, found := basicSizes[t.Name]; found {
return sa[0], sa[1], true
}
return 0, 0, false // named type of unknown size
case *ast.SelectorExpr:
if pkg, isIdent := t.X.(*ast.Ident); isIdent && pkg.Name == "unsafe" && t.Sel.Name == "Pointer" {
return 8, 8, true
}
return 0, 0, false
case *ast.ParenExpr:
return typeSizeAlign(t.X)
case *ast.StarExpr:
return 8, 8, true // pointer
case *ast.MapType, *ast.ChanType, *ast.FuncType:
return 8, 8, true // map / chan / func are pointer-sized
case *ast.InterfaceType:
return 16, 8, true
case *ast.Ellipsis:
return 24, 8, true // variadic parameter is a slice
case *ast.ArrayType:
if t.Len == nil {
return 24, 8, true // slice header
}
n, lenOK := arrayLength(t.Len)
es, ea, elemOK := typeSizeAlign(t.Elt)
if !lenOK || !elemOK {
return 0, 0, false
}
return n * es, ea, true
case *ast.StructType:
return structSizeAlign(t.Fields)
}
return 0, 0, false
}
// structSizeAlign lays out a struct's fields and returns its size (rounded up
// to its alignment) and alignment.
func structSizeAlign(fields *ast.FieldList) (size, align int, ok bool) {
size, align, ok = fieldsSizeAlign(fields)
if !ok {
return 0, 0, false
}
return alignUp(size, align), align, true
}
// arrayLength evaluates a constant array-length expression (a literal, for the
// kernels this toolkit targets).
func arrayLength(e ast.Expr) (int, bool) {
if lit, ok := e.(*ast.BasicLit); ok && lit.Kind == token.INT {
if v, err := strconv.Atoi(lit.Value); err == nil {
return v, true
}
}
return 0, false
}
func alignUp(offset, align int) int {
if align <= 1 {
return offset
}
return (offset + align - 1) &^ (align - 1)
}