// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package format import ( "testing" "sourcedock.dev/petrbalvin/gasm-sdk/ast" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // TestFlagListCanonical pins the canonical rendering of the TEXT and GLOBL // flags operand: the names stay glued to their '|' separators, the operands // take one space after each comma, and the spelling survives whatever // layout it arrives in. Every shape mixes its flags with a frame size, // which is where the operand boundaries live. func TestFlagListCanonical(t *testing.T) { for _, c := range []struct{ in, want string }{ {"TEXT ·f(SB),NOSPLIT|NOFRAME,$0-16\n\tRET\n", "TEXT ·f(SB), NOSPLIT|NOFRAME, $0-16\n\tRET\n"}, {"TEXT ·f(SB) , NOSPLIT|NOFRAME , $0-16\n\tRET\n", "TEXT ·f(SB), NOSPLIT|NOFRAME, $0-16\n\tRET\n"}, {"TEXT ·f(SB), NOSPLIT|NOFRAME|TOPFRAME, $0\n\tRET\n", "TEXT ·f(SB), NOSPLIT|NOFRAME|TOPFRAME, $0\n\tRET\n"}, {"TEXT ·f(SB), 2|4, $4096-0\n\tRET\n", "TEXT ·f(SB), 2|4, $4096-0\n\tRET\n"}, {"TEXT ·f(SB), (NOSPLIT|NOFRAME), $0-8\n\tRET\n", "TEXT ·f(SB), (NOSPLIT|NOFRAME), $0-8\n\tRET\n"}, {"TEXT ·foo(SB), NOSPLIT, $-0-24\n\tRET\n", "TEXT ·foo(SB), NOSPLIT, $-0-24\n\tRET\n"}, {"GLOBL ·mask(SB),RODATA|NOPTR,$8\n", "GLOBL ·mask(SB), RODATA|NOPTR, $8\n"}, {"GLOBL ·mask(SB), 10, $8\n", "GLOBL ·mask(SB), 10, $8\n"}, } { got := Source(c.in) if got != c.want { t.Errorf("formatting %q:\n got %q\n want %q", c.in, got, c.want) continue } // The standing invariants over the rendered line: the output is its // own canonical form, and formatting changed layout, never tokens. if again := Source(got); again != got { t.Errorf("not idempotent for %q:\n%q", c.in, again) } if in, out := tokenView(c.in), tokenView(got); !sameView(in, out) { t.Errorf("formatting %q changed the token stream:\ninput: %v\noutput: %v", c.in, viewString(in), viewString(out)) } } } // TestFlagListFormatRoundTrip covers the parser's side of the same shapes: // a header that parses cleanly still parses cleanly after formatting, and // the flags value and canonical names survive the pass unchanged. func TestFlagListFormatRoundTrip(t *testing.T) { for _, src := range []string{ "TEXT ·f(SB),NOSPLIT|NOFRAME,$0-16\n\tRET\n", "TEXT ·f(SB), 2|4, $4096-0\n\tRET\n", "TEXT ·f(SB), (NOSPLIT|NOFRAME), $0-8\n\tRET\n", "GLOBL ·mask(SB),RODATA|NOPTR,$8\n", } { before, errs := parser.Parse("in.s", src) if len(errs) > 0 { t.Fatalf("parse %q: %v", src, errs) } after, errs := parser.Parse("in.s", Source(src)) if len(errs) > 0 { t.Fatalf("formatted %q no longer parses: %v", src, errs) } beforeD, afterD := firstFlaggedDecl(before), firstFlaggedDecl(after) if beforeD == nil || afterD == nil { t.Fatalf("%q: a TEXT or GLOBL declaration was lost by formatting", src) } beforeFlags, beforeVal := declFlags(beforeD) afterFlags, afterVal := declFlags(afterD) if beforeVal != afterVal { t.Errorf("%q: FlagVal %d became %d", src, beforeVal, afterVal) } if len(beforeFlags) != len(afterFlags) { t.Errorf("%q: flags %v became %v", src, beforeFlags, afterFlags) continue } for i := range beforeFlags { if beforeFlags[i] != afterFlags[i] { t.Errorf("%q: flags %v became %v", src, beforeFlags, afterFlags) break } } } } // firstFlaggedDecl returns the file's first TEXT or GLOBL declaration, the // two directives whose operand carries flags. func firstFlaggedDecl(f *ast.File) ast.Decl { for _, d := range f.Decls { switch d.(type) { case *ast.Text, *ast.Globl: return d } } return nil } // declFlags returns a declaration's flags operand: the canonical names and // the folded value. func declFlags(d ast.Decl) ([]string, int64) { switch dd := d.(type) { case *ast.Text: return dd.Flags, dd.FlagVal case *ast.Globl: return dd.Flags, dd.FlagVal } return nil, 0 }