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:
2026-07-06 09:49:50 +02:00
commit d5a4a6de45
53 changed files with 13166 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Command gen regenerates the architecture instruction tables from the Go
// toolchain's own assembler source. Go's Plan 9 assembler defines the exact,
// complete set of mnemonics it accepts for each architecture in
// $GOROOT/src/cmd/internal/obj/<arch>/anames.go; this tool extracts those
// names so gasm-devkit supports every instruction the real assembler does,
// with no hand-maintained (and therefore inevitably incomplete) lists.
//
// Usage (via the justfile):
//
// just gen
//
// The generated files are committed; regenerating requires a Go installation
// but the toolkit itself has no dependency on the toolchain source at runtime.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// archDirs maps a gasm-devkit architecture name to its obj sub-directory.
var archDirs = []struct {
arch string
sub string
}{
{"amd64", "x86"},
{"arm64", "arm64"},
{"riscv", "riscv"},
{"loong64", "loong64"},
}
func main() {
goroot := strings.TrimSpace(runGoEnvGOROOT())
if goroot == "" {
fatal("could not determine GOROOT")
}
// The common opcodes shared by every architecture (RET, JMP, NOP, CALL,
// TEXT, FUNCDATA, …) live in cmd/internal/obj/util.go.
commonPath := filepath.Join(goroot, "src", "cmd", "internal", "obj", "util.go")
common, err := extractInstrs(commonPath)
if err != nil {
fatal("extract common: %v", err)
}
common = filterCommon(common)
if err := writeCommon(common); err != nil {
fatal("write common: %v", err)
}
fmt.Printf("%-8s %4d instructions -> arch/common_gen.go\n", "common", len(common))
for _, a := range archDirs {
path := filepath.Join(goroot, "src", "cmd", "internal", "obj", a.sub, "anames.go")
names, err := extractInstrs(path)
if err != nil {
fatal("extract %s: %v", a.arch, err)
}
if err := writeGen(a.arch, a.sub, names); err != nil {
fatal("write %s: %v", a.arch, err)
}
fmt.Printf("%-8s %4d instructions -> arch/%s_gen.go\n", a.arch, len(names), a.arch)
}
}
// filterCommon drops opcode names that are not user-writable instructions.
func filterCommon(names []string) []string {
drop := map[string]bool{"XXX": true, "LAST": true}
var out []string
for _, n := range names {
if !drop[n] {
out = append(out, n)
}
}
return out
}
// writeCommon emits arch/common_gen.go.
func writeCommon(names []string) error {
var b strings.Builder
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n\n")
b.WriteString("package arch\n\n")
b.WriteString("// commonGeneratedInstrs is the set of opcodes shared by every architecture\n")
b.WriteString("// (RET, JMP, NOP, CALL, TEXT, FUNCDATA, PCDATA, …).\n")
b.WriteString("var commonGeneratedInstrs = []string{\n")
for _, n := range names {
fmt.Fprintf(&b, "\t%q,\n", n)
}
b.WriteString("}\n")
return os.WriteFile(filepath.Join("arch", "common_gen.go"), []byte(b.String()), 0o644)
}
// extractInstrs parses an anames.go file and returns the sorted, de-duplicated
// instruction names from its `var Anames = []string{...}` literal.
func extractInstrs(path string) ([]string, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil, err
}
seen := map[string]bool{}
var names []string
for _, decl := range f.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.VAR {
continue
}
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok || len(vs.Names) == 0 || vs.Names[0].Name != "Anames" {
continue
}
for _, val := range vs.Values {
cl, ok := val.(*ast.CompositeLit)
if !ok {
continue
}
for _, elt := range cl.Elts {
if lit := stringLit(elt); lit != "" && lit != "LAST" && !seen[lit] {
seen[lit] = true
names = append(names, lit)
}
}
}
}
}
sort.Strings(names)
return names, nil
}
// stringLit returns the string value of a composite-literal element, whether it
// is a plain literal or a keyed entry such as `obj.A_ARCHSPECIFIC: "AAA"`.
func stringLit(elt ast.Expr) string {
switch e := elt.(type) {
case *ast.BasicLit:
if e.Kind == token.STRING {
return strings.Trim(e.Value, `"`)
}
case *ast.KeyValueExpr:
return stringLit(e.Value)
}
return ""
}
// writeGen emits arch/<arch>_gen.go.
func writeGen(arch, sub string, names []string) error {
var b strings.Builder
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n\n")
b.WriteString("package arch\n\n")
b.WriteString("// " + arch + "GeneratedInstrs is the complete set of " + arch +
" mnemonics accepted by\n// Go's Plan 9 assembler.\n")
b.WriteString("var " + arch + "GeneratedInstrs = []string{\n")
for _, n := range names {
fmt.Fprintf(&b, "\t%q,\n", n)
}
b.WriteString("}\n")
return os.WriteFile(filepath.Join("arch", arch+"_gen.go"), []byte(b.String()), 0o644)
}
func runGoEnvGOROOT() string {
out, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return ""
}
return string(out)
}
func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
os.Exit(1)
}