feat(lint): eleven new rules over directives, data and addressing
Assisted-by: GLM 5.3
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// checkDataDecls validates the DATA and GLOBL structure of a file: the
|
||||
// initialiser widths the language allows, values that actually fit them, and
|
||||
// the GLOBL declaration that sizes each initialised symbol. Every check is
|
||||
// calibrated against both assemblers: what go tool asm and gasm asm each
|
||||
// reject or silently accept decides the severity, and the differences are
|
||||
// spelled out in the message.
|
||||
func checkDataDecls(f *ast.File, cfg Config) []Diagnostic {
|
||||
// The GLOBL registry: every declared symbol keyed by package, name and
|
||||
// static marker. Both assemblers pair DATA to GLOBL by name regardless
|
||||
// of order, so the registry is built before any DATA is checked.
|
||||
type globlDecl struct {
|
||||
size int64
|
||||
hasSize bool
|
||||
}
|
||||
globls := map[string]globlDecl{}
|
||||
for _, d := range f.Decls {
|
||||
g, ok := d.(*ast.Globl)
|
||||
if !ok || g.Name == nil || g.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
gd := globlDecl{}
|
||||
if g.Size != nil && g.Size.Imm.HasVal {
|
||||
gd.size, gd.hasSize = g.Size.Imm.Val, true
|
||||
}
|
||||
globls[dataSymKey(g.Name)] = gd
|
||||
}
|
||||
|
||||
var out []Diagnostic
|
||||
for _, d := range f.Decls {
|
||||
dd, ok := d.(*ast.Data)
|
||||
if !ok || dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
name := dd.Name.Name
|
||||
if dd.Name.Static {
|
||||
name += "<>"
|
||||
}
|
||||
w := int64(dd.Width)
|
||||
|
||||
// Width. An integer initialiser allows exactly 1, 2, 4 or 8, a
|
||||
// float needs its full 4 or 8 IEEE-754 bytes, and a string allows
|
||||
// any width (its bytes are zero-padded to the field, which GOROOT's
|
||||
// own messages rely on, /84 and all). The suffix itself is never
|
||||
// optional. Both assemblers reject the rest (go tool asm: "bad int
|
||||
// size for DATA argument", "bad float size", "expect /size").
|
||||
isFloat := dd.Value != nil && dd.Value.Imm.Float != "" && !dd.Value.Imm.HasVal
|
||||
isStr := dd.Value != nil && dd.Value.Imm.Str != "" && !dd.Value.Imm.HasVal
|
||||
if !cfg.Disable[CodeDataWidth] {
|
||||
var msg string
|
||||
switch {
|
||||
case w == 0:
|
||||
msg = "DATA requires a /width suffix"
|
||||
case isFloat && w != 4 && w != 8:
|
||||
msg = fmt.Sprintf("DATA width %d cannot hold a floating-point value, want 4 or 8", w)
|
||||
case !isFloat && !isStr && w != 1 && w != 2 && w != 4 && w != 8:
|
||||
msg = fmt.Sprintf("DATA width must be 1, 2, 4 or 8 for an integer value, got %d", w)
|
||||
}
|
||||
if msg != "" {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dd.Keyword.Pos,
|
||||
End: dd.Keyword.End,
|
||||
Severity: Error,
|
||||
Code: CodeDataWidth,
|
||||
Message: msg,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Value fit. An integer wider than the field is silently truncated
|
||||
// by both assemblers, and a string longer than the field is rejected
|
||||
// by both (go tool asm: "bad string size"; a shorter one is
|
||||
// zero-padded, which is the documented layout, not a defect).
|
||||
if v := dd.Value; v != nil {
|
||||
if v.Imm.Str != "" && !v.Imm.HasVal && !cfg.Disable[CodeDataStringWidth] {
|
||||
if text, err := strconv.Unquote(v.Imm.Str); err == nil && int64(len(text)) > w {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dd.Keyword.Pos,
|
||||
End: dd.Keyword.End,
|
||||
Severity: Error,
|
||||
Code: CodeDataStringWidth,
|
||||
Message: fmt.Sprintf("string of %d bytes does not fit DATA width %d", len(text), w),
|
||||
})
|
||||
}
|
||||
}
|
||||
if v.Imm.HasVal && !cfg.Disable[CodeDataValueOverflow] {
|
||||
val := v.Imm.Val
|
||||
if v.Imm.Neg {
|
||||
val = -val
|
||||
}
|
||||
if lo, hi := dataValueRange(w); val < lo || val > hi {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dd.Keyword.Pos,
|
||||
End: dd.Keyword.End,
|
||||
Severity: Warning,
|
||||
Code: CodeDataValueOverflow,
|
||||
Message: fmt.Sprintf("DATA value %d does not fit width %d and is silently truncated: "+
|
||||
"both assemblers keep only the low %d bytes", val, w, w),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The GLOBL pairing: gasm requires a matching GLOBL and bounds every
|
||||
// initialiser by its size, while go tool asm sizes an unpaired symbol
|
||||
// implicitly and grows a too-small one silently, so the declared size
|
||||
// stops matching the data.
|
||||
g, declared := globls[dataSymKey(dd.Name)]
|
||||
if !declared {
|
||||
if !cfg.Disable[CodeDataNoGlobl] {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dd.Keyword.Pos,
|
||||
End: dd.Keyword.End,
|
||||
Severity: Warning,
|
||||
Code: CodeDataNoGlobl,
|
||||
Message: fmt.Sprintf("DATA initialiser for %q has no GLOBL declaration in this file; "+
|
||||
"gasm asm requires one, go tool asm sizes the symbol implicitly", name),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if g.hasSize && !cfg.Disable[CodeDataExceedsGlobl] {
|
||||
if end := dd.Name.Offset + w; dd.Name.Offset < 0 || end > g.size {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dd.Keyword.Pos,
|
||||
End: dd.Keyword.End,
|
||||
Severity: Warning,
|
||||
Code: CodeDataExceedsGlobl,
|
||||
Message: fmt.Sprintf("DATA %s+%d/%d exceeds the GLOBL size %d; "+
|
||||
"go tool asm grows the symbol silently, gasm asm rejects the file",
|
||||
name, dd.Name.Offset, w, g.size),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dataSymKey identifies a data symbol within a file: package prefix, base
|
||||
// name and the file-local <> marker, the same identity both assemblers pair
|
||||
// DATA and GLOBL by.
|
||||
func dataSymKey(sym *ast.Symbol) string {
|
||||
key := sym.Pkg + "\x00" + sym.Name
|
||||
if sym.Static {
|
||||
key += "\x00static"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// dataValueRange returns the closed range of integers an initialiser of
|
||||
// width w bytes round-trips: the low half of the unsigned range is read back
|
||||
// as negative, so -2^(8w-1) through 2^(8w)-1 all survive the truncation. At
|
||||
// width 8 every int64 is in range, and the unsigned bound is not
|
||||
// representable, so the signed extremes stand in.
|
||||
func dataValueRange(w int64) (lo, hi int64) {
|
||||
if w == 8 {
|
||||
return -1 << 63, 1<<63 - 1
|
||||
}
|
||||
return -(1 << (8*w - 1)), (1 << (8 * w)) - 1
|
||||
}
|
||||
Reference in New Issue
Block a user