191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package format
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
|
)
|
|
|
|
func TestGolden(t *testing.T) {
|
|
in := "#include \"textflag.h\"\n" +
|
|
"\n" +
|
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
|
"MOVQ swin_base+0(FP), SI\n" +
|
|
"LEAQ (SI)(BX*4), R9\n" +
|
|
"ANDQ $-8, R10\n" +
|
|
"VFMADD231PD Z14, Z12, Z10\n" +
|
|
"RET\n"
|
|
|
|
want := "#include \"textflag.h\"\n" +
|
|
"\n" +
|
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
|
"\tMOVQ swin_base+0(FP), SI\n" +
|
|
"\tLEAQ (SI)(BX*4), R9\n" +
|
|
"\tANDQ $-8, R10\n" +
|
|
"\tVFMADD231PD Z14, Z12, Z10\n" +
|
|
"\tRET\n"
|
|
|
|
got := Source(in)
|
|
if got != want {
|
|
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestDocCommentIndent checks that a doc comment preceding a TEXT directive
|
|
// sits at column 0 even when another function (ending in RET) precedes it;
|
|
// the RET must terminate the previous body for indentation purposes.
|
|
func TestDocCommentIndent(t *testing.T) {
|
|
in := "#include \"textflag.h\"\n" +
|
|
"\n" +
|
|
"// func first()\n" +
|
|
"TEXT ·first(SB), NOSPLIT, $0\n" +
|
|
"XORQ AX, AX\n" +
|
|
"RET\n" +
|
|
"\n" +
|
|
"// func second()\n" +
|
|
"TEXT ·second(SB), NOSPLIT, $0\n" +
|
|
"RET\n"
|
|
|
|
want := "#include \"textflag.h\"\n" +
|
|
"\n" +
|
|
"// func first()\n" +
|
|
"TEXT ·first(SB), NOSPLIT, $0\n" +
|
|
"\tXORQ AX, AX\n" +
|
|
"\tRET\n" +
|
|
"\n" +
|
|
"// func second()\n" +
|
|
"TEXT ·second(SB), NOSPLIT, $0\n" +
|
|
"\tRET\n"
|
|
|
|
got := Source(in)
|
|
if got != want {
|
|
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
|
}
|
|
// Body comments stay indented.
|
|
body := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n// inside the body\nXORQ AX, AX\nRET\n"
|
|
gotBody := Source(body)
|
|
if !strings.Contains(gotBody, "\t// inside the body\n") {
|
|
t.Fatalf("body comment must stay indented:\n%q", gotBody)
|
|
}
|
|
}
|
|
|
|
// 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(in)
|
|
if got != want {
|
|
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
|
}
|
|
if again := Source(got); again != got {
|
|
t.Fatalf("not idempotent:\n%q", again)
|
|
}
|
|
}
|
|
|
|
func TestOperandSpacing(t *testing.T) {
|
|
cases := map[string]string{
|
|
"4(SI)": "4(SI)",
|
|
"(SI)(BX*4)": "(SI)(BX*4)",
|
|
"$-8": "$-8",
|
|
"$0x80020100": "$0x80020100",
|
|
"swin_base+0(FP)": "swin_base+0(FP)",
|
|
"mask24<>(SB)": "mask24<>(SB)",
|
|
"·idx16+0(SB)/4": "·idx16+0(SB)/4",
|
|
}
|
|
for in, want := range cases {
|
|
toks := lexOperands(in)
|
|
if got := renderOps(toks); got != want {
|
|
t.Errorf("renderOps(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// lexOperands lexes a single operand string and drops the EOF token.
|
|
func lexOperands(s string) []token.Token {
|
|
toks := lexer.Tokenize(s)
|
|
return toks[:len(toks)-1] // drop trailing EOF
|
|
}
|
|
|
|
func TestIdempotent(t *testing.T) {
|
|
src, err := os.ReadFile("../testdata/sample_amd64.s")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
once := Source(string(src))
|
|
twice := Source(once)
|
|
if once != twice {
|
|
t.Fatal("formatting is not idempotent on the fixture")
|
|
}
|
|
}
|
|
|
|
// TestRoundTrip checks that formatting produces source that still parses
|
|
// cleanly on the in-repository fixture.
|
|
func TestRoundTrip(t *testing.T) {
|
|
files := []string{"../testdata/sample_amd64.s"}
|
|
for _, path := range files {
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
formatted := Source(string(src))
|
|
if _, errs := parser.Parse(path, formatted); len(errs) > 0 {
|
|
t.Errorf("formatted %s no longer parses: %v", path, errs)
|
|
}
|
|
if strings.TrimSpace(formatted) == "" {
|
|
t.Errorf("formatted %s is empty", path)
|
|
}
|
|
}
|
|
}
|