Skip to content

Commit 55be21a

Browse files
ckganesanantonmedv
authored andcommitted
Fix -1 not in [] expressions (#590)
1 parent dc76d4c commit 55be21a

File tree

5 files changed

+164
-46
lines changed

5 files changed

+164
-46
lines changed

compiler/compiler_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,39 @@ func TestCompile_optimizes_jumps(t *testing.T) {
541541
{vm.OpFetch, 0},
542542
},
543543
},
544+
{
545+
`-1 not in [1, 2, 5]`,
546+
[]op{
547+
{vm.OpPush, 0},
548+
{vm.OpPush, 1},
549+
{vm.OpIn, 0},
550+
{vm.OpNot, 0},
551+
},
552+
},
553+
{
554+
`1 + 8 not in [1, 2, 5]`,
555+
[]op{
556+
{vm.OpPush, 0},
557+
{vm.OpPush, 1},
558+
{vm.OpIn, 0},
559+
{vm.OpNot, 0},
560+
},
561+
},
562+
{
563+
`true ? false : 8 not in [1, 2, 5]`,
564+
[]op{
565+
{vm.OpTrue, 0},
566+
{vm.OpJumpIfFalse, 3},
567+
{vm.OpPop, 0},
568+
{vm.OpFalse, 0},
569+
{vm.OpJump, 5},
570+
{vm.OpPop, 0},
571+
{vm.OpPush, 0},
572+
{vm.OpPush, 1},
573+
{vm.OpIn, 0},
574+
{vm.OpNot, 0},
575+
},
576+
},
544577
}
545578

