Skip to content

Commit

Permalink
Add ConstExpr
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv authored Mar 10, 2020
1 parent 93941b0 commit 28f06f1
Show file tree
Hide file tree
Showing 19 changed files with 609 additions and 282 deletions.
23 changes: 23 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion cmd/exe/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions cmd/exe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ func printAst() {
check(err)

if opt {
optimizer.Optimize(&tree.Node)
err = optimizer.Optimize(&tree.Node, nil)
check(err)
}
}

Expand All @@ -111,7 +112,8 @@ func printDisassemble() {
check(err)

if opt {
optimizer.Optimize(&tree.Node)
err = optimizer.Optimize(&tree.Node, nil)
check(err)
}
}

Expand All @@ -130,7 +132,8 @@ func runProgram() {
check(err)

if opt {
optimizer.Optimize(&tree.Node)
err = optimizer.Optimize(&tree.Node, nil)
check(err)
}
}

Expand Down
1 change: 1 addition & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions checker/patcher.go → compiler/patcher.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
}
15 changes: 15 additions & 0 deletions docs/Optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
31 changes: 31 additions & 0 deletions docs/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]`.
37 changes: 28 additions & 9 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package expr

import (
"fmt"
"github.com/antonmedv/expr/file"
"reflect"

"github.com/antonmedv/expr/checker"
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
77 changes: 77 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

Expand Down Expand Up @@ -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
//
Expand Down
Loading

0 comments on commit 28f06f1

Please sign in to comment.