mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Add filter()[-1] optimization
This commit is contained in:
+13
-3
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
| .................^
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user