// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package lint import ( "fmt" "strings" "sourcedock.dev/petrbalvin/gasm-sdk/arch" "sourcedock.dev/petrbalvin/gasm-sdk/ast" ) // reservedRegister returns the platform-reserved general-purpose register of // the architecture, or "" when it has none. arm64 reserves R18 for platform // use: the OS may own it (the Windows TEB, the Darwin el0 TLS), the Go // toolchain never allocates it, and the Go assembler offers no spelling that // even addresses it, so a kernel writing R18 cannot be assembled by the tool // it must ship with. riscv64, loong64 and amd64 reserve no register beyond // the ones the Go ABI fixes, which the register-clobber audit covers. func reservedRegister(a arch.Arch) string { if a == arch.ARM64 { return "R18" } return "" } // scanReservedRegisterWrites flags instructions whose destination is the // platform-reserved register. Only the Plan 9 destination (the last // operand) is checked: reads such as MOVD (R18), R0 are legitimate address // uses, and multi-register stores (STP) keep the rule silent rather than // guess. Like the label heuristics, the check is suppressed in macro-using // files, where an opaque macro may save and restore the register. func scanReservedRegisterWrites(t *ast.Text, a arch.Arch) []Diagnostic { res := reservedRegister(a) if res == "" { return nil } var out []Diagnostic for _, s := range t.Body { in, ok := s.(*ast.Instr) if !ok || len(in.Operands) == 0 { continue } dst := in.Operands[dstIndex(in)] if r := gprName(dst, a); r != "" && strings.EqualFold(r, res) { out = append(out, Diagnostic{ Pos: dst.Pos, Severity: Warning, Code: CodeReservedRegister, Message: fmt.Sprintf("write to %s, the platform-reserved register: the OS may own it and the Go assembler cannot spell it", res), }) } } return out }