diff --git a/format/fuzz_test.go b/format/fuzz_test.go index ee451df..c12d7d6 100644 --- a/format/fuzz_test.go +++ b/format/fuzz_test.go @@ -204,6 +204,68 @@ func dump(v any) string { return b.String() } +// FuzzFormatExpansionRoundTrip hammers the formatter against the assembly +// path's reading of the file: macro expansion and include splicing. The +// token view pins every token the formatter carries over, and beyond that a +// file the expander reads cleanly must expand to exactly the same statement +// sequence after formatting, because distinctions the macro language draws +// from layout (the adjacency of a #define name and its '(', the backslash +// continuations that split a body into statements) live in spacing a pure +// token count cannot see. The seed corpus carries the repository's kernels, +// so a plain `go test` run replays every seed as a regression case. +func FuzzFormatExpansionRoundTrip(f *testing.F) { + for _, pattern := range []string{ + "../testdata/*.s", + "../testdata/verify/*.s", + } { + files, _ := filepath.Glob(pattern) + for _, path := range files { + if b, err := os.ReadFile(path); err == nil { + f.Add(string(b)) + } + } + } + f.Add("#define A(x) x+1\nTEXT ·f(SB), $0\n\tA(2)\n\tRET\n") + f.Add("#define A (x) x+1\nA\n") + f.Add("#define M (BX)\nTEXT ·f(SB), $0\n\tMOVL M, AX\n\tRET\n") + f.Add("#define M() \\\n\tADD $1, R0 \\\n\tSUB $2, R1\nTEXT ·f(SB), $0\n\tM()\n\tRET\n") + f.Add("#define PAIR(v) \\\n\tADD $v, R0; \\\n\tSUB $v, R1\nTEXT ·f(SB), $0\n\tPAIR(3)\n\tRET\n") + f.Add("#define A A\nA\n") // self-reference + f.Add("#define A(x) ((x))\nA(A(A(1)))\n") // nested invocation + f.Add("#define A 1\n#define B A\nB\n") // chained object macros + f.Add("#ifdef X\n#else\n#endif\n") // conditionals + f.Add("#define A(x) x x\nA(A(A(1)))\n") // amplifying body + f.Add("#include \"textflag.h\"\n") // the never-spliced header + f.Add("#include \"no/such/header.h\"\n") // unresolvable include + f.Add("#define A(x)\nA()\n") // empty body + f.Add("TEXT ·f(SB), $0\n\tM\n\tRET\n") // undefined name, no table entry + + f.Fuzz(func(t *testing.T, src string) { + out := Source(src) + in, o := tokenView(src), tokenView(out) + if !sameView(in, o) { + t.Fatalf("formatting changed the token stream:\ninput: %v\noutput: %v\n%s", viewString(in), viewString(o), out) + } + if hasStrayIllegal(src) { + // The formatter documents dropping stray runes; their files have + // no expansion to preserve, and the token view above is a weaker + // contract that already held. + return + } + before, errs := parser.ParseWithOptions("in.s", src, parser.Options{Expand: true}) + if len(errs) > 0 { + return + } + after, errs := parser.ParseWithOptions("out.s", out, parser.Options{Expand: true}) + if len(errs) > 0 { + t.Fatalf("formatted output of expandable input no longer expands: %v\n%s", errs[0], out) + } + if got, want := stmtSignature(after), stmtSignature(before); !reflect.DeepEqual(got, want) { + t.Fatalf("formatting changed the expansion:\nbefore: %v\nafter: %v\n%s", want, got, out) + } + }) +} + func dumpValue(v reflect.Value, b *strings.Builder) { switch v.Kind() { case reflect.Pointer, reflect.Interface: