diff --git a/gg/grammar/example_test.go b/gg/grammar/example_test.go new file mode 100644 index 0000000..4cc2b42 --- /dev/null +++ b/gg/grammar/example_test.go @@ -0,0 +1,164 @@ +package grammar_test + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "code.betamike.com/mediocregopher/ginger/gg/grammar" + "golang.org/x/exp/slices" +) + +/* +This example demonstrates how to describe the following EBNF using the grammar +package: + +``` + ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + ::= + + ::= "-" + ::= | + + ::= + ::= ")" | "," + ::= ")" | + ::= "(" + + ::= | +``` + +*/ + +// Element represents an element of a list, which can be either a number or a +// sub-list. +type Element struct { + Number int64 + List []Element +} + +func (e Element) String() string { + if e.List != nil { + listElStrs := make([]string, len(e.List)) + for i := range e.List { + listElStrs[i] = e.List[i].String() + } + return fmt.Sprintf("(%s)", strings.Join(listElStrs, ",")) + } + + return fmt.Sprint(e.Number) +} + +func s(str string) fmt.Stringer { + return grammar.Stringer{S: str} +} + +var ( + digit = grammar.RuneFunc( + s("digit"), func(r rune) bool { return '0' <= r && r <= '9' }, + ) + + positiveNumber = grammar.StringFromRunes(grammar.OneOrMore(digit)) + + negativeNumber = grammar.Reduction( + s("negative-number"), + grammar.Rune('-'), + positiveNumber, + func( + neg grammar.Located[rune], posNum grammar.Located[string], + ) grammar.Located[string] { + return grammar.Locate(neg.Location, string(neg.Value)+posNum.Value) + }, + ) + + number = grammar.Mapping( + s("number"), + grammar.FirstOf(negativeNumber, positiveNumber), + func(str grammar.Located[string]) Element { + i, err := strconv.ParseInt(str.Value, 10, 64) + if err != nil { + panic(fmt.Errorf("parsing %q as int: %w", str, err)) + } + return Element{Number: i} + }, + ) + + // Because the list/element definitions are recursive it requires using + // SymbolPtrs, which is easier to do via a global initialization function + // like this. + list = func() grammar.Symbol[Element] { + + type listState []Element + + var ( + listTail = new(grammar.SymbolPtr[listState]) + list = new(grammar.SymbolPtr[Element]) + element = new(grammar.SymbolPtr[Element]) + + // Right parenthesis indicates the end of a list, at which point we + // can initialize the state which gets returned down the stack. We + // pass rightParen as the Stringer because we want that to still be + // the string identifier for this branch of the symbol, for error + // purposes. + rightParen = grammar.Rune(')') + listTerm = grammar.Mapping( + rightParen, + rightParen, + func(grammar.Located[rune]) listState { return listState{} }, + ) + + listElTail = grammar.Reduction[Element, listState, listState]( + element, // Stringer + element, + listTail, + func(el Element, ls listState) listState { + ls = append(ls, el) + return ls + }, + ) + + listHead = grammar.FirstOf(listTerm, listElTail) + ) + + listTail.Symbol = grammar.FirstOf( + listTerm, + grammar.Prefixed(grammar.Rune(','), listElTail), + ) + + list.Symbol = grammar.Reduction[ + grammar.Located[rune], listState, Element, + ]( + s("list"), + grammar.Rune('('), + listHead, + func(_ grammar.Located[rune], ls listState) Element { + slices.Reverse(ls) + return Element{List: []Element(ls)} + }, + ) + + element.Symbol = grammar.FirstOf[Element](number, list) + + return list + }() +) + +func Example() { + r := grammar.NewReader(bytes.NewBufferString( + `()` + `(1,(2,-3),4)` + `(ERROR`, + )) + + l1, err := list.Decode(r) + fmt.Println(l1, err) + + l2, err := list.Decode(r) + fmt.Println(l2, err) + + _, err = list.Decode(r) + fmt.Println(err) + + // Output: + // () + // (1,(2,-3),4) + // 1:16: expected ')' or number or list +}