Files
gasm-sdk/arch/arch.go
T

286 lines
7.8 KiB
Go
Raw Permalink Normal View History

// 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)
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 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
}