// Copyright (c) 2026 Petr BalvĂ­n (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 }