285 lines
9.6 KiB
Go
285 lines
9.6 KiB
Go
// 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-sdk supports every instruction the real assembler does,
|
|
// with no hand-maintained (and therefore inevitably incomplete) lists.
|
|
//
|
|
// The same data feeds the generated instruction appendices of the assembly
|
|
// language reference, docs/asm/INSTRUCTIONS-<ARCH>.md, so that the reference
|
|
// cannot drift from the tables it documents.
|
|
//
|
|
// 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"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
|
)
|
|
|
|
// archDirs maps a gasm-sdk architecture name to its obj sub-directory.
|
|
var archDirs = []struct {
|
|
arch string
|
|
sub string
|
|
}{
|
|
{"amd64", "x86"},
|
|
{"arm64", "arm64"},
|
|
{"riscv", "riscv"},
|
|
{"loong64", "loong64"},
|
|
}
|
|
|
|
// docPages maps an architecture to its generated appendix in the language
|
|
// reference. The amd64 page carries a per-mnemonic encodability column,
|
|
// decided by asm.Encodable, which mirrors the encoder's own dispatch; the
|
|
// other targets have no single cheap predicate, so their pages carry the
|
|
// inventory and point at the live measurement instead.
|
|
var docPages = []struct {
|
|
arch arch.Arch
|
|
title string
|
|
file string
|
|
anames string
|
|
encodable bool
|
|
}{
|
|
{arch.AMD64, "AMD64", "INSTRUCTIONS-AMD64.md", "cmd/internal/obj/x86/anames.go", true},
|
|
{arch.ARM64, "ARM64", "INSTRUCTIONS-ARM64.md", "cmd/internal/obj/arm64/anames.go", false},
|
|
{arch.RISCV, "RISC-V 64", "INSTRUCTIONS-RISCV64.md", "cmd/internal/obj/riscv/anames.go", false},
|
|
{arch.LOONG64, "LoongArch 64", "INSTRUCTIONS-LOONG64.md", "cmd/internal/obj/loong64/anames.go", false},
|
|
}
|
|
|
|
func main() {
|
|
goroot := strings.TrimSpace(runGoEnvGOROOT())
|
|
if goroot == "" {
|
|
fatal("could not determine GOROOT")
|
|
}
|
|
version := strings.TrimSpace(runGoEnv("GOVERSION"))
|
|
// 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))
|
|
|
|
names := map[string][]string{}
|
|
for _, a := range archDirs {
|
|
path := filepath.Join(goroot, "src", "cmd", "internal", "obj", a.sub, "anames.go")
|
|
names[a.arch], err = extractInstrs(path)
|
|
if err != nil {
|
|
fatal("extract %s: %v", a.arch, err)
|
|
}
|
|
if err := writeGen(a.arch, a.sub, names[a.arch]); 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]), a.arch)
|
|
}
|
|
|
|
for _, p := range docPages {
|
|
if err := writeDocPage(p.arch, p.title, p.file, p.anames, version, p.encodable); err != nil {
|
|
fatal("write %s: %v", p.file, err)
|
|
}
|
|
fmt.Printf("%-8s -> docs/asm/%s\n", p.arch, p.file)
|
|
}
|
|
}
|
|
|
|
// 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-sdk _gen; DO NOT EDIT.\n")
|
|
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
|
b.WriteString("//\n")
|
|
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
|
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\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-sdk _gen; DO NOT EDIT.\n")
|
|
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
|
b.WriteString("//\n")
|
|
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
|
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\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)
|
|
}
|
|
|
|
// writeDocPage emits docs/asm/<file>, the generated instruction appendix of
|
|
// the language reference for one architecture: every mnemonic the toolchain
|
|
// accepts, with the curated summary where the architecture table carries one
|
|
// and, on amd64, a per-mnemonic encodability column.
|
|
func writeDocPage(a arch.Arch, title, file, anames, version string, encodable bool) error {
|
|
table := arch.ForArch(a)
|
|
instrs := table.Instructions()
|
|
|
|
var b strings.Builder
|
|
b.WriteString("# " + title + ": instruction inventory\n\n")
|
|
b.WriteString("Generated by gasm-sdk's `_gen` from the Go toolchain's instruction table\n")
|
|
b.WriteString("(`" + anames + "`, " + version + "); DO NOT EDIT. This page lists every mnemonic\n")
|
|
b.WriteString("`go tool asm` accepts on this target, which is the upper bound of the\n")
|
|
b.WriteString("language on it: a name absent here is not an instruction of the target,\n")
|
|
b.WriteString("and a name present here may still be one gasm's encoder cannot emit yet.\n\n")
|
|
|
|
encodableCount := 0
|
|
if encodable {
|
|
b.WriteString("The `gasm encodes` column reports whether gasm's encoder can emit the\n")
|
|
b.WriteString("mnemonic today; the gap is the encoder backlog, measured live by\n")
|
|
b.WriteString("`gasm audit-instructions`.\n\n")
|
|
b.WriteString("| Mnemonic | gasm encodes | Notes |\n")
|
|
b.WriteString("|---|---|---|\n")
|
|
for _, in := range instrs {
|
|
ok := asm.Encodable(in.Name)
|
|
if ok {
|
|
encodableCount++
|
|
}
|
|
b.WriteString("| `" + in.Name + "` | " + yesNo(ok) + " | " + in.Summary + " |\n")
|
|
}
|
|
b.WriteString("\n")
|
|
fmt.Fprintf(&b, "Recognised: %d mnemonics. gasm encodes: %d.\n", len(instrs), encodableCount)
|
|
} else {
|
|
b.WriteString("The inventory carries no per-mnemonic encoder column: on this target\n")
|
|
b.WriteString("encodability is decided per operand shape, and the live measured\n")
|
|
b.WriteString("coverage is reported by `gasm audit-instructions`.\n\n")
|
|
b.WriteString("| Mnemonic | Notes |\n")
|
|
b.WriteString("|---|---|\n")
|
|
for _, in := range instrs {
|
|
b.WriteString("| `" + in.Name + "` | " + in.Summary + " |\n")
|
|
}
|
|
b.WriteString("\n")
|
|
fmt.Fprintf(&b, "Recognised: %d mnemonics.\n", len(instrs))
|
|
}
|
|
return os.WriteFile(filepath.Join("docs", "asm", file), []byte(b.String()), 0o644)
|
|
}
|
|
|
|
// yesNo renders a boolean as the word the appendix tables use.
|
|
func yesNo(v bool) string {
|
|
if v {
|
|
return "yes"
|
|
}
|
|
return "no"
|
|
}
|
|
|
|
func runGoEnvGOROOT() string {
|
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// runGoEnv runs `go env` for a single variable.
|
|
func runGoEnv(name string) string {
|
|
out, err := exec.Command("go", "env", name).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)
|
|
}
|