mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Add ConstExpr
This commit is contained in:
@@ -147,6 +147,29 @@ func Benchmark_callFast(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_callConstExpr(b *testing.B) {
|
||||
env := map[string]interface{}{
|
||||
"Fn": func(s ...interface{}) interface{} { return s[0].(string)+s[1].(string) == s[2].(string) },
|
||||
}
|
||||
|
||||
program, err := expr.Compile(`Fn("a", "b", "ab")`, expr.Env(env), expr.ConstExpr("Fn"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var out interface{}
|
||||
for n := 0; n < b.N; n++ {
|
||||
out, err = vm.Run(program, env)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if !out.(bool) {
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_largeStructAccess(b *testing.B) {
|
||||
type Env struct {
|
||||
Data [1024 * 1024 * 10]byte
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ func debugger() {
|
||||
check(err)
|
||||
|
||||
if opt {
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
check(err)
|
||||
}
|
||||
|
||||
program, err := compiler.Compile(tree, nil)
|
||||
|
||||
+6
-3
@@ -91,7 +91,8 @@ func printAst() {
|
||||
check(err)
|
||||
|
||||
if opt {
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
check(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +112,8 @@ func printDisassemble() {
|
||||
check(err)
|
||||
|
||||
if opt {
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
check(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +132,8 @@ func runProgram() {
|
||||
check(err)
|
||||
|
||||
if opt {
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
check(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
|
||||
index: make(map[interface{}]uint16),
|
||||
locations: make(map[int]file.Location),
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
c.mapEnv = config.MapEnv
|
||||
c.cast = config.Expect
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package checker
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/internal/conf"
|
||||
"github.com/antonmedv/expr/parser"
|
||||
)
|
||||
|
||||
type operatorPatcher struct {
|
||||
@@ -38,10 +37,10 @@ func (p *operatorPatcher) Exit(node *ast.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func PatchOperators(tree *parser.Tree, config *conf.Config) {
|
||||
func PatchOperators(node *ast.Node, config *conf.Config) {
|
||||
if len(config.Operators) == 0 {
|
||||
return
|
||||
}
|
||||
patcher := &operatorPatcher{ops: config.Operators, types: config.Types}
|
||||
ast.Walk(&tree.Node, patcher)
|
||||
ast.Walk(node, patcher)
|
||||
}
|
||||
@@ -61,3 +61,18 @@ Will be replaced with binary operator:
|
||||
```
|
||||
|
||||
Ranges computed on compile stage, repleced with preallocated slices.
|
||||
|
||||
## Const expr
|
||||
|
||||
If some function marked as constant expression with `expr.ConstExpr`. It will be replaced with result
|
||||
of call, if all arguments are constants.
|
||||
|
||||
```go
|
||||
expr.ConstExpt("fib")
|
||||
```
|
||||
|
||||
```js
|
||||
fib(42)
|
||||
```
|
||||
|
||||
Will be replaced with result of `fib(42)` on compile step. No need to calculate it during runtime.
|
||||
|
||||
@@ -199,3 +199,34 @@ func main() {
|
||||
fmt.Printf("%v", visitor.identifiers) // outputs [foo bar]
|
||||
}
|
||||
```
|
||||
|
||||
## ConstExpr
|
||||
|
||||
Expr has support for constant expression evaluation during compile time.
|
||||
|
||||
```go
|
||||
func fib(n int) int {
|
||||
if n <= 1 {
|
||||
return n
|
||||
}
|
||||
return fib(n-1) + fib(n-2)
|
||||
}
|
||||
|
||||
code := `[fib(5), fib(3+3), fib(dyn)]`
|
||||
|
||||
env := map[string]interface{}{
|
||||
"fib": fib,
|
||||
"dyn": 0,
|
||||
}
|
||||
|
||||
options := []expr.Option{
|
||||
expr.Env(env),
|
||||
expr.ConstExpr("fib"), // Mark fib func as constant expression.
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, options...)
|
||||
```
|
||||
|
||||
Only `fib(5)` and `fib(6)` calculated on Compile, `fib(dyn)` can be called at runtime.
|
||||
|
||||
Resulting program will be equal to `[5, 8, fib(dyn)]`.
|
||||
@@ -2,6 +2,7 @@ package expr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/checker"
|
||||
@@ -44,17 +45,18 @@ func Eval(input string, env interface{}) (interface{}, error) {
|
||||
// as well as all fields of embedded structs and struct itself.
|
||||
// If map is passed, all items will be treated as variables.
|
||||
// Methods defined on this type will be available as functions.
|
||||
func Env(i interface{}) Option {
|
||||
func Env(env interface{}) Option {
|
||||
return func(c *conf.Config) {
|
||||
if _, ok := i.(map[string]interface{}); ok {
|
||||
if _, ok := env.(map[string]interface{}); ok {
|
||||
c.MapEnv = true
|
||||
} else {
|
||||
if reflect.ValueOf(i).Kind() == reflect.Map {
|
||||
c.DefaultType = reflect.TypeOf(i).Elem()
|
||||
if reflect.ValueOf(env).Kind() == reflect.Map {
|
||||
c.DefaultType = reflect.TypeOf(env).Elem()
|
||||
}
|
||||
}
|
||||
c.Strict = true
|
||||
c.Types = conf.CreateTypesTable(i)
|
||||
c.Types = conf.CreateTypesTable(env)
|
||||
c.Env = env
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +77,14 @@ func Operator(operator string, fn ...string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// ConstExpr defines func expression as constant. If all argument to this function is constants,
|
||||
// then it can be replaced by result of this func call on compile step.
|
||||
func ConstExpr(fn string) Option {
|
||||
return func(c *conf.Config) {
|
||||
c.ConstExpr(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// AsBool tells the compiler to expect boolean result.
|
||||
func AsBool() Option {
|
||||
return func(c *conf.Config) {
|
||||
@@ -106,8 +116,9 @@ func Optimize(b bool) Option {
|
||||
// Compile parses and compiles given input expression to bytecode program.
|
||||
func Compile(input string, ops ...Option) (*vm.Program, error) {
|
||||
config := &conf.Config{
|
||||
Operators: make(map[string][]string),
|
||||
Optimize: true,
|
||||
Operators: make(map[string][]string),
|
||||
ConstExprFns: make(map[string]reflect.Value),
|
||||
Optimize: true,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
@@ -127,10 +138,18 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checker.PatchOperators(tree, config)
|
||||
|
||||
// Patch operators before Optimize, as we may also mark it as ConstExpr.
|
||||
compiler.PatchOperators(&tree.Node, config)
|
||||
|
||||
if config.Optimize {
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, config)
|
||||
if err != nil {
|
||||
if fileError, ok := err.(*file.Error); ok {
|
||||
return nil, fmt.Errorf("%v", fileError.Format(tree.Source))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
program, err := compiler.Compile(tree, config)
|
||||
|
||||
@@ -234,6 +234,46 @@ func ExampleOperator() {
|
||||
// Output: true
|
||||
}
|
||||
|
||||
func fib(n int) int {
|
||||
if n <= 1 {
|
||||
return n
|
||||
}
|
||||
return fib(n-1) + fib(n-2)
|
||||
}
|
||||
|
||||
func ExampleConstExpr() {
|
||||
code := `[fib(5), fib(3+3), fib(dyn)]`
|
||||
|
||||
env := map[string]interface{}{
|
||||
"fib": fib,
|
||||
"dyn": 0,
|
||||
}
|
||||
|
||||
options := []expr.Option{
|
||||
expr.Env(env),
|
||||
expr.ConstExpr("fib"), // Mark fib func as constant expression.
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, options...)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Only fib(5) and fib(6) calculated on Compile, fib(dyn) can be called at runtime.
|
||||
env["dyn"] = 7
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%v\n", output)
|
||||
|
||||
// Output: [5 8 13]
|
||||
}
|
||||
|
||||
func ExampleAllowUndefinedVariables() {
|
||||
code := `name == nil ? "Hello, world!" : sprintf("Hello, %v!", name)`
|
||||
|
||||
@@ -866,6 +906,43 @@ func TestExpr_calls_with_nil(t *testing.T) {
|
||||
require.Equal(t, true, out)
|
||||
}
|
||||
|
||||
func TestConstExpr_error(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"divide": func(a, b int) int { return a / b },
|
||||
}
|
||||
|
||||
_, err := expr.Compile(
|
||||
`1 + divide(1, 0)`,
|
||||
expr.Env(env),
|
||||
expr.ConstExpr("divide"),
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "compile error: integer divide by zero (1:5)\n | 1 + divide(1, 0)\n | ....^", err.Error())
|
||||
}
|
||||
|
||||
func TestConstExpr_error_wrong_type(t *testing.T) {
|
||||
env := map[string]interface{}{
|
||||
"divide": 0,
|
||||
}
|
||||
|
||||
_, err := expr.Compile(
|
||||
`1 + divide(1, 0)`,
|
||||
expr.Env(env),
|
||||
expr.ConstExpr("divide"),
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "const expression \"divide\" must be a function", err.Error())
|
||||
}
|
||||
|
||||
func TestConstExpr_error_no_env(t *testing.T) {
|
||||
_, err := expr.Compile(
|
||||
`1 + divide(1, 0)`,
|
||||
expr.ConstExpr("divide"),
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "no environment for const expression: divide", err.Error())
|
||||
}
|
||||
|
||||
//
|
||||
// Mock types
|
||||
//
|
||||
|
||||
+46
-18
@@ -2,42 +2,48 @@ package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antonmedv/expr/vm"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MapEnv bool
|
||||
Types TypesTable
|
||||
Operators OperatorsTable
|
||||
Expect reflect.Kind
|
||||
Optimize bool
|
||||
Strict bool
|
||||
DefaultType reflect.Type
|
||||
Env interface{}
|
||||
MapEnv bool
|
||||
Types TypesTable
|
||||
Operators OperatorsTable
|
||||
Expect reflect.Kind
|
||||
Optimize bool
|
||||
Strict bool
|
||||
DefaultType reflect.Type
|
||||
ConstExprFns map[string]reflect.Value
|
||||
err error
|
||||
}
|
||||
|
||||
func New(i interface{}) *Config {
|
||||
func New(env interface{}) *Config {
|
||||
var mapEnv bool
|
||||
var mapValueType reflect.Type
|
||||
if _, ok := i.(map[string]interface{}); ok {
|
||||
if _, ok := env.(map[string]interface{}); ok {
|
||||
mapEnv = true
|
||||
} else {
|
||||
if reflect.ValueOf(i).Kind() == reflect.Map {
|
||||
mapValueType = reflect.TypeOf(i).Elem()
|
||||
if reflect.ValueOf(env).Kind() == reflect.Map {
|
||||
mapValueType = reflect.TypeOf(env).Elem()
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
MapEnv: mapEnv,
|
||||
Types: CreateTypesTable(i),
|
||||
Optimize: true,
|
||||
Strict: true,
|
||||
DefaultType: mapValueType,
|
||||
Env: env,
|
||||
MapEnv: mapEnv,
|
||||
Types: CreateTypesTable(env),
|
||||
Optimize: true,
|
||||
Strict: true,
|
||||
DefaultType: mapValueType,
|
||||
ConstExprFns: make(map[string]reflect.Value),
|
||||
}
|
||||
}
|
||||
|
||||
// Check validates the compiler configuration.
|
||||
func (c *Config) Check() error {
|
||||
// check that all functions that define operator overloading
|
||||
// Check that all functions that define operator overloading
|
||||
// exist in environment and have correct signatures.
|
||||
for op, fns := range c.Operators {
|
||||
for _, fn := range fns {
|
||||
@@ -54,5 +60,27 @@ func (c *Config) Check() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
// Check that all ConstExprFns are functions.
|
||||
for name, fn := range c.ConstExprFns {
|
||||
if fn.Kind() != reflect.Func {
|
||||
return fmt.Errorf("const expression %q must be a function", name)
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *Config) ConstExpr(name string) {
|
||||
if c.Env == nil {
|
||||
c.Error(fmt.Errorf("no environment for const expression: %v", name))
|
||||
return
|
||||
}
|
||||
c.ConstExprFns[name] = vm.FetchFn(c.Env, name)
|
||||
}
|
||||
|
||||
func (c *Config) Error(err error) {
|
||||
if c.err == nil {
|
||||
c.err = err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type constExpr struct {
|
||||
applied bool
|
||||
err error
|
||||
fns map[string]reflect.Value
|
||||
}
|
||||
|
||||
func (*constExpr) Enter(*Node) {}
|
||||
func (c *constExpr) Exit(node *Node) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
msg := fmt.Sprintf("%v", r)
|
||||
// Make message more actual, it's a runtime error, but at compile step.
|
||||
msg = strings.Replace(msg, "runtime error:", "compile error:", 1)
|
||||
c.err = &file.Error{
|
||||
Location: (*node).Location(),
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
patch := func(newNode Node) {
|
||||
c.applied = true
|
||||
patch(node, newNode)
|
||||
}
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *FunctionNode:
|
||||
fn, ok := c.fns[n.Name]
|
||||
if ok {
|
||||
in := make([]reflect.Value, len(n.Arguments))
|
||||
for i := 0; i < len(n.Arguments); i++ {
|
||||
arg := n.Arguments[i]
|
||||
var param interface{}
|
||||
|
||||
switch a := arg.(type) {
|
||||
case *NilNode:
|
||||
param = nil
|
||||
case *IntegerNode:
|
||||
param = a.Value
|
||||
case *FloatNode:
|
||||
param = a.Value
|
||||
case *BoolNode:
|
||||
param = a.Value
|
||||
case *StringNode:
|
||||
param = a.Value
|
||||
case *ConstantNode:
|
||||
param = a.Value
|
||||
|
||||
default:
|
||||
return // Const expr optimization not applicable.
|
||||
}
|
||||
|
||||
if param == nil && reflect.TypeOf(param) == nil {
|
||||
// In case of nil value and nil type use this hack,
|
||||
// otherwise reflect.Call will panic on zero value.
|
||||
in[i] = reflect.ValueOf(¶m).Elem()
|
||||
} else {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
|
||||
out := fn.Call(in)
|
||||
constNode := &ConstantNode{Value: out[0].Interface()}
|
||||
patch(constNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type constRange struct{}
|
||||
|
||||
func (*constRange) Enter(*Node) {}
|
||||
func (*constRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == ".." {
|
||||
if min, ok := n.Left.(*IntegerNode); ok {
|
||||
if max, ok := n.Right.(*IntegerNode); ok {
|
||||
size := max.Value - min.Value + 1
|
||||
// In this case array is too big. Skip generation,
|
||||
// and wait for memory budget detection on runtime.
|
||||
if size > 1e6 {
|
||||
return
|
||||
}
|
||||
value := make([]int, size)
|
||||
for i := range value {
|
||||
value[i] = min.Value + i
|
||||
}
|
||||
patch(node, &ConstantNode{
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type fold struct {
|
||||
applied bool
|
||||
}
|
||||
|
||||
func (*fold) Enter(*Node) {}
|
||||
func (fold *fold) Exit(node *Node) {
|
||||
patch := func(newNode Node) {
|
||||
fold.applied = true
|
||||
patch(node, newNode)
|
||||
}
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *UnaryNode:
|
||||
switch n.Operator {
|
||||
case "-":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: -i.Value})
|
||||
}
|
||||
case "+":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: i.Value})
|
||||
}
|
||||
}
|
||||
|
||||
case *BinaryNode:
|
||||
switch n.Operator {
|
||||
case "+":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value + b.Value})
|
||||
}
|
||||
}
|
||||
if a, ok := n.Left.(*StringNode); ok {
|
||||
if b, ok := n.Right.(*StringNode); ok {
|
||||
patch(&StringNode{Value: a.Value + b.Value})
|
||||
}
|
||||
}
|
||||
case "-":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value - b.Value})
|
||||
}
|
||||
}
|
||||
case "*":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value * b.Value})
|
||||
}
|
||||
}
|
||||
case "/":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value / b.Value})
|
||||
}
|
||||
}
|
||||
case "%":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value % b.Value})
|
||||
}
|
||||
}
|
||||
case "**":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&FloatNode{Value: math.Pow(float64(a.Value), float64(b.Value))})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ArrayNode:
|
||||
if len(n.Nodes) > 0 {
|
||||
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]int, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*IntegerNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]string, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*StringNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type inArray struct{}
|
||||
|
||||
func (*inArray) Enter(*Node) {}
|
||||
func (*inArray) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if array, ok := n.Right.(*ArrayNode); ok {
|
||||
if len(array.Nodes) > 0 {
|
||||
t := n.Left.Type()
|
||||
if t == nil || t.Kind() != reflect.Int {
|
||||
// This optimization can be only performed if left side is int type,
|
||||
// as runtime.in func uses reflect.Map.MapIndex and keys of map must,
|
||||
// be same as checked value type.
|
||||
goto string
|
||||
}
|
||||
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(map[int]struct{})
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*IntegerNode).Value] = struct{}{}
|
||||
}
|
||||
patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(map[string]struct{})
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*StringNode).Value] = struct{}{}
|
||||
}
|
||||
patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
)
|
||||
|
||||
type inRange struct{}
|
||||
|
||||
func (*inRange) Enter(*Node) {}
|
||||
func (*inRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if rng, ok := n.Right.(*BinaryNode); ok && rng.Operator == ".." {
|
||||
if from, ok := rng.Left.(*IntegerNode); ok {
|
||||
if to, ok := rng.Right.(*IntegerNode); ok {
|
||||
patch(node, &BinaryNode{
|
||||
Operator: "and",
|
||||
Left: &BinaryNode{
|
||||
Operator: ">=",
|
||||
Left: n.Left,
|
||||
Right: from,
|
||||
},
|
||||
Right: &BinaryNode{
|
||||
Operator: "<=",
|
||||
Left: n.Left,
|
||||
Right: to,
|
||||
},
|
||||
})
|
||||
if n.Operator == "not in" {
|
||||
patch(node, &UnaryNode{
|
||||
Operator: "not",
|
||||
Node: *node,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-241
@@ -1,258 +1,36 @@
|
||||
package optimizer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
. "github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/internal/conf"
|
||||
)
|
||||
|
||||
type inArray struct{}
|
||||
type fold struct {
|
||||
applied bool
|
||||
}
|
||||
type inRange struct{}
|
||||
type constRange struct{}
|
||||
|
||||
func Optimize(node *Node) {
|
||||
func Optimize(node *Node, config *conf.Config) error {
|
||||
Walk(node, &inArray{})
|
||||
limit := 1000
|
||||
for {
|
||||
for limit := 1000; limit >= 0; limit-- {
|
||||
fold := &fold{}
|
||||
Walk(node, fold)
|
||||
limit--
|
||||
if !fold.applied || limit == 0 {
|
||||
if !fold.applied {
|
||||
break
|
||||
}
|
||||
}
|
||||
if config != nil && len(config.ConstExprFns) > 0 {
|
||||
for limit := 100; limit >= 0; limit-- {
|
||||
constExpr := &constExpr{
|
||||
fns: config.ConstExprFns,
|
||||
}
|
||||
Walk(node, constExpr)
|
||||
if constExpr.err != nil {
|
||||
return constExpr.err
|
||||
}
|
||||
if !constExpr.applied {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Walk(node, &inRange{})
|
||||
Walk(node, &constRange{})
|
||||
}
|
||||
|
||||
type Map map[int]struct{}
|
||||
|
||||
func (m Map) MarshalJSON() ([]byte, error) {
|
||||
array := make([]int, 0, len(m))
|
||||
for key := range m {
|
||||
array = append(array, key)
|
||||
}
|
||||
return json.Marshal(array)
|
||||
}
|
||||
|
||||
func (*inArray) Enter(node *Node) {}
|
||||
func (*inArray) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if array, ok := n.Right.(*ArrayNode); ok {
|
||||
if len(array.Nodes) > 0 {
|
||||
t := n.Left.Type()
|
||||
if t == nil || t.Kind() != reflect.Int {
|
||||
// This optimization can be only performed if left side is int type,
|
||||
// as runtime.in func uses reflect.Map.MapIndex and keys of map must,
|
||||
// be same as checked value type.
|
||||
goto string
|
||||
}
|
||||
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(Map)
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*IntegerNode).Value] = struct{}{}
|
||||
}
|
||||
patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range array.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make(map[string]struct{})
|
||||
for _, a := range array.Nodes {
|
||||
value[a.(*StringNode).Value] = struct{}{}
|
||||
}
|
||||
patch(node, &BinaryNode{
|
||||
Operator: n.Operator,
|
||||
Left: n.Left,
|
||||
Right: &ConstantNode{Value: value},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*fold) Enter(node *Node) {}
|
||||
func (fold *fold) Exit(node *Node) {
|
||||
patch := func(newNode Node) {
|
||||
fold.applied = true
|
||||
patch(node, newNode)
|
||||
}
|
||||
|
||||
switch n := (*node).(type) {
|
||||
case *UnaryNode:
|
||||
switch n.Operator {
|
||||
case "-":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: -i.Value})
|
||||
}
|
||||
case "+":
|
||||
if i, ok := n.Node.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: i.Value})
|
||||
}
|
||||
}
|
||||
|
||||
case *BinaryNode:
|
||||
switch n.Operator {
|
||||
case "+":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value + b.Value})
|
||||
}
|
||||
}
|
||||
if a, ok := n.Left.(*StringNode); ok {
|
||||
if b, ok := n.Right.(*StringNode); ok {
|
||||
patch(&StringNode{Value: a.Value + b.Value})
|
||||
}
|
||||
}
|
||||
case "-":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value - b.Value})
|
||||
}
|
||||
}
|
||||
case "*":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value * b.Value})
|
||||
}
|
||||
}
|
||||
case "/":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value / b.Value})
|
||||
}
|
||||
}
|
||||
case "%":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&IntegerNode{Value: a.Value % b.Value})
|
||||
}
|
||||
}
|
||||
case "**":
|
||||
if a, ok := n.Left.(*IntegerNode); ok {
|
||||
if b, ok := n.Right.(*IntegerNode); ok {
|
||||
patch(&FloatNode{Value: math.Pow(float64(a.Value), float64(b.Value))})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ArrayNode:
|
||||
if len(n.Nodes) > 0 {
|
||||
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*IntegerNode); !ok {
|
||||
goto string
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]int, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*IntegerNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
string:
|
||||
for _, a := range n.Nodes {
|
||||
if _, ok := a.(*StringNode); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
{
|
||||
value := make([]string, len(n.Nodes))
|
||||
for i, a := range n.Nodes {
|
||||
value[i] = a.(*StringNode).Value
|
||||
}
|
||||
patch(&ConstantNode{Value: value})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*inRange) Enter(node *Node) {}
|
||||
func (*inRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == "in" || n.Operator == "not in" {
|
||||
if rng, ok := n.Right.(*BinaryNode); ok && rng.Operator == ".." {
|
||||
if from, ok := rng.Left.(*IntegerNode); ok {
|
||||
if to, ok := rng.Right.(*IntegerNode); ok {
|
||||
patch(node, &BinaryNode{
|
||||
Operator: "and",
|
||||
Left: &BinaryNode{
|
||||
Operator: ">=",
|
||||
Left: n.Left,
|
||||
Right: from,
|
||||
},
|
||||
Right: &BinaryNode{
|
||||
Operator: "<=",
|
||||
Left: n.Left,
|
||||
Right: to,
|
||||
},
|
||||
})
|
||||
if n.Operator == "not in" {
|
||||
patch(node, &UnaryNode{
|
||||
Operator: "not",
|
||||
Node: *node,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*constRange) Enter(node *Node) {}
|
||||
func (*constRange) Exit(node *Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *BinaryNode:
|
||||
if n.Operator == ".." {
|
||||
if min, ok := n.Left.(*IntegerNode); ok {
|
||||
if max, ok := n.Right.(*IntegerNode); ok {
|
||||
size := max.Value - min.Value + 1
|
||||
// In this case array is too big. Skip generation,
|
||||
// and wait for memory budget detection on runtime.
|
||||
if size > 1e6 {
|
||||
return
|
||||
}
|
||||
value := make([]int, size)
|
||||
for i := range value {
|
||||
value[i] = min.Value + i
|
||||
}
|
||||
patch(node, &ConstantNode{
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func patch(node *Node, newNode Node) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package optimizer_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
@@ -16,7 +17,8 @@ func TestOptimize_constant_folding(t *testing.T) {
|
||||
tree, err := parser.Parse(`[1,2,3][5*5-25]`)
|
||||
require.NoError(t, err)
|
||||
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &ast.IndexNode{
|
||||
Node: &ast.ConstantNode{Value: []int{1, 2, 3}},
|
||||
@@ -35,12 +37,13 @@ func TestOptimize_in_array(t *testing.T) {
|
||||
_, err = checker.Check(tree, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &ast.BinaryNode{
|
||||
Operator: "in",
|
||||
Left: &ast.IdentifierNode{Value: "v"},
|
||||
Right: &ast.ConstantNode{Value: optimizer.Map{1: {}, 2: {}, 3: {}}},
|
||||
Right: &ast.ConstantNode{Value: map[int]struct{}{1: {}, 2: {}, 3: {}}},
|
||||
}
|
||||
|
||||
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
|
||||
@@ -50,7 +53,8 @@ func TestOptimize_in_range(t *testing.T) {
|
||||
tree, err := parser.Parse(`age in 18..31`)
|
||||
require.NoError(t, err)
|
||||
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
left := &ast.IdentifierNode{
|
||||
Value: "age",
|
||||
@@ -80,7 +84,8 @@ func TestOptimize_const_range(t *testing.T) {
|
||||
tree, err := parser.Parse(`-1..1`)
|
||||
require.NoError(t, err)
|
||||
|
||||
optimizer.Optimize(&tree.Node)
|
||||
err = optimizer.Optimize(&tree.Node, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &ast.ConstantNode{
|
||||
Value: []int{-1, 0, 1},
|
||||
@@ -88,3 +93,24 @@ func TestOptimize_const_range(t *testing.T) {
|
||||
|
||||
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
|
||||
}
|
||||
|
||||
func TestOptimize_const_expr(t *testing.T) {
|
||||
tree, err := parser.Parse(`upper("hello")`)
|
||||
require.NoError(t, err)
|
||||
|
||||
env := map[string]interface{}{
|
||||
"upper": strings.ToUpper,
|
||||
}
|
||||
|
||||
config := conf.New(env)
|
||||
config.ConstExpr("upper")
|
||||
|
||||
err = optimizer.Optimize(&tree.Node, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &ast.ConstantNode{
|
||||
Value: "HELLO",
|
||||
}
|
||||
|
||||
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
|
||||
}
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ func slice(array, from, to interface{}) interface{} {
|
||||
panic(fmt.Sprintf("cannot slice %v", from))
|
||||
}
|
||||
|
||||
func fetchFn(from interface{}, name string) reflect.Value {
|
||||
func FetchFn(from interface{}, name string) reflect.Value {
|
||||
v := reflect.ValueOf(from)
|
||||
|
||||
// Methods can be defined on any type.
|
||||
|
||||
@@ -272,7 +272,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
out := fetchFn(env, call.Name).Call(in)
|
||||
out := FetchFn(env, call.Name).Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
|
||||
case OpCallFast:
|
||||
@@ -281,7 +281,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} {
|
||||
for i := call.Size - 1; i >= 0; i-- {
|
||||
in[i] = vm.pop()
|
||||
}
|
||||
fn := fetchFn(env, call.Name).Interface()
|
||||
fn := FetchFn(env, call.Name).Interface()
|
||||
vm.push(fn.(func(...interface{}) interface{})(in...))
|
||||
|
||||
case OpMethod:
|
||||
@@ -297,7 +297,7 @@ func (vm *VM) Run(program *Program, env interface{}) interface{} {
|
||||
in[i] = reflect.ValueOf(param)
|
||||
}
|
||||
}
|
||||
out := fetchFn(vm.pop(), call.Name).Call(in)
|
||||
out := FetchFn(vm.pop(), call.Name).Call(in)
|
||||
vm.push(out[0].Interface())
|
||||
|
||||
case OpArray:
|
||||
|
||||
Reference in New Issue
Block a user