Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConstExpr #96

Merged
merged 7 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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