Skip to content

Commit

Permalink
Simplify "not in" operators
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Nov 6, 2022
1 parent 7c1d2ee commit 3d4c219
Show file tree
Hide file tree
Showing 12 changed files with 350 additions and 263 deletions.
8 changes: 1 addition & 7 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,12 @@ type UnaryNode struct {

type BinaryNode struct {
base
Regexp *regexp.Regexp
Operator string
Left Node
Right Node
}

type MatchesNode struct {
base
Regexp *regexp.Regexp
Left Node
Right Node
}

type ChainNode struct {
base
Node Node
Expand Down
3 changes: 0 additions & 3 deletions ast/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ func Walk(node *Node, v Visitor) {
case *BinaryNode:
Walk(&n.Left, v)
Walk(&n.Right, v)
case *MatchesNode:
Walk(&n.Left, v)
Walk(&n.Right, v)
case *ChainNode:
Walk(&n.Node, v)
case *MemberNode:
Expand Down
66 changes: 33 additions & 33 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package checker
import (
"fmt"
"reflect"
"regexp"

"github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/conf"
Expand Down Expand Up @@ -78,8 +79,6 @@ func (v *visitor) visit(node ast.Node) (reflect.Type, info) {
t, i = v.UnaryNode(n)
case *ast.BinaryNode:
t, i = v.BinaryNode(n)
case *ast.MatchesNode:
t, i = v.MatchesNode(n)
case *ast.ChainNode:
t, i = v.ChainNode(n)
case *ast.MemberNode:
Expand Down Expand Up @@ -232,23 +231,6 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) (reflect.Type, info) {
return boolType, info{}
}

case "in", "not in":
if (isString(l) || isAny(l)) && isStruct(r) {
return boolType, info{}
}
if isMap(r) {
return boolType, info{}
}
if isArray(r) {
return boolType, info{}
}
if isAny(l) && anyOf(r, isString, isArray, isMap) {
return boolType, info{}
}
if isAny(l) && isAny(r) {
return boolType, info{}
}

case "<", ">", ">=", "<=":
if isNumber(l) && isNumber(r) {
return boolType, info{}
Expand Down Expand Up @@ -315,6 +297,38 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) (reflect.Type, info) {
return anyType, info{}
}

case "in":
if (isString(l) || isAny(l)) && isStruct(r) {
return boolType, info{}
}
if isMap(r) {
return boolType, info{}
}
if isArray(r) {
return boolType, info{}
}
if isAny(l) && anyOf(r, isString, isArray, isMap) {
return boolType, info{}
}
if isAny(l) && isAny(r) {
return boolType, info{}
}

case "matches":
if s, ok := node.Right.(*ast.StringNode); ok {
r, err := regexp.Compile(s.Value)
if err != nil {
return v.error(node, err.Error())
}
node.Regexp = r
}
if isString(l) && isString(r) {
return boolType, info{}
}
if or(l, r, isString) {
return boolType, info{}
}

case "contains", "startsWith", "endsWith":
if isString(l) && isString(r) {
return boolType, info{}
Expand All @@ -340,20 +354,6 @@ func (v *visitor) BinaryNode(node *ast.BinaryNode) (reflect.Type, info) {
return v.error(node, `invalid operation: %v (mismatched types %v and %v)`, node.Operator, l, r)
}

func (v *visitor) MatchesNode(node *ast.MatchesNode) (reflect.Type, info) {
l, _ := v.visit(node.Left)
r, _ := v.visit(node.Right)

if isString(l) && isString(r) {
return boolType, info{}
}
if or(l, r, isString) {
return boolType, info{}
}

return v.error(node, `invalid operation: matches (mismatched types %v and %v)`, l, r)
}

func (v *visitor) ChainNode(node *ast.ChainNode) (reflect.Type, info) {
return v.visit(node.Node)
}
Expand Down
1 change: 1 addition & 0 deletions checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var successTests = []string{
"String in MapOfFoo",
"String matches 'ok'",
"String matches Any",
"String not matches Any",
"String not in ArrayOfFoo",
"StringPtr == nil",
"[1, 2, 3] == []",
Expand Down
39 changes: 15 additions & 24 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ func (c *compiler) compile(node ast.Node) {
c.UnaryNode(n)
case *ast.BinaryNode:
c.BinaryNode(n)
case *ast.MatchesNode:
c.MatchesNode(n)
case *ast.ChainNode:
c.ChainNode(n)
case *ast.MemberNode:
Expand Down Expand Up @@ -329,17 +327,6 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) {
c.compile(node.Right)
c.patchJump(end)

case "in":
c.compile(node.Left)
c.compile(node.Right)
c.emit(OpIn)

case "not in":
c.compile(node.Left)
c.compile(node.Right)
c.emit(OpIn)
c.emit(OpNot)

case "<":
c.compile(node.Left)
c.compile(node.Right)
Expand Down Expand Up @@ -390,6 +377,21 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) {
c.compile(node.Right)
c.emit(OpExponent)

case "in":
c.compile(node.Left)
c.compile(node.Right)
c.emit(OpIn)

case "matches":
if node.Regexp != nil {
c.compile(node.Left)
c.emit(OpMatchesConst, c.addConstant(node.Regexp))
} else {
c.compile(node.Left)
c.compile(node.Right)
c.emit(OpMatches)
}

case "contains":
c.compile(node.Left)
c.compile(node.Right)
Expand All @@ -416,17 +418,6 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) {
}
}

func (c *compiler) MatchesNode(node *ast.MatchesNode) {
if node.Regexp != nil {
c.compile(node.Left)
c.emit(OpMatchesConst, c.addConstant(node.Regexp))
return
}
c.compile(node.Left)
c.compile(node.Right)
c.emit(OpMatches)
}

func (c *compiler) ChainNode(node *ast.ChainNode) {
c.chains = append(c.chains, []int{})
c.compile(node.Node)
Expand Down
2 changes: 1 addition & 1 deletion optimizer/in_array.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type inArray struct{}
func (*inArray) Visit(node *Node) {
switch n := (*node).(type) {
case *BinaryNode:
if n.Operator == "in" || n.Operator == "not in" {
if n.Operator == "in" {
if array, ok := n.Right.(*ArrayNode); ok {
if len(array.Nodes) > 0 {
t := n.Left.Type()
Expand Down
8 changes: 1 addition & 7 deletions optimizer/in_range.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type inRange struct{}
func (*inRange) Visit(node *Node) {
switch n := (*node).(type) {
case *BinaryNode:
if n.Operator == "in" || n.Operator == "not in" {
if n.Operator == "in" {
if rng, ok := n.Right.(*BinaryNode); ok && rng.Operator == ".." {
if from, ok := rng.Left.(*IntegerNode); ok {
if to, ok := rng.Right.(*IntegerNode); ok {
Expand All @@ -26,12 +26,6 @@ func (*inRange) Visit(node *Node) {
Right: to,
},
})
if n.Operator == "not in" {
Patch(node, &UnaryNode{
Operator: "not",
Node: *node,
})
}
}
}
}
Expand Down
19 changes: 14 additions & 5 deletions parser/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ func (l *lexer) emitEOF() {
l.startLoc = l.loc
}

func (l *lexer) skip() {
l.start = l.end
l.startLoc = l.loc
}

func (l *lexer) word() string {
return l.input[l.start:l.end]
}
Expand All @@ -118,22 +123,26 @@ func (l *lexer) acceptRun(valid string) {
l.backup()
}

func (l *lexer) acceptWord(word string) bool {
pos, loc, prev := l.end, l.loc, l.prev

// Skip spaces (U+0020) if any
func (l *lexer) skipSpaces() {
r := l.peek()
for ; r == ' '; r = l.peek() {
l.next()
}
l.skip()
}

func (l *lexer) acceptWord(word string) bool {
pos, loc, prev := l.end, l.loc, l.prev

l.skipSpaces()

for _, ch := range word {
if l.next() != ch {
l.end, l.loc, l.prev = pos, loc, prev
return false
}
}
if r = l.peek(); r != ' ' && r != eof {
if r := l.peek(); r != ' ' && r != eof {
l.end, l.loc, l.prev = pos, loc, prev
return false
}
Expand Down
12 changes: 8 additions & 4 deletions parser/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ var lexTests = []lexTest{
{
`not in not abc not i not(false) not in not in`,
[]Token{
{Kind: Operator, Value: "not in"},
{Kind: Operator, Value: "not"},
{Kind: Operator, Value: "in"},
{Kind: Operator, Value: "not"},
{Kind: Identifier, Value: "abc"},
{Kind: Operator, Value: "not"},
Expand All @@ -121,8 +122,10 @@ var lexTests = []lexTest{
{Kind: Bracket, Value: "("},
{Kind: Identifier, Value: "false"},
{Kind: Bracket, Value: ")"},
{Kind: Operator, Value: "not in"},
{Kind: Operator, Value: "not in"},
{Kind: Operator, Value: "not"},
{Kind: Operator, Value: "in"},
{Kind: Operator, Value: "not"},
{Kind: Operator, Value: "in"},
{Kind: EOF},
},
},
Expand All @@ -137,7 +140,8 @@ var lexTests = []lexTest{
{
"not in",
[]Token{
{Kind: Operator, Value: "not in"},
{Kind: Operator, Value: "not"},
{Kind: Operator, Value: "in"},
{Kind: EOF},
},
},
Expand Down
26 changes: 21 additions & 5 deletions parser/lexer/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,29 @@ loop:
}

func not(l *lexer) stateFn {
switch l.acceptWord("in") {
case true:
l.emitValue(Operator, "not in")
case false:
l.emitValue(Operator, "not")
l.emit(Operator)

l.skipSpaces()

pos, loc, prev := l.end, l.loc, l.prev

// Get the next word.
for {
r := l.next()
if IsAlphaNumeric(r) {
// absorb
} else {
l.backup()
break
}
}

switch l.word() {
case "in", "matches", "contains", "startsWith", "endsWith":
l.emit(Operator)
default:
l.end, l.loc, l.prev = pos, loc, prev
}
return root
}

Expand Down
Loading

0 comments on commit 3d4c219

Please sign in to comment.