import code to its own repo
This commit is contained in:
parent
00332200ca
commit
b86872c58e
17
Makefile
Normal file
17
Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
SOURCES = $(shell find . -name '*.go')
|
||||
|
||||
default: build
|
||||
|
||||
imports:
|
||||
@goimports -w $(SOURCES)
|
||||
|
||||
build: $(SOURCES)
|
||||
go build ./...
|
||||
|
||||
check: build
|
||||
go test ./... -test.short -run ^Test
|
||||
|
||||
fmt: $(SOURCES)
|
||||
@gofmt -w -s $(SOURCES)
|
||||
|
||||
precommit: build check fmt
|
211
boot.go
Normal file
211
boot.go
Normal file
@ -0,0 +1,211 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var errInvalidDefinition = errors.New("invalid syntax definition")
|
||||
|
||||
func stringToCommitType(s string) CommitType {
|
||||
switch s {
|
||||
case "alias":
|
||||
return Alias
|
||||
case "doc":
|
||||
return Documentation
|
||||
case "root":
|
||||
return Root
|
||||
default:
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
func checkBootDefinitionLength(d []string) error {
|
||||
if len(d) < 3 {
|
||||
return errInvalidDefinition
|
||||
}
|
||||
|
||||
switch d[0] {
|
||||
case "chars", "class":
|
||||
if len(d) < 4 {
|
||||
return errInvalidDefinition
|
||||
}
|
||||
|
||||
case "quantifier":
|
||||
if len(d) != 6 {
|
||||
return errInvalidDefinition
|
||||
}
|
||||
|
||||
case "sequence", "choice":
|
||||
if len(d) < 4 {
|
||||
return errInvalidDefinition
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseClass(c []rune) (not bool, chars []rune, ranges [][]rune, err error) {
|
||||
if c[0] == '^' {
|
||||
not = true
|
||||
c = c[1:]
|
||||
}
|
||||
|
||||
for {
|
||||
if len(c) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var c0 rune
|
||||
c0, c = c[0], c[1:]
|
||||
switch c0 {
|
||||
case '[', ']', '^', '-':
|
||||
err = errInvalidDefinition
|
||||
return
|
||||
}
|
||||
|
||||
if c0 == '\\' {
|
||||
if len(c) == 0 {
|
||||
err = errInvalidDefinition
|
||||
return
|
||||
}
|
||||
|
||||
c0, c = unescapeChar(c[0]), c[1:]
|
||||
}
|
||||
|
||||
if len(c) < 2 || c[0] != '-' {
|
||||
chars = append(chars, c0)
|
||||
continue
|
||||
}
|
||||
|
||||
var c1 rune
|
||||
c1, c = c[1], c[2:]
|
||||
if c1 == '\\' {
|
||||
if len(c) == 0 {
|
||||
err = errInvalidDefinition
|
||||
return
|
||||
}
|
||||
|
||||
c1, c = unescapeChar(c[0]), c[1:]
|
||||
}
|
||||
|
||||
ranges = append(ranges, []rune{c0, c1})
|
||||
}
|
||||
}
|
||||
|
||||
func defineBootAnything(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
return s.AnyChar(d[1], ct)
|
||||
}
|
||||
|
||||
func defineBootClass(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
|
||||
not, chars, ranges, err := parseClass([]rune(d[3]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Class(d[1], ct, not, chars, ranges)
|
||||
}
|
||||
|
||||
func defineBootCharSequence(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
|
||||
chars, err := unescape('\\', []rune{'"', '\\'}, []rune(d[3]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.CharSequence(d[1], ct, chars)
|
||||
}
|
||||
|
||||
func defineBootQuantifier(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
|
||||
var (
|
||||
min, max int
|
||||
err error
|
||||
)
|
||||
|
||||
if min, err = strconv.Atoi(d[4]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if max, err = strconv.Atoi(d[5]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Quantifier(d[1], ct, d[3], min, max)
|
||||
}
|
||||
|
||||
func defineBootSequence(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
return s.Sequence(d[1], ct, d[3:]...)
|
||||
}
|
||||
|
||||
func defineBootChoice(s *Syntax, d []string) error {
|
||||
ct := stringToCommitType(d[2])
|
||||
return s.Choice(d[1], ct, d[3:]...)
|
||||
}
|
||||
|
||||
func defineBoot(s *Syntax, d []string) error {
|
||||
switch d[0] {
|
||||
case "anything":
|
||||
return defineBootAnything(s, d)
|
||||
case "class":
|
||||
return defineBootClass(s, d)
|
||||
case "chars":
|
||||
return defineBootCharSequence(s, d)
|
||||
case "quantifier":
|
||||
return defineBootQuantifier(s, d)
|
||||
case "sequence":
|
||||
return defineBootSequence(s, d)
|
||||
case "choice":
|
||||
return defineBootChoice(s, d)
|
||||
default:
|
||||
return errInvalidDefinition
|
||||
}
|
||||
}
|
||||
|
||||
func defineAllBoot(s *Syntax, defs [][]string) error {
|
||||
for _, d := range defs {
|
||||
if err := defineBoot(s, d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initBoot(t Trace, definitions [][]string) (*Syntax, error) {
|
||||
s := NewSyntax(t)
|
||||
if err := defineAllBoot(s, definitions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, s.Init()
|
||||
}
|
||||
|
||||
func bootSyntax(t Trace) (*Syntax, error) {
|
||||
b, err := initBoot(t, bootDefinitions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open("syntax.p")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
doc, err := b.Parse(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := NewSyntax(t)
|
||||
return s, define(s, doc)
|
||||
}
|
73
boot_test.go
Normal file
73
boot_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBoot(t *testing.T) {
|
||||
var trace Trace
|
||||
// trace = NewTrace(2)
|
||||
|
||||
b, err := initBoot(trace, bootDefinitions)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open("syntax.p")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
n0, err := b.Parse(f)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
s0 := NewSyntax(trace)
|
||||
if err := define(s0, n0); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
n1, err := s0.Parse(f)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
checkNode(t, n1, n0)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
s1 := NewSyntax(trace)
|
||||
if err := define(s1, n1); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
n2, err := s1.Parse(f)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
checkNode(t, n2, n1)
|
||||
}
|
285
bootsyntax.go
Normal file
285
bootsyntax.go
Normal file
@ -0,0 +1,285 @@
|
||||
package parse
|
||||
|
||||
var bootDefinitions = [][]string{{
|
||||
"chars", "space", "alias", " ",
|
||||
}, {
|
||||
"chars", "tab", "alias", "\\t",
|
||||
}, {
|
||||
"chars", "nl", "alias", "\\n",
|
||||
}, {
|
||||
"chars", "backspace", "alias", "\\b",
|
||||
}, {
|
||||
"chars", "formfeed", "alias", "\\f",
|
||||
}, {
|
||||
"chars", "carryreturn", "alias", "\\r",
|
||||
}, {
|
||||
"chars", "verticaltab", "alias", "\\v",
|
||||
}, {
|
||||
"choice",
|
||||
"ws",
|
||||
"alias",
|
||||
"space",
|
||||
"tab",
|
||||
"nl",
|
||||
"backspace",
|
||||
"formfeed",
|
||||
"carryreturn",
|
||||
"verticaltab",
|
||||
}, {
|
||||
"chars", "open-block-comment", "alias", "/*",
|
||||
}, {
|
||||
"chars", "close-block-comment", "alias", "*/",
|
||||
}, {
|
||||
"chars", "star", "alias", "*",
|
||||
}, {
|
||||
"class", "not-slash", "alias", "^/",
|
||||
}, {
|
||||
"class", "not-star", "alias", "^*",
|
||||
}, {
|
||||
"chars", "double-slash", "alias", "//",
|
||||
}, {
|
||||
"class", "not-nl", "alias", "^\\n",
|
||||
}, {
|
||||
"sequence", "not-block-close", "alias", "star", "not-slash",
|
||||
}, {
|
||||
"choice", "block-comment-char", "alias", "not-block-close", "not-star",
|
||||
}, {
|
||||
"quantifier", "block-comment-body", "alias", "block-comment-char", "0", "-1",
|
||||
}, {
|
||||
"sequence",
|
||||
"block-comment",
|
||||
"alias",
|
||||
"open-block-comment",
|
||||
"block-comment-body",
|
||||
"close-block-comment",
|
||||
}, {
|
||||
"quantifier", "not-nls", "alias", "not-nl", "0", "-1",
|
||||
}, {
|
||||
"sequence", "line-comment", "alias", "double-slash", "not-nls",
|
||||
}, {
|
||||
"choice", "comment-segment", "alias", "block-comment", "line-comment",
|
||||
}, {
|
||||
"quantifier", "wss", "alias", "ws", "0", "-1",
|
||||
}, {
|
||||
"quantifier", "optional-nl", "alias", "nl", "0", "1",
|
||||
}, {
|
||||
"choice",
|
||||
"ws-no-nl",
|
||||
"alias",
|
||||
"space",
|
||||
"tab",
|
||||
"backspace",
|
||||
"formfeed",
|
||||
"carryreturn",
|
||||
"verticaltab",
|
||||
}, {
|
||||
"sequence",
|
||||
"continue-comment-segment",
|
||||
"alias",
|
||||
"ws-no-nl",
|
||||
"optional-nl",
|
||||
"ws-no-nl",
|
||||
"comment-segment",
|
||||
}, {
|
||||
"quantifier", "continue-comment", "alias", "continue-comment-segment", "0", "-1",
|
||||
}, {
|
||||
"sequence",
|
||||
"comment",
|
||||
"none",
|
||||
"comment-segment",
|
||||
"continue-comment",
|
||||
}, {
|
||||
"choice", "wsc", "alias", "ws", "comment",
|
||||
}, {
|
||||
"quantifier", "wscs", "alias", "wsc", "0", "-1",
|
||||
}, {
|
||||
"anything", "anything", "alias",
|
||||
}, {
|
||||
"chars", "any-char", "none", ".",
|
||||
}, {
|
||||
"chars", "open-square", "alias", "[",
|
||||
}, {
|
||||
"chars", "close-square", "alias", "]",
|
||||
}, {
|
||||
"chars", "class-not", "none", "^",
|
||||
}, {
|
||||
"chars", "dash", "alias", "-",
|
||||
}, {
|
||||
"quantifier", "optional-class-not", "alias", "class-not", "0", "1",
|
||||
}, {
|
||||
"class", "not-class-control", "alias", "^\\\\\\[\\]\\^\\-",
|
||||
}, {
|
||||
"chars", "escape", "alias", "\\\\",
|
||||
}, {
|
||||
"sequence", "escaped-char", "alias", "escape", "anything",
|
||||
}, {
|
||||
"choice", "class-char", "none", "not-class-control", "escaped-char",
|
||||
}, {
|
||||
"sequence", "char-range", "none", "class-char", "dash", "class-char",
|
||||
}, {
|
||||
"choice", "char-or-range", "alias", "class-char", "char-range",
|
||||
}, {
|
||||
"quantifier", "chars-or-ranges", "alias", "char-or-range", "0", "-1",
|
||||
}, {
|
||||
"sequence", "char-class", "none", "open-square", "optional-class-not", "chars-or-ranges", "close-square",
|
||||
}, {
|
||||
"chars", "double-quote", "alias", "\\\"",
|
||||
}, {
|
||||
"class", "not-char-sequence-control", "alias", "^\\\\\"",
|
||||
}, {
|
||||
"choice", "sequence-char", "none", "not-char-sequence-control", "escaped-char",
|
||||
}, {
|
||||
"quantifier", "char-sequence-chars", "alias", "sequence-char", "0", "-1",
|
||||
}, {
|
||||
"sequence", "char-sequence", "none", "double-quote", "char-sequence-chars", "double-quote",
|
||||
}, {
|
||||
"choice", "terminal", "alias", "any-char", "char-class", "char-sequence",
|
||||
}, {
|
||||
"class", "symbol-char", "alias", "^\\\\ \\n\\t\\b\\f\\r\\v\\b/.\\[\\]\\\"{}\\^+*?|():=;",
|
||||
}, {
|
||||
"quantifier", "symbol-chars", "alias", "symbol-char", "1", "-1",
|
||||
}, {
|
||||
"sequence", "symbol", "none", "symbol-chars",
|
||||
}, {
|
||||
"chars", "open-paren", "alias", "(",
|
||||
}, {
|
||||
"chars", "close-paren", "alias", ")",
|
||||
}, {
|
||||
"sequence", "group", "alias", "open-paren", "wscs", "expression", "wscs", "close-paren",
|
||||
}, {
|
||||
"chars", "open-brace", "alias", "{",
|
||||
}, {
|
||||
"chars", "close-brace", "alias", "}",
|
||||
}, {
|
||||
"class", "digit", "alias", "0-9",
|
||||
}, {
|
||||
"quantifier", "number", "alias", "digit", "1", "-1",
|
||||
}, {
|
||||
"sequence", "count", "none", "number",
|
||||
}, {
|
||||
"sequence", "count-quantifier", "none", "open-brace", "wscs", "count", "wscs", "close-brace",
|
||||
}, {
|
||||
"sequence", "range-from", "none", "number",
|
||||
}, {
|
||||
"sequence", "range-to", "none", "number",
|
||||
}, {
|
||||
"chars", "comma", "alias", ",",
|
||||
}, {
|
||||
"sequence",
|
||||
"range-quantifier",
|
||||
"none",
|
||||
"open-brace",
|
||||
"wscs",
|
||||
"range-from",
|
||||
"wscs",
|
||||
"comma",
|
||||
"wscs",
|
||||
"range-to",
|
||||
"close-brace",
|
||||
}, {
|
||||
"chars", "one-or-more", "none", "+",
|
||||
}, {
|
||||
"chars", "zero-or-more", "none", "*",
|
||||
}, {
|
||||
"chars", "zero-or-one", "none", "?",
|
||||
}, {
|
||||
"choice",
|
||||
"quantity",
|
||||
"alias",
|
||||
"count-quantifier",
|
||||
"range-quantifier",
|
||||
"one-or-more",
|
||||
"zero-or-more",
|
||||
"zero-or-one",
|
||||
}, {
|
||||
"choice", "quantifiable", "alias", "terminal", "symbol", "group",
|
||||
}, {
|
||||
"sequence", "quantifier", "none", "quantifiable", "wscs", "quantity",
|
||||
}, {
|
||||
"choice", "item", "alias", "terminal", "symbol", "group", "quantifier",
|
||||
}, {
|
||||
"sequence", "item-continue", "alias", "wscs", "item",
|
||||
}, {
|
||||
"quantifier", "items-continue", "alias", "item-continue", "0", "-1",
|
||||
}, {
|
||||
"sequence", "sequence", "none", "item", "items-continue",
|
||||
}, {
|
||||
"choice", "element", "alias", "terminal", "symbol", "group", "quantifier", "sequence",
|
||||
}, {
|
||||
"chars", "pipe", "alias", "|",
|
||||
}, {
|
||||
"sequence", "element-continue", "alias", "wscs", "pipe", "wscs", "element",
|
||||
}, {
|
||||
"quantifier", "elements-continue", "alias", "element-continue", "1", "-1",
|
||||
}, {
|
||||
"sequence", "choice", "none", "element", "elements-continue",
|
||||
}, {
|
||||
"choice",
|
||||
"expression",
|
||||
"alias",
|
||||
"terminal",
|
||||
"symbol",
|
||||
"group",
|
||||
"quantifier",
|
||||
"sequence",
|
||||
"choice",
|
||||
}, {
|
||||
"chars", "alias", "none", "alias",
|
||||
}, {
|
||||
"chars", "doc", "none", "doc",
|
||||
}, {
|
||||
"chars", "root", "none", "root",
|
||||
}, {
|
||||
"choice", "flag", "alias", "alias", "doc", "root",
|
||||
}, {
|
||||
"chars", "colon", "alias", ":",
|
||||
}, {
|
||||
"sequence", "flag-tag", "alias", "colon", "flag",
|
||||
}, {
|
||||
"quantifier", "flags", "alias", "flag-tag", "0", "-1",
|
||||
}, {
|
||||
"chars", "equal", "alias", "=",
|
||||
}, {
|
||||
"sequence", "definition", "none", "symbol", "flags", "wscs", "equal", "wscs", "expression",
|
||||
}, {
|
||||
"chars", "semicolon", "alias", ";",
|
||||
}, {
|
||||
"choice", "wsc-or-semicolon", "alias", "wsc", "semicolon",
|
||||
}, {
|
||||
"quantifier", "wsc-or-semicolons", "alias", "wsc-or-semicolon", "0", "-1",
|
||||
}, {
|
||||
"sequence",
|
||||
"subsequent-definition",
|
||||
"alias",
|
||||
"wscs",
|
||||
"semicolon",
|
||||
"wsc-or-semicolons",
|
||||
"definition",
|
||||
}, {
|
||||
"quantifier",
|
||||
"subsequent-definitions",
|
||||
"alias",
|
||||
"subsequent-definition",
|
||||
"0",
|
||||
"-1",
|
||||
}, {
|
||||
"sequence",
|
||||
"definitions",
|
||||
"alias",
|
||||
"definition",
|
||||
"subsequent-definitions",
|
||||
}, {
|
||||
"quantifier",
|
||||
"opt-definitions",
|
||||
"alias",
|
||||
"definitions",
|
||||
"0",
|
||||
"1",
|
||||
}, {
|
||||
"sequence",
|
||||
"syntax",
|
||||
"root",
|
||||
"wsc-or-semicolons",
|
||||
"opt-definitions",
|
||||
"wsc-or-semicolons",
|
||||
}}
|
94
cache.go
Normal file
94
cache.go
Normal file
@ -0,0 +1,94 @@
|
||||
package parse
|
||||
|
||||
type cacheItem struct {
|
||||
name string
|
||||
node *Node
|
||||
}
|
||||
|
||||
type tokenCache struct {
|
||||
match []*cacheItem // TODO: potential optimization can be to use a balanced binary tree
|
||||
noMatch []string
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
tokens []*tokenCache // TODO: try with pointers, too
|
||||
}
|
||||
|
||||
func (c *cache) get(offset int, name string) (*Node, bool, bool) {
|
||||
if len(c.tokens) <= offset {
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
tc := c.tokens[offset]
|
||||
if tc == nil {
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
for _, i := range tc.noMatch {
|
||||
if i == name {
|
||||
return nil, false, true
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range tc.match {
|
||||
if i.name == name {
|
||||
return i.node, true, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
func (c *cache) setOne(offset int, name string, n *Node) {
|
||||
}
|
||||
|
||||
func (c *cache) set(offset int, name string, n *Node) {
|
||||
if len(c.tokens) <= offset {
|
||||
if cap(c.tokens) > offset {
|
||||
c.tokens = c.tokens[:offset+1]
|
||||
} else {
|
||||
c.tokens = c.tokens[:cap(c.tokens)]
|
||||
for len(c.tokens) <= offset {
|
||||
c.tokens = append(c.tokens, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tc := c.tokens[offset]
|
||||
if tc == nil {
|
||||
tc = &tokenCache{}
|
||||
c.tokens[offset] = tc
|
||||
}
|
||||
|
||||
if n == nil {
|
||||
for _, i := range tc.match {
|
||||
if i.name == name {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range tc.noMatch {
|
||||
if i == name {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tc.noMatch = append(tc.noMatch, name)
|
||||
return
|
||||
}
|
||||
|
||||
for _, i := range tc.match {
|
||||
if i.name == name {
|
||||
if n.tokenLength() > i.node.tokenLength() {
|
||||
i.node = n
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tc.match = append(tc.match, &cacheItem{
|
||||
name: name,
|
||||
node: n,
|
||||
})
|
||||
}
|
108
char.go
Normal file
108
char.go
Normal file
@ -0,0 +1,108 @@
|
||||
package parse
|
||||
|
||||
type charParser struct {
|
||||
name string
|
||||
commit CommitType
|
||||
any bool
|
||||
not bool
|
||||
chars []rune
|
||||
ranges [][]rune
|
||||
includedBy []parser
|
||||
}
|
||||
|
||||
func newChar(
|
||||
name string,
|
||||
ct CommitType,
|
||||
any, not bool,
|
||||
chars []rune,
|
||||
ranges [][]rune,
|
||||
) *charParser {
|
||||
return &charParser{
|
||||
name: name,
|
||||
commit: ct,
|
||||
any: any,
|
||||
not: not,
|
||||
chars: chars,
|
||||
ranges: ranges,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *charParser) nodeName() string { return p.name }
|
||||
|
||||
func (p *charParser) parser(r *registry, path []string) (parser, error) {
|
||||
if stringsContain(path, p.name) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
r.setParser(p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *charParser) commitType() CommitType {
|
||||
return p.commit
|
||||
}
|
||||
|
||||
func (p *charParser) setIncludedBy(i parser, path []string) {
|
||||
if stringsContain(path, p.name) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
p.includedBy = append(p.includedBy, i)
|
||||
}
|
||||
|
||||
func (p *charParser) cacheIncluded(*context, *Node) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
func (p *charParser) match(t rune) bool {
|
||||
if p.any {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, ci := range p.chars {
|
||||
if ci == t {
|
||||
return !p.not
|
||||
}
|
||||
}
|
||||
|
||||
for _, ri := range p.ranges {
|
||||
if t >= ri[0] && t <= ri[1] {
|
||||
return !p.not
|
||||
}
|
||||
}
|
||||
|
||||
return p.not
|
||||
}
|
||||
|
||||
func (p *charParser) parse(t Trace, c *context) {
|
||||
t = t.Extend(p.name)
|
||||
t.Out1("parsing char", c.offset)
|
||||
|
||||
if p.commit&Documentation != 0 {
|
||||
t.Out1("fail, doc")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
if m, ok := c.fromCache(p.name); ok {
|
||||
t.Out1("found in cache, match:", m)
|
||||
return
|
||||
}
|
||||
|
||||
if tok, ok := c.token(); ok && p.match(tok) {
|
||||
t.Out1("success", string(tok))
|
||||
n := newNode(p.name, p.commit, c.offset, c.offset+1)
|
||||
c.cache.set(c.offset, p.name, n)
|
||||
for _, i := range p.includedBy {
|
||||
i.cacheIncluded(c, n)
|
||||
}
|
||||
|
||||
c.success(n)
|
||||
return
|
||||
} else {
|
||||
t.Out1("fail", string(tok))
|
||||
c.cache.set(c.offset, p.name, nil)
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
}
|
180
choice.go
Normal file
180
choice.go
Normal file
@ -0,0 +1,180 @@
|
||||
package parse
|
||||
|
||||
type choiceDefinition struct {
|
||||
name string
|
||||
commit CommitType
|
||||
elements []string
|
||||
}
|
||||
|
||||
type choiceParser struct {
|
||||
name string
|
||||
commit CommitType
|
||||
elements []parser
|
||||
including []parser
|
||||
}
|
||||
|
||||
func newChoice(name string, ct CommitType, elements []string) *choiceDefinition {
|
||||
return &choiceDefinition{
|
||||
name: name,
|
||||
commit: ct,
|
||||
elements: elements,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *choiceDefinition) nodeName() string { return d.name }
|
||||
|
||||
// could store and cache everything that it fulfils
|
||||
|
||||
func (d *choiceDefinition) parser(r *registry, path []string) (parser, error) {
|
||||
p, ok := r.parser(d.name)
|
||||
if ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
cp := &choiceParser{
|
||||
name: d.name,
|
||||
commit: d.commit,
|
||||
}
|
||||
|
||||
r.setParser(cp)
|
||||
|
||||
var elements []parser
|
||||
path = append(path, d.name)
|
||||
for _, e := range d.elements {
|
||||
element, ok := r.parser(e)
|
||||
if ok {
|
||||
elements = append(elements, element)
|
||||
element.setIncludedBy(cp, path)
|
||||
continue
|
||||
}
|
||||
|
||||
elementDefinition, ok := r.definition(e)
|
||||
if !ok {
|
||||
return nil, parserNotFound(e)
|
||||
}
|
||||
|
||||
element, err := elementDefinition.parser(r, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
element.setIncludedBy(cp, path)
|
||||
elements = append(elements, element)
|
||||
}
|
||||
|
||||
cp.elements = elements
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (d *choiceDefinition) commitType() CommitType {
|
||||
return d.commit
|
||||
}
|
||||
|
||||
func (p *choiceParser) nodeName() string { return p.name }
|
||||
|
||||
func (p *choiceParser) setIncludedBy(i parser, path []string) {
|
||||
if stringsContain(path, p.name) {
|
||||
return
|
||||
}
|
||||
|
||||
p.including = append(p.including, i)
|
||||
}
|
||||
|
||||
func (p *choiceParser) cacheIncluded(c *context, n *Node) {
|
||||
if !c.excluded(n.from, p.name) {
|
||||
return
|
||||
}
|
||||
|
||||
nc := newNode(p.name, p.commit, n.from, n.to)
|
||||
nc.append(n)
|
||||
c.cache.set(nc.from, p.name, nc)
|
||||
|
||||
// maybe it is enough to cache only those that are on the path
|
||||
for _, i := range p.including {
|
||||
i.cacheIncluded(c, nc)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *choiceParser) parse(t Trace, c *context) {
|
||||
t = t.Extend(p.name)
|
||||
t.Out1("parsing choice", c.offset)
|
||||
|
||||
if p.commit&Documentation != 0 {
|
||||
t.Out1("fail, doc")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
if m, ok := c.fromCache(p.name); ok {
|
||||
t.Out1("found in cache, match:", m)
|
||||
return
|
||||
}
|
||||
|
||||
if c.excluded(c.offset, p.name) {
|
||||
t.Out1("excluded")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
c.exclude(c.offset, p.name)
|
||||
defer c.include(c.offset, p.name)
|
||||
|
||||
node := newNode(p.name, p.commit, c.offset, c.offset)
|
||||
var match bool
|
||||
|
||||
for {
|
||||
elements := p.elements
|
||||
var foundMatch bool
|
||||
|
||||
// TODO: this can be the entry point for a transformation that enables the
|
||||
// processing of massive amounts of autogenerated rules in parallel in a
|
||||
// continously, dynamically cached way. E.g. teach a machine that learns
|
||||
// everything from a public library.
|
||||
|
||||
t.Out2("elements again")
|
||||
for len(elements) > 0 {
|
||||
t.Out2("in the choice", c.offset, node.from, elements[0].nodeName())
|
||||
elements[0].parse(t, c)
|
||||
elements = elements[1:]
|
||||
c.offset = node.from
|
||||
|
||||
if !c.match || match && c.node.tokenLength() <= node.tokenLength() {
|
||||
t.Out2("skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
t.Out2("appending", c.node.tokenLength(), node.tokenLength(),
|
||||
"\"", string(c.tokens[node.from:node.to]), "\"",
|
||||
"\"", string(c.tokens[c.node.from:c.node.to]), "\"",
|
||||
c.node.Name,
|
||||
)
|
||||
match = true
|
||||
foundMatch = true
|
||||
// node.clear()
|
||||
node = newNode(p.name, p.commit, c.offset, c.offset) // TODO: review caching conditions
|
||||
node.append(c.node)
|
||||
|
||||
c.cache.set(node.from, p.name, node)
|
||||
for _, i := range p.including {
|
||||
i.cacheIncluded(c, node)
|
||||
}
|
||||
|
||||
// TODO: a simple break here can force PEG-style "priority" choices
|
||||
}
|
||||
|
||||
if !foundMatch {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
t.Out1("choice, success")
|
||||
t.Out2("choice done", node.nodeLength())
|
||||
c.success(node)
|
||||
return
|
||||
}
|
||||
|
||||
t.Out1("fail")
|
||||
c.cache.set(node.from, p.name, nil)
|
||||
c.fail(node.from)
|
||||
}
|
152
context.go
Normal file
152
context.go
Normal file
@ -0,0 +1,152 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
reader io.RuneReader
|
||||
offset int
|
||||
readOffset int
|
||||
readErr error
|
||||
eof bool
|
||||
cache *cache
|
||||
tokens []rune
|
||||
match bool
|
||||
node *Node
|
||||
isExcluded [][]string
|
||||
}
|
||||
|
||||
func newContext(r io.RuneReader) *context {
|
||||
return &context{
|
||||
reader: r,
|
||||
cache: &cache{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *context) read() bool {
|
||||
if c.eof || c.readErr != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
t, n, err := c.reader.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if n == 0 {
|
||||
c.eof = true
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
c.readErr = err
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
c.readOffset++
|
||||
|
||||
if t == unicode.ReplacementChar {
|
||||
c.readErr = ErrInvalidCharacter
|
||||
return false
|
||||
}
|
||||
|
||||
c.tokens = append(c.tokens, t)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *context) token() (rune, bool) {
|
||||
if c.offset == c.readOffset {
|
||||
if !c.read() {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
return c.tokens[c.offset], true
|
||||
}
|
||||
|
||||
func (c *context) excluded(offset int, name string) bool {
|
||||
if len(c.isExcluded) <= offset {
|
||||
return false
|
||||
}
|
||||
|
||||
return stringsContain(c.isExcluded[offset], name)
|
||||
}
|
||||
|
||||
func (c *context) exclude(offset int, name string) {
|
||||
if len(c.isExcluded) <= offset {
|
||||
c.isExcluded = append(c.isExcluded, nil)
|
||||
if cap(c.isExcluded) > offset {
|
||||
c.isExcluded = c.isExcluded[:offset+1]
|
||||
} else {
|
||||
c.isExcluded = append(
|
||||
c.isExcluded[:cap(c.isExcluded)],
|
||||
make([][]string, offset+1-cap(c.isExcluded))...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
c.isExcluded[offset] = append(c.isExcluded[offset], name)
|
||||
}
|
||||
|
||||
func (c *context) include(offset int, name string) {
|
||||
if len(c.isExcluded) <= offset {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(c.isExcluded[offset]) - 1; i >= 0; i-- {
|
||||
if c.isExcluded[offset][i] == name {
|
||||
c.isExcluded[offset] = append(c.isExcluded[offset][:i], c.isExcluded[offset][i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *context) fromCache(name string) (bool, bool) {
|
||||
n, m, ok := c.cache.get(c.offset, name)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
|
||||
if m {
|
||||
c.success(n)
|
||||
} else {
|
||||
c.fail(c.offset)
|
||||
}
|
||||
|
||||
return m, true
|
||||
}
|
||||
|
||||
func (c *context) success(n *Node) {
|
||||
c.node = n
|
||||
c.offset = n.to
|
||||
c.match = true
|
||||
}
|
||||
|
||||
func (c *context) fail(offset int) {
|
||||
c.offset = offset
|
||||
c.match = false
|
||||
}
|
||||
|
||||
func (c *context) finalize() error {
|
||||
if c.node.to < c.readOffset {
|
||||
return ErrUnexpectedCharacter
|
||||
}
|
||||
|
||||
if !c.eof {
|
||||
c.read()
|
||||
if !c.eof {
|
||||
if c.readErr != nil {
|
||||
return c.readErr
|
||||
}
|
||||
|
||||
return ErrUnexpectedCharacter
|
||||
}
|
||||
}
|
||||
|
||||
c.node.commit()
|
||||
if c.node.commitType&Alias != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.node.applyTokens(c.tokens)
|
||||
return nil
|
||||
}
|
274
define.go
Normal file
274
define.go
Normal file
@ -0,0 +1,274 @@
|
||||
package parse
|
||||
|
||||
import "strconv"
|
||||
|
||||
func runesContain(rs []rune, r rune) bool {
|
||||
for _, ri := range rs {
|
||||
if ri == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func unescapeChar(c rune) rune {
|
||||
switch c {
|
||||
case 'n':
|
||||
return '\n'
|
||||
case 't':
|
||||
return '\t'
|
||||
case 'b':
|
||||
return '\b'
|
||||
case 'f':
|
||||
return '\f'
|
||||
case 'r':
|
||||
return '\r'
|
||||
case 'v':
|
||||
return '\v'
|
||||
default:
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
func unescape(escape rune, banned []rune, chars []rune) ([]rune, error) {
|
||||
var (
|
||||
unescaped []rune
|
||||
escaped bool
|
||||
)
|
||||
|
||||
for _, ci := range chars {
|
||||
if escaped {
|
||||
unescaped = append(unescaped, unescapeChar(ci))
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case ci == escape:
|
||||
escaped = true
|
||||
case runesContain(banned, ci):
|
||||
return nil, ErrInvalidCharacter
|
||||
default:
|
||||
unescaped = append(unescaped, ci)
|
||||
}
|
||||
}
|
||||
|
||||
if escaped {
|
||||
return nil, ErrInvalidCharacter
|
||||
}
|
||||
|
||||
return unescaped, nil
|
||||
}
|
||||
|
||||
func dropComments(n *Node) *Node {
|
||||
ncc := *n
|
||||
nc := &ncc
|
||||
|
||||
nc.Nodes = nil
|
||||
for _, ni := range n.Nodes {
|
||||
if ni.Name == "comment" {
|
||||
continue
|
||||
}
|
||||
|
||||
nc.Nodes = append(nc.Nodes, dropComments(ni))
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func flagsToCommitType(n []*Node) CommitType {
|
||||
var ct CommitType
|
||||
for _, ni := range n {
|
||||
switch ni.Name {
|
||||
case "alias":
|
||||
ct |= Alias
|
||||
case "doc":
|
||||
ct |= Documentation
|
||||
case "root":
|
||||
ct |= Root
|
||||
}
|
||||
}
|
||||
|
||||
return ct
|
||||
}
|
||||
|
||||
func toRune(c string) rune {
|
||||
return []rune(c)[0]
|
||||
}
|
||||
|
||||
func nodeChar(n *Node) rune {
|
||||
s := n.Text()
|
||||
if s[0] == '\\' {
|
||||
return unescapeChar(toRune(s[1:]))
|
||||
}
|
||||
|
||||
return toRune(s)
|
||||
}
|
||||
|
||||
func defineMembers(s *Syntax, name string, n ...*Node) ([]string, error) {
|
||||
var refs []string
|
||||
for i, ni := range n {
|
||||
nmi := childName(name, i)
|
||||
switch ni.Name {
|
||||
case "symbol":
|
||||
refs = append(refs, ni.Text())
|
||||
default:
|
||||
refs = append(refs, nmi)
|
||||
if err := defineExpression(s, nmi, Alias, ni); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func defineClass(s *Syntax, name string, ct CommitType, n []*Node) error {
|
||||
var (
|
||||
not bool
|
||||
chars []rune
|
||||
ranges [][]rune
|
||||
)
|
||||
|
||||
if len(n) > 0 && n[0].Name == "class-not" {
|
||||
not, n = true, n[1:]
|
||||
}
|
||||
|
||||
for _, c := range n {
|
||||
switch c.Name {
|
||||
case "class-char":
|
||||
chars = append(chars, nodeChar(c))
|
||||
case "char-range":
|
||||
ranges = append(ranges, []rune{nodeChar(c.Nodes[0]), nodeChar(c.Nodes[1])})
|
||||
}
|
||||
}
|
||||
|
||||
return s.Class(name, ct, not, chars, ranges)
|
||||
}
|
||||
|
||||
func defineCharSequence(s *Syntax, name string, ct CommitType, charNodes []*Node) error {
|
||||
var chars []rune
|
||||
for _, ci := range charNodes {
|
||||
chars = append(chars, nodeChar(ci))
|
||||
}
|
||||
|
||||
return s.CharSequence(name, ct, chars)
|
||||
}
|
||||
|
||||
func defineQuantifier(s *Syntax, name string, ct CommitType, n *Node, q *Node) error {
|
||||
refs, err := defineMembers(s, name, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var min, max int
|
||||
switch q.Name {
|
||||
case "count-quantifier":
|
||||
min, err = strconv.Atoi(q.Nodes[0].Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
max = min
|
||||
case "range-quantifier":
|
||||
min = 0
|
||||
max = -1
|
||||
for _, rq := range q.Nodes {
|
||||
switch rq.Name {
|
||||
case "range-from":
|
||||
min, err = strconv.Atoi(rq.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "range-to":
|
||||
max, err = strconv.Atoi(rq.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
}
|
||||
case "one-or-more":
|
||||
min, max = 1, -1
|
||||
case "zero-or-more":
|
||||
min, max = 0, -1
|
||||
case "zero-or-one":
|
||||
min, max = 0, 1
|
||||
}
|
||||
|
||||
return s.Quantifier(name, ct, refs[0], min, max)
|
||||
}
|
||||
|
||||
func defineSequence(s *Syntax, name string, ct CommitType, n ...*Node) error {
|
||||
refs, err := defineMembers(s, name, n...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // TODO: try to make this expressed in the syntax (maybe as sequences need either a quantififer or not
|
||||
// // one item? or by maintaining the excluded and caching in the sequence in a similar way when there is
|
||||
// // only one item?) how does this effect the quantifiers?
|
||||
// if len(refs) == 1 {
|
||||
// return s.Choice(name, ct, refs[0])
|
||||
// }
|
||||
|
||||
return s.Sequence(name, ct, refs...)
|
||||
}
|
||||
|
||||
func defineChoice(s *Syntax, name string, ct CommitType, n ...*Node) error {
|
||||
refs, err := defineMembers(s, name, n...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Choice(name, ct, refs...)
|
||||
}
|
||||
|
||||
func defineExpression(s *Syntax, name string, ct CommitType, expression *Node) error {
|
||||
var err error
|
||||
switch expression.Name {
|
||||
case "any-char":
|
||||
err = s.AnyChar(name, ct)
|
||||
case "char-class":
|
||||
err = defineClass(s, name, ct, expression.Nodes)
|
||||
case "char-sequence":
|
||||
err = defineCharSequence(s, name, ct, expression.Nodes)
|
||||
case "symbol":
|
||||
err = defineSequence(s, name, ct, expression)
|
||||
case "quantifier":
|
||||
err = defineQuantifier(s, name, ct, expression.Nodes[0], expression.Nodes[1])
|
||||
case "sequence":
|
||||
err = defineSequence(s, name, ct, expression.Nodes...)
|
||||
case "choice":
|
||||
err = defineChoice(s, name, ct, expression.Nodes...)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func defineDefinition(s *Syntax, n *Node) error {
|
||||
return defineExpression(
|
||||
s,
|
||||
n.Nodes[0].Text(),
|
||||
flagsToCommitType(n.Nodes[1:len(n.Nodes)-1]),
|
||||
n.Nodes[len(n.Nodes)-1],
|
||||
)
|
||||
}
|
||||
|
||||
func define(s *Syntax, n *Node) error {
|
||||
if n.Name != "syntax" {
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
|
||||
n = dropComments(n)
|
||||
|
||||
for _, ni := range n.Nodes {
|
||||
if err := defineDefinition(s, ni); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
57
eskip.p
Normal file
57
eskip.p
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Eskip routing configuration format for Skipper: https://github.com/zalando/skipper
|
||||
*/
|
||||
|
||||
// TODO: definition with comment, doc = comment, or just replace comment
|
||||
|
||||
eskip:root = (expression | definitions)?;
|
||||
|
||||
comment-line:alias = "//" [^\n]*;
|
||||
space:alias = [ \b\f\r\t\v];
|
||||
comment:alias = comment-line (space* "\n" space* comment-line)*;
|
||||
|
||||
wsc:alias = [ \b\f\n\r\t\v] | comment;
|
||||
|
||||
decimal-digit:alias = [0-9];
|
||||
octal-digit:alias = [0-7];
|
||||
hexa-digit:alias = [0-9a-fA-F];
|
||||
|
||||
decimal:alias = [1-9] decimal-digit*;
|
||||
octal:alias = "0" octal-digit*;
|
||||
hexa:alias = "0" [xX] hexa-digit+;
|
||||
int = decimal | octal | hexa;
|
||||
|
||||
exponent:alias = [eE] [+\-]? decimal-digit+;
|
||||
float = decimal-digit+ "." decimal-digit* exponent?
|
||||
| "." decimal-digit+ exponent?
|
||||
| decimal-digit+ exponent;
|
||||
|
||||
number:alias = "-"? (int | float);
|
||||
|
||||
string = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
regexp = "/" ([^\\/] | "\\" .)* "/";
|
||||
symbol = [a-zA-Z_] [a-zA-z0-9_]*;
|
||||
|
||||
arg:alias = number | string | regexp;
|
||||
args:alias = arg (wsc* "," wsc* arg)*;
|
||||
term:alias = symbol wsc* "(" wsc* args? wsc* ")";
|
||||
|
||||
predicate = term;
|
||||
predicates:alias = "*" | predicate (wsc* "&&" wsc* predicate)*;
|
||||
|
||||
filter = term;
|
||||
filters:alias = filter (wsc* "->" wsc* filter)*;
|
||||
|
||||
address:alias = string;
|
||||
shunt = "<shunt>";
|
||||
loopback = "<loopback>";
|
||||
backend:alias = address | shunt | loopback;
|
||||
|
||||
expression = predicates (wsc* "->" wsc* filters)? wsc* "->" wsc* backend;
|
||||
|
||||
id:alias = symbol;
|
||||
definition = id wsc* ":" wsc* expression;
|
||||
|
||||
free-sep:alias = (wsc | ";");
|
||||
sep:alias = wsc* ";" free-sep*;
|
||||
definitions:alias = free-sep* definition (sep definition)* free-sep*;
|
749
eskip_test.go
Normal file
749
eskip_test.go
Normal file
@ -0,0 +1,749 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zalando/skipper/eskip"
|
||||
)
|
||||
|
||||
const (
|
||||
maxID = 27
|
||||
meanID = 9
|
||||
|
||||
setPathChance = 0.72
|
||||
maxPathTags = 12
|
||||
meanPathTags = 2
|
||||
maxPathTag = 24
|
||||
meanPathTag = 9
|
||||
|
||||
setHostChance = 0.5
|
||||
maxHost = 48
|
||||
meanHost = 24
|
||||
|
||||
setPathRegexpChance = 0.45
|
||||
maxPathRegexp = 36
|
||||
meanPathRegexp = 12
|
||||
|
||||
setMethodChance = 0.1
|
||||
|
||||
setHeadersChance = 0.3
|
||||
maxHeadersLength = 6
|
||||
meanHeadersLength = 1
|
||||
maxHeaderKeyLength = 18
|
||||
meanHeaderKeyLength = 12
|
||||
maxHeaderValueLength = 48
|
||||
meanHeaderValueLength = 6
|
||||
|
||||
setHeaderRegexpChance = 0.05
|
||||
maxHeaderRegexpsLength = 3
|
||||
meanHeaderRegexpsLength = 1
|
||||
maxHeaderRegexpLength = 12
|
||||
meanHeaderRegexpLength = 6
|
||||
|
||||
maxTermNameLength = 15
|
||||
meanTermNameLength = 6
|
||||
maxTermArgsLength = 6
|
||||
meanTermArgsLength = 1
|
||||
floatArgChance = 0.1
|
||||
intArgChance = 0.3
|
||||
maxTermStringLength = 24
|
||||
meanTermStringLength = 6
|
||||
|
||||
maxPredicatesLength = 4
|
||||
meanPredicatesLength = 1
|
||||
|
||||
maxFiltersLength = 18
|
||||
meanFiltersLength = 3
|
||||
|
||||
loopBackendChance = 0.05
|
||||
shuntBackendChance = 0.1
|
||||
maxBackend = 48
|
||||
meanBackend = 15
|
||||
)
|
||||
|
||||
func takeChance(c float64) bool {
|
||||
return rand.Float64() < c
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
return generateString(maxID, meanID)
|
||||
}
|
||||
|
||||
func generatePath() string {
|
||||
if !takeChance(setPathChance) {
|
||||
return ""
|
||||
}
|
||||
|
||||
l := randomLength(maxPathTags, meanPathTags)
|
||||
p := append(make([]string, 0, l+1), "")
|
||||
for i := 0; i < l; i++ {
|
||||
p = append(p, generateString(maxPathTag, meanPathTag))
|
||||
}
|
||||
|
||||
return strings.Join(p, "/")
|
||||
}
|
||||
|
||||
func generateHostRegexps() []string {
|
||||
if !takeChance(setHostChance) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{generateString(maxHost, meanHost)}
|
||||
}
|
||||
|
||||
func generatePathRegexps() []string {
|
||||
if !takeChance(setPathRegexpChance) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{generateString(maxPathRegexp, meanPathRegexp)}
|
||||
}
|
||||
|
||||
func generateMethod() string {
|
||||
if !takeChance(setMethodChance) {
|
||||
return ""
|
||||
}
|
||||
|
||||
methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
|
||||
return methods[rand.Intn(len(methods))]
|
||||
}
|
||||
|
||||
func generateHeaders() map[string]string {
|
||||
if !takeChance(setHeadersChance) {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := make(map[string]string)
|
||||
for i := 0; i < randomLength(maxHeadersLength, meanHeadersLength); i++ {
|
||||
h[generateString(maxHeaderKeyLength, meanHeaderKeyLength)] =
|
||||
generateString(maxHeaderValueLength, meanHeaderValueLength)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func generateHeaderRegexps() map[string][]string {
|
||||
if !takeChance(setHeaderRegexpChance) {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := make(map[string][]string)
|
||||
for i := 0; i < randomLength(maxHeaderRegexpsLength, meanHeaderRegexpsLength); i++ {
|
||||
k := generateString(maxHeaderKeyLength, meanHeaderKeyLength)
|
||||
for i := 0; i < randomLength(maxHeaderRegexpLength, meanHeaderRegexpLength); i++ {
|
||||
h[k] = append(h[k], generateString(maxHeaderValueLength, meanHeaderValueLength))
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func generateTerm() (string, []interface{}) {
|
||||
n := generateString(maxTermNameLength, meanTermNameLength)
|
||||
al := randomLength(maxTermArgsLength, meanTermArgsLength)
|
||||
a := make([]interface{}, 0, al)
|
||||
for i := 0; i < al; i++ {
|
||||
at := rand.Float64()
|
||||
switch {
|
||||
case at < floatArgChance:
|
||||
a = append(a, rand.NormFloat64())
|
||||
case at < intArgChance:
|
||||
a = append(a, rand.Int())
|
||||
default:
|
||||
a = append(a, generateString(maxTermStringLength, meanTermStringLength))
|
||||
}
|
||||
}
|
||||
|
||||
return n, a
|
||||
}
|
||||
|
||||
func generatePredicates() []*eskip.Predicate {
|
||||
l := randomLength(maxPredicatesLength, meanPredicatesLength)
|
||||
p := make([]*eskip.Predicate, 0, l)
|
||||
for i := 0; i < l; i++ {
|
||||
pi := &eskip.Predicate{}
|
||||
pi.Name, pi.Args = generateTerm()
|
||||
p = append(p, pi)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func generateFilters() []*eskip.Filter {
|
||||
l := randomLength(maxFiltersLength, meanFiltersLength)
|
||||
f := make([]*eskip.Filter, 0, l)
|
||||
for i := 0; i < l; i++ {
|
||||
fi := &eskip.Filter{}
|
||||
fi.Name, fi.Args = generateTerm()
|
||||
f = append(f, fi)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func generateBackend() (eskip.BackendType, string) {
|
||||
t := rand.Float64()
|
||||
switch {
|
||||
case t < loopBackendChance:
|
||||
return eskip.LoopBackend, ""
|
||||
case t < loopBackendChance+shuntBackendChance:
|
||||
return eskip.ShuntBackend, ""
|
||||
default:
|
||||
return eskip.NetworkBackend, generateString(maxBackend, meanBackend)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRoute() *eskip.Route {
|
||||
r := &eskip.Route{}
|
||||
r.Id = generateID()
|
||||
r.Path = generatePath()
|
||||
r.HostRegexps = generateHostRegexps()
|
||||
r.PathRegexps = generatePathRegexps()
|
||||
r.Method = generateMethod()
|
||||
r.Headers = generateHeaders()
|
||||
r.HeaderRegexps = generateHeaderRegexps()
|
||||
r.Predicates = generatePredicates()
|
||||
r.Filters = generateFilters()
|
||||
r.BackendType, r.Backend = generateBackend()
|
||||
return r
|
||||
}
|
||||
|
||||
func generateEskip(l int) []*eskip.Route {
|
||||
r := make([]*eskip.Route, 0, l)
|
||||
for i := 0; i < l; i++ {
|
||||
r = append(r, generateRoute())
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func parseEskipInt(s string) (int, error) {
|
||||
i, err := strconv.ParseInt(s, 0, 64)
|
||||
return int(i), err
|
||||
}
|
||||
|
||||
func parseEskipFloat(s string) (float64, error) {
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
return f, err
|
||||
}
|
||||
|
||||
func unquote(s string, escapedChars string) (string, error) {
|
||||
if len(s) < 2 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b := make([]byte, 0, len(s)-2)
|
||||
var escaped bool
|
||||
for _, bi := range []byte(s[1 : len(s)-1]) {
|
||||
if escaped {
|
||||
switch bi {
|
||||
case 'b':
|
||||
bi = '\b'
|
||||
case 'f':
|
||||
bi = '\f'
|
||||
case 'n':
|
||||
bi = '\n'
|
||||
case 'r':
|
||||
bi = '\r'
|
||||
case 't':
|
||||
bi = '\t'
|
||||
case 'v':
|
||||
bi = '\v'
|
||||
}
|
||||
|
||||
b = append(b, bi)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ec := range []byte(escapedChars) {
|
||||
if ec == bi {
|
||||
return "", errors.New("invalid quote")
|
||||
}
|
||||
}
|
||||
|
||||
if bi == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
b = append(b, bi)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func unquoteString(s string) (string, error) {
|
||||
return unquote(s, "\"")
|
||||
}
|
||||
|
||||
func unquoteRegexp(s string) (string, error) {
|
||||
return unquote(s, "/")
|
||||
}
|
||||
|
||||
func nodeToArg(n *Node) (interface{}, error) {
|
||||
switch n.Name {
|
||||
case "int":
|
||||
return parseEskipInt(n.Text())
|
||||
case "float":
|
||||
return parseEskipFloat(n.Text())
|
||||
case "string":
|
||||
return unquoteString(n.Text())
|
||||
case "regexp":
|
||||
return unquoteRegexp(n.Text())
|
||||
default:
|
||||
return nil, errors.New("invalid arg")
|
||||
}
|
||||
}
|
||||
|
||||
func nodeToTerm(n *Node) (string, []interface{}, error) {
|
||||
if len(n.Nodes) < 1 || n.Nodes[0].Name != "symbol" {
|
||||
return "", nil, errors.New("invalid term")
|
||||
}
|
||||
|
||||
name := n.Nodes[0].Text()
|
||||
|
||||
var args []interface{}
|
||||
for _, ni := range n.Nodes[1:] {
|
||||
a, err := nodeToArg(ni)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
args = append(args, a)
|
||||
}
|
||||
|
||||
return name, args, nil
|
||||
}
|
||||
|
||||
func nodeToPredicate(r *eskip.Route, n *Node) error {
|
||||
name, args, err := nodeToTerm(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "Path":
|
||||
if len(args) != 1 {
|
||||
return errors.New("invalid path predicate")
|
||||
}
|
||||
|
||||
p, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid path predicate")
|
||||
}
|
||||
|
||||
r.Path = p
|
||||
case "Host":
|
||||
if len(args) != 1 {
|
||||
return errors.New("invalid host predicate")
|
||||
}
|
||||
|
||||
h, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid host predicate")
|
||||
}
|
||||
|
||||
r.HostRegexps = append(r.HostRegexps, h)
|
||||
case "PathRegexp":
|
||||
if len(args) != 1 {
|
||||
return errors.New("invalid path regexp predicate")
|
||||
}
|
||||
|
||||
p, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid path regexp predicate")
|
||||
}
|
||||
|
||||
r.PathRegexps = append(r.PathRegexps, p)
|
||||
case "Method":
|
||||
if len(args) != 1 {
|
||||
return errors.New("invalid method predicate")
|
||||
}
|
||||
|
||||
m, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid method predicate")
|
||||
}
|
||||
|
||||
r.Method = m
|
||||
case "Header":
|
||||
if len(args) != 2 {
|
||||
return errors.New("invalid header predicate")
|
||||
}
|
||||
|
||||
name, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid header predicate")
|
||||
}
|
||||
|
||||
value, ok := args[1].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid header predicate")
|
||||
}
|
||||
|
||||
if r.Headers == nil {
|
||||
r.Headers = make(map[string]string)
|
||||
}
|
||||
|
||||
r.Headers[name] = value
|
||||
case "HeaderRegexp":
|
||||
if len(args) != 2 {
|
||||
return errors.New("invalid header regexp predicate")
|
||||
}
|
||||
|
||||
name, ok := args[0].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid header regexp predicate")
|
||||
}
|
||||
|
||||
value, ok := args[1].(string)
|
||||
if !ok {
|
||||
return errors.New("invalid header regexp predicate")
|
||||
}
|
||||
|
||||
if r.HeaderRegexps == nil {
|
||||
r.HeaderRegexps = make(map[string][]string)
|
||||
}
|
||||
|
||||
r.HeaderRegexps[name] = append(r.HeaderRegexps[name], value)
|
||||
default:
|
||||
r.Predicates = append(r.Predicates, &eskip.Predicate{Name: name, Args: args})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func nodeToFilter(n *Node) (*eskip.Filter, error) {
|
||||
name, args, err := nodeToTerm(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &eskip.Filter{Name: name, Args: args}, nil
|
||||
}
|
||||
|
||||
func nodeToBackend(r *eskip.Route, n *Node) error {
|
||||
switch n.Name {
|
||||
case "string":
|
||||
b, err := unquoteString(n.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.BackendType = eskip.NetworkBackend
|
||||
r.Backend = b
|
||||
case "shunt":
|
||||
r.BackendType = eskip.ShuntBackend
|
||||
case "loopback":
|
||||
r.BackendType = eskip.LoopBackend
|
||||
default:
|
||||
return errors.New("invalid backend type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func nodeToEskipDefinition(n *Node) (*eskip.Route, error) {
|
||||
ns := n.Nodes
|
||||
if len(ns) < 2 || len(ns[1].Nodes) == 0 {
|
||||
return nil, fmt.Errorf("invalid definition length: %d", len(ns))
|
||||
}
|
||||
|
||||
r := &eskip.Route{}
|
||||
|
||||
if ns[0].Name != "symbol" {
|
||||
return nil, errors.New("invalid definition id")
|
||||
}
|
||||
|
||||
r.Id, ns = ns[0].Text(), ns[1].Nodes
|
||||
|
||||
predicates:
|
||||
for i, ni := range ns {
|
||||
switch ni.Name {
|
||||
case "predicate":
|
||||
if err := nodeToPredicate(r, ni); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "filter", "string", "shunt", "loopback":
|
||||
ns = ns[i:]
|
||||
break predicates
|
||||
default:
|
||||
return nil, errors.New("invalid definition item among predicates")
|
||||
}
|
||||
}
|
||||
|
||||
filters:
|
||||
for i, ni := range ns {
|
||||
switch ni.Name {
|
||||
case "filter":
|
||||
f, err := nodeToFilter(ni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Filters = append(r.Filters, f)
|
||||
case "string", "shunt", "loopback":
|
||||
ns = ns[i:]
|
||||
break filters
|
||||
default:
|
||||
return nil, errors.New("invalid definition item among filters")
|
||||
}
|
||||
}
|
||||
|
||||
if len(ns) != 1 {
|
||||
return nil, fmt.Errorf("invalid definition backend, remaining definition length: %d, %s",
|
||||
len(ns), n.Text())
|
||||
}
|
||||
|
||||
if err := nodeToBackend(r, ns[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func treeToEskip(n []*Node) ([]*eskip.Route, error) {
|
||||
r := make([]*eskip.Route, 0, len(n))
|
||||
for _, ni := range n {
|
||||
d, err := nodeToEskipDefinition(ni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r = append(r, d)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func checkTerm(t *testing.T, gotName, expectedName string, gotArgs, expectedArgs []interface{}) {
|
||||
if gotName != expectedName {
|
||||
t.Error("invalid term name")
|
||||
return
|
||||
}
|
||||
|
||||
// legacy bug support
|
||||
for i := len(expectedArgs) - 1; i >= 0; i-- {
|
||||
if _, ok := expectedArgs[i].(int); ok {
|
||||
expectedArgs = append(expectedArgs[:i], expectedArgs[i+1:]...)
|
||||
continue
|
||||
}
|
||||
|
||||
if v, ok := expectedArgs[i].(float64); ok && v < 0 {
|
||||
gotArgs = append(gotArgs[:i], gotArgs[i+1:]...)
|
||||
expectedArgs = append(expectedArgs[:i], expectedArgs[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotArgs) != len(expectedArgs) {
|
||||
t.Error("invalid term args length", len(gotArgs), len(expectedArgs))
|
||||
return
|
||||
}
|
||||
|
||||
for i, a := range gotArgs {
|
||||
if a != expectedArgs[i] {
|
||||
t.Error("invalid term arg")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkPredicates(t *testing.T, got, expected *eskip.Route) {
|
||||
if got.Path != expected.Path {
|
||||
t.Error("invalid path")
|
||||
return
|
||||
}
|
||||
|
||||
if len(got.HostRegexps) != len(expected.HostRegexps) {
|
||||
t.Error("invalid host length")
|
||||
return
|
||||
}
|
||||
|
||||
for i, h := range got.HostRegexps {
|
||||
if h != expected.HostRegexps[i] {
|
||||
t.Error("invalid host")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(got.PathRegexps) != len(expected.PathRegexps) {
|
||||
t.Error("invalid path regexp length", len(got.PathRegexps), len(expected.PathRegexps))
|
||||
return
|
||||
}
|
||||
|
||||
for i, h := range got.PathRegexps {
|
||||
if h != expected.PathRegexps[i] {
|
||||
t.Error("invalid path regexp")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if got.Method != expected.Method {
|
||||
t.Error("invalid method")
|
||||
return
|
||||
}
|
||||
|
||||
if len(got.Headers) != len(expected.Headers) {
|
||||
t.Error("invalid headers length")
|
||||
return
|
||||
}
|
||||
|
||||
for n, h := range got.Headers {
|
||||
he, ok := expected.Headers[n]
|
||||
if !ok {
|
||||
t.Error("invalid header name")
|
||||
return
|
||||
}
|
||||
|
||||
if he != h {
|
||||
t.Error("invalid header")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(got.HeaderRegexps) != len(expected.HeaderRegexps) {
|
||||
t.Error("invalid header regexp length")
|
||||
return
|
||||
}
|
||||
|
||||
for n, h := range got.HeaderRegexps {
|
||||
he, ok := expected.HeaderRegexps[n]
|
||||
if !ok {
|
||||
t.Error("invalid header regexp name")
|
||||
return
|
||||
}
|
||||
|
||||
if len(h) != len(he) {
|
||||
t.Error("invalid header regexp item length")
|
||||
return
|
||||
}
|
||||
|
||||
for i, hi := range h {
|
||||
if hi != he[i] {
|
||||
t.Error("invalid header regexp")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(got.Predicates) != len(expected.Predicates) {
|
||||
t.Error("invalid predicates length")
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range got.Predicates {
|
||||
checkTerm(
|
||||
t,
|
||||
p.Name, expected.Predicates[i].Name,
|
||||
p.Args, expected.Predicates[i].Args,
|
||||
)
|
||||
|
||||
if t.Failed() {
|
||||
t.Log(p.Name, expected.Predicates[i].Name)
|
||||
t.Log(p.Args, expected.Predicates[i].Args)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkFilters(t *testing.T, got, expected []*eskip.Filter) {
|
||||
if len(got) != len(expected) {
|
||||
t.Error("invalid filters length")
|
||||
return
|
||||
}
|
||||
|
||||
for i, f := range got {
|
||||
checkTerm(
|
||||
t,
|
||||
f.Name, expected[i].Name,
|
||||
f.Args, expected[i].Args,
|
||||
)
|
||||
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkBackend(t *testing.T, got, expected *eskip.Route) {
|
||||
if got.BackendType != expected.BackendType {
|
||||
t.Error("invalid backend type")
|
||||
return
|
||||
}
|
||||
|
||||
if got.Backend != expected.Backend {
|
||||
t.Error("invalid backend")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func checkRoute(t *testing.T, got, expected *eskip.Route) {
|
||||
if got.Id != expected.Id {
|
||||
t.Error("invalid route id")
|
||||
return
|
||||
}
|
||||
|
||||
checkPredicates(t, got, expected)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
checkFilters(t, got.Filters, expected.Filters)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
checkBackend(t, got, expected)
|
||||
}
|
||||
|
||||
func checkEskip(t *testing.T, got, expected []*eskip.Route) {
|
||||
if len(got) != len(expected) {
|
||||
t.Error("invalid length", len(got), len(expected))
|
||||
return
|
||||
}
|
||||
|
||||
for i, ri := range got {
|
||||
checkRoute(t, ri, expected[i])
|
||||
if t.Failed() {
|
||||
t.Log(ri.String())
|
||||
t.Log(expected[i].String())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func eskipTreeToEskip(n *Node) ([]*eskip.Route, error) {
|
||||
return treeToEskip(n.Nodes)
|
||||
}
|
||||
|
||||
func TestEskip(t *testing.T) {
|
||||
r := generateEskip(1 << 9)
|
||||
e := eskip.Print(true, r...)
|
||||
b := bytes.NewBufferString(e)
|
||||
s, err := testSyntax("eskip.p", 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := s.Parse(b)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
rback, err := eskipTreeToEskip(n)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
checkEskip(t, rback, r)
|
||||
}
|
14
json.p
Normal file
14
json.p
Normal file
@ -0,0 +1,14 @@
|
||||
// JSON (http://www.json.org)
|
||||
ws:alias = [ \b\f\n\r\t];
|
||||
true = "true";
|
||||
false = "false";
|
||||
null = "null";
|
||||
string = "\"" ([^\\"\b\f\n\r\t] | "\\" (["\\/bfnrt] | "u" [0-9a-f]{4}))* "\"";
|
||||
number = "-"? ("0" | [1-9][0-9]*) ("." [0-9]+)? ([eE] [+\-]? [0-9]+)?;
|
||||
entry = string ws* ":" ws* value;
|
||||
object = "{" ws* (entry (ws* "," ws* entry)*)? ws* "}";
|
||||
array = "[" ws* (value (ws* "," ws* value)*)? ws* "]";
|
||||
value:alias = true | false | null | string | number | object | array;
|
||||
json = value;
|
||||
|
||||
// TODO: value should be an alias but test it first like this
|
557
json_test.go
Normal file
557
json_test.go
Normal file
@ -0,0 +1,557 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type jsonValueType int
|
||||
|
||||
const (
|
||||
jsonNone jsonValueType = iota
|
||||
jsonTrue
|
||||
jsonFalse
|
||||
jsonNull
|
||||
jsonString
|
||||
jsonNumber
|
||||
jsonObject
|
||||
jsonArray
|
||||
)
|
||||
|
||||
const (
|
||||
maxStringLength = 64
|
||||
meanStringLength = 18
|
||||
maxKeyLength = 24
|
||||
meanKeyLength = 6
|
||||
maxObjectLength = 12
|
||||
meanObjectLength = 6
|
||||
maxArrayLength = 64
|
||||
meanArrayLength = 8
|
||||
)
|
||||
|
||||
func randomLength(max, mean int) int {
|
||||
return int(rand.NormFloat64()*float64(max)/math.MaxFloat64 + float64(mean))
|
||||
}
|
||||
|
||||
func generateString(max, mean int) string {
|
||||
l := randomLength(max, mean)
|
||||
b := make([]byte, l)
|
||||
for i := range b {
|
||||
b[i] = byte(rand.Intn(int('z')-int('a')+1)) + 'a'
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func generateJSONString() string {
|
||||
return generateString(maxStringLength, meanStringLength)
|
||||
}
|
||||
|
||||
func generateJSONNumber() interface{} {
|
||||
if rand.Intn(2) == 1 {
|
||||
return rand.NormFloat64()
|
||||
}
|
||||
|
||||
n := rand.Int()
|
||||
if rand.Intn(2) == 0 {
|
||||
return n
|
||||
}
|
||||
|
||||
return -n
|
||||
}
|
||||
|
||||
func generateKey() string {
|
||||
return generateString(maxKeyLength, meanKeyLength)
|
||||
}
|
||||
|
||||
func generateJSONObject(minDepth int) map[string]interface{} {
|
||||
l := randomLength(maxObjectLength, meanObjectLength)
|
||||
o := make(map[string]interface{})
|
||||
for i := 0; i < l; i++ {
|
||||
o[generateKey()] = generateJSON(0)
|
||||
}
|
||||
|
||||
if minDepth > 0 {
|
||||
o[generateKey()] = generateJSON(minDepth)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func generateJSONArray(minDepth int) []interface{} {
|
||||
l := randomLength(maxArrayLength, meanArrayLength)
|
||||
a := make([]interface{}, l, l+1)
|
||||
for i := 0; i < l; i++ {
|
||||
a[i] = generateJSON(0)
|
||||
}
|
||||
|
||||
if minDepth > 0 {
|
||||
a = append(a, generateJSON(minDepth))
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func generateJSONObjectOrArray(minDepth int) interface{} {
|
||||
if rand.Intn(2) == 0 {
|
||||
return generateJSONObject(minDepth - 1)
|
||||
}
|
||||
|
||||
return generateJSONArray(minDepth - 1)
|
||||
}
|
||||
|
||||
func generateJSON(minDepth int) interface{} {
|
||||
if minDepth > 0 {
|
||||
return generateJSONObjectOrArray(minDepth)
|
||||
}
|
||||
|
||||
switch jsonValueType(rand.Intn(int(jsonNumber)) + 1) {
|
||||
case jsonTrue:
|
||||
return true
|
||||
case jsonFalse:
|
||||
return false
|
||||
case jsonNull:
|
||||
return nil
|
||||
case jsonString:
|
||||
return generateJSONString()
|
||||
case jsonNumber:
|
||||
return generateJSONNumber()
|
||||
default:
|
||||
panic("invalid json type")
|
||||
}
|
||||
}
|
||||
|
||||
func unqouteJSONString(t string) (string, error) {
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(t), &s)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func parseJSONNumber(t string) (interface{}, error) {
|
||||
n := json.Number(t)
|
||||
if i, err := n.Int64(); err == nil {
|
||||
return int(i), nil
|
||||
}
|
||||
|
||||
return n.Float64()
|
||||
}
|
||||
|
||||
func nodeToJSONObject(n *Node) (map[string]interface{}, error) {
|
||||
o := make(map[string]interface{})
|
||||
for _, ni := range n.Nodes {
|
||||
if len(ni.Nodes) != 2 {
|
||||
return nil, errors.New("invalid json object")
|
||||
}
|
||||
|
||||
key, err := unqouteJSONString(ni.Nodes[0].Text())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val, err := treeToJSON(ni.Nodes[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o[key] = val
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func nodeToJSONArray(n *Node) ([]interface{}, error) {
|
||||
a := make([]interface{}, 0, len(n.Nodes))
|
||||
for _, ni := range n.Nodes {
|
||||
item, err := treeToJSON(ni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a = append(a, item)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func treeToJSON(n *Node) (interface{}, error) {
|
||||
switch n.Name {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
case "null":
|
||||
return nil, nil
|
||||
case "string":
|
||||
return unqouteJSONString(n.Text())
|
||||
case "number":
|
||||
return parseJSONNumber(n.Text())
|
||||
case "object":
|
||||
return nodeToJSONObject(n)
|
||||
case "array":
|
||||
return nodeToJSONArray(n)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid json node name: %s", n.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func checkJSON(t *testing.T, got, expected interface{}) {
|
||||
if expected == nil {
|
||||
if got != nil {
|
||||
t.Error("expected nil", got)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch v := expected.(type) {
|
||||
case bool:
|
||||
if v != got.(bool) {
|
||||
t.Error("expected bool", got)
|
||||
}
|
||||
case string:
|
||||
if v != got.(string) {
|
||||
t.Error("expected string", got)
|
||||
}
|
||||
case int:
|
||||
if v != got.(int) {
|
||||
t.Error("expected int", got)
|
||||
}
|
||||
case float64:
|
||||
if v != got.(float64) {
|
||||
t.Error("expected float64", got)
|
||||
}
|
||||
case map[string]interface{}:
|
||||
o, ok := got.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("expected object", got)
|
||||
return
|
||||
}
|
||||
|
||||
if len(v) != len(o) {
|
||||
t.Error("invalid object length, expected: %d, got: %d", len(v), len(o))
|
||||
return
|
||||
}
|
||||
|
||||
for key, val := range v {
|
||||
gotVal, ok := o[key]
|
||||
if !ok {
|
||||
t.Error("expected key not found: %s", key)
|
||||
return
|
||||
}
|
||||
|
||||
checkJSON(t, gotVal, val)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
a, ok := got.([]interface{})
|
||||
if !ok {
|
||||
t.Error("expected array", got)
|
||||
}
|
||||
|
||||
if len(v) != len(a) {
|
||||
t.Error("invalid array length, expected: %d, got: %d", len(v), len(a))
|
||||
return
|
||||
}
|
||||
|
||||
for i := range v {
|
||||
checkJSON(t, a[i], v[i])
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Error("unexpected parsed type", v)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonTreeToJSON(n *Node) (interface{}, error) {
|
||||
if n.Name != "json" {
|
||||
return nil, fmt.Errorf("invalid root node name: %s", n.Name)
|
||||
}
|
||||
|
||||
if len(n.Nodes) != 1 {
|
||||
return nil, fmt.Errorf("invalid root node length: %d", len(n.Nodes))
|
||||
}
|
||||
|
||||
return treeToJSON(n.Nodes[0])
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
test(t, "json.p", "value", []testItem{{
|
||||
msg: "true",
|
||||
text: "true",
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "true",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "false",
|
||||
text: "false",
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "false",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "null",
|
||||
text: "null",
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "null",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "string",
|
||||
text: `"\"\\n\b\t\uabcd"`,
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "number",
|
||||
text: "6.62e-34",
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "number",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "object",
|
||||
text: `{
|
||||
"true": true,
|
||||
"false": false,
|
||||
"null": null,
|
||||
"string": "string",
|
||||
"number": 42,
|
||||
"object": {},
|
||||
"array": []
|
||||
}`,
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "object",
|
||||
Nodes: []*Node{{
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "true",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "false",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "null",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "string",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "number",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "object",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "array",
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "array",
|
||||
text: `[true, false, null, "string", 42, {
|
||||
"true": true,
|
||||
"false": false,
|
||||
"null": null,
|
||||
"string": "string",
|
||||
"number": 42,
|
||||
"object": {},
|
||||
"array": []
|
||||
}, []]`,
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "array",
|
||||
Nodes: []*Node{{
|
||||
Name: "true",
|
||||
}, {
|
||||
Name: "false",
|
||||
}, {
|
||||
Name: "null",
|
||||
}, {
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "number",
|
||||
}, {
|
||||
Name: "object",
|
||||
Nodes: []*Node{{
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "true",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "false",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "null",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "string",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "number",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "object",
|
||||
}},
|
||||
}, {
|
||||
Name: "entry",
|
||||
Nodes: []*Node{{
|
||||
Name: "string",
|
||||
}, {
|
||||
Name: "array",
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
Name: "array",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "bugfix, 100",
|
||||
text: "100",
|
||||
node: &Node{
|
||||
Name: "json",
|
||||
Nodes: []*Node{{
|
||||
Name: "number",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}})
|
||||
}
|
||||
|
||||
func TestRandomJSON(t *testing.T) {
|
||||
j := generateJSON(48)
|
||||
b, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(b)
|
||||
|
||||
s, err := testSyntax("json.p", 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
testParse := func(t *testing.T, buf io.Reader) {
|
||||
n, err := s.Parse(buf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
jback, err := jsonTreeToJSON(n)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
checkJSON(t, jback, j)
|
||||
}
|
||||
|
||||
t.Run("unindented", func(t *testing.T) {
|
||||
testParse(t, buf)
|
||||
})
|
||||
|
||||
indented := bytes.NewBuffer(nil)
|
||||
if err := json.Indent(indented, b, "", " "); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("indented", func(t *testing.T) {
|
||||
testParse(t, indented)
|
||||
})
|
||||
|
||||
indentedTabs := bytes.NewBuffer(nil)
|
||||
if err := json.Indent(indentedTabs, b, "", "\t"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("indented with tabs", func(t *testing.T) {
|
||||
testParse(t, indentedTabs)
|
||||
})
|
||||
}
|
29
keyval.p
Normal file
29
keyval.p
Normal file
@ -0,0 +1,29 @@
|
||||
ws:alias = [ \b\f\r\t\v];
|
||||
wsnl:alias = ws | "\n";
|
||||
|
||||
comment-line:alias = "#" [^\n]*;
|
||||
comment = comment-line (ws* "\n" ws* comment-line)*;
|
||||
|
||||
wsc:alias = ws | comment-line;
|
||||
wsnlc:alias = wsnl | comment-line;
|
||||
|
||||
quoted:alias = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
symbol-non-ws:alias = ([^\\"\n=#.\[\] \b\f\r\t\v] | "\\" .)+;
|
||||
symbol = symbol-non-ws (ws* symbol-non-ws)* | quoted;
|
||||
|
||||
key-form:alias = symbol (ws* "." ws* symbol)*;
|
||||
key = key-form;
|
||||
group-key = (comment "\n" ws*)? "[" ws* key-form ws* "]";
|
||||
|
||||
value-chars:alias = ([^\\"\n=# \b\f\r\t\v] | "\\" .)+;
|
||||
value = value-chars (ws* value-chars)* | quoted;
|
||||
key-val = (comment "\n" ws*)? (key | key? ws* "=" ws* value?);
|
||||
|
||||
entry:alias = group-key | key-val;
|
||||
doc:root = (entry (ws* comment-line)? | wsnlc)*;
|
||||
|
||||
// TODO: not tested
|
||||
// set as root for streaming:
|
||||
single-entry = (entry (ws* comment-line)?
|
||||
| wsnlc* entry (ws* comment-line)?)
|
||||
[];
|
394
keyval_test.go
Normal file
394
keyval_test.go
Normal file
@ -0,0 +1,394 @@
|
||||
package parse
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestKeyVal(t *testing.T) {
|
||||
test(t, "keyval.p", "doc", []testItem{{
|
||||
msg: "empty",
|
||||
}, {
|
||||
msg: "a comment",
|
||||
text: "# a comment",
|
||||
}, {
|
||||
msg: "a key",
|
||||
text: "a key",
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
to: 5,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
to: 5,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
to: 5,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a key with a preceeding whitespace",
|
||||
text: " a key",
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
from: 1,
|
||||
to: 6,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
from: 1,
|
||||
to: 6,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 1,
|
||||
to: 6,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a key and a comment",
|
||||
text: `
|
||||
# a comment
|
||||
|
||||
a key
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
from: 20,
|
||||
to: 25,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
from: 20,
|
||||
to: 25,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 20,
|
||||
to: 25,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a key value pair",
|
||||
text: "a key = a value",
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
to: 15,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
to: 5,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
to: 5,
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
from: 8,
|
||||
to: 15,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "key value pairs with a comment at the end of line",
|
||||
text: `
|
||||
a key = a value # a comment
|
||||
another key = another value # another comment
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
from: 11,
|
||||
to: 32,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
from: 11,
|
||||
to: 16,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 11,
|
||||
to: 16,
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
from: 25,
|
||||
to: 32,
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
from: 61,
|
||||
to: 88,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
from: 61,
|
||||
to: 72,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 61,
|
||||
to: 72,
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
from: 75,
|
||||
to: 88,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "value without a key",
|
||||
text: "= a value",
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
to: 9,
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
from: 2,
|
||||
to: 9,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a key value pair with comment",
|
||||
text: `
|
||||
# a comment
|
||||
a key = a value
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
from: 4,
|
||||
to: 34,
|
||||
Nodes: []*Node{{
|
||||
Name: "comment",
|
||||
from: 4,
|
||||
to: 15,
|
||||
}, {
|
||||
Name: "key",
|
||||
from: 19,
|
||||
to: 24,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 19,
|
||||
to: 24,
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
from: 27,
|
||||
to: 34,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a key with multiple symbols",
|
||||
text: "a key . with.multiple.symbols=a value",
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
to: 37,
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
from: 0,
|
||||
to: 29,
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
from: 0,
|
||||
to: 5,
|
||||
}, {
|
||||
Name: "symbol",
|
||||
from: 8,
|
||||
to: 12,
|
||||
}, {
|
||||
Name: "symbol",
|
||||
from: 13,
|
||||
to: 21,
|
||||
}, {
|
||||
Name: "symbol",
|
||||
from: 22,
|
||||
to: 29,
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
from: 30,
|
||||
to: 37,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a group key",
|
||||
text: `
|
||||
# a comment
|
||||
[a group key.empty]
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "group-key",
|
||||
from: 4,
|
||||
to: 38,
|
||||
Nodes: []*Node{{
|
||||
Name: "comment",
|
||||
from: 4,
|
||||
to: 15,
|
||||
}, {
|
||||
Name: "symbol",
|
||||
from: 20,
|
||||
to: 31,
|
||||
}, {
|
||||
Name: "symbol",
|
||||
from: 32,
|
||||
to: 37,
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
msg: "a group key with multiple values",
|
||||
text: `
|
||||
[foo.bar.baz]
|
||||
= one
|
||||
= two
|
||||
= three
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "group-key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "a group key with multiple values, in a single line",
|
||||
text: "[foo.bar.baz] = one = two = three",
|
||||
nodes: []*Node{{
|
||||
Name: "group-key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "value",
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "full example",
|
||||
text: `
|
||||
# a keyval document
|
||||
|
||||
key1 = foo
|
||||
key1.a = bar
|
||||
key1.b = baz
|
||||
|
||||
key2 = qux
|
||||
|
||||
# foo bar baz values
|
||||
[foo.bar.baz]
|
||||
a = 1
|
||||
b = 2 # even
|
||||
c = 3
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "group-key",
|
||||
Nodes: []*Node{{
|
||||
Name: "comment",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}, {
|
||||
Name: "key-val",
|
||||
Nodes: []*Node{{
|
||||
Name: "key",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "value",
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}})
|
||||
}
|
527
mml.p
Normal file
527
mml.p
Normal file
@ -0,0 +1,527 @@
|
||||
// whitespace is ignored except for \n which is only ignored
|
||||
// most of the time, but can serve as separator in:
|
||||
// - list
|
||||
// - struct
|
||||
// - function args
|
||||
// - statements
|
||||
// - list, struct and function type constraints
|
||||
ws:alias = " " | "\b" | "\f" | "\r" | "\t" | "\v";
|
||||
wsnl:alias = ws | "\n";
|
||||
wsc:alias = ws | comment;
|
||||
wsnlc:alias = wsc | "\n";
|
||||
|
||||
// comments can be line or block comments
|
||||
line-comment-content = [^\n]*;
|
||||
line-comment:alias = "//" line-comment-content;
|
||||
block-comment-content = ([^*] | "*" [^/])*;
|
||||
block-comment:alias = "/*" block-comment-content "*/";
|
||||
comment-part:alias = line-comment | block-comment;
|
||||
comment = comment-part (ws* "\n"? ws* comment-part)*;
|
||||
|
||||
decimal-digit:alias = [0-9];
|
||||
octal-digit:alias = [0-7];
|
||||
hexa-digit:alias = [0-9a-fA-F];
|
||||
|
||||
// interger examples: 42, 0666, 0xfff
|
||||
decimal:alias = [1-9] decimal-digit*;
|
||||
octal:alias = "0" octal-digit*;
|
||||
hexa:alias = "0" [xX] hexa-digit+;
|
||||
int = decimal | octal | hexa;
|
||||
|
||||
// float examples: .0, 0., 3.14, 1E-12
|
||||
exponent:alias = [eE] [+\-]? decimal-digit+;
|
||||
float = decimal-digit+ "." decimal-digit* exponent?
|
||||
| "." decimal-digit+ exponent?
|
||||
| decimal-digit+ exponent;
|
||||
|
||||
// string example: "Hello, world!"
|
||||
// only \ and " need to be escaped, e.g. allows new lines
|
||||
// common escaped chars get unescaped, the rest gets unescaped to themselves
|
||||
string = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
|
||||
true = "true";
|
||||
false = "false";
|
||||
bool:alias = true | false;
|
||||
|
||||
// symbols normally can have only \w chars: fooBar_baz
|
||||
// basic symbols cannot start with a digit
|
||||
// some positions allow strings to be used as symbols, e.g: let "123" 123
|
||||
// when this is not possible, dynamic symbols need to be used, but they are
|
||||
// not allowed in every case, e.g: {symbol(foo()): "bar"}
|
||||
// TODO: needs decision log for dynamic symbol
|
||||
// TODO: exclude keywords
|
||||
//
|
||||
// dynamic symbol decision log:
|
||||
// - every value is equatable
|
||||
// - structs can act as hashtables (optimization is transparent)
|
||||
// - in structs, must differentiate between symbol and value of a symbol when used as a key
|
||||
// - js style [a] would be enough for the structs
|
||||
// - the variables in a scope are like fields in a struct
|
||||
// - [a] would be ambigous with the list as an expression
|
||||
// - a logical loophole is closed with symbol(a)
|
||||
// - dynamic-symbols need to be handled differently in match expressions and type expressions
|
||||
symbol = [a-zA-Z_][a-zA-Z_0-9]*;
|
||||
static-symbol:alias = symbol | string;
|
||||
dynamic-symbol = "symbol" wsc* "(" wsnlc* expression wsnlc* ")";
|
||||
symbol-expression:alias = static-symbol | dynamic-symbol;
|
||||
|
||||
// TODO: what happens when a dynamic symbol gets exported?
|
||||
|
||||
// list items are separated by comma or new line (or both)
|
||||
/*
|
||||
[]
|
||||
[a, b, c]
|
||||
[
|
||||
a
|
||||
b
|
||||
c
|
||||
]
|
||||
[1, 2, a..., [b, c], [d, [e]]...]
|
||||
*/
|
||||
spread-expression = primary-expression wsc* "...";
|
||||
list-sep:alias = wsc* ("," | "\n") (wsnlc | ",")*;
|
||||
list-item:alias = expression | spread-expression;
|
||||
expression-list:alias = list-item (list-sep list-item)*;
|
||||
|
||||
// list example: [1, 2, 3]
|
||||
// lists can be constructed with other lists: [l1..., l2...]
|
||||
list-fact:alias = "[" (wsnlc | ",")* expression-list? (wsnlc | ",")* "]";
|
||||
list = list-fact;
|
||||
mutable-list = "~" wsnlc* list-fact;
|
||||
|
||||
indexer-symbol = "[" wsnlc* expression wsnlc* "]";
|
||||
entry = (symbol-expression | indexer-symbol) wsnlc* ":" wsnlc* expression;
|
||||
entry-list:alias = (entry | spread-expression) (list-sep (entry | spread-expression))*;
|
||||
struct-fact:alias = "{" (wsnlc | ",")* entry-list? (wsnlc | ",")* "}";
|
||||
struct = struct-fact;
|
||||
mutable-struct = "~" wsnlc* struct-fact;
|
||||
|
||||
channel = "<>" | "<" wsnlc* int wsnlc* ">";
|
||||
|
||||
and-expression:doc = "and" wsc* "(" (wsnlc | ",")* expression-list? (wsnlc | ",")* ")";
|
||||
or-expression:doc = "or" wsc* "(" (wsnlc | ",")* expression-list? (wsnlc | ",")* ")";
|
||||
|
||||
// TODO: use collect
|
||||
argument-list:alias = static-symbol (list-sep static-symbol)*;
|
||||
collect-symbol = "..." wsnlc* static-symbol;
|
||||
function-fact:alias = "(" (wsnlc | ",")*
|
||||
argument-list?
|
||||
(wsnlc | ",")*
|
||||
collect-symbol?
|
||||
(wsnlc | ",")* ")" wsnlc*
|
||||
expression;
|
||||
function = "fn" wsnlc* function-fact; // can it ever cause a conflict with call and grouping?
|
||||
effect = "fn" wsnlc* "~" wsnlc* function-fact;
|
||||
|
||||
/*
|
||||
a[42]
|
||||
a[3:9]
|
||||
a[:9]
|
||||
a[3:]
|
||||
a[b][c][d]
|
||||
a.foo
|
||||
a."foo"
|
||||
a.symbol(foo)
|
||||
*/
|
||||
range-from = expression;
|
||||
range-to = expression;
|
||||
range-expression:alias = range-from? wsnlc* ":" wsnlc* range-to?;
|
||||
indexer-expression:alias = expression | range-expression;
|
||||
expression-indexer:alias = primary-expression wsc* "[" wsnlc* indexer-expression wsnlc* "]";
|
||||
symbol-indexer:alias = primary-expression wsnlc* "." wsnlc* symbol-expression; // TODO: test with a float on a new line
|
||||
indexer = expression-indexer | symbol-indexer;
|
||||
|
||||
function-application = primary-expression wsc* "(" (wsnlc | ",")* expression-list? (wsnlc | ",")* ")";
|
||||
|
||||
if = "if" wsnlc* expression wsnlc* block
|
||||
(wsnlc* "else" wsnlc* "if" wsnlc* expression wsnlc* block)*
|
||||
(wsnlc* "else" wsnlc* block)?;
|
||||
|
||||
default = "default" wsnlc* ":";
|
||||
default-line:alias = default (wsnlc | ";")* statement?;
|
||||
case = "case" wsnlc* expression wsnlc* ":";
|
||||
case-line:alias = case (wsnlc | ";")* statement?;
|
||||
switch = "switch" wsnlc* expression? wsnlc* "{" (wsnlc | ";")*
|
||||
((case-line | default-line) (sep (case-line | default-line | statement))*)?
|
||||
(wsnlc | ";")* "}";
|
||||
// TODO: empty case not handled
|
||||
|
||||
int-type = "int";
|
||||
float-type = "float";
|
||||
string-type = "string";
|
||||
bool-type = "bool";
|
||||
error-type = "error";
|
||||
|
||||
primitive-type:alias = int-type
|
||||
| float-type
|
||||
| string-type
|
||||
| bool-type
|
||||
| error-type;
|
||||
|
||||
type-alias-name:alias = static-symbol;
|
||||
|
||||
static-range-from = int;
|
||||
static-range-to = int;
|
||||
static-range-expression:alias = static-range-from? wsnlc* ":" wsnlc* static-range-to?;
|
||||
items-quantifier = int | static-range-expression;
|
||||
// TODO: maybe this can be confusing with matching constants. Shall we support matching constants, values?
|
||||
|
||||
items-type = items-quantifier
|
||||
| type-set (wsnlc* ":" wsnlc* items-quantifier)?
|
||||
| static-symbol wsnlc* type-set (wsnlc* ":" wsnlc* items-quantifier)?;
|
||||
|
||||
destructure-item = type-set | static-symbol wsnlc* type-set;
|
||||
|
||||
collect-destructure-item = "..." wsnlc* destructure-item?
|
||||
(wsnlc* ":" items-quantifier)?;
|
||||
list-destructure-type = destructure-item
|
||||
(list-sep destructure-item)*
|
||||
(list-sep collect-destructure-item)?
|
||||
| collect-destructure-item;
|
||||
list-type-fact:alias = "[" (wsnlc | ",")*
|
||||
(items-type | list-destructure-type)?
|
||||
(wsnlc | ",")* "]";
|
||||
list-type = list-type-fact;
|
||||
mutable-list-type = "~" wsnlc* list-type-fact;
|
||||
|
||||
destructure-match-item = match-set
|
||||
| static-symbol wsnlc* match-set
|
||||
| static-symbol wsnlc* static-symbol wsnlc* match-set;
|
||||
|
||||
collect-destructure-match-item = "..." wsnlc* destructure-match-item?
|
||||
(wsnlc* ":" items-quantifier)?;
|
||||
list-destructure-match = destructure-match-item
|
||||
(list-sep destructure-match-item)*
|
||||
(list-sep collect-destructure-match-item)?
|
||||
| collect-destructure-match-item;
|
||||
list-match-fact:alias = "[" (wsnlc | ",")*
|
||||
(list-destructure-match | items-type)?
|
||||
(wsnlc | ",")* "]";
|
||||
list-match = list-match-fact;
|
||||
mutable-list-match = "~" wsnlc* list-match;
|
||||
|
||||
entry-type = static-symbol (wsnlc* ":" wsnlc* destructure-item)?;
|
||||
entry-types:alias = entry-type (list-sep entry-type)*;
|
||||
struct-type-fact:alias = "{" (wsnlc | ",")* entry-types? (wsnlc | ",")* "}";
|
||||
struct-type = struct-type-fact;
|
||||
mutable-struct-type = "~" wsnlc* struct-type-fact;
|
||||
|
||||
entry-match = static-symbol (wsnlc* ":" wsnlc* destructure-match-item)?;
|
||||
entry-matches:alias = entry-match (list-sep entry-match)*;
|
||||
struct-match-fact:alias = "{" (wsnlc | ",")* entry-matches? (wsnlc | ",")* "}";
|
||||
struct-match = struct-match-fact;
|
||||
mutable-struct-match = "~" wsnlc* struct-match-fact;
|
||||
|
||||
arg-type = type-set | static-symbol wsnlc* type-set;
|
||||
args-type:alias = arg-type (list-sep arg-type)*;
|
||||
function-type-fact:alias = "(" wsnlc* args-type? wsnlc* ")"
|
||||
(wsc* (type-set | static-symbol wsc* type-set))?;
|
||||
function-type = "fn" wsnlc* function-type-fact;
|
||||
effect-type = "fn" wsnlc* "~" wsnlc* function-type-fact;
|
||||
|
||||
// TODO: heavy naming crime
|
||||
|
||||
receive-direction = "receive";
|
||||
send-direction = "send";
|
||||
channel-type = "<" wsnlc*
|
||||
(receive-direction | send-direction)? wsnlc*
|
||||
destructure-item?
|
||||
wsnlc* ">";
|
||||
|
||||
type-fact-group:alias = "(" wsnlc* type-fact wsnlc* ")";
|
||||
type-fact:alias = primitive-type
|
||||
| type-alias-name
|
||||
| list-type
|
||||
| mutable-list-type
|
||||
| struct-type
|
||||
| mutable-struct-type
|
||||
| function-type
|
||||
| effect-type
|
||||
| channel-type
|
||||
| type-fact-group;
|
||||
|
||||
type-set:alias = type-fact (wsnlc* "|" wsnlc* type-fact)*;
|
||||
type-expression:alias = type-set | static-symbol wsc* type-set;
|
||||
|
||||
match-fact:alias = list-match
|
||||
| mutable-list-match
|
||||
| struct-match
|
||||
| mutable-struct-match;
|
||||
|
||||
match-set:alias = type-set | match-fact;
|
||||
match-expression:alias = match-set | static-symbol wsc* match-set;
|
||||
|
||||
match-case = "case" wsnlc* match-expression wsnlc* ":";
|
||||
match-case-line:alias = match-case (wsnlc | ";")* statement?;
|
||||
match = "match" wsnlc* expression wsnlc* "{" (wsnlc | ";")*
|
||||
((match-case-line | default-line)
|
||||
(sep (match-case-line | default-line | statement))*)?
|
||||
(wsnlc | ";")* "}";
|
||||
|
||||
conditional:alias = if
|
||||
| switch
|
||||
| match;
|
||||
|
||||
receive-call = "receive" wsc* "(" (wsnlc | ",")* expression (wsnlc | ",")* ")";
|
||||
receive-op = "<-" wsc* primary-expression;
|
||||
receive-expression-group:alias = "(" wsnlc* receive-expression wsnlc* ")";
|
||||
receive-expression:alias = receive-call | receive-op | receive-expression-group;
|
||||
|
||||
receive-assign-capture:alias = assignable wsnlc* ("=" wsnlc*)? receive-expression;
|
||||
receive-assignment = "set" wsnlc* receive-assign-capture;
|
||||
receive-assignment-equal = assignable wsnlc* "=" wsnlc* receive-expression;
|
||||
receive-capture:alias = symbol-expression wsnlc* ("=" wsnlc*)? receive-expression;
|
||||
receive-definition = "let" wsnlc* receive-capture;
|
||||
receive-mutable-definition = "let" wcnl* "~" wsnlc* receive-capture;
|
||||
receive-statement:alias = receive-assignment | receive-definition;
|
||||
|
||||
send-call:alias = "send" wsc* "(" (wsnlc | ",")* expression list-sep expression (wsnlc | ",")* ")";
|
||||
send-op:alias = primary-expression wsc* "<-" wsc* expression;
|
||||
send-call-group:alias = "(" wsnlc* send wsnlc* ")";
|
||||
send = send-call | send-op | send-call-group;
|
||||
|
||||
close = "close" wsc* "(" (wsnlc | ",")* expression (wsnlc | ",")* ")";
|
||||
|
||||
communication-group:alias = "(" wsnlc* communication wsnlc* ")";
|
||||
communication:alias = receive-expression | receive-statement | send | communication-group;
|
||||
|
||||
select-case = "case" wsnlc* communication wsnlc* ":";
|
||||
select-case-line:alias = select-case (wsnlc | ";")* statement?;
|
||||
select = "select" wsnlc* "{" (wsnlc | ";")*
|
||||
((select-case-line | default-line)
|
||||
(sep (select-case-line | default-line | statement))*)?
|
||||
(wsnlc | ";")* "}";
|
||||
|
||||
go = "go" wsnlc* function-application;
|
||||
|
||||
/*
|
||||
require . = "mml/foo"
|
||||
require bar = "mml/foo"
|
||||
require . "mml/foo"
|
||||
require bar "mml/foo"
|
||||
require "mml/foo"
|
||||
require (
|
||||
. = "mml/foo"
|
||||
bar = "mml/foo"
|
||||
. "mml/foo"
|
||||
bar "mml/foo"
|
||||
"mml/foo"
|
||||
)
|
||||
require ()
|
||||
*/
|
||||
require-inline = ".";
|
||||
require-fact = string
|
||||
| (static-symbol | require-inline) (wsnlc* "=")? wsnlc* string;
|
||||
require-facts:alias = require-fact (list-sep require-fact)*;
|
||||
require-statement:alias = "require" wsnlc* require-fact;
|
||||
require-statement-group:alias = "require" wsc* "(" (wsnlc | ",")*
|
||||
require-facts?
|
||||
(wsnlc | ",")* ")";
|
||||
require = require-statement | require-statement-group;
|
||||
|
||||
panic = "panic" wsc* "(" (wsnlc | ",")* expression (wsnlc | ",")* ")";
|
||||
recover = "recover" wsc* "(" (wsnlc | ",")* ")";
|
||||
|
||||
block = "{" (wsnlc | ";")* statements? (wsnlc | ";")* "}";
|
||||
expression-group:alias = "(" wsnlc* expression wsnlc* ")";
|
||||
|
||||
primary-expression:alias = int
|
||||
| float
|
||||
| string
|
||||
| bool
|
||||
| symbol
|
||||
| dynamic-symbol
|
||||
| list
|
||||
| mutable-list
|
||||
| struct
|
||||
| mutable-struct
|
||||
| channel
|
||||
| and-expression // only documentation
|
||||
| or-expression // only documentation
|
||||
| function
|
||||
| effect
|
||||
| indexer
|
||||
| function-application // pseudo-expression
|
||||
| conditional // pseudo-expression
|
||||
| receive-call
|
||||
| select // pseudo-expression
|
||||
| recover
|
||||
| block // pseudo-expression
|
||||
| expression-group;
|
||||
|
||||
plus = "+";
|
||||
minus = "-";
|
||||
logical-not = "!";
|
||||
binary-not = "^";
|
||||
unary-operator:alias = plus | minus | logical-not | binary-not;
|
||||
unary-expression = unary-operator wsc* primary-expression | receive-op;
|
||||
|
||||
mul = "*";
|
||||
div = "/";
|
||||
mod = "%";
|
||||
lshift = "<<";
|
||||
rshift = ">>";
|
||||
binary-and = "&";
|
||||
and-not = "&^";
|
||||
|
||||
add = "+";
|
||||
sub = "-";
|
||||
binary-or = "|";
|
||||
xor = "^";
|
||||
|
||||
eq = "==";
|
||||
not-eq = "!=";
|
||||
less = "<";
|
||||
less-or-eq = "<=";
|
||||
greater = ">";
|
||||
greater-or-eq = ">=";
|
||||
|
||||
logical-and = "&&";
|
||||
logical-or = "||";
|
||||
|
||||
chain = "->";
|
||||
|
||||
binary-op0:alias = mul | div | mod | lshift | rshift | binary-and | and-not;
|
||||
binary-op1:alias = add | sub | binary-or | xor;
|
||||
binary-op2:alias = eq | not-eq | less | less-or-eq | greater | greater-or-eq;
|
||||
binary-op3:alias = logical-and;
|
||||
binary-op4:alias = logical-or;
|
||||
binary-op5:alias = chain;
|
||||
|
||||
operand0:alias = primary-expression | unary-expression;
|
||||
operand1:alias = operand0 | binary0;
|
||||
operand2:alias = operand1 | binary1;
|
||||
operand3:alias = operand2 | binary2;
|
||||
operand4:alias = operand3 | binary3;
|
||||
operand5:alias = operand4 | binary4;
|
||||
|
||||
binary0 = operand0 wsc* binary-op0 wsc* operand0;
|
||||
binary1 = operand1 wsc* binary-op1 wsc* operand1;
|
||||
binary2 = operand2 wsc* binary-op2 wsc* operand2;
|
||||
binary3 = operand3 wsc* binary-op3 wsc* operand3;
|
||||
binary4 = operand4 wsc* binary-op4 wsc* operand4;
|
||||
binary5 = operand5 wsc* binary-op5 wsc* operand5;
|
||||
|
||||
binary-expression:alias = binary0 | binary1 | binary2 | binary3 | binary4 | binary5;
|
||||
|
||||
ternary-expression = expression wsnlc* "?" wsnlc* expression wsnlc* ":" wsnlc* expression;
|
||||
|
||||
expression:alias = primary-expression
|
||||
| unary-expression
|
||||
| binary-expression
|
||||
| ternary-expression;
|
||||
|
||||
// TODO: code()
|
||||
// TODO: observability
|
||||
|
||||
break = "break";
|
||||
continue = "continue";
|
||||
loop-control:alias = break | continue;
|
||||
|
||||
in-expression = static-symbol wsnlc* "in" wsnlc* (expression | range-expression);
|
||||
loop-expression = expression | in-expression;
|
||||
loop = "for" wsnlc* (block | loop-expression wsnlc* block);
|
||||
|
||||
/*
|
||||
a = b
|
||||
set c = d
|
||||
set e f
|
||||
set (
|
||||
g = h
|
||||
i j
|
||||
)
|
||||
*/
|
||||
assignable:alias = symbol-expression | indexer;
|
||||
assign-capture = assignable wsnlc* ("=" wsnlc*)? expression;
|
||||
assign-set:alias = "set" wsnlc* assign-capture;
|
||||
assign-equal = assignable wsnlc* "=" wsnlc* expression;
|
||||
assign-captures:alias = assign-capture (list-sep assign-capture)*;
|
||||
assign-group:alias = "set" wsnlc* "(" (wsnlc | ",")* assign-captures? (wsnlc | ",")* ")";
|
||||
assignment = assign-set | assign-equal | assign-group;
|
||||
|
||||
/*
|
||||
let a = b
|
||||
let c d
|
||||
let ~ e = f
|
||||
let ~ g h
|
||||
let (
|
||||
i = j
|
||||
k l
|
||||
~ m = n
|
||||
~ o p
|
||||
)
|
||||
let ~ (
|
||||
q = r
|
||||
s t
|
||||
)
|
||||
*/
|
||||
value-capture-fact:alias = symbol-expression wsnlc* ("=" wsnlc*)? expression;
|
||||
value-capture = value-capture-fact;
|
||||
mutable-capture = "~" wsnlc* value-capture-fact;
|
||||
value-definition = "let" wsnlc* (value-capture | mutable-capture);
|
||||
value-captures:alias = value-capture (list-sep value-capture)*;
|
||||
mixed-captures:alias = (value-capture | mutable-capture) (list-sep (value-capture | mutable-capture))*;
|
||||
value-definition-group = "let" wsnlc* "(" (wsnlc | ",")* mixed-captures? (wsnlc | ",")* ")";
|
||||
mutable-definition-group = "let" wsnlc* "~" wsnlc* "(" (wsnlc | ",")* value-captures? (wsnlc | ",")* ")";
|
||||
|
||||
/*
|
||||
fn a() b
|
||||
fn ~ c() d
|
||||
fn (
|
||||
e() f
|
||||
~ g() h
|
||||
)
|
||||
fn ~ (
|
||||
i()
|
||||
j()
|
||||
)
|
||||
*/
|
||||
function-definition-fact:alias = static-symbol wsnlc* function-fact;
|
||||
function-capture = function-definition-fact;
|
||||
effect-capture = "~" wsnlc* function-definition-fact;
|
||||
function-definition = "fn" wsnlc* (function-capture | effect-capture);
|
||||
function-captures:alias = function-capture (list-sep function-capture)*;
|
||||
mixed-function-captures:alias = (function-capture | effect-capture)
|
||||
(list-sep (function-capture | effect-capture))*;
|
||||
function-definition-group = "fn" wsnlc* "(" (wsnlc | ",")*
|
||||
mixed-function-captures?
|
||||
(wsnlc | ",")* ")";
|
||||
effect-definition-group = "fn" wsnlc* "~" wsnlc* "(" (wsnlc | ",")*
|
||||
function-captures?
|
||||
(wsnlc | ",")* ")";
|
||||
|
||||
definition:alias = value-definition
|
||||
| value-definition-group
|
||||
| mutable-definition-group
|
||||
| function-definition
|
||||
| function-definition-group
|
||||
| effect-definition-group;
|
||||
|
||||
// TODO: cannot do:
|
||||
// type alias a int|fn () string|error
|
||||
// needs grouping of type-set
|
||||
|
||||
type-alias = "type" wsnlc* "alias" wsnlc* static-symbol wsnlc* type-set;
|
||||
type-constraint = "type" wsnlc* static-symbol wsnlc* type-set;
|
||||
|
||||
statement-group:alias = "(" wsnlc* statement wsnlc* ")";
|
||||
|
||||
statement:alias = send
|
||||
| close
|
||||
| panic
|
||||
| require
|
||||
| loop-control
|
||||
| go
|
||||
| loop
|
||||
| assignment
|
||||
| definition
|
||||
| expression
|
||||
| type-alias
|
||||
| type-constraint
|
||||
| statement-group;
|
||||
|
||||
shebang-command = [^\n]*;
|
||||
shebang = "#!" shebang-command "\n";
|
||||
sep:alias = wsc* (";" | "\n") (wsnlc | ";")*;
|
||||
statements:alias = statement (sep statement)*;
|
||||
mml:root = shebang? (wsnlc | ";")* statements? (wsnlc | ";")*;
|
2791
mml_test.go
Normal file
2791
mml_test.go
Normal file
File diff suppressed because it is too large
Load Diff
740
next_test.go
Normal file
740
next_test.go
Normal file
@ -0,0 +1,740 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testItem struct {
|
||||
msg string
|
||||
text string
|
||||
fail bool
|
||||
node *Node
|
||||
nodes []*Node
|
||||
ignorePosition bool
|
||||
}
|
||||
|
||||
func testSyntaxReader(r io.Reader, traceLevel int) (*Syntax, error) {
|
||||
trace := NewTrace(0)
|
||||
|
||||
b, err := bootSyntax(trace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doc, err := b.Parse(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trace = NewTrace(traceLevel)
|
||||
s := NewSyntax(trace)
|
||||
if err := define(s, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func testSyntaxString(s string, traceLevel int) (*Syntax, error) {
|
||||
return testSyntaxReader(bytes.NewBufferString(s), traceLevel)
|
||||
}
|
||||
|
||||
func testSyntax(file string, traceLevel int) (*Syntax, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
return testSyntaxReader(f, traceLevel)
|
||||
}
|
||||
|
||||
func checkNodesPosition(t *testing.T, left, right []*Node, position bool) {
|
||||
if len(left) != len(right) {
|
||||
t.Error("length doesn't match", len(left), len(right))
|
||||
return
|
||||
}
|
||||
|
||||
for len(left) > 0 {
|
||||
checkNodePosition(t, left[0], right[0], position)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
left, right = left[1:], right[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func checkNodePosition(t *testing.T, left, right *Node, position bool) {
|
||||
if (left == nil) != (right == nil) {
|
||||
t.Error("nil reference doesn't match", left == nil, right == nil)
|
||||
return
|
||||
}
|
||||
|
||||
if left == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if left.Name != right.Name {
|
||||
t.Error("name doesn't match", left.Name, right.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if position && left.from != right.from {
|
||||
t.Error("from doesn't match", left.Name, left.from, right.from)
|
||||
return
|
||||
}
|
||||
|
||||
if position && left.to != right.to {
|
||||
t.Error("to doesn't match", left.Name, left.to, right.to)
|
||||
return
|
||||
}
|
||||
|
||||
if len(left.Nodes) != len(right.Nodes) {
|
||||
t.Error("length doesn't match", left.Name, len(left.Nodes), len(right.Nodes))
|
||||
t.Log(left)
|
||||
t.Log(right)
|
||||
for {
|
||||
if len(left.Nodes) > 0 {
|
||||
t.Log("<", left.Nodes[0])
|
||||
left.Nodes = left.Nodes[1:]
|
||||
}
|
||||
|
||||
if len(right.Nodes) > 0 {
|
||||
t.Log(">", right.Nodes[0])
|
||||
right.Nodes = right.Nodes[1:]
|
||||
}
|
||||
|
||||
if len(left.Nodes) == 0 && len(right.Nodes) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
checkNodesPosition(t, left.Nodes, right.Nodes, position)
|
||||
}
|
||||
|
||||
func checkNodes(t *testing.T, left, right []*Node) {
|
||||
checkNodesPosition(t, left, right, true)
|
||||
}
|
||||
|
||||
func checkNode(t *testing.T, left, right *Node) {
|
||||
checkNodePosition(t, left, right, true)
|
||||
}
|
||||
|
||||
func checkNodesIgnorePosition(t *testing.T, left, right []*Node) {
|
||||
checkNodesPosition(t, left, right, false)
|
||||
}
|
||||
|
||||
func checkNodeIgnorePosition(t *testing.T, left, right *Node) {
|
||||
checkNodePosition(t, left, right, false)
|
||||
}
|
||||
|
||||
func testReaderTrace(t *testing.T, r io.Reader, rootName string, traceLevel int, tests []testItem) {
|
||||
s, err := testSyntaxReader(r, traceLevel)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
defer func() { t.Log("\ntotal duration", time.Since(start)) }()
|
||||
|
||||
for _, ti := range tests {
|
||||
t.Run(ti.msg, func(t *testing.T) {
|
||||
n, err := s.Parse(bytes.NewBufferString(ti.text))
|
||||
|
||||
if ti.fail && err == nil {
|
||||
t.Error("failed to fail")
|
||||
return
|
||||
} else if !ti.fail && err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if ti.fail {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(n)
|
||||
|
||||
cn := checkNode
|
||||
if ti.ignorePosition {
|
||||
cn = checkNodeIgnorePosition
|
||||
}
|
||||
|
||||
if ti.node != nil {
|
||||
cn(t, n, ti.node)
|
||||
} else {
|
||||
cn(t, n, &Node{
|
||||
Name: rootName,
|
||||
from: 0,
|
||||
to: len(ti.text),
|
||||
Nodes: ti.nodes,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testStringTrace(t *testing.T, s string, traceLevel int, tests []testItem) {
|
||||
testReaderTrace(t, bytes.NewBufferString(s), "", traceLevel, tests)
|
||||
}
|
||||
|
||||
func testString(t *testing.T, s string, tests []testItem) {
|
||||
testStringTrace(t, s, 0, tests)
|
||||
}
|
||||
|
||||
func testTrace(t *testing.T, file, rootName string, traceLevel int, tests []testItem) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
testReaderTrace(t, f, rootName, traceLevel, tests)
|
||||
}
|
||||
|
||||
func test(t *testing.T, file, rootName string, tests []testItem) {
|
||||
testTrace(t, file, rootName, 0, tests)
|
||||
}
|
||||
|
||||
func TestRecursion(t *testing.T) {
|
||||
testString(
|
||||
t,
|
||||
`A = "a" | A "a"`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, right, left, commit",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" | "a" A`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, right, right, commit",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" A | "a"`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, left, right, commit",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = A "a" | "a"`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, left, left, commit",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A':alias = "a" | A' "a"; A = A'`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, right, left, alias",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A':alias = "a" | "a" A'; A = A'`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, right, right, alias",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A':alias = "a" A' | "a"; A = A'`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, left, right, alias",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A':alias = A' "a" | "a"; A = A'`,
|
||||
[]testItem{{
|
||||
msg: "recursion in choice, left, left, alias",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestSequence(t *testing.T) {
|
||||
testString(
|
||||
t,
|
||||
`AB = "a" | "a"? "a"? "b" "b"`,
|
||||
[]testItem{{
|
||||
msg: "sequence with optional items",
|
||||
text: "abb",
|
||||
node: &Node{
|
||||
Name: "AB",
|
||||
to: 3,
|
||||
},
|
||||
}, {
|
||||
msg: "sequence with optional items, none",
|
||||
text: "bb",
|
||||
node: &Node{
|
||||
Name: "AB",
|
||||
to: 2,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" | (A?)*`,
|
||||
[]testItem{{
|
||||
msg: "sequence in choice with redundant quantifier",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
Nodes: []*Node{{
|
||||
Name: "A",
|
||||
}, {
|
||||
Name: "A",
|
||||
}, {
|
||||
Name: "A",
|
||||
}},
|
||||
},
|
||||
ignorePosition: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = ("a"*)*`,
|
||||
[]testItem{{
|
||||
msg: "sequence with redundant quantifier",
|
||||
text: "aaa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestQuantifiers(t *testing.T) {
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{0} "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero, fail",
|
||||
text: "aba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{1} "a"`,
|
||||
[]testItem{{
|
||||
msg: "one, missing",
|
||||
text: "aa",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "one",
|
||||
text: "aba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}, {
|
||||
msg: "one, too much",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{3} "a"`,
|
||||
[]testItem{{
|
||||
msg: "three, missing",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "three",
|
||||
text: "abbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 5,
|
||||
},
|
||||
}, {
|
||||
msg: "three, too much",
|
||||
text: "abbbba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{0,1} "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero or one explicit, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit",
|
||||
text: "aba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit, too much",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{,1} "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero or one explicit, omit zero, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit, omit zero",
|
||||
text: "aba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit, omit zero, too much",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"? "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero or one explicit, shortcut, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit, shortcut",
|
||||
text: "aba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 3,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or one explicit, shortcut, too much",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{0,3} "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero or three, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three",
|
||||
text: "abbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 5,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three, too much",
|
||||
text: "abbbba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{,3} "a"`,
|
||||
[]testItem{{
|
||||
msg: "zero or three, omit zero, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three, omit zero",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three, omit zero",
|
||||
text: "abbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 5,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or three, omit zero, too much",
|
||||
text: "abbbba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{1,3} "a"`,
|
||||
[]testItem{{
|
||||
msg: "one or three, missing",
|
||||
text: "aa",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "one or three",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}, {
|
||||
msg: "one or three",
|
||||
text: "abbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 5,
|
||||
},
|
||||
}, {
|
||||
msg: "one or three, too much",
|
||||
text: "abbbba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testString(
|
||||
t,
|
||||
`A = "a" "b"{3,5} "a"`,
|
||||
[]testItem{{
|
||||
msg: "three or five, missing",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "three or five",
|
||||
text: "abbbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 6,
|
||||
},
|
||||
}, {
|
||||
msg: "three or five",
|
||||
text: "abbbbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 7,
|
||||
},
|
||||
}, {
|
||||
msg: "three or five, too much",
|
||||
text: "abbbbbba",
|
||||
fail: true,
|
||||
}},
|
||||
)
|
||||
|
||||
testStringTrace(
|
||||
t,
|
||||
`A = "a" "b"{0,} "a"`,
|
||||
1,
|
||||
[]testItem{{
|
||||
msg: "zero or more, explicit, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or more, explicit",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testStringTrace(
|
||||
t,
|
||||
`A = "a" "b"* "a"`,
|
||||
1,
|
||||
[]testItem{{
|
||||
msg: "zero or more, shortcut, missing",
|
||||
text: "aa",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 2,
|
||||
},
|
||||
}, {
|
||||
msg: "zero or more, shortcut",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testStringTrace(
|
||||
t,
|
||||
`A = "a" "b"{1,} "a"`,
|
||||
1,
|
||||
[]testItem{{
|
||||
msg: "one or more, explicit, missing",
|
||||
text: "aa",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "one or more, explicit",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testStringTrace(
|
||||
t,
|
||||
`A = "a" "b"+ "a"`,
|
||||
1,
|
||||
[]testItem{{
|
||||
msg: "one or more, shortcut, missing",
|
||||
text: "aa",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "one or more, shortcut",
|
||||
text: "abba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 4,
|
||||
},
|
||||
}},
|
||||
)
|
||||
|
||||
testStringTrace(
|
||||
t,
|
||||
`A = "a" "b"{3,} "a"`,
|
||||
1,
|
||||
[]testItem{{
|
||||
msg: "three or more, explicit, missing",
|
||||
text: "abba",
|
||||
fail: true,
|
||||
}, {
|
||||
msg: "three or more, explicit",
|
||||
text: "abbbba",
|
||||
node: &Node{
|
||||
Name: "A",
|
||||
to: 6,
|
||||
},
|
||||
}},
|
||||
)
|
||||
}
|
89
node.go
Normal file
89
node.go
Normal file
@ -0,0 +1,89 @@
|
||||
package parse
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
Nodes []*Node
|
||||
commitType CommitType
|
||||
from, to int
|
||||
tokens []rune
|
||||
}
|
||||
|
||||
func newNode(name string, ct CommitType, from, to int) *Node {
|
||||
return &Node{
|
||||
Name: name,
|
||||
commitType: ct,
|
||||
from: from,
|
||||
to: to,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) tokenLength() int {
|
||||
return n.to - n.from
|
||||
}
|
||||
|
||||
func (n *Node) nodeLength() int {
|
||||
return len(n.Nodes)
|
||||
}
|
||||
|
||||
func findNode(in, n *Node) {
|
||||
if n == in {
|
||||
panic(fmt.Errorf("found self in %s", in.Name))
|
||||
}
|
||||
|
||||
for _, ni := range n.Nodes {
|
||||
findNode(in, ni)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) append(p *Node) {
|
||||
findNode(n, p)
|
||||
n.Nodes = append(n.Nodes, p)
|
||||
// TODO: check rather if n.from <= p.from??? or panic if less? or check rather node length and commit
|
||||
// happens in the end anyway?
|
||||
if n.from == 0 && n.to == 0 {
|
||||
n.from = p.from
|
||||
}
|
||||
|
||||
n.to = p.to
|
||||
}
|
||||
|
||||
func (n *Node) clear() {
|
||||
n.from = 0
|
||||
n.to = 0
|
||||
n.Nodes = nil
|
||||
}
|
||||
|
||||
func (n *Node) applyTokens(t []rune) {
|
||||
n.tokens = t
|
||||
for _, ni := range n.Nodes {
|
||||
ni.applyTokens(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) commit() {
|
||||
var nodes []*Node
|
||||
for _, ni := range n.Nodes {
|
||||
ni.commit()
|
||||
if ni.commitType&Alias != 0 {
|
||||
nodes = append(nodes, ni.Nodes...)
|
||||
} else {
|
||||
nodes = append(nodes, ni)
|
||||
}
|
||||
}
|
||||
|
||||
n.Nodes = nodes
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
if n.from >= len(n.tokens) || n.to > len(n.tokens) {
|
||||
return n.Name + ":incomplete"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d:%d:%s", n.Name, n.from, n.to, n.Text())
|
||||
}
|
||||
|
||||
func (n *Node) Text() string {
|
||||
return string(n.tokens[n.from:n.to])
|
||||
}
|
69
parse.go
Normal file
69
parse.go
Normal file
@ -0,0 +1,69 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type definition interface {
|
||||
nodeName() string
|
||||
parser(*registry, []string) (parser, error)
|
||||
commitType() CommitType
|
||||
}
|
||||
|
||||
type parser interface {
|
||||
nodeName() string
|
||||
setIncludedBy(parser, []string)
|
||||
cacheIncluded(*context, *Node)
|
||||
parse(Trace, *context)
|
||||
}
|
||||
|
||||
var errCannotIncludeParsers = errors.New("cannot include parsers")
|
||||
|
||||
func parserNotFound(name string) error {
|
||||
return fmt.Errorf("parser not found: %s", name)
|
||||
}
|
||||
|
||||
func stringsContain(ss []string, s string) bool {
|
||||
for _, si := range ss {
|
||||
if si == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func copyIncludes(to, from map[string]CommitType) {
|
||||
if from == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for name, ct := range from {
|
||||
to[name] = ct
|
||||
}
|
||||
}
|
||||
|
||||
func mergeIncludes(left, right map[string]CommitType) map[string]CommitType {
|
||||
m := make(map[string]CommitType)
|
||||
copyIncludes(m, left)
|
||||
copyIncludes(m, right)
|
||||
return m
|
||||
}
|
||||
|
||||
func parse(t Trace, p parser, c *context) (*Node, error) {
|
||||
p.parse(t, c)
|
||||
if c.readErr != nil {
|
||||
return nil, c.readErr
|
||||
}
|
||||
|
||||
if !c.match {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
if err := c.finalize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.node, nil
|
||||
}
|
172
quantifier.go
Normal file
172
quantifier.go
Normal file
@ -0,0 +1,172 @@
|
||||
package parse
|
||||
|
||||
type quantifierDefinition struct {
|
||||
name string
|
||||
commit CommitType
|
||||
min, max int
|
||||
item string
|
||||
}
|
||||
|
||||
type quantifierParser struct {
|
||||
name string
|
||||
commit CommitType
|
||||
min, max int
|
||||
item parser
|
||||
includedBy []parser
|
||||
}
|
||||
|
||||
func newQuantifier(name string, ct CommitType, item string, min, max int) *quantifierDefinition {
|
||||
return &quantifierDefinition{
|
||||
name: name,
|
||||
commit: ct,
|
||||
min: min,
|
||||
max: max,
|
||||
item: item,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *quantifierDefinition) nodeName() string { return d.name }
|
||||
|
||||
func (d *quantifierDefinition) parser(r *registry, path []string) (parser, error) {
|
||||
if stringsContain(path, d.name) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
p, ok := r.parser(d.name)
|
||||
if ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
qp := &quantifierParser{
|
||||
name: d.name,
|
||||
commit: d.commit,
|
||||
min: d.min,
|
||||
max: d.max,
|
||||
}
|
||||
|
||||
r.setParser(qp)
|
||||
|
||||
item, ok := r.parser(d.item)
|
||||
if !ok {
|
||||
itemDefinition, ok := r.definition(d.item)
|
||||
if !ok {
|
||||
return nil, parserNotFound(d.item)
|
||||
}
|
||||
|
||||
var err error
|
||||
item, err = itemDefinition.parser(r, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
qp.item = item
|
||||
return qp, nil
|
||||
}
|
||||
|
||||
func (d *quantifierDefinition) commitType() CommitType { return d.commit }
|
||||
func (p *quantifierParser) nodeName() string { return p.name }
|
||||
|
||||
// TODO: merge the quantifier into the sequence
|
||||
// DOC: sequences are hungry and are not revisited, a*a cannot match anything.
|
||||
// DOC: how to match a tailing a? (..)*a | .(..)*a
|
||||
|
||||
func (p *quantifierParser) setIncludedBy(i parser, path []string) {
|
||||
if stringsContain(path, p.name) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
p.includedBy = append(p.includedBy, i)
|
||||
}
|
||||
|
||||
func (p *quantifierParser) cacheIncluded(*context, *Node) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
func (p *quantifierParser) parse(t Trace, c *context) {
|
||||
t = t.Extend(p.name)
|
||||
t.Out1("parsing quantifier", c.offset)
|
||||
|
||||
if p.commit&Documentation != 0 {
|
||||
t.Out1("fail, doc")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
if c.excluded(c.offset, p.name) {
|
||||
t.Out1("excluded")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
c.exclude(c.offset, p.name)
|
||||
defer c.include(c.offset, p.name)
|
||||
|
||||
node := newNode(p.name, p.commit, c.offset, c.offset)
|
||||
|
||||
// this way of checking the cache definitely needs the testing of the russ cox form
|
||||
for {
|
||||
if p.max >= 0 && node.nodeLength() == p.max {
|
||||
t.Out1("success, max reached")
|
||||
c.cache.set(node.from, p.name, node)
|
||||
for _, i := range p.includedBy {
|
||||
i.cacheIncluded(c, node)
|
||||
}
|
||||
|
||||
c.success(node)
|
||||
return
|
||||
}
|
||||
|
||||
t.Out2("next quantifier item")
|
||||
|
||||
// n, m, ok := c.cache.get(c.offset, p.item.nodeName())
|
||||
m, ok := c.fromCache(p.item.nodeName())
|
||||
if ok {
|
||||
t.Out1("quantifier item found in cache, match:", m, c.offset, c.node.tokenLength())
|
||||
if m {
|
||||
node.append(c.node)
|
||||
if c.node.tokenLength() > 0 {
|
||||
t.Out2("taking next after cached found")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if node.nodeLength() >= p.min {
|
||||
t.Out1("success, no more match")
|
||||
c.cache.set(node.from, p.name, node)
|
||||
for _, i := range p.includedBy {
|
||||
i.cacheIncluded(c, node)
|
||||
}
|
||||
|
||||
c.success(node)
|
||||
} else {
|
||||
t.Out1("fail, min not reached")
|
||||
c.cache.set(node.from, p.name, nil)
|
||||
c.fail(node.from)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.item.parse(t, c)
|
||||
if !c.match || c.node.tokenLength() == 0 {
|
||||
if node.nodeLength() >= p.min {
|
||||
t.Out1("success, no more match")
|
||||
c.cache.set(node.from, p.name, node)
|
||||
for _, i := range p.includedBy {
|
||||
i.cacheIncluded(c, node)
|
||||
}
|
||||
|
||||
c.success(node)
|
||||
} else {
|
||||
t.Out1("fail, min not reached")
|
||||
c.cache.set(node.from, p.name, nil)
|
||||
c.fail(node.from)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
node.append(c.node)
|
||||
}
|
||||
}
|
36
registry.go
Normal file
36
registry.go
Normal file
@ -0,0 +1,36 @@
|
||||
package parse
|
||||
|
||||
type registry struct {
|
||||
definitions map[string]definition
|
||||
parsers map[string]parser
|
||||
}
|
||||
|
||||
func newRegistry() *registry {
|
||||
return ®istry{
|
||||
definitions: make(map[string]definition),
|
||||
parsers: make(map[string]parser),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) definition(name string) (definition, bool) {
|
||||
d, ok := r.definitions[name]
|
||||
return d, ok
|
||||
}
|
||||
|
||||
func (r *registry) parser(name string) (parser, bool) {
|
||||
p, ok := r.parsers[name]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (r *registry) setDefinition(d definition) error {
|
||||
if _, ok := r.definitions[d.nodeName()]; ok {
|
||||
return duplicateDefinition(d.nodeName())
|
||||
}
|
||||
|
||||
r.definitions[d.nodeName()] = d
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) setParser(p parser) {
|
||||
r.parsers[p.nodeName()] = p
|
||||
}
|
14
scheme.p
Normal file
14
scheme.p
Normal file
@ -0,0 +1,14 @@
|
||||
// TODO: comment
|
||||
|
||||
ws:alias = [ \b\f\n\r\t\v];
|
||||
comment:alias = ";" [^\n]*;
|
||||
wsc:alias = ws | comment;
|
||||
number = "-"? ("0" | [1-9][0-9]*) ("." [0-9]+)? ([eE] [+\-]? [0-9]+)?;
|
||||
string = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
symbol = ([^\\ \n\t\b\f\r\v\"()\[\]#] | "\\" .)+;
|
||||
list-form:alias = "(" wsc* (expression wsc*)* ")"
|
||||
| "[" wsc* (expression wsc*)* "]";
|
||||
list = list-form;
|
||||
vector = "#" list-form;
|
||||
expression:alias = number | string | symbol | list;
|
||||
scheme = wsc* (expression wsc*)*;
|
84
scheme_test.go
Normal file
84
scheme_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package parse
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestScheme(t *testing.T) {
|
||||
test(t, "scheme.p", "scheme", []testItem{{
|
||||
msg: "empty",
|
||||
}, {
|
||||
msg: "a function",
|
||||
text: `
|
||||
(define (foo a b c)
|
||||
(let ([bar (+ a b c)]
|
||||
[baz (- a b c)])
|
||||
(* bar baz)))
|
||||
`,
|
||||
nodes: []*Node{{
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}})
|
||||
}
|
187
sequence.go
Normal file
187
sequence.go
Normal file
@ -0,0 +1,187 @@
|
||||
package parse
|
||||
|
||||
type sequenceDefinition struct {
|
||||
name string
|
||||
commit CommitType
|
||||
items []string
|
||||
}
|
||||
|
||||
type sequenceParser struct {
|
||||
name string
|
||||
commit CommitType
|
||||
items []parser
|
||||
including []parser
|
||||
}
|
||||
|
||||
func newSequence(name string, ct CommitType, items []string) *sequenceDefinition {
|
||||
return &sequenceDefinition{
|
||||
name: name,
|
||||
commit: ct,
|
||||
items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sequenceDefinition) nodeName() string { return d.name }
|
||||
|
||||
func (d *sequenceDefinition) parser(r *registry, path []string) (parser, error) {
|
||||
if stringsContain(path, d.name) {
|
||||
panic(errCannotIncludeParsers)
|
||||
}
|
||||
|
||||
p, ok := r.parser(d.name)
|
||||
if ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
sp := &sequenceParser{
|
||||
name: d.name,
|
||||
commit: d.commit,
|
||||
}
|
||||
|
||||
r.setParser(sp)
|
||||
|
||||
var items []parser
|
||||
path = append(path, d.name)
|
||||
for _, name := range d.items {
|
||||
item, ok := r.parser(name)
|
||||
if ok {
|
||||
items = append(items, item)
|
||||
continue
|
||||
}
|
||||
|
||||
itemDefinition, ok := r.definition(name)
|
||||
if !ok {
|
||||
return nil, parserNotFound(name)
|
||||
}
|
||||
|
||||
item, err := itemDefinition.parser(r, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
// for single items, acts like a choice
|
||||
if len(items) == 1 {
|
||||
items[0].setIncludedBy(sp, path)
|
||||
}
|
||||
|
||||
sp.items = items
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
func (d *sequenceDefinition) commitType() CommitType {
|
||||
return d.commit
|
||||
}
|
||||
|
||||
func (p *sequenceParser) nodeName() string { return p.name }
|
||||
|
||||
func (p *sequenceParser) setIncludedBy(i parser, path []string) {
|
||||
if stringsContain(path, p.name) {
|
||||
return
|
||||
}
|
||||
|
||||
p.including = append(p.including, i)
|
||||
}
|
||||
|
||||
func (p *sequenceParser) cacheIncluded(c *context, n *Node) {
|
||||
if !c.excluded(n.from, p.name) {
|
||||
return
|
||||
}
|
||||
|
||||
nc := newNode(p.name, p.commit, n.from, n.to)
|
||||
nc.append(n)
|
||||
c.cache.set(nc.from, p.name, nc)
|
||||
|
||||
// maybe it is enough to cache only those that are on the path
|
||||
for _, i := range p.including {
|
||||
i.cacheIncluded(c, nc)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
should be possible to parse:
|
||||
|
||||
a = "0"
|
||||
b = "1"
|
||||
c = a* e b
|
||||
d = a | c
|
||||
e = b | d
|
||||
|
||||
input: 111
|
||||
*/
|
||||
|
||||
func (p *sequenceParser) parse(t Trace, c *context) {
|
||||
t = t.Extend(p.name)
|
||||
t.Out1("parsing sequence", c.offset)
|
||||
|
||||
if p.commit&Documentation != 0 {
|
||||
t.Out1("fail, doc")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: maybe we can check the cache here? no because that would exclude the continuations
|
||||
|
||||
if c.excluded(c.offset, p.name) {
|
||||
t.Out1("excluded")
|
||||
c.fail(c.offset)
|
||||
return
|
||||
}
|
||||
|
||||
c.exclude(c.offset, p.name)
|
||||
defer c.include(c.offset, p.name)
|
||||
|
||||
items := p.items
|
||||
node := newNode(p.name, p.commit, c.offset, c.offset)
|
||||
|
||||
for len(items) > 0 {
|
||||
t.Out2("next sequence item")
|
||||
// n, m, ok := c.cache.get(c.offset, items[0].nodeName())
|
||||
m, ok := c.fromCache(items[0].nodeName())
|
||||
if ok {
|
||||
t.Out1("sequence item found in cache, match:", m, items[0].nodeName(), c.offset)
|
||||
if m {
|
||||
t.Out2("sequence item from cache:", c.node.Name, len(c.node.Nodes), c.node.from)
|
||||
node.append(c.node)
|
||||
items = items[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
c.cache.set(node.from, p.name, nil)
|
||||
c.fail(node.from)
|
||||
return
|
||||
}
|
||||
|
||||
items[0].parse(t, c)
|
||||
items = items[1:]
|
||||
|
||||
if !c.match {
|
||||
t.Out1("fail, item failed")
|
||||
c.cache.set(node.from, p.name, nil)
|
||||
c.fail(node.from)
|
||||
return
|
||||
}
|
||||
|
||||
if c.node.tokenLength() > 0 {
|
||||
t.Out2("appending sequence item", c.node.Name, len(c.node.Nodes))
|
||||
node.append(c.node)
|
||||
}
|
||||
}
|
||||
|
||||
t.Out1("success, items parsed")
|
||||
t.Out2("nodes", node.nodeLength())
|
||||
if node.Name == "group" {
|
||||
t.Out2("caching group", node.from, node.Nodes[2].Name, node.Nodes[2].nodeLength())
|
||||
}
|
||||
|
||||
// is this cached item ever taken?
|
||||
c.cache.set(node.from, p.name, node)
|
||||
for _, i := range p.including {
|
||||
i.cacheIncluded(c, node)
|
||||
}
|
||||
|
||||
t.Out2("caching sequence and included by done")
|
||||
c.success(node)
|
||||
}
|
9
sexpr.p
Normal file
9
sexpr.p
Normal file
@ -0,0 +1,9 @@
|
||||
ws:alias = [ \b\f\n\r\t\v];
|
||||
comment:alias = ";" [^\n]*;
|
||||
wsc:alias = ws | comment;
|
||||
number = "-"? ("0" | [1-9][0-9]*) ("." [0-9]+)? ([eE] [+\-]? [0-9]+)?;
|
||||
string = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
symbol = ([^\\ \n\t\b\f\r\v\"()] | "\\" .)+;
|
||||
list = "(" wsc* (expression wsc*)* ")";
|
||||
expression:alias = number | string | symbol | list;
|
||||
s-expression = expression;
|
71
sexpr_test.go
Normal file
71
sexpr_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package parse
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSExpr(t *testing.T) {
|
||||
test(t, "sexpr.p", "s-expression", []testItem{{
|
||||
msg: "number",
|
||||
text: "42",
|
||||
nodes: []*Node{{
|
||||
Name: "number",
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "string",
|
||||
text: "\"foo\"",
|
||||
nodes: []*Node{{
|
||||
Name: "string",
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "symbol",
|
||||
text: "foo",
|
||||
nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "nil",
|
||||
text: "()",
|
||||
nodes: []*Node{{
|
||||
Name: "list",
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "list",
|
||||
text: "(foo bar baz)",
|
||||
nodes: []*Node{{
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}, {
|
||||
msg: "embedded list",
|
||||
text: "(foo (bar (baz)) qux)",
|
||||
nodes: []*Node{{
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}, {
|
||||
Name: "list",
|
||||
Nodes: []*Node{{
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
Name: "symbol",
|
||||
}},
|
||||
}},
|
||||
ignorePosition: true,
|
||||
}})
|
||||
}
|
158
syntax.go
Normal file
158
syntax.go
Normal file
@ -0,0 +1,158 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type CommitType int
|
||||
|
||||
const (
|
||||
None CommitType = 0
|
||||
Alias CommitType = 1 << iota
|
||||
Documentation
|
||||
Root
|
||||
)
|
||||
|
||||
type Syntax struct {
|
||||
trace Trace
|
||||
registry *registry
|
||||
initialized bool
|
||||
initFailed bool
|
||||
rootSet bool
|
||||
root definition
|
||||
parser parser
|
||||
}
|
||||
|
||||
var (
|
||||
ErrSyntaxInitialized = errors.New("syntax initialized")
|
||||
ErrInitFailed = errors.New("init failed")
|
||||
ErrNoParsersDefined = errors.New("no parsers defined")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrInvalidCharacter = errors.New("invalid character") // two use cases: utf8 and boot
|
||||
ErrUnexpectedCharacter = errors.New("unexpected character")
|
||||
ErrInvalidSyntax = errors.New("invalid syntax")
|
||||
ErrRootAlias = errors.New("root node cannot be an alias")
|
||||
)
|
||||
|
||||
func duplicateDefinition(name string) error {
|
||||
return fmt.Errorf("duplicate definition: %s", name)
|
||||
}
|
||||
|
||||
func NewSyntax(t Trace) *Syntax {
|
||||
if t == nil {
|
||||
t = NewTrace(0)
|
||||
}
|
||||
|
||||
return &Syntax{
|
||||
trace: t,
|
||||
registry: newRegistry(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Syntax) register(d definition) error {
|
||||
if s.initialized {
|
||||
return ErrSyntaxInitialized
|
||||
}
|
||||
|
||||
if d.commitType()&Root != 0 {
|
||||
s.root = d
|
||||
s.rootSet = true
|
||||
} else if !s.rootSet {
|
||||
s.root = d
|
||||
}
|
||||
|
||||
return s.registry.setDefinition(d)
|
||||
}
|
||||
|
||||
func (s *Syntax) AnyChar(name string, ct CommitType) error {
|
||||
return s.register(newChar(name, ct, true, false, nil, nil))
|
||||
}
|
||||
|
||||
func (s *Syntax) Class(name string, ct CommitType, not bool, chars []rune, ranges [][]rune) error {
|
||||
return s.register(newChar(name, ct, false, not, chars, ranges))
|
||||
}
|
||||
|
||||
func childName(name string, childIndex int) string {
|
||||
return fmt.Sprintf("%s:%d", name, childIndex)
|
||||
}
|
||||
|
||||
func (s *Syntax) CharSequence(name string, ct CommitType, chars []rune) error {
|
||||
var refs []string
|
||||
for i, ci := range chars {
|
||||
ref := childName(name, i)
|
||||
refs = append(refs, ref)
|
||||
if err := s.register(newChar(ref, Alias, false, false, []rune{ci}, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.Sequence(name, ct, refs...)
|
||||
}
|
||||
|
||||
func (s *Syntax) Quantifier(name string, ct CommitType, item string, min, max int) error {
|
||||
return s.register(newQuantifier(name, ct, item, min, max))
|
||||
}
|
||||
|
||||
func (s *Syntax) Sequence(name string, ct CommitType, items ...string) error {
|
||||
return s.register(newSequence(name, ct, items))
|
||||
}
|
||||
|
||||
func (s *Syntax) Choice(name string, ct CommitType, elements ...string) error {
|
||||
return s.register(newChoice(name, ct, elements))
|
||||
}
|
||||
|
||||
func (s *Syntax) Read(r io.Reader) error {
|
||||
if s.initialized {
|
||||
return ErrSyntaxInitialized
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Syntax) Init() error {
|
||||
if s.initFailed {
|
||||
return ErrInitFailed
|
||||
}
|
||||
|
||||
if s.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.root == nil {
|
||||
return ErrNoParsersDefined
|
||||
}
|
||||
|
||||
if s.root.commitType()&Alias != 0 {
|
||||
return ErrRootAlias
|
||||
}
|
||||
|
||||
var err error
|
||||
s.parser, err = s.root.parser(s.registry, nil)
|
||||
if err != nil {
|
||||
s.initFailed = true
|
||||
return err
|
||||
}
|
||||
|
||||
s.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Syntax) Generate(w io.Writer) error {
|
||||
if err := s.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Syntax) Parse(r io.Reader) (*Node, error) {
|
||||
if err := s.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := newContext(bufio.NewReader(r))
|
||||
return parse(s.trace, s.parser, c)
|
||||
}
|
78
syntax.p
Normal file
78
syntax.p
Normal file
@ -0,0 +1,78 @@
|
||||
ws:alias = " " | "\t" | "\n" | "\b" | "\f" | "\r" | "\v";
|
||||
wsc:alias = ws | comment;
|
||||
|
||||
block-comment:alias = "/*" ("*" [^/] | [^*])* "*/";
|
||||
line-comment:alias = "//" [^\n]*;
|
||||
comment-segment:alias = line-comment | block-comment;
|
||||
ws-no-nl:alias = " " | "\t" | "\b" | "\f" | "\r" | "\v";
|
||||
comment = comment-segment (ws-no-nl* "\n"? ws-no-nl* comment-segment)*;
|
||||
|
||||
any-char = "."; // equivalent to [^]
|
||||
|
||||
// TODO: document matching terminal: []
|
||||
|
||||
// TODO: handle char class equivalences
|
||||
|
||||
// TODO: enable streaming
|
||||
|
||||
// TODO: set route function in generated code?
|
||||
|
||||
// caution: newline is accepted
|
||||
class-not = "^";
|
||||
class-char = [^\\\[\]\^\-] | "\\" .;
|
||||
char-range = class-char "-" class-char;
|
||||
char-class = "[" class-not? (class-char | char-range)* "]";
|
||||
|
||||
// caution: newline is accepted
|
||||
sequence-char = [^\\"] | "\\" .;
|
||||
char-sequence = "\"" sequence-char* "\"";
|
||||
|
||||
// TODO: this can be mixed up with sequence. Is it fine? fix this, see mml symbol
|
||||
terminal:alias = any-char | char-class | char-sequence;
|
||||
|
||||
symbol = [^\\ \n\t\b\f\r\v/.\[\]\"{}\^+*?|():=;]+;
|
||||
|
||||
group:alias = "(" wsc* expression wsc* ")";
|
||||
|
||||
number:alias = [0-9]+;
|
||||
count = number;
|
||||
count-quantifier = "{" wsc* count wsc* "}";
|
||||
range-from = number;
|
||||
range-to = number;
|
||||
range-quantifier = "{" wsc* range-from? wsc* "," wsc* range-to? wsc* "}";
|
||||
one-or-more = "+";
|
||||
zero-or-more = "*";
|
||||
zero-or-one = "?";
|
||||
quantity:alias = count-quantifier
|
||||
| range-quantifier
|
||||
| one-or-more
|
||||
| zero-or-more
|
||||
| zero-or-one;
|
||||
|
||||
quantifier = (terminal | symbol | group) wsc* quantity;
|
||||
|
||||
item:alias = terminal | symbol | group | quantifier;
|
||||
sequence = item (wsc* item)+;
|
||||
|
||||
element:alias = terminal | symbol | group | quantifier | sequence;
|
||||
|
||||
// DOC: once cached, doesn't try again, even in a new context, therefore the order may matter
|
||||
choice = element (wsc* "|" wsc* element)+;
|
||||
|
||||
// DOC: not having 'not' needs some tricks sometimes
|
||||
|
||||
expression:alias = terminal
|
||||
| symbol
|
||||
| group
|
||||
| quantifier
|
||||
| sequence
|
||||
| choice;
|
||||
|
||||
alias = "alias";
|
||||
doc = "doc";
|
||||
root = "root";
|
||||
flag:alias = alias | doc | root;
|
||||
definition = symbol (":" flag)* wsc* "=" wsc* expression;
|
||||
|
||||
definitions:alias = definition (wsc* ";" (wsc | ";")* definition)*;
|
||||
syntax:root = (wsc | ";")* definitions? (wsc | ";")*;
|
72
trace.go
Normal file
72
trace.go
Normal file
@ -0,0 +1,72 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Trace interface {
|
||||
Out(...interface{})
|
||||
Out1(...interface{})
|
||||
Out2(...interface{})
|
||||
Out3(...interface{})
|
||||
Extend(string) Trace
|
||||
}
|
||||
|
||||
type DefaultTrace struct {
|
||||
level int
|
||||
path string
|
||||
}
|
||||
|
||||
type NopTrace struct{}
|
||||
|
||||
func NewTrace(level int) *DefaultTrace {
|
||||
return &DefaultTrace{
|
||||
level: level,
|
||||
path: "/",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) printlnLevel(l int, a ...interface{}) {
|
||||
if l > t.level {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, append([]interface{}{t.path}, a...)...)
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) Out(a ...interface{}) {
|
||||
t.printlnLevel(0, a...)
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) Out1(a ...interface{}) {
|
||||
t.printlnLevel(1, a...)
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) Out2(a ...interface{}) {
|
||||
t.printlnLevel(2, a...)
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) Out3(a ...interface{}) {
|
||||
t.printlnLevel(3, a...)
|
||||
}
|
||||
|
||||
func (t *DefaultTrace) Extend(name string) Trace {
|
||||
var p string
|
||||
if t.path == "/" {
|
||||
p = t.path + name
|
||||
} else {
|
||||
p = t.path + "/" + name
|
||||
}
|
||||
|
||||
return &DefaultTrace{
|
||||
level: t.level,
|
||||
path: p,
|
||||
}
|
||||
}
|
||||
|
||||
func (NopTrace) Out(...interface{}) {}
|
||||
func (NopTrace) Out1(...interface{}) {}
|
||||
func (NopTrace) Out2(...interface{}) {}
|
||||
func (NopTrace) Out3(...interface{}) {}
|
||||
func (t NopTrace) Extend(string) Trace { return t }
|
Loading…
Reference in New Issue
Block a user