546579
for _, test := range tests {

expr_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,10 @@ func TestExpr(t *testing.T) {
785785
`Two not in 0..1`,
786786
true,
787787
},
788+
{
789+
`-1 not in [1]`,
790+
true,
791+
},
788792
{
789793
`Int32 in [10, 20]`,
790794
false,
@@ -797,6 +801,10 @@ func TestExpr(t *testing.T) {
797801
`String matches ("^" + String + "$")`,
798802
true,
799803
},
804+
{
805+
`'foo' + 'bar' not matches 'foobar'`,
806+
false,
807+
},
800808
{
801809
`"foobar" contains "bar"`,
802810
true,

parser/operator/operator.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ func IsBoolean(op string) bool {
2020
return op == "and" || op == "or" || op == "&&" || op == "||"
2121
}
2222

23+
func AllowedNegateSuffix(op string) bool {
24+
switch op {
25+
case "contains", "matches", "startsWith", "endsWith", "in":
26+
return true
27+
default:
28+
return false
29+
}
30+
}
31+
2332
var Unary = map[string]Operator{
2433
"not": {50, Left},
2534
"!": {50, Left},

parser/parser.go

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -126,73 +126,80 @@ func (p *parser) expect(kind Kind, values ...string) {
126126
// parse functions
127127

128128
func (p *parser) parseExpression(precedence int) Node {
129-
if precedence == 0 {
130-
if p.current.Is(Operator, "let") {
131-
return p.parseVariableDeclaration()
132-
}
129+
if precedence == 0 && p.current.Is(Operator, "let") {
130+
return p.parseVariableDeclaration()
133131
}
134132

135133
nodeLeft := p.parsePrimary()
136134

137135
prevOperator := ""
138136
opToken := p.current
139137
for opToken.Is(Operator) && p.err == nil {
140-
negate := false
138+
negate := opToken.Is(Operator, "not")
141139
var notToken Token
142140

143141
// Handle "not *" operator, like "not in" or "not contains".
144-
if opToken.Is(Operator, "not") {
142+
if negate {
143+
currentPos := p.pos
145144
p.next()
146-
notToken = p.current
147-
negate = true
148-
opToken = p.current
145+
if operator.AllowedNegateSuffix(p.current.Value) {
146+
if op, ok := operator.Binary[p.current.Value]; ok && op.Precedence >= precedence {
147+
notToken = p.current
148+
opToken = p.current
149+
} else {
150+
p.pos = currentPos
151+
p.current = opToken
152+
break
153+
}
154+
} else {
155+
p.error("unexpected token %v", p.current)
156+
break
157+
}
149158
}
150159

151-
if op, ok := operator.Binary[opToken.Value]; ok {
152-
if op.Precedence >= precedence {
153-
p.next()
160+
if op, ok := operator.Binary[opToken.Value]; ok && op.Precedence >= precedence {
161+
p.next()
154162

155-
if opToken.Value == "|" {
156-
identToken := p.current
157-
p.expect(Identifier)
158-
nodeLeft = p.parseCall(identToken, []Node{nodeLeft}, true)
159-
goto next
160-
}
163+
if opToken.Value == "|" {
164+
identToken := p.current
165+
p.expect(Identifier)
166+
nodeLeft = p.parseCall(identToken, []Node{nodeLeft}, true)
167+
goto next
168+
}
161169

162-
if prevOperator == "??" && opToken.Value != "??" && !opToken.Is(Bracket, "(") {
163-
p.errorAt(opToken, "Operator (%v) and coalesce expressions (??) cannot be mixed. Wrap either by parentheses.", opToken.Value)
164-
break
165-
}
170+
if prevOperator == "??" && opToken.Value != "??" && !opToken.Is(Bracket, "(") {
171+
p.errorAt(opToken, "Operator (%v) and coalesce expressions (??) cannot be mixed. Wrap either by parentheses.", opToken.Value)
172+
break
173+
}
166174

167-
if operator.IsComparison(opToken.Value) {
168-
nodeLeft = p.parseComparison(nodeLeft, opToken, op.Precedence)
169-
goto next
170-
}
175+
if operator.IsComparison(opToken.Value) {
176+
nodeLeft = p.parseComparison(nodeLeft, opToken, op.Precedence)
177+
goto next
178+
}
171179

172-
var nodeRight Node
173-
if op.Associativity == operator.Left {
174-
nodeRight = p.parseExpression(op.Precedence + 1)
175-
} else {
176-
nodeRight = p.parseExpression(op.Precedence)
177-
}
180+
var nodeRight Node
181+
if op.Associativity == operator.Left {
182+
nodeRight = p.parseExpression(op.Precedence + 1)
183+
} else {
184+
nodeRight = p.parseExpression(op.Precedence)
185+
}
178186

179-
nodeLeft = &BinaryNode{
180-
Operator: opToken.Value,
181-
Left: nodeLeft,
182-
Right: nodeRight,
183-
}
184-
nodeLeft.SetLocation(opToken.Location)
187+
nodeLeft = &BinaryNode{
188+
Operator: opToken.Value,
189+
Left: nodeLeft,
190+
Right: nodeRight,
191+
}
192+
nodeLeft.SetLocation(opToken.Location)
185193

186-
if negate {
187-
nodeLeft = &UnaryNode{
188-
Operator: "not",
189-
Node: nodeLeft,
190-
}
191-
nodeLeft.SetLocation(notToken.Location)
194+
if negate {
195+
nodeLeft = &UnaryNode{
196+
Operator: "not",
197+
Node: nodeLeft,
192198
}
193-
194-
goto next
199+
nodeLeft.SetLocation(notToken.Location)
195200
}
201+
202+
goto next
196203
}
197204
break
198205

parser/parser_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,62 @@ world`},
365365
&UnaryNode{Operator: "not",
366366
Node: &IdentifierNode{Value: "in_var"}},
367367
},
368+
{
369+
"-1 not in [1, 2, 3, 4]",
370+
&UnaryNode{Operator: "not",
371+
Node: &BinaryNode{Operator: "in",
372+
Left: &UnaryNode{Operator: "-", Node: &IntegerNode{Value: 1}},
373+
Right: &ArrayNode{Nodes: []Node{
374+
&IntegerNode{Value: 1},
375+
&IntegerNode{Value: 2},
376+
&IntegerNode{Value: 3},
377+
&IntegerNode{Value: 4},
378+
}}}},
379+
},
380+
{
381+
"1*8 not in [1, 2, 3, 4]",
382+
&UnaryNode{Operator: "not",
383+
Node: &BinaryNode{Operator: "in",
384+
Left: &BinaryNode{Operator: "*",
385+
Left: &IntegerNode{Value: 1},
386+
Right: &IntegerNode{Value: 8},
387+
},
388+
Right: &ArrayNode{Nodes: []Node{
389+
&IntegerNode{Value: 1},
390+
&IntegerNode{Value: 2},
391+
&IntegerNode{Value: 3},
392+
&IntegerNode{Value: 4},
393+
}}}},
394+
},
395+
{
396+
"2==2 ? false : 3 not in [1, 2, 5]",
397+
&ConditionalNode{
398+
Cond: &BinaryNode{
399+
Operator: "==",
400+
Left: &IntegerNode{Value: 2},
401+
Right: &IntegerNode{Value: 2},
402+
},
403+
Exp1: &BoolNode{Value: false},
404+
Exp2: &UnaryNode{
405+
Operator: "not",
406+
Node: &BinaryNode{
407+
Operator: "in",
408+
Left: &IntegerNode{Value: 3},
409+
Right: &ArrayNode{Nodes: []Node{
410+
&IntegerNode{Value: 1},
411+
&IntegerNode{Value: 2},
412+
&IntegerNode{Value: 5},
413+
}}}}},
414+
},
415+
{
416+
"'foo' + 'bar' not matches 'foobar'",
417+
&UnaryNode{Operator: "not",
418+
Node: &BinaryNode{Operator: "matches",
419+
Left: &BinaryNode{Operator: "+",
420+
Left: &StringNode{Value: "foo"},
421+
Right: &StringNode{Value: "bar"}},
422+
Right: &StringNode{Value: "foobar"}}},
423+
},
368424
{
369425
"all(Tickets, #)",
370426
&BuiltinNode{
@@ -706,6 +762,11 @@ invalid float literal: strconv.ParseFloat: parsing "0o1E+1": invalid syntax (1:6
706762
invalid float literal: strconv.ParseFloat: parsing "1E": invalid syntax (1:2)
707763
| 1E
708764
| .^
765+
766+
1 not == [1, 2, 5]
767+
unexpected token Operator("==") (1:7)
768+
| 1 not == [1, 2, 5]
769+
| ......^
709770
`
710771

711772
func TestParse_error(t *testing.T) {

0 commit comments

Comments
 (0)