// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package parser import ( "errors" "os" "path/filepath" "strings" "testing" ) // corpusFiles seeds a fuzz target with the repository's kernels, so a plain // `go test` run replays every seed as a regression case and CI exercises them // without any fuzzing budget. func corpusFiles(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)) } } } } // FuzzParse hammers the parser with arbitrary input. The contract: no panic, // always a usable file whether or not diagnostics were reported, and every // diagnostic carries a real position and a non-empty message. func FuzzParse(f *testing.F) { corpusFiles(f) f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n") f.Add("garbage ### ??? ::: \xff\xfe\n") f.Add("#define A(x) x+1\nTEXT ·f(SB), $0\n\tA(2)\n\tRET\n") f.Add("DATA t<>+0(SB)/8, $1\nGLOBL t<>(SB), RODATA, $8\n") f.Add("TEXT ·f(SB), $0\n\tJMP (AX)\n\tCALL (BX)\n\tRET\n") f.Add("\xef\xbb\xbfTEXT ·f(SB), $0\n") // BOM f.Add("CALL internal∕runtime∕atomic·Xchg(SB)\n") // U+2215 path, U+00B7 name f.Add("TEXT ·f(SB), $0\n\tJMP -3(PC)\n\tRET\n") // negative PC jump f.Add("DATA d<>+0(SB)/8, $0xFFFFFFFFFFFFFFFF\n") // unsigned 64-bit immediate f.Add("TEXT ·f(SB), $0-0xFFFFFFFFFFFFFFFF\n\tRET\n") // argument area overflow f.Add("TEXT ·f(SB), $0\n\tMOVD $(1<<0|1<<9), R0\n\tRET\n") // constant expression f.Add("TEXT ·f(SB), $0\n\tADD $-8, R10; SUB $~63, R11\n\tRET\n") // Constant-expression nesting: an immediate and a displacement read // through the recursive folder, so deep parenthesis groups probe its // depth handling on every plain run. The immediate seed is deep enough // that, before the folder carried a depth bound, it grew the goroutine // stack past its limit with a fatal error. f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("(", 4_000_000) + "1" + strings.Repeat(")", 4_000_000) + ", R0\n\tRET\n") f.Add("TEXT ·f(SB), $0\n\tMOVQ " + strings.Repeat("(", 2000) + "1" + strings.Repeat(")", 2000) + "(AX), BX\n\tRET\n") f.Add("TEXT ·f(SB), $0\n\tMOVD $" + strings.Repeat("~", 5000) + "1, R0\n\tRET\n") f.Fuzz(func(t *testing.T, src string) { file, errs := Parse("fuzz.s", src) if file == nil { t.Fatal("Parse returned a nil file") } for _, err := range errs { var perr Error if !errors.As(err, &perr) { t.Fatalf("diagnostic %v is not a parser Error", err) } if !perr.Pos.IsValid() { t.Fatalf("diagnostic %q carries no position", perr.Msg) } if strings.TrimSpace(perr.Msg) == "" { t.Fatalf("diagnostic at %v carries no message", perr.Pos) } } }) } // FuzzParseExpand hammers the preprocessing path, macro expansion and include // splicing included, with arbitrary input. The contract is FuzzParse's: no // panic, a usable file, and diagnostics that name a place and a reason. func FuzzParseExpand(f *testing.F) { corpusFiles(f) f.Add("#define A 1\nTEXT ·f(SB), $0\n\tMOVD $A, R0\n\tRET\n") f.Add("#define A(x) x+1\nA(2)\n") f.Add("#define A(x) ((x))\nA(A(A(1)))\n") f.Add("#define A\nTEXT ·f(SB), $0\n\tRET\n") // empty body f.Add("#define A A\nA\n") // self-reference f.Add("#define A B\n#define B A\nA\n") // mutual recursion f.Add("#ifdef X\n#else\n#endif\n") f.Add("#ifndef X\none\n#else\ntwo\n#endif\n") f.Add("#undef Y\n") f.Add("#include \"textflag.h\"\n") f.Add("#include \"no/such/header.h\"\n") f.Add("#line 7 \"f.s\"\n") f.Add("#define M(x) \\\n\tADD $x, R0 \\\n\tSUB $x, R1\nM(1)\n") // continuations // A body that repeats its argument multiplies every nesting level; the // expansion must stop at the work budget, not produce the exponential // result it names. f.Add("#define A(x) x x x x x x x x x x\nA(A(A(A(A(A(A(A(A(A(1))))))))))\n") f.Fuzz(func(t *testing.T, src string) { file, errs := ParseWithOptions("fuzz.s", src, Options{Expand: true}) if file == nil { t.Fatal("ParseWithOptions returned a nil file") } for _, err := range errs { var perr Error if !errors.As(err, &perr) { t.Fatalf("diagnostic %v is not a parser Error", err) } if !perr.Pos.IsValid() { t.Fatalf("diagnostic %q carries no position", perr.Msg) } if strings.TrimSpace(perr.Msg) == "" { t.Fatalf("diagnostic at %v carries no message", perr.Pos) } } }) }