Skip to content

Commit

Permalink
Export EBNF.
Browse files Browse the repository at this point in the history
Fixes #1.

Example from the test:

    EBNF = (Production)* .
    Production = ident "=" Expression (Expression)* "." .
    Expression = Sequence ("|" Sequence)* .
    Sequence = Term (Term)* .
    Term = ident | Literal | Range | Group | EBNFOption | Repetition .
    Literal = string .
    Range = string "…" string .
    Group = "(" Expression ")" .
    EBNFOption = "[" Expression "]" .
    Repetition = "{" Expression "}" .
  • Loading branch information
alecthomas committed Sep 12, 2020
1 parent 318127c commit fc7769c
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
120 changes: 120 additions & 0 deletions ebnf.go
@@ -0,0 +1,120 @@
package participle

import (
"fmt"
"strings"
)

// EBNF for the grammar.
//
// Productions are always upper case. Lexer tokens are always lower case.
func (p *Parser) EBNF() string {
seen := map[node]bool{}
outp := []*ebnfp{}
ebnf(p.root, seen, nil, &outp)
out := []string{}
for _, p := range outp {
out = append(out, fmt.Sprintf("%s = %s .", p.name, p.out))
}
return strings.Join(out, "\n")
}

type ebnfp struct {
name string
out string
}

func ebnf(n node, seen map[node]bool, p *ebnfp, outp *[]*ebnfp) {
switch n := n.(type) {
case *disjunction:
for i, next := range n.nodes {
if i > 0 {
p.out += " | "
}
ebnf(next, seen, p, outp)
}
return

case *strct:
name := strings.ToUpper(n.typ.Name()[:1]) + n.typ.Name()[1:]
if p != nil {
p.out += name
}
if seen[n] {
return
}
seen[n] = true
p = &ebnfp{name: name}
*outp = append(*outp, p)
ebnf(n.expr, seen, p, outp)
return

case *sequence:
ebnf(n.node, seen, p, outp)
if n.next != nil {
p.out += " "
ebnf(n.next, seen, p, outp)
}
return

case *parseable:
p.out += n.t.Name()

case *capture:
ebnf(n.node, seen, p, outp)

case *reference:
p.out += strings.ToLower(n.identifier)

case *optional:
ebnf(n.node, seen, p, outp)
p.out += "?"

case *repetition:
ebnf(n.node, seen, p, outp)
p.out += "*"

case *negation:
p.out += "!"
ebnf(n.node, seen, p, outp)
return

case *literal:
p.out += fmt.Sprintf("%q", n.s)

case *group:
composite := (n.mode != groupMatchOnce) && compositeNode(map[node]bool{}, n)

if composite {
p.out += "("
}
if child, ok := n.expr.(*group); ok && child.mode == groupMatchOnce {
ebnf(child.expr, seen, p, outp)
} else if child, ok := n.expr.(*capture); ok {
if grandchild, ok := child.node.(*group); ok && grandchild.mode == groupMatchOnce {
ebnf(grandchild.expr, seen, p, outp)
} else {
ebnf(n.expr, seen, p, outp)
}
} else {
ebnf(n.expr, seen, p, outp)
}
if composite {
p.out += ")"
}
switch n.mode {
case groupMatchNonEmpty:
p.out += "!"
case groupMatchZeroOrOne:
p.out += "?"
case groupMatchZeroOrMore:
p.out += "*"
case groupMatchOneOrMore:
p.out += "+"
}
return

default:
panic(fmt.Sprintf("unsupported node type %T", n))
}
}
25 changes: 25 additions & 0 deletions ebnf_test.go
@@ -0,0 +1,25 @@
package participle

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestEBNF(t *testing.T) {
parser := mustTestParser(t, &EBNF{})
expected := `
EBNF = (Production)* .
Production = ident "=" Expression (Expression)* "." .
Expression = Sequence ("|" Sequence)* .
Sequence = Term (Term)* .
Term = ident | Literal | Range | Group | EBNFOption | Repetition .
Literal = string .
Range = string "…" string .
Group = "(" Expression ")" .
EBNFOption = "[" Expression "]" .
Repetition = "{" Expression "}" .
`
require.Equal(t, strings.TrimSpace(expected), parser.EBNF())
}
2 changes: 1 addition & 1 deletion parser_test.go
Expand Up @@ -274,7 +274,7 @@ type EBNF struct {
Productions []*Production `{ @@ }`
}

func TestEBNF(t *testing.T) {
func TestEBNFParser(t *testing.T) {
parser := mustTestParser(t, &EBNF{})

expected := &EBNF{
Expand Down

0 comments on commit fc7769c

Please sign in to comment.