// 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" ) // checkFuncdata validates the structure of FUNCDATA and PCDATA directives, // which carry the GC stack-map information. The checks are deliberately // shallow, they confirm the operands are well formed and that a literal index // is within the small range the runtime uses, and never try to interpret a // named index constant such as $PCDATA_StackMapIndex. func checkFuncdata(t *ast.Text, cfg Config) []Diagnostic { var out []Diagnostic for _, s := range t.Body { in, ok := s.(*ast.Instr) if !ok { continue } switch strings.ToUpper(in.Mnemonic.Text) { case "FUNCDATA": out = append(out, checkFunCDATA(in, cfg)...) case "PCDATA": out = append(out, checkPCDATA(in, cfg)...) } } return out } // checkFunCDATA validates `FUNCDATA $index, symbol(SB)`. func checkFunCDATA(in *ast.Instr, cfg Config) []Diagnostic { if cfg.Disable[CodeFuncdata] { return nil } var out []Diagnostic if len(in.Operands) != 2 { return []Diagnostic{{ Pos: in.Mnemonic.Pos, End: in.Mnemonic.End, Severity: Warning, Code: CodeFuncdata, Message: fmt.Sprintf("FUNCDATA expects 2 operands (index, symbol), got %d", len(in.Operands)), }} } out = append(out, checkIndex(in.Operands[0], "FUNCDATA", maxFuncdataIndex)...) if sym := in.Operands[1].Addr.Sym; in.Operands[1].Kind != ast.OpAddr || sym == nil { out = append(out, Diagnostic{ Pos: in.Operands[1].Pos, Severity: Warning, Code: CodeFuncdata, Message: "FUNCDATA second operand must be a symbol reference", }) } return out } // checkPCDATA validates `PCDATA $index, $value`. func checkPCDATA(in *ast.Instr, cfg Config) []Diagnostic { if cfg.Disable[CodeFuncdata] { return nil } if len(in.Operands) != 2 { return []Diagnostic{{ Pos: in.Mnemonic.Pos, End: in.Mnemonic.End, Severity: Warning, Code: CodeFuncdata, Message: fmt.Sprintf("PCDATA expects 2 operands (index, value), got %d", len(in.Operands)), }} } var out []Diagnostic out = append(out, checkIndex(in.Operands[0], "PCDATA", maxPcdataIndex)...) if in.Operands[1].Kind != ast.OpImmediate { out = append(out, Diagnostic{ Pos: in.Operands[1].Pos, Severity: Warning, Code: CodeFuncdata, Message: "PCDATA value must be an immediate", }) } return out } // The largest literal FUNCDATA and PCDATA indices the Go 1.27 runtime // defines (internal/abi/symtab.go, which pkg/include/funcdata.h mirrors): // FUNCDATA_WrapInfo is 7 and PCDATA_PanicBounds is 4. A literal above them // addresses metadata no runtime reads, which in hand-written assembly is // near-certainly a typo. const ( maxFuncdataIndex = 7 maxPcdataIndex = 4 ) // checkIndex validates an immediate index operand. A literal index must lie // in the range the runtime defines; a named constant (e.g. $PCDATA_StackMapIndex) // cannot be evaluated and is accepted without a range check. func checkIndex(op *ast.Operand, directive string, max int64) []Diagnostic { if op.Kind != ast.OpImmediate { return []Diagnostic{{ Pos: op.Pos, Severity: Warning, Code: CodeFuncdata, Message: directive + " index must be an immediate", }} } if op.Imm.HasVal && (op.Imm.Val < 0 || op.Imm.Val > max) { return []Diagnostic{{ Pos: op.Pos, Severity: Warning, Code: CodeFuncdata, Message: fmt.Sprintf("%s index %d is outside the valid range 0-%d", directive, op.Imm.Val, max), }} } return nil }