diff --git a/lint/lint.go b/lint/lint.go index 6a8a014..a02f4b7 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -449,6 +449,11 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m out = append(out, checkABI0Args(t)...) } + // Non-portable register spellings (RAX/EAX under go tool asm). + if cfg.Arch == arch.AMD64 && !cfg.Disable[CodeNonportableRegister] { + out = append(out, scanNonportableRegisters(t)...) + } + // FUNCDATA / PCDATA structural validation. out = append(out, checkFuncdata(t, cfg)...) diff --git a/lint/lint_test.go b/lint/lint_test.go index 3bfd70d..451b7a6 100644 --- a/lint/lint_test.go +++ b/lint/lint_test.go @@ -411,3 +411,28 @@ TEXT ·f(SB), NOSPLIT, $0 t.Fatalf("byte register in L op must be flagged: %+v", diags) } } + +func TestNonportableRegisterName(t *testing.T) { + diags := lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + MOVQ RAX, RBX + MOVQ EAX, R9 + RET +`) + // RAX and RBX (and EAX) are gasm aliases of AX/BX; R9 is canonical. + if got := codes(diags)[CodeNonportableRegister]; got != 3 { + t.Fatalf("want 3 nonportable-register-name, got %d: %+v", got, diags) + } + + diags = lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + MOVQ AX, BX + MOVQ (BX)(R9*4), R12 + RET +`) + if codes(diags)[CodeNonportableRegister] != 0 { + t.Fatalf("canonical names must not be flagged: %+v", diags) + } +} diff --git a/lint/registers.go b/lint/registers.go new file mode 100644 index 0000000..814fb63 --- /dev/null +++ b/lint/registers.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package lint + +import ( + "fmt" + "strings" + + "sourcedock.dev/petrbalvin/gasm-devkit/ast" + "sourcedock.dev/petrbalvin/gasm-devkit/token" +) + +// CodeNonportableRegister flags R/E-prefixed amd64 register spellings. +const CodeNonportableRegister = "nonportable-register-name" + +// amd64WordNames are the canonical 64-bit register spellings of go tool asm: +// the assembler has no RAX/EAX/RBX forms, and a file using them assembles +// under gasm but fails under the toolchain it must ship with. +var amd64WordNames = map[string]string{ + "AX": "AX", "BX": "BX", "CX": "CX", "DX": "DX", + "SI": "SI", "DI": "DI", "BP": "BP", "SP": "SP", +} + +// nonportableRegister maps a gasm-only register alias to the canonical go +// tool asm spelling. R or E followed by a canonical word name is the only +// alias family; R8–R15 are already canonical. +func nonportableRegister(name string) (string, bool) { + up := strings.ToUpper(name) + if len(up) != 3 { + return "", false + } + var rest string + switch up[0] { + case 'R', 'E': + rest = up[1:] + default: + return "", false + } + if canon, ok := amd64WordNames[rest]; ok && canon != up { + return canon, true + } + return "", false +} + +// scanNonportableRegisters returns one diagnostic per distinct non-portable +// register spelling in t. gasm neither requires nor registers these names +// (they parse as bare symbols), and go tool asm rejects them outright, so a +// kernel containing one only links through the gasm goobj path, never +// through a normal go build. +func scanNonportableRegisters(t *ast.Text) []Diagnostic { + seen := map[string]bool{} + var out []Diagnostic + report := func(name string, pos token.Position) { + canon, ok := nonportableRegister(name) + if !ok { + return + } + key := strings.ToUpper(name) + if seen[key] { + return + } + seen[key] = true + out = append(out, Diagnostic{ + Pos: pos, + Severity: Warning, + Code: CodeNonportableRegister, + Message: fmt.Sprintf("register %q is a gasm extension; go tool asm spells it %s", name, canon), + }) + } + for _, s := range t.Body { + in, ok := s.(*ast.Instr) + if !ok { + continue + } + for _, op := range in.Operands { + if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" { + report(op.Addr.Sym.Name, op.Pos) + } + if op.Addr.Base != "" { + report(op.Addr.Base, op.Pos) + } + if op.Addr.Index != "" { + report(op.Addr.Index, op.Pos) + } + } + } + return out +}