289 lines
8.0 KiB
Go
289 lines
8.0 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package arch provides architecture-specific metadata for GAsm: the register
|
|
// files and instruction tables for amd64 and arm64. The metadata powers
|
|
// completion, hover documentation, semantic highlighting and the "unknown
|
|
// instruction" lint. It is pure data with no dependency on the parser, so it
|
|
// can be consulted from any layer.
|
|
package arch
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
// Arch identifies a target instruction set.
|
|
type Arch string
|
|
|
|
// Supported architectures.
|
|
const (
|
|
AMD64 Arch = "amd64"
|
|
ARM64 Arch = "arm64"
|
|
RISCV Arch = "riscv"
|
|
LOONG64 Arch = "loong64"
|
|
Unknown Arch = ""
|
|
)
|
|
|
|
// FromFilename guesses the target architecture from a source file name. Go
|
|
// assembly files conventionally carry a GOARCH suffix such as "_amd64.s",
|
|
// "_arm64.s", "_riscv64.s" or "_loong64.s". It returns Unknown when no suffix
|
|
// matches.
|
|
func FromFilename(name string) Arch {
|
|
lower := strings.ToLower(name)
|
|
switch {
|
|
case strings.Contains(lower, "_amd64"):
|
|
return AMD64
|
|
case strings.Contains(lower, "_arm64"):
|
|
return ARM64
|
|
case strings.Contains(lower, "_riscv64"), strings.Contains(lower, "_riscv"):
|
|
return RISCV
|
|
case strings.Contains(lower, "_loong64"), strings.Contains(lower, "_loong"):
|
|
return LOONG64
|
|
default:
|
|
return Unknown
|
|
}
|
|
}
|
|
|
|
// RegClass classifies a register for highlighting and completion grouping.
|
|
type RegClass int
|
|
|
|
// Register classes.
|
|
const (
|
|
GPR RegClass = iota // general-purpose integer register
|
|
GPRSub // sized sub-register (AL, R8D, …)
|
|
Vector // SSE/AVX/AVX-512 vector (X/Y/Z)
|
|
Mask // AVX-512 mask register (K)
|
|
Float // arm64 floating-point register (F)
|
|
VecARM // arm64 SIMD/vector register (V)
|
|
VecSIMD // architecture-neutral SIMD/vector register (LoongArch LSX/LASX)
|
|
Special // architecture-special register
|
|
)
|
|
|
|
// String returns a short label for the class.
|
|
func (c RegClass) String() string {
|
|
switch c {
|
|
case GPR:
|
|
return "general-purpose"
|
|
case GPRSub:
|
|
return "sub-register"
|
|
case Vector:
|
|
return "vector"
|
|
case Mask:
|
|
return "mask"
|
|
case Float:
|
|
return "float"
|
|
case VecARM:
|
|
return "vector (arm64)"
|
|
case VecSIMD:
|
|
return "vector"
|
|
case Special:
|
|
return "special"
|
|
default:
|
|
return "register"
|
|
}
|
|
}
|
|
|
|
// Register describes one architectural register.
|
|
type Register struct {
|
|
Name string
|
|
Class RegClass
|
|
Desc string
|
|
}
|
|
|
|
// Instr describes one instruction mnemonic.
|
|
type Instr struct {
|
|
Name string
|
|
Summary string
|
|
// MinOps and MaxOps bound the operand count; -1 means "unknown/variable"
|
|
// and disables the operand-count lint for that instruction.
|
|
MinOps int
|
|
MaxOps int
|
|
}
|
|
|
|
// Table is the metadata for one architecture.
|
|
type Table struct {
|
|
Arch Arch
|
|
regs map[string]Register
|
|
regList []Register
|
|
instrs map[string]Instr
|
|
instrList []Instr
|
|
}
|
|
|
|
func newTable(a Arch, regs []Register, instrs []Instr) *Table {
|
|
t := &Table{
|
|
Arch: a,
|
|
regs: make(map[string]Register, len(regs)),
|
|
regList: regs,
|
|
instrs: make(map[string]Instr, len(instrs)),
|
|
instrList: instrs,
|
|
}
|
|
for _, r := range regs {
|
|
t.regs[strings.ToUpper(r.Name)] = r
|
|
}
|
|
for _, in := range instrs {
|
|
t.instrs[strings.ToUpper(in.Name)] = in
|
|
}
|
|
return t
|
|
}
|
|
|
|
// IsRegister reports whether name is a register of this architecture.
|
|
func (t *Table) IsRegister(name string) bool {
|
|
_, ok := t.regs[strings.ToUpper(name)]
|
|
return ok
|
|
}
|
|
|
|
// Register returns the named register.
|
|
func (t *Table) Register(name string) (Register, bool) {
|
|
r, ok := t.regs[strings.ToUpper(name)]
|
|
return r, ok
|
|
}
|
|
|
|
// Registers returns all registers in definition order.
|
|
func (t *Table) Registers() []Register { return t.regList }
|
|
|
|
// Lookup returns the metadata for a mnemonic (case-insensitive).
|
|
func (t *Table) Lookup(mnemonic string) (Instr, bool) {
|
|
key := strings.ToUpper(mnemonic)
|
|
if in, ok := t.instrs[key]; ok {
|
|
return in, true
|
|
}
|
|
// arm64 load/store instructions take a .P (post-index) or .W (pre-index)
|
|
// addressing suffix that the assembler front-end strips; mirror that so the
|
|
// base instruction is still recognised.
|
|
if t.Arch == ARM64 {
|
|
for _, suffix := range []string{".P", ".W"} {
|
|
if base, ok := strings.CutSuffix(key, suffix); ok {
|
|
if in, found := t.instrs[base]; found {
|
|
return in, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// amd64 EVEX instructions take a .Z zeroing suffix (masking is written as
|
|
// an explicit K operand rather than a suffix); strip it so the base
|
|
// instruction is still recognised.
|
|
if t.Arch == AMD64 {
|
|
if base, ok := strings.CutSuffix(key, ".Z"); ok {
|
|
if in, found := t.instrs[base]; found {
|
|
return in, true
|
|
}
|
|
}
|
|
}
|
|
return Instr{}, false
|
|
}
|
|
|
|
// Instructions returns all instructions in definition order.
|
|
func (t *Table) Instructions() []Instr { return t.instrList }
|
|
|
|
// pseudoRegs are the Plan 9 pseudo-registers, valid on every architecture.
|
|
var pseudoRegs = map[string]string{
|
|
"FP": "frame pointer: references function arguments and results",
|
|
"SP": "stack pointer: the top of the local stack frame",
|
|
"SB": "static base: references global symbols",
|
|
"PC": "program counter",
|
|
}
|
|
|
|
// IsPseudoReg reports whether name is a Plan 9 pseudo-register.
|
|
func IsPseudoReg(name string) bool {
|
|
_, ok := pseudoRegs[strings.ToUpper(name)]
|
|
return ok
|
|
}
|
|
|
|
// PseudoRegDesc returns the description of a pseudo-register.
|
|
func PseudoRegDesc(name string) (string, bool) {
|
|
d, ok := pseudoRegs[strings.ToUpper(name)]
|
|
return d, ok
|
|
}
|
|
|
|
var (
|
|
amd64Table *Table
|
|
arm64Table *Table
|
|
riscvTable *Table
|
|
loong64Table *Table
|
|
)
|
|
|
|
func init() {
|
|
amd64Table = buildAMD64()
|
|
arm64Table = buildARM64()
|
|
riscvTable = buildRISCV()
|
|
loong64Table = buildLOONG64()
|
|
}
|
|
|
|
// ForArch returns the table for a, or the amd64 table for Unknown so that
|
|
// callers always get a usable default.
|
|
func ForArch(a Arch) *Table {
|
|
switch a {
|
|
case ARM64:
|
|
return arm64Table
|
|
case RISCV:
|
|
return riscvTable
|
|
case LOONG64:
|
|
return loong64Table
|
|
default:
|
|
return amd64Table
|
|
}
|
|
}
|
|
|
|
// fixedArity lists the few instructions whose operand count is reliable on
|
|
// every architecture; relaxCounts leaves these untouched.
|
|
var fixedArity = map[string]bool{
|
|
"RET": true, "NOP": true, "JMP": true, "CALL": true, "UNDEF": true,
|
|
}
|
|
|
|
// relaxCounts clears operand-count bounds for every instruction except the
|
|
// fixed-arity ones. It is applied to architectures (arm64, riscv64, loong64)
|
|
// whose instructions have too many operand forms for a single fixed count to be
|
|
// reliable, so the operand-count lint stays silent rather than guess.
|
|
func relaxCounts(instrs []Instr) []Instr {
|
|
for i := range instrs {
|
|
if !fixedArity[strings.ToUpper(instrs[i].Name)] {
|
|
instrs[i].MinOps = -1
|
|
instrs[i].MaxOps = -1
|
|
}
|
|
}
|
|
return instrs
|
|
}
|
|
|
|
// mergedInstrs combines the common opcode list with an architecture-specific
|
|
// list (de-duplicated, common first) and enriches the result with the curated
|
|
// summaries map.
|
|
func mergedInstrs(summaries map[string]Instr, nameSets ...[]string) []Instr {
|
|
seen := make(map[string]bool)
|
|
var names []string
|
|
for _, set := range nameSets {
|
|
for _, n := range set {
|
|
if !seen[n] {
|
|
seen[n] = true
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
}
|
|
return buildInstrs(names, summaries)
|
|
}
|
|
|
|
// buildInstrs merges the complete generated instruction name list with a
|
|
// curated summaries map (keyed by upper-case mnemonic). Instructions without a
|
|
// curated entry get an empty summary and an unknown operand count, which keeps
|
|
// the operand-count lint silent for them.
|
|
func buildInstrs(names []string, summaries map[string]Instr) []Instr {
|
|
out := make([]Instr, 0, len(names))
|
|
for _, n := range names {
|
|
if in, ok := summaries[strings.ToUpper(n)]; ok {
|
|
in.Name = n
|
|
out = append(out, in)
|
|
} else {
|
|
out = append(out, Instr{Name: n, MinOps: -1, MaxOps: -1})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// toMap converts a curated instruction slice into an upper-case-keyed map.
|
|
func toMap(list []Instr) map[string]Instr {
|
|
m := make(map[string]Instr, len(list))
|
|
for _, in := range list {
|
|
m[strings.ToUpper(in.Name)] = in
|
|
}
|
|
return m
|
|
}
|