Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9370f9c3ee | ||
|
|
e98680597d |
+156
-24
@@ -11,7 +11,9 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
@@ -26,7 +28,7 @@ import (
|
|||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version is the release version, stamped at build time via
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||||
var version = "0.6.0"
|
var version = "0.8.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -47,30 +49,71 @@ func main() {
|
|||||||
case "lsp":
|
case "lsp":
|
||||||
os.Exit(cmdLSP(os.Args[2:]))
|
os.Exit(cmdLSP(os.Args[2:]))
|
||||||
case "version", "--version", "-V":
|
case "version", "--version", "-V":
|
||||||
fmt.Printf("gasm %s\n", version)
|
os.Exit(cmdVersion())
|
||||||
case "help", "-h", "--help":
|
case "help", "--help", "-h":
|
||||||
usage(os.Stdout)
|
usage(os.Stdout)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "gasm: unknown command %q\n\n", os.Args[1])
|
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
|
||||||
usage(os.Stderr)
|
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdVersion prints the release version.
|
||||||
|
func cmdVersion() int {
|
||||||
|
fmt.Printf("gasm %s\n", version)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler
|
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
|
||||||
|
|
||||||
|
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
|
||||||
|
language server for Plan 9 assembly into one self-contained binary.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
gasm tokens <file> print the lexical token stream
|
gasm <command> [arguments]
|
||||||
gasm parse <file> parse and report syntax errors
|
gasm [flags]
|
||||||
gasm fmt [-w] <file...> canonicalise formatting (-w writes in place)
|
|
||||||
gasm lint <file...> run static checks
|
Commands:
|
||||||
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2)
|
tokens print the lexical token stream
|
||||||
gasm lsp run the language server over stdio
|
parse parse and report syntax errors
|
||||||
gasm version print the version
|
fmt canonicalise formatting (gofmt for assembly)
|
||||||
|
lint run static checks
|
||||||
|
asm assemble .s files to machine code (amd64)
|
||||||
|
lsp run the language server over stdio
|
||||||
|
version print the version (same as --version)
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help show this help
|
||||||
|
-V, --version print the version
|
||||||
|
|
||||||
|
Run "gasm <command> -h" for a command's usage and flags.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gasm fmt reformat every .s below the current directory
|
||||||
|
gasm lint go-flac/*.s run static checks over the kernels
|
||||||
|
gasm asm -o k.bin kern_amd64.s
|
||||||
`, version)
|
`, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
|
||||||
|
// proper usage block: the one-line usage, the long description and the flag
|
||||||
|
// defaults. The flag package routes -h/--help to fs.Usage and exits 0.
|
||||||
|
func newCommand(name, usageLine, long string) *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet(name, flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
w := fs.Output()
|
||||||
|
fmt.Fprintf(w, "Usage: %s\n\n%s\n", usageLine, strings.TrimSpace(long))
|
||||||
|
hasFlags := false
|
||||||
|
fs.VisitAll(func(*flag.Flag) { hasFlags = true })
|
||||||
|
if hasFlags {
|
||||||
|
fmt.Fprintln(w, "\nFlags:")
|
||||||
|
fs.PrintDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
// readSource returns the contents of path, or stdin when path is "-".
|
// readSource returns the contents of path, or stdin when path is "-".
|
||||||
func readSource(path string) (string, error) {
|
func readSource(path string) (string, error) {
|
||||||
if path == "-" {
|
if path == "-" {
|
||||||
@@ -82,7 +125,10 @@ func readSource(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdTokens(args []string) int {
|
func cmdTokens(args []string) int {
|
||||||
fs := flag.NewFlagSet("tokens", flag.ExitOnError)
|
fs := newCommand("tokens", "gasm tokens <file>", `
|
||||||
|
Print the lexical token stream of FILE: position, token kind and text, one
|
||||||
|
token per line. FILE may be "-" to read standard input.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
|
||||||
@@ -100,7 +146,11 @@ func cmdTokens(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdParse(args []string) int {
|
func cmdParse(args []string) int {
|
||||||
fs := flag.NewFlagSet("parse", flag.ExitOnError)
|
fs := newCommand("parse", "gasm parse <file>", `
|
||||||
|
Parse FILE and report syntax errors on stderr. On success, print how many
|
||||||
|
declarations and TEXT functions the file contains. FILE may be "-" to read
|
||||||
|
standard input.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
|
||||||
@@ -130,15 +180,49 @@ func cmdParse(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdFmt(args []string) int {
|
func cmdFmt(args []string) int {
|
||||||
fs := flag.NewFlagSet("fmt", flag.ExitOnError)
|
fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
|
||||||
|
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
||||||
|
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
||||||
|
blank line before each label, TEXT and GLOBL block). Formatting is
|
||||||
|
idempotent and preserves every line, comments included.
|
||||||
|
|
||||||
|
With no paths — or a directory path — every .s file below it is reformatted
|
||||||
|
in place and the changed files are listed, the way go fmt does; "." and "_"
|
||||||
|
directories are skipped. Explicit file paths print to stdout unless -w is
|
||||||
|
given.
|
||||||
|
`)
|
||||||
write := fs.Bool("w", false, "write result to the source file")
|
write := fs.Bool("w", false, "write result to the source file")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() == 0 {
|
// Like go fmt: with no arguments, or with a directory argument, every .s
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm fmt [-w] <file...>")
|
// file below the directory is formatted in place and the names of the
|
||||||
return 2
|
// changed files are listed; explicit file arguments keep the -w / stdout
|
||||||
|
// behaviour.
|
||||||
|
paths := fs.Args()
|
||||||
|
dirMode := len(paths) == 0
|
||||||
|
if dirMode {
|
||||||
|
paths = []string{"."}
|
||||||
|
}
|
||||||
|
var files []string
|
||||||
|
for _, p := range paths {
|
||||||
|
info, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
dirMode = true
|
||||||
|
found, err := asmFiles(p)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
files = append(files, found...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, p)
|
||||||
}
|
}
|
||||||
rc := 0
|
rc := 0
|
||||||
for _, path := range fs.Args() {
|
for _, path := range files {
|
||||||
src, err := readSource(path)
|
src, err := readSource(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
@@ -146,11 +230,15 @@ func cmdFmt(args []string) int {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out := format.Source(path, src)
|
out := format.Source(path, src)
|
||||||
if *write {
|
if dirMode || *write {
|
||||||
if out != src {
|
if out != src {
|
||||||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
rc = 1
|
rc = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dirMode {
|
||||||
|
fmt.Println(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -160,8 +248,40 @@ func cmdFmt(args []string) int {
|
|||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asmFiles collects the .s files below dir, skipping directories whose name
|
||||||
|
// starts with "." or "_" — as the go tooling does, which keeps .git and
|
||||||
|
// scratch or reference trees (e.g. _refs) untouched.
|
||||||
|
func asmFiles(dir string) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
if path != dir && (strings.HasPrefix(d.Name(), ".") || strings.HasPrefix(d.Name(), "_")) {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(d.Name(), ".s") {
|
||||||
|
out = append(out, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
func cmdLint(args []string) int {
|
func cmdLint(args []string) int {
|
||||||
fs := flag.NewFlagSet("lint", flag.ExitOnError)
|
fs := newCommand("lint", "gasm lint <file...>", `
|
||||||
|
Run the static checks over the given files and print diagnostics as
|
||||||
|
"file:line:col: severity: message [code]". The exit status is non-zero when
|
||||||
|
an error-severity diagnostic is found; warnings (e.g. the register-clobber
|
||||||
|
audit) do not affect it.
|
||||||
|
|
||||||
|
Rules include unknown-instruction, operand-count, undefined-label,
|
||||||
|
duplicate-label, missing-ret, missing-textflag-include, abi-argsize,
|
||||||
|
unreachable-code, register-clobber and funcdata-pcdata.
|
||||||
|
`)
|
||||||
disable := fs.String("disable", "", "comma-separated rule codes to disable")
|
disable := fs.String("disable", "", "comma-separated rule codes to disable")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() == 0 {
|
if fs.NArg() == 0 {
|
||||||
@@ -202,7 +322,13 @@ func cmdLint(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdLSP(args []string) int {
|
func cmdLSP(args []string) int {
|
||||||
fs := flag.NewFlagSet("lsp", flag.ExitOnError)
|
fs := newCommand("lsp", "gasm lsp", `
|
||||||
|
Run the language server over standard input/output: JSON-RPC 2.0 with
|
||||||
|
Content-Length framing. Point an LSP-capable editor at the binary and
|
||||||
|
associate it with .s files; the target architecture is inferred from the file
|
||||||
|
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
|
||||||
|
hover, document symbols, diagnostics and semantic-token highlighting.
|
||||||
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
srv := lsp.New(os.Stdin, os.Stdout)
|
srv := lsp.New(os.Stdin, os.Stdout)
|
||||||
if err := srv.Run(); err != nil {
|
if err := srv.Run(); err != nil {
|
||||||
@@ -213,7 +339,13 @@ func cmdLSP(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdAsm(args []string) int {
|
func cmdAsm(args []string) int {
|
||||||
fs := flag.NewFlagSet("asm", flag.ExitOnError)
|
fs := newCommand("asm", "gasm asm [-o out.bin] <file>", `
|
||||||
|
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
|
||||||
|
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
||||||
|
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
||||||
|
resolved RIP-relative — and printed as a hex dump. With -o the concatenated
|
||||||
|
image (functions followed by the data section) is written to a file instead.
|
||||||
|
`)
|
||||||
out := fs.String("o", "", "write the concatenated machine code to this file")
|
out := fs.String("o", "", "write the concatenated machine code to this file")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
|
|||||||
+70
-5
@@ -52,6 +52,54 @@ func capture(fn func() int) (stdout, stderr string, code int) {
|
|||||||
return string(ob), string(eb), code
|
return string(ob), string(eb), code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCmdFmtRecursive checks the go-fmt-style directory mode: with no
|
||||||
|
// arguments every .s file below the working directory is formatted in place
|
||||||
|
// ("." and "_" directories skipped), changed files are listed, and a second
|
||||||
|
// run is a no-op.
|
||||||
|
func TestCmdFmtRecursive(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
t.Chdir(tmp)
|
||||||
|
unformatted := []byte("TEXT ·f(SB),NOSPLIT,$0\nRET\n")
|
||||||
|
write := func(path string) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, unformatted, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("a_amd64.s")
|
||||||
|
write(filepath.Join("sub", "b_amd64.s"))
|
||||||
|
write(filepath.Join("_refs", "c_amd64.s"))
|
||||||
|
write(filepath.Join(".git", "d_amd64.s"))
|
||||||
|
|
||||||
|
out, errOut, code := capture(func() int { return cmdFmt(nil) })
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d (%s)", code, errOut)
|
||||||
|
}
|
||||||
|
if out != "a_amd64.s\n"+filepath.Join("sub", "b_amd64.s")+"\n" {
|
||||||
|
t.Errorf("listed files unexpected:\n%s", out)
|
||||||
|
}
|
||||||
|
for _, p := range []string{"a_amd64.s", filepath.Join("sub", "b_amd64.s")} {
|
||||||
|
b, _ := os.ReadFile(p)
|
||||||
|
if !strings.Contains(string(b), "\tRET") {
|
||||||
|
t.Errorf("%s not formatted in place:\n%s", p, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range []string{filepath.Join("_refs", "c_amd64.s"), filepath.Join(".git", "d_amd64.s")} {
|
||||||
|
b, _ := os.ReadFile(p)
|
||||||
|
if string(b) != string(unformatted) {
|
||||||
|
t.Errorf("%s must not be touched:\n%s", p, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: everything is canonical, nothing is listed.
|
||||||
|
out, _, code = capture(func() int { return cmdFmt(nil) })
|
||||||
|
if code != 0 || out != "" {
|
||||||
|
t.Errorf("second pass: code=%d out=%q, want a no-op", code, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCmdTokens(t *testing.T) {
|
func TestCmdTokens(t *testing.T) {
|
||||||
path := writeTemp(t, "f_amd64.s", clean)
|
path := writeTemp(t, "f_amd64.s", clean)
|
||||||
out, _, code := capture(func() int { return cmdTokens([]string{path}) })
|
out, _, code := capture(func() int { return cmdTokens([]string{path}) })
|
||||||
@@ -152,15 +200,32 @@ func TestCmdFmtWrite(t *testing.T) {
|
|||||||
func TestUsage(t *testing.T) {
|
func TestUsage(t *testing.T) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
usage(&b)
|
usage(&b)
|
||||||
if !strings.Contains(b.String(), "gasm") {
|
out := b.String()
|
||||||
t.Errorf("usage text unexpected:\n%s", b.String())
|
for _, want := range []string{
|
||||||
|
"gasm", "Commands:", "Flags:", "--help", "--version",
|
||||||
|
"tokens", "parse", "fmt", "lint", "asm", "lsp", "version",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("usage text missing %q:\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdVersion(t *testing.T) {
|
||||||
|
out, _, code := capture(func() int { return cmdVersion() })
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, version) {
|
||||||
|
t.Errorf("version output %q does not mention %q", out, version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdArgErrors(t *testing.T) {
|
func TestCmdArgErrors(t *testing.T) {
|
||||||
// Missing file arguments produce a usage error (code 2).
|
// A missing path is an error (code 1); cmdFmt with no arguments is the
|
||||||
if _, _, code := capture(func() int { return cmdFmt(nil) }); code != 2 {
|
// recursive mode now, covered by TestCmdFmtRecursive.
|
||||||
t.Errorf("cmdFmt() code = %d, want 2", code)
|
if _, _, code := capture(func() int { return cmdFmt([]string{"no/such/path"}) }); code != 1 {
|
||||||
|
t.Errorf("cmdFmt(missing path) code = %d, want 1", code)
|
||||||
}
|
}
|
||||||
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
|
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
|
||||||
t.Errorf("cmdLint() code = %d, want 2", code)
|
t.Errorf("cmdLint() code = %d, want 2", code)
|
||||||
|
|||||||
@@ -157,9 +157,15 @@ Two deeper analyses sit on top of the AST:
|
|||||||
### `format`
|
### `format`
|
||||||
|
|
||||||
The formatter works on the **token stream, not the AST**, so it preserves
|
The formatter works on the **token stream, not the AST**, so it preserves
|
||||||
every line — comments and blanks included. It only normalises indentation,
|
every line — comments and blanks included. It normalises indentation, operand
|
||||||
operand spacing and per-function mnemonic alignment. It is idempotent and its
|
spacing, per-function mnemonic alignment and blank-line layout: a new block
|
||||||
output always round-trips through the parser.
|
(a label, `TEXT` or `GLOBL`) is preceded by exactly one blank line (comments
|
||||||
|
leading a block stay with it), runs of blanks collapse to one, and a `RET`
|
||||||
|
terminates the body so the next function's doc comment stays at column 0. It
|
||||||
|
is idempotent and its output always round-trips through the parser. With a
|
||||||
|
directory argument — or none — it reformats every `.s` file below it in
|
||||||
|
place and lists the files changed, the way `go fmt` does (`.` and `_`
|
||||||
|
directories are skipped).
|
||||||
|
|
||||||
### `lsp`
|
### `lsp`
|
||||||
|
|
||||||
|
|||||||
+81
-12
@@ -27,14 +27,6 @@ func Source(path, src string) string {
|
|||||||
mnemLen int
|
mnemLen int
|
||||||
funcID int
|
funcID int
|
||||||
}
|
}
|
||||||
const (
|
|
||||||
kBlank = iota
|
|
||||||
kComment
|
|
||||||
kPreproc
|
|
||||||
kDirective
|
|
||||||
kLabel
|
|
||||||
kInstr
|
|
||||||
)
|
|
||||||
|
|
||||||
infos := make([]info, len(lines))
|
infos := make([]info, len(lines))
|
||||||
funcID := -1
|
funcID := -1
|
||||||
@@ -70,8 +62,8 @@ func Source(path, src string) string {
|
|||||||
infos[i] = inf
|
infos[i] = inf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: render.
|
// Second pass: render each line.
|
||||||
var b strings.Builder
|
outs := make([]outLine, 0, len(lines))
|
||||||
inBody := false
|
inBody := false
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
inf := infos[i]
|
inf := infos[i]
|
||||||
@@ -106,10 +98,87 @@ func Source(path, src string) string {
|
|||||||
inBody = false
|
inBody = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(strings.TrimRight(out, " \t"))
|
outs = append(outs, outLine{kind: inf.kind, text: strings.TrimRight(out, " \t")})
|
||||||
|
}
|
||||||
|
return normalizeSpacing(outs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line classification, shared by the formatting passes.
|
||||||
|
const (
|
||||||
|
kBlank = iota
|
||||||
|
kComment
|
||||||
|
kPreproc
|
||||||
|
kDirective
|
||||||
|
kLabel
|
||||||
|
kInstr
|
||||||
|
)
|
||||||
|
|
||||||
|
// outLine is one rendered line together with its classification.
|
||||||
|
type outLine struct {
|
||||||
|
kind int
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeSpacing enforces the canonical blank-line layout: runs of blank
|
||||||
|
// lines collapse to one, and a new block — a label, or a TEXT or GLOBL
|
||||||
|
// directive — is preceded by exactly one blank line. Comments immediately
|
||||||
|
// above a block belong to it, so the blank line is inserted before them. No
|
||||||
|
// blank line is forced at the top of the file, right after a TEXT (the
|
||||||
|
// function's first label), or between stacked labels that share an address.
|
||||||
|
func normalizeSpacing(outs []outLine) string {
|
||||||
|
blockStart := func(ol outLine) bool {
|
||||||
|
switch ol.kind {
|
||||||
|
case kLabel:
|
||||||
|
return true
|
||||||
|
case kDirective:
|
||||||
|
// TEXT and GLOBL open a block; DATA continues a GLOBL block.
|
||||||
|
return strings.HasPrefix(ol.text, "TEXT") || strings.HasPrefix(ol.text, "GLOBL")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
insert := make([]bool, len(outs))
|
||||||
|
for i, ol := range outs {
|
||||||
|
if !blockStart(ol) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := i
|
||||||
|
for j > 0 && outs[j-1].kind == kComment {
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
if j == 0 {
|
||||||
|
continue // top of file
|
||||||
|
}
|
||||||
|
switch prev := outs[j-1]; {
|
||||||
|
case prev.kind == kBlank, prev.kind == kLabel:
|
||||||
|
continue // already separated, or stacked labels
|
||||||
|
case prev.kind == kDirective && strings.HasPrefix(prev.text, "TEXT"):
|
||||||
|
continue // the function's first label
|
||||||
|
}
|
||||||
|
insert[j] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
prevBlank := true // also suppresses leading blanks
|
||||||
|
for i, ol := range outs {
|
||||||
|
if insert[i] && !prevBlank {
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
}
|
}
|
||||||
return b.String()
|
if ol.kind == kBlank {
|
||||||
|
if !prevBlank {
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
prevBlank = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(ol.text)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
prevBlank = false
|
||||||
|
}
|
||||||
|
out := strings.TrimRight(b.String(), "\n")
|
||||||
|
if out == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return out + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderInstr renders an instruction line: a tab, the mnemonic padded to the
|
// renderInstr renders an instruction line: a tab, the mnemonic padded to the
|
||||||
|
|||||||
@@ -77,6 +77,64 @@ func TestDocCommentIndent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBlankLines checks the blank-line canonicalisation: exactly one blank
|
||||||
|
// line before a new block (a label, or TEXT/GLOBL), runs of blanks collapsed
|
||||||
|
// to one, and no blank forced after TEXT, between stacked labels, or at the
|
||||||
|
// top of the file. Leading comments belong to the block they precede.
|
||||||
|
func TestBlankLines(t *testing.T) {
|
||||||
|
in := "#include \"textflag.h\"\n" +
|
||||||
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||||
|
"first:\n" + // first label: no blank after TEXT
|
||||||
|
"XORQ AX, AX\n" +
|
||||||
|
"JMP next\n" + // unlabeled glue: fmt inserts a blank before next:
|
||||||
|
"next:\n" +
|
||||||
|
"stacked:\n" + // stacked labels share an address: no blank between
|
||||||
|
"INCQ AX\n" +
|
||||||
|
"\n" +
|
||||||
|
"\n" + // two blanks collapse to one
|
||||||
|
"// separated block\n" + // comment belongs to the label below
|
||||||
|
"later:\n" +
|
||||||
|
"RET\n" +
|
||||||
|
"// func g()\n" + // doc comment: blank goes before it
|
||||||
|
"TEXT ·g(SB), NOSPLIT, $0\n" +
|
||||||
|
"RET\n" +
|
||||||
|
"GLOBL ·mask(SB), RODATA, $8\n" + // blank before GLOBL…
|
||||||
|
"DATA ·mask+0(SB)/4, $1\n" + // …but not before DATA
|
||||||
|
"\n" +
|
||||||
|
"\n" +
|
||||||
|
"\n" // trailing blanks dropped
|
||||||
|
|
||||||
|
want := "#include \"textflag.h\"\n" +
|
||||||
|
"\n" +
|
||||||
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||||
|
"first:\n" +
|
||||||
|
"\tXORQ AX, AX\n" +
|
||||||
|
"\tJMP next\n" +
|
||||||
|
"\n" +
|
||||||
|
"next:\n" +
|
||||||
|
"stacked:\n" +
|
||||||
|
"\tINCQ AX\n" +
|
||||||
|
"\n" +
|
||||||
|
"\t// separated block\n" + // body comment before a label stays indented
|
||||||
|
"later:\n" +
|
||||||
|
"\tRET\n" +
|
||||||
|
"\n" +
|
||||||
|
"// func g()\n" +
|
||||||
|
"TEXT ·g(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tRET\n" +
|
||||||
|
"\n" +
|
||||||
|
"GLOBL ·mask(SB), RODATA, $8\n" +
|
||||||
|
"DATA ·mask+0(SB)/4, $1\n"
|
||||||
|
|
||||||
|
got := Source("b_amd64.s", in)
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
||||||
|
}
|
||||||
|
if again := Source("b_amd64.s", got); again != got {
|
||||||
|
t.Fatalf("not idempotent:\n%q", again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOperandSpacing(t *testing.T) {
|
func TestOperandSpacing(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"4(SI)": "4(SI)",
|
"4(SI)": "4(SI)",
|
||||||
|
|||||||
Reference in New Issue
Block a user