Skip to content

Commit

Permalink
Compile regexp during compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Aug 5, 2018
1 parent 0c841b9 commit 3578406
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 23 deletions.
38 changes: 28 additions & 10 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,6 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) {
}
return !ok, nil

case "matches":
if isText(left) && isText(right) {
matched, err := regexp.MatchString(toText(right), toText(left))
if err != nil {
return nil, err
}
return matched, nil
}
return nil, fmt.Errorf("operator matches not defined on (%T, %T)", left, right)

case "~":
if isText(left) && isText(right) {
return toText(left) + toText(right), nil
Expand Down Expand Up @@ -255,6 +245,34 @@ func makeRange(min, max int64) ([]float64, error) {
return a, nil
}

func (n matchesNode) eval(env interface{}) (interface{}, error) {
left, err := Run(n.left, env)
if err != nil {
return nil, err
}

if n.r != nil {
if isText(left) {
return n.r.MatchString(toText(left)), nil
}
}

right, err := Run(n.right, env)
if err != nil {
return nil, err
}

if isText(left) && isText(right) {
matched, err := regexp.MatchString(toText(right), toText(left))
if err != nil {
return nil, err
}
return matched, nil
}

return nil, fmt.Errorf("operator matches doesn't defined on (%T, %T): %v", left, right, n)
}

func (n propertyNode) eval(env interface{}) (interface{}, error) {
v, err := Run(n.node, env)
if err != nil {
Expand Down
27 changes: 26 additions & 1 deletion eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ var evalTests = []evalTest{
nil,
true,
},
{
`"seafood" matches "sea" ~ "food"`,
nil,
true,
},
{
`not ("seafood" matches "[0-9]+") ? "a" : "b"`,
nil,
Expand Down Expand Up @@ -317,7 +322,27 @@ var evalErrorTests = []evalErrorTest{
{
`"seafood" matches "a(b"`,
nil,
`error parsing regexp:`,
"error parsing regexp: missing closing ): `a(b`",
},
{
`"seafood" matches "a" ~ ")b"`,
nil,
"error parsing regexp: unexpected ): `a)b`",
},
{
`1 matches "1" ~ "2"`,
nil,
"operator matches doesn't defined on (float64, string): (1 matches (\"1\" ~ \"2\"))",
},
{
`1 matches "1"`,
nil,
"operator matches doesn't defined on (float64, string): (1 matches \"1\")",
},
{
`"1" matches 1`,
nil,
"operator matches doesn't defined on (string, float64): (\"1\" matches 1)",
},
{
`0 ? 1 : 2`,
Expand Down
8 changes: 8 additions & 0 deletions node.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package expr

import "regexp"

// Node represents items of abstract syntax tree.
type Node interface{}

Expand Down Expand Up @@ -36,6 +38,12 @@ type binaryNode struct {
right Node
}

type matchesNode struct {
r *regexp.Regexp
left Node
right Node
}

type propertyNode struct {
node Node
property Node
Expand Down
14 changes: 13 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package expr

import (
"fmt"
"regexp"
"strconv"
"unicode/utf8"
)
Expand Down Expand Up @@ -179,7 +180,18 @@ func (p *parser) parseExpression(precedence int) (Node, error) {
}
}

node = binaryNode{operator: token.value, left: node, right: expr}
if token.is(operator, "matches") {
var r *regexp.Regexp
if s, ok := expr.(textNode); ok {
r, err = regexp.Compile(s.value)
if err != nil {
return nil, p.errorf("%v", err)
}
}
node = matchesNode{r: r, left: node, right: expr}
} else {
node = binaryNode{operator: token.value, left: node, right: expr}
}
token = p.current
continue
}
Expand Down
50 changes: 39 additions & 11 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ var parseTests = []parseTest{
"a ?: b",
conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}},
},
{
`"foo" matches "/foo/"`,
binaryNode{"matches", textNode{"foo"}, textNode{"/foo/"}},
},
{
"foo.bar().foo().baz[33]",
propertyNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}}, identifierNode{"foo"}, []Node{}}, identifierNode{"baz"}}, numberNode{33}},
Expand Down Expand Up @@ -169,6 +165,10 @@ var parseErrorTests = []parseErrorTest{
"{-}",
"a map key must be a",
},
{
"a matches 'a)(b'",
"error parsing regexp: unexpected )",
},
}

func TestParse(t *testing.T) {
Expand Down Expand Up @@ -196,14 +196,42 @@ func TestParseError(t *testing.T) {
}
}

func TestParseErrorPosition(t *testing.T) {
_, err := Parse("foo() + bar(**)")
if err == nil {
err = fmt.Errorf("<nil>")
func TestParse_matches(t *testing.T) {
node, err := Parse(`foo matches "foo"`)
if err != nil {
t.Fatal(err)
}

m, ok := node.(matchesNode)
if !ok {
t.Fatalf("expected to me matchesNode, got %T", node)
}

if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, textNode{"foo"}) {
t.Fatalf("left or right side of matches operator invalid: %#v", m)
}

if m.r == nil {
t.Fatal("regexp should be compiled")
}
}

func TestParse_matches_dynamic(t *testing.T) {
node, err := Parse(`foo matches regex`)
if err != nil {
t.Fatal(err)
}

m, ok := node.(matchesNode)
if !ok {
t.Fatalf("expected to me matchesNode, got %T", node)
}

if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, nameNode{"regex"}) {
t.Fatalf("left or right side of matches operator invalid: %#v", m)
}

expected := "unexpected token operator(**)\nfoo() + bar(**)\n------------^"
if err.Error() != expected {
t.Errorf("\ngot\n\t%+v\nexpected\n\t%v", err.Error(), expected)
if m.r != nil {
t.Fatal("regexp should not be compiled")
}
}
4 changes: 4 additions & 0 deletions print.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (n binaryNode) String() string {
return fmt.Sprintf("(%v %v %v)", n.left, n.operator, n.right)
}

func (n matchesNode) String() string {
return fmt.Sprintf("(%v matches %v)", n.left, n.right)
}

func (n propertyNode) String() string {
switch n.property.(type) {
case identifierNode:
Expand Down
8 changes: 8 additions & 0 deletions print_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ var printTests = []printTest{
binaryNode{"and", binaryNode{"or", nameNode{"a"}, nameNode{"b"}}, nameNode{"c"}},
"((a or b) and c)",
},
{
matchesNode{left: nameNode{"foo"}, right: textNode{"foobar"}},
"(foo matches \"foobar\")",
},
{
conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}},
"a ? a : b",
},
}

func TestPrint(t *testing.T) {
Expand Down

0 comments on commit 3578406

Please sign in to comment.