From 9179d5cc7a78973a6887c6ac997634568448d7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 21:33:18 +0200 Subject: [PATCH] test(format): pin the canonical rendering of the flags operand Assisted-by: GLM 5.3 Flash --- format/flaglist_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 format/flaglist_test.go diff --git a/format/flaglist_test.go b/format/flaglist_test.go new file mode 100644 index 0000000..3375a12 --- /dev/null +++ b/format/flaglist_test.go @@ -0,0 +1,108 @@ +// 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 +}