// Copyright (c) 2026 Petr Balvín (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) } } }