// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package format import ( "slices" "strings" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // TestFormatPreservesMacroAdjacency pins the distinction the macro language // draws from the source's layout: a '(' glued to a #define name makes the // macro parameterised, while one separated by a space leaves it an object // macro whose body happens to open with a parenthesis. Both spellings are // valid input and each must format to itself, because swapping one for the // other changes what every use of the macro expands to. func TestFormatPreservesMacroAdjacency(t *testing.T) { for _, in := range []string{ "#define A(x) x+1\n", "#define A (x) x+1\n", "#define A(x) y x y\n", "#define A ()\n", } { if got := Source(in); got != in { t.Errorf("formatting %q:\n got %q", in, got) } } } // TestFormatPreservesExpansionSemantics proves the formatter's contract on // the assembly path: a file the expander reads cleanly must expand to exactly // the same statements after formatting. Each case is a macro shape whose // meaning lives in the source's layout, where a formatter that only counts // tokens cannot see the difference it destroys. func TestFormatPreservesExpansionSemantics(t *testing.T) { cases := []struct { name string src string }{ { // An object macro whose body opens with a parenthesis: gluing the // '(' to the name would turn it into a parameterised one, and the // bare use would stop expanding at all. name: "object macro with parenthesised body", src: "#define M (BX)\n" + "TEXT ·f(SB), $0\n" + "\tMOVL M, AX\n" + "\tRET\n", }, { // A zero-argument macro invoked on a line whose mnemonic padding // opens a gap before the '(': the invocation must still expand, // because the parentheses are tokens however far apart. name: "zero-argument macro with alignment padding", src: "#define A() ADD $1, R0\n" + "TEXT ·f(SB), $0\n" + "\tA()\n" + "\tRET\n", }, { // The arm64 style: a macro body written across continuations // without ';' separators. The expansion splits the body at the // line boundaries, so flattening the continuations would fuse two // instructions into one operand list. name: "continuation body without semicolons", src: "#define M() \\\n" + "\tADD $1, R0 \\\n" + "\tSUB $2, R1\n" + "TEXT ·f(SB), $0\n" + "\tM()\n" + "\tRET\n", }, { // The mixed style of the runtime sources: continuations that also // carry ';' separators inside some of their lines. name: "continuation body with semicolons", src: "#define PAIR(v) \\\n" + "\tADD $v, R0; \\\n" + "\tSUB $v, R1\n" + "TEXT ·f(SB), $0\n" + "\tPAIR(3)\n" + "\tRET\n", }, { // A parameterised macro whose formal list itself spans a // continuation: the body opens on the next line, and the formal // list must stay glued to the name. name: "continuation after the formal list", src: "#define M(a, b) \\\n" + "\tADD $a, b\n" + "TEXT ·f(SB), $0\n" + "\tM(1, R0)\n" + "\tRET\n", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { out := Source(tc.src) if again := Source(out); again != out { t.Fatalf("not idempotent:\n%s", again) } assertSameExpansion(t, tc.src, out) }) } } // assertSameExpansion expands src and formatted with the macro machinery and // compares the statement sequences they produce. func assertSameExpansion(t *testing.T, src, formatted string) { t.Helper() before, errs := parser.ParseWithOptions("in.s", src, parser.Options{Expand: true}) if len(errs) > 0 { t.Fatalf("source does not expand: %v", errs) } after, errs := parser.ParseWithOptions("out.s", formatted, parser.Options{Expand: true}) if len(errs) > 0 { t.Fatalf("formatted source does not expand: %v\n%s", errs, formatted) } want, got := stmtSignature(before), stmtSignature(after) if !slices.Equal(got, want) { t.Fatalf("expansion changed:\n--- before ---\n%q\n--- after ---\n%q\n%s", want, got, formatted) } } // TestContinuationBodyNotFlattened pins the rendered spelling of a // continuation body directly: the backslash boundaries the input carried are // the canonical form, not a flattened single line. func TestContinuationBodyNotFlattened(t *testing.T) { in := "#define M() \\\n\tADD $1, R0 \\\n\tSUB $2, R1\n" if got := Source(in); got != in { t.Fatalf("continuation body must render to itself:\n got %q\nwant %q", got, in) } if strings.Contains(strings.ReplaceAll(Source(in), "\\\n", ""), "ADD $1, R0 SUB") { t.Fatal("continuation body was flattened into one physical line") } }