Add filter()[-1] optimization

This commit is contained in:
Anton Medvedev
2023-08-24 11:15:43 +02:00
parent 8c52bd6d8b
commit 166d8b8ce9
13 changed files with 250 additions and 59 deletions
+13 -3
View File
@@ -60,6 +60,16 @@ var Builtins = []*Function{
Predicate: true,
Types: types(new(func([]interface{}, func(interface{}) bool) []interface{})),
},
{
Name: "map",
Predicate: true,
Types: types(new(func([]interface{}, func(interface{}) interface{}) []interface{})),
},
{
Name: "count",
Predicate: true,
Types: types(new(func([]interface{}, func(interface{}) bool) int)),
},
{
Name: "find",
Predicate: true,
@@ -71,12 +81,12 @@ var Builtins = []*Function{
Types: types(new(func([]interface{}, func(interface{}) bool) int)),
},
{
Name: "map",
Name: "findLast",
Predicate: true,
Types: types(new(func([]interface{}, func(interface{}) interface{}) []interface{})),
Types: types(new(func([]interface{}, func(interface{}) bool) interface{})),
},
{
Name: "count",
Name: "findLastIndex",
Predicate: true,
Types: types(new(func([]interface{}, func(interface{}) bool) int)),
},
+13 -14
View File
@@ -604,11 +604,11 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
closure.NumIn() == 1 && isAny(closure.In(0)) {
if !isBool(closure.Out(0)) && !isAny(closure.Out(0)) {
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
return v.error(node.Arguments[1], "predicate should return boolean (got %v)", closure.Out(0).String())
}
return boolType, info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
case "filter":
collection, _ := v.visit(node.Arguments[0])
@@ -625,14 +625,14 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
closure.NumIn() == 1 && isAny(closure.In(0)) {
if !isBool(closure.Out(0)) && !isAny(closure.Out(0)) {
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
return v.error(node.Arguments[1], "predicate should return boolean (got %v)", closure.Out(0).String())
}
if isAny(collection) {
return arrayType, info{}
}
return reflect.SliceOf(collection.Elem()), info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
case "map":
collection, _ := v.visit(node.Arguments[0])
@@ -650,7 +650,7 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
return reflect.SliceOf(closure.Out(0)), info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
case "count":
collection, _ := v.visit(node.Arguments[0])
@@ -666,14 +666,14 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
closure.NumOut() == 1 &&
closure.NumIn() == 1 && isAny(closure.In(0)) {
if !isBool(closure.Out(0)) && !isAny(closure.Out(0)) {
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
return v.error(node.Arguments[1], "predicate should return boolean (got %v)", closure.Out(0).String())
}
return integerType, info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
case "find":
case "find", "findLast":
collection, _ := v.visit(node.Arguments[0])
if !isArray(collection) && !isAny(collection) {
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
@@ -688,16 +688,16 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
closure.NumIn() == 1 && isAny(closure.In(0)) {
if !isBool(closure.Out(0)) && !isAny(closure.Out(0)) {
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
return v.error(node.Arguments[1], "predicate should return boolean (got %v)", closure.Out(0).String())
}
if isAny(collection) {
return anyType, info{}
}
return collection.Elem(), info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
case "findIndex":
case "findIndex", "findLastIndex":
collection, _ := v.visit(node.Arguments[0])
if !isArray(collection) && !isAny(collection) {
return v.error(node.Arguments[0], "builtin %v takes only array (got %v)", node.Name, collection)
@@ -712,12 +712,11 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
closure.NumIn() == 1 && isAny(closure.In(0)) {
if !isBool(closure.Out(0)) && !isAny(closure.Out(0)) {
return v.error(node.Arguments[1], "closure should return boolean (got %v)", closure.Out(0).String())
return v.error(node.Arguments[1], "predicate should return boolean (got %v)", closure.Out(0).String())
}
return integerType, info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
return v.error(node.Arguments[1], "predicate should has one input and one output param")
}
if id, ok := builtin.Index[node.Name]; ok {
+4 -4
View File
@@ -414,22 +414,22 @@ builtin count takes only array (got int) (1:7)
| ......^
count(ArrayOfInt, {#})
closure should return boolean (got int) (1:19)
predicate should return boolean (got int) (1:19)
| count(ArrayOfInt, {#})
| ..................^
all(ArrayOfInt, {# + 1})
closure should return boolean (got int) (1:17)
predicate should return boolean (got int) (1:17)
| all(ArrayOfInt, {# + 1})
| ................^
filter(ArrayOfFoo, {.Bar.Baz})
closure should return boolean (got string) (1:20)
predicate should return boolean (got string) (1:20)
| filter(ArrayOfFoo, {.Bar.Baz})
| ...................^
find(ArrayOfFoo, {.Bar.Baz})
closure should return boolean (got string) (1:18)
predicate should return boolean (got string) (1:18)
| find(ArrayOfFoo, {.Bar.Baz})
| .................^
+60
View File
@@ -760,6 +760,48 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "findLast":
c.compile(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoopBackwards(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.emit(OpPointer)
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
if node.Throws {
c.emit(OpPush, c.addConstant(fmt.Errorf("reflect: slice index out of range")))
c.emit(OpThrow)
} else {
c.emit(OpNil)
}
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "findLastIndex":
c.compile(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoopBackwards(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.emit(OpGetIndex)
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
c.emit(OpNil)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
}
if id, ok := builtin.Index[node.Name]; ok {
@@ -801,6 +843,24 @@ func (c *compiler) emitLoop(body func()) {
c.patchJump(end)
}
func (c *compiler) emitLoopBackwards(body func()) {
c.emit(OpGetLen)
c.emit(OpInt, 1)
c.emit(OpSubtract)
c.emit(OpSetIndex)
begin := len(c.bytecode)
c.emit(OpGetIndex)
c.emit(OpInt, 0)
c.emit(OpMoreOrEqual)
end := c.emit(OpJumpIfFalse, placeholder)
body()
c.emit(OpDecrementIndex)
c.emit(OpJumpBackward, c.calcBackwardJump(begin))
c.patchJump(end)
}
func (c *compiler) ClosureNode(node *ast.ClosureNode) {
c.compile(node.Node)
}
+35 -21
View File
@@ -984,33 +984,47 @@ func TestExpr(t *testing.T) {
`first(filter(ArrayOfFoo, false))`,
nil,
},
{
`findLast(1..9, # % 2 == 0)`,
8,
},
{
`findLastIndex(1..9, # % 2 == 0)`,
7,
},
{
`filter(1..9, # % 2 == 0)[-1]`,
8,
},
{
`last(filter(1..9, # % 2 == 0))`,
8,
},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
require.NoError(t, err, "compile error")
{
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
require.NoError(t, err, "compile error")
got, err := expr.Run(program, env)
require.NoError(t, err, "run error")
assert.Equal(t, tt.want, got)
})
}
for _, tt := range tests {
t.Run("Unoptimized "+tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code, expr.Optimize(false))
require.NoError(t, err, "unoptimized")
got, err := expr.Run(program, env)
require.NoError(t, err, "run error")
assert.Equal(t, tt.want, got)
}
{
program, err := expr.Compile(tt.code, expr.Optimize(false))
require.NoError(t, err, "unoptimized")
got, err := expr.Run(program, env)
require.NoError(t, err, "unoptimized")
assert.Equal(t, tt.want, got, "unoptimized")
})
}
for _, tt := range tests {
t.Run("Eval "+tt.code, func(t *testing.T) {
got, err := expr.Eval(tt.code, env)
require.NoError(t, err, "eval")
assert.Equal(t, tt.want, got, "eval")
got, err := expr.Run(program, env)
require.NoError(t, err, "unoptimized")
assert.Equal(t, tt.want, got, "unoptimized")
}
{
got, err := expr.Eval(tt.code, env)
require.NoError(t, err, "eval")
assert.Equal(t, tt.want, got, "eval")
}
})
}
}
+36
View File
@@ -0,0 +1,36 @@
package optimizer
import (
. "github.com/antonmedv/expr/ast"
)
type filterLast struct{}
func (*filterLast) Visit(node *Node) {
if member, ok := (*node).(*MemberNode); ok && member.Property != nil && !member.Optional {
if prop, ok := member.Property.(*IntegerNode); ok && prop.Value == -1 {
if filter, ok := member.Node.(*BuiltinNode); ok &&
filter.Name == "filter" &&
len(filter.Arguments) == 2 {
Patch(node, &BuiltinNode{
Name: "findLast",
Arguments: filter.Arguments,
Throws: true, // to match the behavior of filter()[-1]
})
}
}
}
if first, ok := (*node).(*BuiltinNode); ok &&
first.Name == "last" &&
len(first.Arguments) == 1 {
if filter, ok := first.Arguments[0].(*BuiltinNode); ok &&
filter.Name == "filter" &&
len(filter.Arguments) == 2 {
Patch(node, &BuiltinNode{
Name: "findLast",
Arguments: filter.Arguments,
Throws: false, // as last() will return nil if not found
})
}
}
}
+1
View File
@@ -35,5 +35,6 @@ func Optimize(node *Node, config *conf.Config) error {
Walk(node, &constRange{})
Walk(node, &filterLen{})
Walk(node, &filterFirst{})
Walk(node, &filterLast{})
return nil
}
+56
View File
@@ -221,3 +221,59 @@ func TestOptimize_filter_first(t *testing.T) {
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
}
func TestOptimize_filter_minus_1(t *testing.T) {
tree, err := parser.Parse(`filter(users, .Name == "Bob")[-1]`)
require.NoError(t, err)
err = optimizer.Optimize(&tree.Node, nil)
require.NoError(t, err)
expected := &ast.BuiltinNode{
Name: "findLast",
Arguments: []ast.Node{
&ast.IdentifierNode{Value: "users"},
&ast.ClosureNode{
Node: &ast.BinaryNode{
Operator: "==",
Left: &ast.MemberNode{
Node: &ast.PointerNode{},
Property: &ast.StringNode{Value: "Name"},
},
Right: &ast.StringNode{Value: "Bob"},
},
},
},
Throws: true,
}
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
}
func TestOptimize_filter_last(t *testing.T) {
tree, err := parser.Parse(`last(filter(users, .Name == "Bob"))`)
require.NoError(t, err)
err = optimizer.Optimize(&tree.Node, nil)
require.NoError(t, err)
expected := &ast.BuiltinNode{
Name: "findLast",
Arguments: []ast.Node{
&ast.IdentifierNode{Value: "users"},
&ast.ClosureNode{
Node: &ast.BinaryNode{
Operator: "==",
Left: &ast.MemberNode{
Node: &ast.PointerNode{},
Property: &ast.StringNode{Value: "Name"},
},
Right: &ast.StringNode{Value: "Bob"},
},
},
},
Throws: false,
}
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
}
+11 -9
View File
@@ -17,15 +17,17 @@ import (
var predicates = map[string]struct {
arity int
}{
"all": {2},
"none": {2},
"any": {2},
"one": {2},
"filter": {2},
"map": {2},
"count": {2},
"find": {2},
"findIndex": {2},
"all": {2},
"none": {2},
"any": {2},
"one": {2},
"filter": {2},
"map": {2},
"count": {2},
"find": {2},
"findIndex": {2},
"findLast": {2},
"findLastIndex": {2},
}
type parser struct {
+1 -4
View File
@@ -13,12 +13,9 @@ import (
)
var keywords = []string{
// Commands:
"exit", "opcodes", "debug",
// Predicate functions:
"map", "filter", "all",
"any", "none", "one",
// Operators:
"and", "or", "in", "not", "not in",
"contains", "matches", "startsWith", "endsWith",
+3 -1
View File
@@ -5,7 +5,7 @@ type Opcode byte
const (
OpInvalid Opcode = iota
OpPush
OpPushInt
OpInt
OpPop
OpStore
OpLoadVar
@@ -66,8 +66,10 @@ const (
OpCast
OpDeref
OpIncrementIndex
OpDecrementIndex
OpIncrementCount
OpGetIndex
OpSetIndex
OpGetCount
OpGetLen
OpPointer
+8 -2
View File
@@ -87,8 +87,8 @@ func (program *Program) Opcodes(w io.Writer) {
case OpPush:
constant("OpPush")
case OpPushInt:
argument("OpPushInt")
case OpInt:
argument("OpInt")
case OpPop:
code("OpPop")
@@ -271,12 +271,18 @@ func (program *Program) Opcodes(w io.Writer) {
case OpIncrementIndex:
code("OpIncrementIndex")
case OpDecrementIndex:
code("OpDecrementIndex")
case OpIncrementCount:
code("OpIncrementCount")
case OpGetIndex:
code("OpGetIndex")
case OpSetIndex:
code("OpSetIndex")
case OpGetCount:
code("OpGetCount")
+9 -1
View File
@@ -99,7 +99,7 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
case OpPush:
vm.push(program.Constants[arg])
case OpPushInt:
case OpInt:
vm.push(arg)
case OpPop:
@@ -438,6 +438,10 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
scope := vm.Scope()
scope.Index++
case OpDecrementIndex:
scope := vm.Scope()
scope.Index--
case OpIncrementCount:
scope := vm.Scope()
scope.Count++
@@ -445,6 +449,10 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
case OpGetIndex:
vm.push(vm.Scope().Index)
case OpSetIndex:
scope := vm.Scope()
scope.Index = vm.pop().(int)
case OpGetCount:
scope := vm.Scope()
vm.push(scope.Count)