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