feat: gasm-devkit 0.1.0 — GAsm lexer, parser, linter, formatter, LSP and amd64 assembler
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
+275
@@ -0,0 +1,275 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user