Skip to content

Commit 2aee523

Browse files
authored
Optimize boolean operations between all, any, none functions (#626)
1 parent e7e72b9 commit 2aee523

File tree

4 files changed

+367
-0
lines changed

4 files changed

+367
-0
lines changed

Diff for: expr_test.go

+189
Original file line numberDiff line numberDiff line change
@@ -901,18 +901,147 @@ func TestExpr(t *testing.T) {
901901
`all(1..3, {# > 0})`,
902902
true,
903903
},
904+
{
905+
`all(1..3, {# > 0}) && all(1..3, {# < 4})`,
906+
true,
907+
},
908+
{
909+
`all(1..3, {# > 2}) && all(1..3, {# < 4})`,
910+
false,
911+
},
912+
{
913+
`all(1..3, {# > 0}) && all(1..3, {# < 2})`,
914+
false,
915+
},
916+
{
917+
`all(1..3, {# > 2}) && all(1..3, {# < 2})`,
918+
false,
919+
},
920+
{
921+
`all(1..3, {# > 0}) || all(1..3, {# < 4})`,
922+
true,
923+
},
924+
{
925+
`all(1..3, {# > 0}) || all(1..3, {# != 2})`,
926+
true,
927+
},
928+
{
929+
`all(1..3, {# != 3}) || all(1..3, {# < 4})`,
930+
true,
931+
},
932+
{
933+
`all(1..3, {# != 3}) || all(1..3, {# != 2})`,
934+
false,
935+
},
904936
{
905937
`none(1..3, {# == 0})`,
906938
true,
907939
},
940+
{
941+
`none(1..3, {# == 0}) && none(1..3, {# == 4})`,
942+
true,
943+
},
944+
{
945+
`none(1..3, {# == 0}) && none(1..3, {# == 3})`,
946+
false,
947+
},
948+
{
949+
`none(1..3, {# == 1}) && none(1..3, {# == 4})`,
950+
false,
951+
},
952+
{
953+
`none(1..3, {# == 1}) && none(1..3, {# == 3})`,
954+
false,
955+
},
956+
{
957+
`none(1..3, {# == 0}) || none(1..3, {# == 4})`,
958+
true,
959+
},
960+
{
961+
`none(1..3, {# == 0}) || none(1..3, {# == 3})`,
962+
true,
963+
},
964+
{
965+
`none(1..3, {# == 1}) || none(1..3, {# == 4})`,
966+
true,
967+
},
968+
{
969+
`none(1..3, {# == 1}) || none(1..3, {# == 3})`,
970+
false,
971+
},
908972
{
909973
`any([1,1,0,1], {# == 0})`,
910974
true,
911975
},
976+
{
977+
`any(1..3, {# == 1}) && any(1..3, {# == 2})`,
978+
true,
979+
},
980+
{
981+
`any(1..3, {# == 0}) && any(1..3, {# == 2})`,
982+
false,
983+
},
984+
{
985+
`any(1..3, {# == 1}) && any(1..3, {# == 4})`,
986+
false,
987+
},
988+
{
989+
`any(1..3, {# == 0}) && any(1..3, {# == 4})`,
990+
false,
991+
},
992+
{
993+
`any(1..3, {# == 1}) || any(1..3, {# == 2})`,
994+
true,
995+
},
996+
{
997+
`any(1..3, {# == 0}) || any(1..3, {# == 2})`,
998+
true,
999+
},
1000+
{
1001+
`any(1..3, {# == 1}) || any(1..3, {# == 4})`,
1002+
true,
1003+
},
1004+
{
1005+
`any(1..3, {# == 0}) || any(1..3, {# == 4})`,
1006+
false,
1007+
},
9121008
{
9131009
`one([1,1,0,1], {# == 0}) and not one([1,0,0,1], {# == 0})`,
9141010
true,
9151011
},
1012+
{
1013+
`one(1..3, {# == 1}) and one(1..3, {# == 2})`,
1014+
true,
1015+
},
1016+
{
1017+
`one(1..3, {# == 1 || # == 2}) and one(1..3, {# == 2})`,
1018+
false,
1019+
},
1020+
{
1021+
`one(1..3, {# == 1}) and one(1..3, {# == 2 || # == 3})`,
1022+
false,
1023+
},
1024+
{
1025+
`one(1..3, {# == 1 || # == 2}) and one(1..3, {# == 2 || # == 3})`,
1026+
false,
1027+
},
1028+
{
1029+
`one(1..3, {# == 1}) or one(1..3, {# == 2})`,
1030+
true,
1031+
},
1032+
{
1033+
`one(1..3, {# == 1 || # == 2}) or one(1..3, {# == 2})`,
1034+
true,
1035+
},
1036+
{
1037+
`one(1..3, {# == 1}) or one(1..3, {# == 2 || # == 3})`,
1038+
true,
1039+
},
1040+
{
1041+
`one(1..3, {# == 1 || # == 2}) or one(1..3, {# == 2 || # == 3})`,
1042+
false,
1043+
},
1044+
9161045
{
9171046
`count(1..30, {# % 3 == 0})`,
9181047
10,
@@ -2524,3 +2653,63 @@ func TestOperatorDependsOnEnv(t *testing.T) {
25242653
require.NoError(t, err)
25252654
assert.Equal(t, 42, out)
25262655
}
2656+
2657+
func TestIssue624(t *testing.T) {
2658+
type tag struct {
2659+
Name string
2660+
}
2661+
2662+
type item struct {
2663+
Tags []tag
2664+
}
2665+
2666+
i := item{
2667+
Tags: []tag{
2668+
{Name: "one"},
2669+
{Name: "two"},
2670+
},
2671+
}
2672+
2673+
rule := `[
2674+
true && true,
2675+
one(Tags, .Name in ["one"]),
2676+
one(Tags, .Name in ["two"]),
2677+
one(Tags, .Name in ["one"]) && one(Tags, .Name in ["two"])
2678+
]`
2679+
resp, err := expr.Eval(rule, i)
2680+
require.NoError(t, err)
2681+
require.Equal(t, []interface{}{true, true, true, true}, resp)
2682+
}
2683+
2684+
func TestPredicateCombination(t *testing.T) {
2685+
tests := []struct {
2686+
code1 string
2687+
code2 string
2688+
}{
2689+
{"all(1..3, {# > 0}) && all(1..3, {# < 4})", "all(1..3, {# > 0 && # < 4})"},
2690+
{"all(1..3, {# > 1}) && all(1..3, {# < 4})", "all(1..3, {# > 1 && # < 4})"},
2691+
{"all(1..3, {# > 0}) && all(1..3, {# < 2})", "all(1..3, {# > 0 && # < 2})"},
2692+
{"all(1..3, {# > 1}) && all(1..3, {# < 2})", "all(1..3, {# > 1 && # < 2})"},
2693+
2694+
{"any(1..3, {# > 0}) || any(1..3, {# < 4})", "any(1..3, {# > 0 || # < 4})"},
2695+
{"any(1..3, {# > 1}) || any(1..3, {# < 4})", "any(1..3, {# > 1 || # < 4})"},
2696+
{"any(1..3, {# > 0}) || any(1..3, {# < 2})", "any(1..3, {# > 0 || # < 2})"},
2697+
{"any(1..3, {# > 1}) || any(1..3, {# < 2})", "any(1..3, {# > 1 || # < 2})"},
2698+
2699+
{"none(1..3, {# > 0}) && none(1..3, {# < 4})", "none(1..3, {# > 0 || # < 4})"},
2700+
{"none(1..3, {# > 1}) && none(1..3, {# < 4})", "none(1..3, {# > 1 || # < 4})"},
2701+
{"none(1..3, {# > 0}) && none(1..3, {# < 2})", "none(1..3, {# > 0 || # < 2})"},
2702+
{"none(1..3, {# > 1}) && none(1..3, {# < 2})", "none(1..3, {# > 1 || # < 2})"},
2703+
}
2704+
for _, tt := range tests {
2705+
t.Run(tt.code1, func(t *testing.T) {
2706+
out1, err := expr.Eval(tt.code1, nil)
2707+
require.NoError(t, err)
2708+
2709+
out2, err := expr.Eval(tt.code2, nil)
2710+
require.NoError(t, err)
2711+
2712+
require.Equal(t, out1, out2)
2713+
})
2714+
}
2715+
}

Diff for: optimizer/optimizer.go

+1
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ func Optimize(node *Node, config *conf.Config) error {
3636
Walk(node, &filterLen{})
3737
Walk(node, &filterLast{})
3838
Walk(node, &filterFirst{})
39+
Walk(node, &predicateCombination{})
3940
return nil
4041
}

Diff for: optimizer/optimizer_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package optimizer_test
22

33
import (
4+
"fmt"
45
"reflect"
56
"strings"
67
"testing"
@@ -339,3 +340,118 @@ func TestOptimize_filter_map_first(t *testing.T) {
339340

340341
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
341342
}
343+
344+
func TestOptimize_predicate_combination(t *testing.T) {
345+
tests := []struct {
346+
op string
347+
fn string
348+
wantOp string
349+
}{
350+
{"and", "all", "and"},
351+
{"&&", "all", "&&"},
352+
{"or", "any", "or"},
353+
{"||", "any", "||"},
354+
{"and", "none", "or"},
355+
{"&&", "none", "||"},
356+
}
357+
358+
for _, tt := range tests {
359+
rule := fmt.Sprintf(`%s(users, .Age > 18 and .Name != "Bob") %s %s(users, .Age < 30)`, tt.fn, tt.op, tt.fn)
360+
t.Run(rule, func(t *testing.T) {
361+
tree, err := parser.Parse(rule)
362+
require.NoError(t, err)
363+
364+
err = optimizer.Optimize(&tree.Node, nil)
365+
require.NoError(t, err)
366+
367+
expected := &ast.BuiltinNode{
368+
Name: tt.fn,
369+
Arguments: []ast.Node{
370+
&ast.IdentifierNode{Value: "users"},
371+
&ast.ClosureNode{
372+
Node: &ast.BinaryNode{
373+
Operator: tt.wantOp,
374+
Left: &ast.BinaryNode{
375+
Operator: "and",
376+
Left: &ast.BinaryNode{
377+
Operator: ">",
378+
Left: &ast.MemberNode{
379+
Node: &ast.PointerNode{},
380+
Property: &ast.StringNode{Value: "Age"},
381+
},
382+
Right: &ast.IntegerNode{Value: 18},
383+
},
384+
Right: &ast.BinaryNode{
385+
Operator: "!=",
386+
Left: &ast.MemberNode{
387+
Node: &ast.PointerNode{},
388+
Property: &ast.StringNode{Value: "Name"},
389+
},
390+
Right: &ast.StringNode{Value: "Bob"},
391+
},
392+
},
393+
Right: &ast.BinaryNode{
394+
Operator: "<",
395+
Left: &ast.MemberNode{
396+
Node: &ast.PointerNode{},
397+
Property: &ast.StringNode{Value: "Age"},
398+
},
399+
Right: &ast.IntegerNode{Value: 30},
400+
},
401+
},
402+
},
403+
},
404+
}
405+
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
406+
})
407+
}
408+
}
409+
410+
func TestOptimize_predicate_combination_nested(t *testing.T) {
411+
tree, err := parser.Parse(`all(users, {all(.Friends, {.Age == 18 })}) && all(users, {all(.Friends, {.Name != "Bob" })})`)
412+
require.NoError(t, err)
413+
414+
err = optimizer.Optimize(&tree.Node, nil)
415+
require.NoError(t, err)
416+
417+
expected := &ast.BuiltinNode{
418+
Name: "all",
419+
Arguments: []ast.Node{
420+
&ast.IdentifierNode{Value: "users"},
421+
&ast.ClosureNode{
422+
Node: &ast.BuiltinNode{
423+
Name: "all",
424+
Arguments: []ast.Node{
425+
&ast.MemberNode{
426+
Node: &ast.PointerNode{},
427+
Property: &ast.StringNode{Value: "Friends"},
428+
},
429+
&ast.ClosureNode{
430+
Node: &ast.BinaryNode{
431+
Operator: "&&",
432+
Left: &ast.BinaryNode{
433+
Operator: "==",
434+
Left: &ast.MemberNode{
435+
Node: &ast.PointerNode{},
436+
Property: &ast.StringNode{Value: "Age"},
437+
},
438+
Right: &ast.IntegerNode{Value: 18},
439+
},
440+
Right: &ast.BinaryNode{
441+
Operator: "!=",
442+
Left: &ast.MemberNode{
443+
Node: &ast.PointerNode{},
444+
Property: &ast.StringNode{Value: "Name"},
445+
},
446+
Right: &ast.StringNode{Value: "Bob"},
447+
},
448+
},
449+
},
450+
},
451+
},
452+
},
453+
},
454+
}
455+
456+
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
457+
}

0 commit comments

Comments
 (0)