// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package format import ( "os" "path/filepath" "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("f_amd64.s", 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("d_amd64.s", 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("b_amd64.s", body) if !strings.Contains(gotBody, "\t// inside the body\n") { t.Fatalf("body comment must stay indented:\n%q", gotBody) } } 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("sample_amd64.s", string(src)) twice := Source("sample_amd64.s", 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 fixture and on the real go-flac kernels when present. func TestRoundTrip(t *testing.T) { files := []string{"../testdata/sample_amd64.s"} real, _ := filepath.Glob("../../go-libraries/go-*/*.s") files = append(files, real...) for _, path := range files { src, err := os.ReadFile(path) if err != nil { t.Fatal(err) } formatted := Source(path, 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) } } }