feat(lint): eleven new rules over directives, data and addressing

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:54 +02:00
parent f405cea5bc
commit 69dcbec8ef
12 changed files with 1431 additions and 19 deletions
+172
View File
@@ -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
}