├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── cmd └── main.go ├── ebnf └── palindrome.ebnf ├── go.mod ├── go.sum ├── process_ebnf.go └── random.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.23', '1.24' ] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Go ${{ matrix.go-version }} 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Build 20 | run: | 21 | go get . 22 | go build -o gromit -v cmd/main.go 23 | - name: Testing 24 | run: | 25 | ./gromit -filename ebnf/palindrome.ebnf -start palindrome 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2025 Sergey Bronnikov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Gromit 2 | 3 | [![Testing](https://github.com/ligurio/gromit/actions/workflows/test.yml/badge.svg)](https://github.com/ligurio/gromit/actions/workflows/test.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/ligurio/gromit)](https://goreportcard.com/report/github.com/ligurio/gromit) 4 | 5 | is a random text generator based on context-free grammars; it uses 6 | an EBNF for grammar definitions. EBNF is an Extended Backus-Naur 7 | Form. It is the standard format for the specification and 8 | documentation of programming languages; it is defined in the 9 | ISO/IEC 14977 standard. 10 | 11 | The input is text satisfying the following grammar (represented 12 | itself in EBNF): 13 | 14 | ``` 15 | Production = name "=" [ Expression ] "." . 16 | Expression = Alternative { "|" Alternative } . 17 | Alternative = Term { Term } . 18 | Term = name | token [ "…" token ] | Group | Option | Repetition . 19 | Group = "(" Expression ")" . 20 | Option = "[" Expression "]" . 21 | Repetition = "{" Expression "}" . 22 | ``` 23 | 24 | ## Usage 25 | 26 | ``` 27 | ~$ go build -o gromit -v cmd/main.go 28 | ~$ ./gromit -filename ebnf/palindrome.ebnf -start palindrome 29 | khbhk 30 | ``` 31 | 32 | ## See also 33 | 34 | - [grammarinator](https://github.com/renatahodovan/grammarinator) 35 | is an ANTLR v4 grammar-based test generator (Python). 36 | - [bnfgen](https://baturin.org/tools/bnfgen/) is a random text 37 | generator based on context-free grammars, it uses a DSL for 38 | grammar definitions that is similar to the familiar BNF, 39 | with two extensions: weighted random selection and deterministic 40 | repetition (OCaml). 41 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | rand "math/rand" 13 | 14 | gromit "github.com/ligurio/gromit" 15 | "golang.org/x/exp/ebnf" 16 | ) 17 | 18 | var ( 19 | name = flag.String("filename", "", "filename with grammar") 20 | action = flag.String("action", "fuzz", "action (possible values: fuzz and dict)") 21 | start = flag.String("start", "", "start string") 22 | seed = flag.Int64("seed", 0, "random seed; if 0, seed is generated (default)") 23 | maxreps = flag.Int("maxreps", 10, "maximum number of repetitions") 24 | depth = flag.Int("depth", 30, "maximum depth") 25 | padding = flag.String("padding", " ", "non-terminal padding characters") 26 | ) 27 | 28 | func main() { 29 | flag.Parse() 30 | 31 | if *seed == 0 { 32 | *seed = time.Now().UnixNano() 33 | } 34 | source := rand.NewSource(*seed) 35 | rng := rand.New(source) 36 | 37 | if *name == "" && *start == "" { 38 | flag.Usage() 39 | fmt.Println("Filename or start string is not specified.") 40 | os.Exit(1) 41 | } 42 | 43 | f, err := os.Open(*name) 44 | if err != nil { 45 | fmt.Println(err) 46 | os.Exit(1) 47 | } 48 | defer f.Close() 49 | 50 | grammar, err := ebnf.Parse(*name, bufio.NewReader(f)) 51 | if err != nil { 52 | fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | 56 | err = ebnf.Verify(grammar, *start) 57 | if err != nil { 58 | fmt.Println(err) 59 | os.Exit(1) 60 | } 61 | 62 | if *action == "dict" { 63 | err = gromit.Dict(os.Stdout, grammar, *start, rng, *padding) 64 | if err != nil { 65 | fmt.Println(err) 66 | os.Exit(1) 67 | } 68 | os.Exit(1) 69 | } 70 | 71 | err = gromit.Random(os.Stdout, grammar, *start, rng, *maxreps, *padding) 72 | if err != nil { 73 | fmt.Println(err) 74 | os.Exit(1) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ebnf/palindrome.ebnf: -------------------------------------------------------------------------------- 1 | palindrome = letter | 2 | "a" palindrome "a" | 3 | "b" palindrome "b" | 4 | "c" palindrome "c" | 5 | "d" palindrome "d" | 6 | "e" palindrome "e" | 7 | "f" palindrome "f" | 8 | "g" palindrome "g" | 9 | "h" palindrome "h" | 10 | "i" palindrome "i" | 11 | "j" palindrome "j" | 12 | "k" palindrome "k" | 13 | "l" palindrome "l" | 14 | "m" palindrome "m" | 15 | "n" palindrome "n" | 16 | "p" palindrome "p" | 17 | "q" palindrome "q" | 18 | "r" palindrome "r" | 19 | "s" palindrome "s" | 20 | "t" palindrome "t" | 21 | "u" palindrome "u" | 22 | "v" palindrome "v" | 23 | "w" palindrome "w" | 24 | "x" palindrome "x" | 25 | "y" palindrome "y" | 26 | "z" palindrome "z" . 27 | letter = "a" … "z" . 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ligurio/gromit 2 | 3 | go 1.23.6 4 | 5 | require golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 2 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 3 | -------------------------------------------------------------------------------- /process_ebnf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package gromit 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "strings" 11 | 12 | "golang.org/x/exp/ebnf" 13 | 14 | rand "math/rand" 15 | ) 16 | 17 | var ErrStartNotFound = errors.New("Start production not found") 18 | var ErrBadRange = errors.New("Bad range") 19 | 20 | func Random(dst io.Writer, grammar ebnf.Grammar, start string, rng *rand.Rand, maxreps int, padding string) error { 21 | production, err := grammar[start] 22 | if !err { 23 | return ErrStartNotFound 24 | } 25 | 26 | return random(dst, grammar, production.Expr, 0, maxreps, rng, padding) 27 | } 28 | 29 | func IsTerminal(expr ebnf.Expression) bool { 30 | switch expr.(type) { 31 | case *ebnf.Name: 32 | name := expr.(*ebnf.Name) 33 | return !IsCapital(name.String) 34 | case *ebnf.Range: 35 | return true 36 | case *ebnf.Token: 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | 43 | func findTerminals(exprs []ebnf.Expression) []ebnf.Expression { 44 | r := make([]ebnf.Expression, 0, len(exprs)) 45 | for _, expr := range exprs { 46 | if IsTerminal(expr) { 47 | r = append(r, expr) 48 | } 49 | } 50 | return r 51 | } 52 | 53 | func random(dst io.Writer, grammar ebnf.Grammar, expr ebnf.Expression, depth int, maxreps int, rng *rand.Rand, padding string) error { 54 | var maxdepth int // FIXME 55 | maxdepth = 100 56 | 57 | if expr == nil { 58 | return nil 59 | } 60 | 61 | switch expr.(type) { 62 | case ebnf.Alternative: 63 | alt := expr.(ebnf.Alternative) 64 | var exprs []ebnf.Expression 65 | if depth > maxdepth { 66 | exprs = findTerminals(alt) 67 | if len(exprs) == 0 { 68 | exprs = alt 69 | } 70 | } else { 71 | exprs = alt 72 | } 73 | err := random(dst, grammar, exprs[rng.Intn(len(exprs))], depth+1, maxreps, rng, padding) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | case *ebnf.Group: 79 | gr := expr.(*ebnf.Group) 80 | err := random(dst, grammar, gr.Body, depth+1, maxreps, rng, padding) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | case *ebnf.Name: 86 | name := expr.(*ebnf.Name) 87 | p := !IsTerminal(expr) 88 | if p { 89 | pad(dst, padding) 90 | } 91 | err := random(dst, grammar, grammar[name.String], depth+1, maxreps, rng, padding) 92 | if err != nil { 93 | return err 94 | } 95 | if p { 96 | pad(dst, padding) 97 | } 98 | 99 | case *ebnf.Option: 100 | opt := expr.(*ebnf.Option) 101 | if depth > maxdepth && !IsTerminal(opt.Body) { 102 | fmt.Println("non-terminal omitted due to having exceeded recursion depth limit") 103 | } else if PickBool() { 104 | err := random(dst, grammar, opt.Body, depth+1, maxreps, rng, padding) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | 110 | case *ebnf.Production: 111 | prod := expr.(*ebnf.Production) 112 | err := random(dst, grammar, prod.Expr, depth+1, maxreps, rng, padding) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | case *ebnf.Range: 118 | rang := expr.(*ebnf.Range) 119 | ch, err := PickString(rang.Begin.String, rang.End.String) 120 | if err != nil { 121 | return err 122 | } 123 | if _, err := io.WriteString(dst, ch); err != nil { 124 | return err 125 | } 126 | 127 | case *ebnf.Repetition: 128 | rep := expr.(*ebnf.Repetition) 129 | if depth > maxdepth && !IsTerminal(rep.Body) { 130 | fmt.Println("Repetition omitted") 131 | } else { 132 | reps := rng.Intn(maxreps + 1) 133 | for i := 0; i < reps; i++ { 134 | err := random(dst, grammar, rep.Body, depth+1, maxreps, rng, padding) 135 | if err != nil { 136 | return err 137 | } 138 | } 139 | } 140 | 141 | case ebnf.Sequence: 142 | seq := expr.(ebnf.Sequence) 143 | for _, e := range seq { 144 | err := random(dst, grammar, e, depth+1, maxreps, rng, padding) 145 | if err != nil { 146 | return err 147 | } 148 | } 149 | 150 | case *ebnf.Token: 151 | tok := expr.(*ebnf.Token) 152 | if _, err := io.WriteString(dst, tok.String); err != nil { 153 | return err 154 | } 155 | pad(dst, padding) 156 | 157 | default: 158 | return fmt.Errorf("Bad expression %g", expr) 159 | } 160 | 161 | return nil 162 | } 163 | 164 | func Dict(dst io.Writer, grammar ebnf.Grammar, start string, rng *rand.Rand, padding string) error { 165 | 166 | production, err := grammar[start] 167 | if !err { 168 | return ErrStartNotFound 169 | } 170 | 171 | return random1(dst, grammar, production.Expr, 0, rng, padding) 172 | } 173 | 174 | func random1(dst io.Writer, grammar ebnf.Grammar, expr ebnf.Expression, depth int, rng *rand.Rand, padding string) error { 175 | switch expr.(type) { 176 | case ebnf.Alternative: 177 | alt := expr.(ebnf.Alternative) 178 | var exprs []ebnf.Expression 179 | exprs = findTerminals(alt) 180 | if len(exprs) == 0 { 181 | exprs = alt 182 | } 183 | for _, e := range exprs { 184 | err := random1(dst, grammar, e, depth+1, rng, padding) 185 | if err != nil { 186 | return err 187 | } 188 | } 189 | 190 | case *ebnf.Group: 191 | gr := expr.(*ebnf.Group) 192 | err := random1(dst, grammar, gr.Body, depth+1, rng, padding) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | case *ebnf.Name: 198 | name := expr.(*ebnf.Name) 199 | p := !IsTerminal(expr) 200 | if p { 201 | pad(dst, padding) 202 | } 203 | err := random1(dst, grammar, grammar[name.String], depth+1, rng, padding) 204 | if err != nil { 205 | return err 206 | } 207 | if p { 208 | pad(dst, padding) 209 | } 210 | 211 | case *ebnf.Option: 212 | opt := expr.(*ebnf.Option) 213 | err := random1(dst, grammar, opt.Body, depth+1, rng, padding) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | case *ebnf.Production: 219 | prod := expr.(*ebnf.Production) 220 | err := random1(dst, grammar, prod.Expr, depth+1, rng, padding) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | case *ebnf.Range: 226 | // do nothing 227 | 228 | case *ebnf.Repetition: 229 | // do nothing 230 | 231 | case ebnf.Sequence: 232 | seq := expr.(ebnf.Sequence) 233 | for _, e := range seq { 234 | err := random1(dst, grammar, e, depth+1, rng, padding) 235 | if err != nil { 236 | return err 237 | } 238 | } 239 | 240 | case *ebnf.Token: 241 | tok := expr.(*ebnf.Token) 242 | if _, err := io.WriteString(dst, dictline(tok.String)+"\n"); err != nil { 243 | return err 244 | } 245 | 246 | default: 247 | log.Fatal("Bad expression", expr) 248 | } 249 | 250 | return nil 251 | } 252 | 253 | func dictline(tok string) string { 254 | var replacer = strings.NewReplacer(" ", "_", "-", "_") 255 | str := replacer.Replace(tok) 256 | return "KEYWORD_" + strings.ToUpper(str) + "=\"" + tok + "\"" 257 | 258 | } 259 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package gromit 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os" 9 | "unicode" 10 | "unicode/utf8" 11 | 12 | rand "math/rand" 13 | ) 14 | 15 | func PickString(begin, end string) (string, error) { 16 | br := []rune(begin) 17 | er := []rune(end) 18 | if len(br) != len(er) { 19 | return "", ErrBadRange 20 | } 21 | ret := make([]rune, len(br)) 22 | for i := range ret { 23 | if int32(br[i]) > int32(er[i]) { 24 | return "", ErrBadRange 25 | } 26 | ret[i] = PickRune(br[i], er[i]) 27 | } 28 | return string(ret), nil 29 | } 30 | 31 | func PickRune(begin, end rune) rune { 32 | return rune(PickInt32(int32(begin), int32(end))) 33 | } 34 | 35 | func PickInt32(begin, end int32) int32 { 36 | if begin > end { 37 | fmt.Println("PickInt32: invalid arguments: begin > end", begin, end) 38 | os.Exit(1) 39 | } 40 | diff := int64(end) - int64(begin) 41 | return int32(int64(begin) + rand.Int63n(diff+1)) 42 | } 43 | 44 | func PickBool() bool { 45 | if rand.Int63()&1 == 1 { 46 | return true 47 | } 48 | return false 49 | } 50 | 51 | func IsCapital(s string) bool { 52 | ch, _ := utf8.DecodeRuneInString(s) 53 | return unicode.IsUpper(ch) 54 | } 55 | 56 | func pad(dst io.Writer, padding string) error { 57 | runes := []rune(padding) 58 | if len(runes) == 0 { 59 | return nil 60 | } 61 | r := runes[rand.Intn(len(runes))] 62 | _, err := io.WriteString(dst, string([]rune{r})) 63 | return err 64 | } 65 | --------------------------------------------------------------------------------