// Copyright (c) 2026 Petr Balvín (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 print the lexical token stream gasm parse parse and report syntax errors gasm fmt [-w] canonicalise formatting (-w writes in place) gasm lint run static checks gasm asm [-o out.bin] 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 ") 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 ") 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] ") 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 ") 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] ") 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 }