2026-10-07 12:35:51 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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",
|
|
|
|
|
},
|
2026-10-07 13:12:35 +02:00
|
|
|
{
|
|
|
|
|
// A label whose name is a macro: the expansion decides what the
|
|
|
|
|
// line becomes (here an instruction with several operands), so
|
|
|
|
|
// the canonical form keeps the line whole instead of splitting
|
|
|
|
|
// the label off, which would change the statement structure.
|
|
|
|
|
name: "label named as a macro",
|
|
|
|
|
src: "#define M B0, C0\n" +
|
|
|
|
|
"TEXT ·f(SB), $0\n" +
|
|
|
|
|
"\tM: A00\n" +
|
|
|
|
|
"\tRET\n",
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-10-07 13:15:34 +02:00
|
|
|
// Content after a label that names a macro: the expansion may
|
|
|
|
|
// start with any token, so the canonical form keeps it on the
|
|
|
|
|
// label line, where the parser accepts a non-mnemonic first
|
|
|
|
|
// token behind a label but not at the start of a line.
|
|
|
|
|
name: "macro behind a label",
|
|
|
|
|
src: "#define M 0\n" +
|
|
|
|
|
"TEXT ·f(SB), $0\n" +
|
|
|
|
|
"\tA: M\n" +
|
|
|
|
|
"\tRET\n",
|
|
|
|
|
},
|
2026-10-07 13:30:58 +02:00
|
|
|
{
|
|
|
|
|
// A stacked label naming a macro: its expansion decides what
|
|
|
|
|
// the line becomes (an empty body makes it vanish), so it is
|
|
|
|
|
// never peeled onto a line of its own.
|
|
|
|
|
name: "stacked label naming a macro",
|
|
|
|
|
src: "#define M\n" +
|
|
|
|
|
"TEXT ·f(SB), $0\n" +
|
|
|
|
|
"\tA: M:\n" +
|
|
|
|
|
"\tRET\n",
|
|
|
|
|
},
|
2026-10-07 13:35:47 +02:00
|
|
|
{
|
|
|
|
|
// A label reached through a selector: the lexer folds NAME and
|
|
|
|
|
// .selector into one identifier, and the object macro behind the
|
|
|
|
|
// name expands with the selector travelling along, so the folded
|
|
|
|
|
// spelling restructures exactly like the bare name.
|
|
|
|
|
name: "label naming a macro through a selector",
|
|
|
|
|
src: "#define A B\n" +
|
|
|
|
|
"TEXT ·f(SB), $0\n" +
|
|
|
|
|
"\tA.: A0\n" +
|
|
|
|
|
"\tRET\n",
|
|
|
|
|
},
|
2026-10-07 13:15:34 +02:00
|
|
|
{
|
|
|
|
|
// A use above its define: the name is not a macro at that point
|
|
|
|
|
// of the file, so splitting would be safe; the whole-file set
|
|
|
|
|
// the formatter keeps is deliberately conservative and leaves
|
|
|
|
|
// the line whole, which preserves the expansion in both
|
|
|
|
|
// directions alike.
|
2026-10-07 13:12:35 +02:00
|
|
|
name: "label before the define of its name",
|
|
|
|
|
src: "TEXT ·f(SB), $0\n" +
|
|
|
|
|
"\tM: A00\n" +
|
|
|
|
|
"\tRET\n" +
|
|
|
|
|
"#define M B0, C0\n",
|
|
|
|
|
},
|
2026-10-07 12:35:51 +02:00
|
|
|
}
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|