97 lines
3.1 KiB
Go
97 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"
|
||
)
|
||
|
||
// 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")...)
|
||
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")...)
|
||
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
|
||
}
|
||
|
||
// checkIndex validates an immediate index operand. A literal index must lie in
|
||
// the small range the runtime uses; a named constant (e.g. $PCDATA_StackMapIndex)
|
||
// cannot be evaluated and is accepted without a range check.
|
||
func checkIndex(op *ast.Operand, directive string) []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 > 10) {
|
||
return []Diagnostic{{
|
||
Pos: op.Pos, Severity: Warning, Code: CodeFuncdata,
|
||
Message: fmt.Sprintf("%s index %d is outside the valid range 0–10", directive, op.Imm.Val),
|
||
}}
|
||
}
|
||
return nil
|
||
}
|