Files
gasm-sdk/cmd/gasm/main.go
T

300 lines
7.5 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Command gasm is the developer frontend for GAsm — Go's Plan 9 assembler.
// It bundles a token dumper, a parser, a formatter, a linter and a language
// server into one binary. Every subcommand works headlessly so it can be
// driven from scripts and CI as well as from an editor.
package main
import (
"flag"
"fmt"
"io"
"os"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/format"
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.4.0"
func main() {
if len(os.Args) < 2 {
usage(os.Stderr)
os.Exit(2)
}
switch os.Args[1] {
case "tokens":
os.Exit(cmdTokens(os.Args[2:]))
case "parse":
os.Exit(cmdParse(os.Args[2:]))
case "fmt":
os.Exit(cmdFmt(os.Args[2:]))
case "lint":
os.Exit(cmdLint(os.Args[2:]))
case "asm":
os.Exit(cmdAsm(os.Args[2:]))
case "lsp":
os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V":
fmt.Printf("gasm %s\n", version)
case "help", "-h", "--help":
usage(os.Stdout)
default:
fmt.Fprintf(os.Stderr, "gasm: unknown command %q\n\n", os.Args[1])
usage(os.Stderr)
os.Exit(2)
}
}
func usage(w io.Writer) {
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler
Usage:
gasm tokens <file> print the lexical token stream
gasm parse <file> parse and report syntax errors
gasm fmt [-w] <file...> canonicalise formatting (-w writes in place)
gasm lint <file...> run static checks
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2)
gasm lsp run the language server over stdio
gasm version print the version
`, version)
}
// readSource returns the contents of path, or stdin when path is "-".
func readSource(path string) (string, error) {
if path == "-" {
b, err := io.ReadAll(os.Stdin)
return string(b), err
}
b, err := os.ReadFile(path)
return string(b), err
}
func cmdTokens(args []string) int {
fs := flag.NewFlagSet("tokens", flag.ExitOnError)
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
return 2
}
src, err := readSource(fs.Arg(0))
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
return 1
}
for _, tok := range lexer.Tokenize(src) {
fmt.Printf("%s\t%s\t%q\n", tok.Pos, tok.Kind, tok.Text)
}
return 0
}
func cmdParse(args []string) int {
fs := flag.NewFlagSet("parse", flag.ExitOnError)
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
return 2
}
path := fs.Arg(0)
src, err := readSource(path)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
return 1
}
file, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
}
if len(errs) > 0 {
return 1
}
funcs := 0
for _, d := range file.Decls {
if _, ok := d.(*ast.Text); ok {
funcs++
}
}
fmt.Printf("%s: OK — %d declarations, %d functions\n", path, len(file.Decls), funcs)
return 0
}
func cmdFmt(args []string) int {
fs := flag.NewFlagSet("fmt", flag.ExitOnError)
write := fs.Bool("w", false, "write result to the source file")
fs.Parse(args)
if fs.NArg() == 0 {
fmt.Fprintln(os.Stderr, "usage: gasm fmt [-w] <file...>")
return 2
}
rc := 0
for _, path := range fs.Args() {
src, err := readSource(path)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
rc = 1
continue
}
out := format.Source(path, src)
if *write {
if out != src {
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
rc = 1
}
}
continue
}
fmt.Print(out)
}
return rc
}
func cmdLint(args []string) int {
fs := flag.NewFlagSet("lint", flag.ExitOnError)
disable := fs.String("disable", "", "comma-separated rule codes to disable")
fs.Parse(args)
if fs.NArg() == 0 {
fmt.Fprintln(os.Stderr, "usage: gasm lint <file...>")
return 2
}
disabled := map[string]bool{}
for _, code := range strings.Split(*disable, ",") {
if code = strings.TrimSpace(code); code != "" {
disabled[code] = true
}
}
hadError := false
for _, path := range fs.Args() {
src, err := readSource(path)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
hadError = true
continue
}
file, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
hadError = true
}
diags := lint.File(file, lint.Config{Arch: arch.FromFilename(path), Disable: disabled})
for _, d := range diags {
fmt.Printf("%s:%d:%d: %s: %s [%s]\n", path, d.Pos.Line, d.Pos.Column, d.Severity, d.Message, d.Code)
if d.Severity == lint.Error {
hadError = true
}
}
}
if hadError {
return 1
}
return 0
}
func cmdLSP(args []string) int {
fs := flag.NewFlagSet("lsp", flag.ExitOnError)
fs.Parse(args)
srv := lsp.New(os.Stdin, os.Stdout)
if err := srv.Run(); err != nil {
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
return 1
}
return 0
}
func cmdAsm(args []string) int {
fs := flag.NewFlagSet("asm", flag.ExitOnError)
out := fs.String("o", "", "write the concatenated machine code to this file")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm asm [-o out.bin] <file>")
return 2
}
path := fs.Arg(0)
if arch.FromFilename(path) != arch.AMD64 {
fmt.Fprintln(os.Stderr, "gasm asm: only amd64 is supported in this Phase 2 increment")
return 1
}
src, err := readSource(path)
if err != nil {
fmt.Fprintln(os.Stderr, "gasm:", err)
return 1
}
f, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
}
if len(errs) > 0 {
return 1
}
img, err := asm.AssembleFile(f)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
return 1
}
if len(img.Funcs) == 0 {
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
return 1
}
for _, fn := range img.Funcs {
code := img.Code[fn.Offset : fn.Offset+fn.Size]
fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size)
for i := 0; i < len(code); i += 16 {
end := i + 16
if end > len(code) {
end = len(code)
}
fmt.Printf(" %04x:", i)
for _, b := range code[i:end] {
fmt.Printf(" %02x", b)
}
fmt.Println()
}
}
if len(img.Data) > 0 {
fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code))
for _, d := range f.Decls {
g, ok := d.(*ast.Globl)
if !ok || g.Name == nil || g.Name.Pseudo != "SB" {
continue
}
size := 0
if g.Size != nil && g.Size.Imm.HasVal {
size = int(g.Size.Imm.Val)
}
fmt.Printf(" %s: %d bytes at 0x%x\n", g.Name.Name, size, img.Symbols[g.Name.Name])
}
for i := 0; i < len(img.Data); i += 16 {
end := i + 16
if end > len(img.Data) {
end = len(img.Data)
}
fmt.Printf(" %04x:", len(img.Code)+i)
for _, b := range img.Data[i:end] {
fmt.Printf(" %02x", b)
}
fmt.Println()
}
}
if *out != "" {
all := img.Bytes()
if err := os.WriteFile(*out, all, 0o644); err != nil {
fmt.Fprintln(os.Stderr, "gasm asm:", err)
return 1
}
fmt.Printf("wrote %d bytes to %s\n", len(all), *out)
}
return 0
}