Skip to content

Commit 28f06f1

Browse files
authored
Add ConstExpr
1 parent 93941b0 commit 28f06f1

File tree

19 files changed

+609
-282
lines changed

19 files changed

+609
-282
lines changed

bench_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,29 @@ func Benchmark_callFast(b *testing.B) {
147147
}
148148
}
149149

150+
func Benchmark_callConstExpr(b *testing.B) {
151+
env := map[string]interface{}{
152+
"Fn": func(s ...interface{}) interface{} { return s[0].(string)+s[1].(string) == s[2].(string) },
153+
}
154+
155+
program, err := expr.Compile(`Fn("a", "b", "ab")`, expr.Env(env), expr.ConstExpr("Fn"))
156+
if err != nil {
157+
b.Fatal(err)
158+
}
159+
160+
var out interface{}
161+
for n := 0; n < b.N; n++ {
162+
out, err = vm.Run(program, env)
163+
}
164+
165+
if err != nil {
166+
b.Fatal(err)
167+
}
168+
if !out.(bool) {
169+
b.Fail()
170+
}
171+
}
172+
150173
func Benchmark_largeStructAccess(b *testing.B) {
151174
type Env struct {
152175
Data [1024 * 1024 * 10]byte

cmd/exe/debugger.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ func debugger() {
2525
check(err)
2626

2727
if opt {
28-
optimizer.Optimize(&tree.Node)
28+
err = optimizer.Optimize(&tree.Node, nil)
29+
check(err)
2930
}
3031

3132
program, err := compiler.Compile(tree, nil)

cmd/exe/main.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ func printAst() {
9191
check(err)
9292

9393
if opt {
94-
optimizer.Optimize(&tree.Node)
94+
err = optimizer.Optimize(&tree.Node, nil)
95+
check(err)
9596
}
9697
}
9798

@@ -111,7 +112,8 @@ func printDisassemble() {
111112
check(err)
112113

113114
if opt {
114-
optimizer.Optimize(&tree.Node)
115+
err = optimizer.Optimize(&tree.Node, nil)
116+
check(err)
115117
}
116118
}
117119

@@ -130,7 +132,8 @@ func runProgram() {
130132
check(err)
131133

132134
if opt {
133-
optimizer.Optimize(&tree.Node)
135+
err = optimizer.Optimize(&tree.Node, nil)
136+
check(err)
134137
}
135138
}
136139

compiler/compiler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
2424
index: make(map[interface{}]uint16),
2525
locations: make(map[int]file.Location),
2626
}
27+
2728
if config != nil {
2829
c.mapEnv = config.MapEnv
2930
c.cast = config.Expect

checker/patcher.go renamed to compiler/patcher.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
package checker
1+
package compiler
22

33
import (
44
"github.com/antonmedv/expr/ast"
55
"github.com/antonmedv/expr/internal/conf"
6-
"github.com/antonmedv/expr/parser"
76
)
87

98
type operatorPatcher struct {
@@ -38,10 +37,10 @@ func (p *operatorPatcher) Exit(node *ast.Node) {
3837
}
3938
}
4039

41-
func PatchOperators(tree *parser.Tree, config *conf.Config) {
40+
func PatchOperators(node *ast.Node, config *conf.Config) {
4241
if len(config.Operators) == 0 {
4342
return
4443
}
4544
patcher := &operatorPatcher{ops: config.Operators, types: config.Types}
46-
ast.Walk(&tree.Node, patcher)
45+
ast.Walk(node, patcher)
4746
}

docs/Optimizations.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,18 @@ Will be replaced with binary operator:
6161
```
6262

6363
Ranges computed on compile stage, repleced with preallocated slices.
64+
65+
## Const expr
66+
67+
If some function marked as constant expression with `expr.ConstExpr`. It will be replaced with result
68+
of call, if all arguments are constants.
69+
70+
```go
71+
expr.ConstExpt("fib")
72+
```
73+
74+
```js
75+
fib(42)
76+
```
77+
78+
Will be replaced with result of `fib(42)` on compile step. No need to calculate it during runtime.

docs/Usage.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,34 @@ func main() {
199199
fmt.Printf("%v", visitor.identifiers) // outputs [foo bar]
200200
}
201201
```
202+
203+
## ConstExpr
204+
205+
Expr has support for constant expression evaluation during compile time.
206+
207+
```go
208+
func fib(n int) int {
209+
if n <= 1 {
210+
return n
211+
}
212+
return fib(n-1) + fib(n-2)
213+
}
214+
215+
code := `[fib(5), fib(3+3), fib(dyn)]`
216+
217+
env := map[string]interface{}{
218+
"fib": fib,
219+
"dyn": 0,
220+
}
221+
222+
options := []expr.Option{
223+
expr.Env(env),
224+
expr.ConstExpr("fib"), // Mark fib func as constant expression.
225+
}
226+
227+
program, err := expr.Compile(code, options...)
228+
```
229+
230+
Only `fib(5)` and `fib(6)` calculated on Compile, `fib(dyn)` can be called at runtime.
231+
232+
Resulting program will be equal to `[5, 8, fib(dyn)]`.

expr.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package expr
22

33
import (
44
"fmt"
5+
"github.com/antonmedv/expr/file"
56
"reflect"
67

78
"github.com/antonmedv/expr/checker"
@@ -44,17 +45,18 @@ func Eval(input string, env interface{}) (interface{}, error) {
4445
// as well as all fields of embedded structs and struct itself.
4546
// If map is passed, all items will be treated as variables.
4647
// Methods defined on this type will be available as functions.
47-
func Env(i interface{}) Option {
48+
func Env(env interface{}) Option {
4849
return func(c *conf.Config) {
49-
if _, ok := i.(map[string]interface{}); ok {
50+
if _, ok := env.(map[string]interface{}); ok {
5051
c.MapEnv = true
5152
} else {
52-
if reflect.ValueOf(i).Kind() == reflect.Map {
53-
c.DefaultType = reflect.TypeOf(i).Elem()
53+
if reflect.ValueOf(env).Kind() == reflect.Map {
54+
c.DefaultType = reflect.TypeOf(env).Elem()
5455
}
5556
}
5657
c.Strict = true
57-
c.Types = conf.CreateTypesTable(i)
58+
c.Types = conf.CreateTypesTable(env)
59+
c.Env = env
5860
}
5961
}
6062

@@ -75,6 +77,14 @@ func Operator(operator string, fn ...string) Option {
7577
}
7678
}
7779

80+
// ConstExpr defines func expression as constant. If all argument to this function is constants,
81+
// then it can be replaced by result of this func call on compile step.
82+
func ConstExpr(fn string) Option {
83+
return func(c *conf.Config) {
84+
c.ConstExpr(fn)
85+
}
86+
}
87+
7888
// AsBool tells the compiler to expect boolean result.
7989
func AsBool() Option {
8090
return func(c *conf.Config) {
@@ -106,8 +116,9 @@ func Optimize(b bool) Option {
106116
// Compile parses and compiles given input expression to bytecode program.
107117
func Compile(input string, ops ...Option) (*vm.Program, error) {
108118
config := &conf.Config{
109-
Operators: make(map[string][]string),
110-
Optimize: true,
119+
Operators: make(map[string][]string),
120+
ConstExprFns: make(map[string]reflect.Value),
121+
Optimize: true,
111122
}
112123

113124
for _, op := range ops {
@@ -127,10 +138,18 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
127138
if err != nil {
128139
return nil, err
129140
}
130-
checker.PatchOperators(tree, config)
141+
142+
// Patch operators before Optimize, as we may also mark it as ConstExpr.
143+
compiler.PatchOperators(&tree.Node, config)
131144

132145
if config.Optimize {
133-
optimizer.Optimize(&tree.Node)
146+
err = optimizer.Optimize(&tree.Node, config)
147+
if err != nil {
148+
if fileError, ok := err.(*file.Error); ok {
149+
return nil, fmt.Errorf("%v", fileError.Format(tree.Source))
150+
}
151+
return nil, err
152+
}
134153
}
135154

136155
program, err := compiler.Compile(tree, config)

expr_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,46 @@ func ExampleOperator() {
234234
// Output: true
235235
}
236236

237+
func fib(n int) int {
238+
if n <= 1 {
239+
return n
240+
}
241+
return fib(n-1) + fib(n-2)
242+
}
243+
244+
func ExampleConstExpr() {
245+
code := `[fib(5), fib(3+3), fib(dyn)]`
246+
247+
env := map[string]interface{}{
248+
"fib": fib,
249+
"dyn": 0,
250+
}
251+
252+
options := []expr.Option{
253+
expr.Env(env),
254+
expr.ConstExpr("fib"), // Mark fib func as constant expression.
255+
}
256+
257+
program, err := expr.Compile(code, options...)
258+
if err != nil {
259+
fmt.Printf("%v", err)
260+
return
261+
}
262+
263+
// Only fib(5) and fib(6) calculated on Compile, fib(dyn) can be called at runtime.
264+
env["dyn"] = 7
265+
266+
output, err := expr.Run(program, env)
267+
if err != nil {
268+
fmt.Printf("%v", err)
269+
return
270+
}
271+
272+
fmt.Printf("%v\n", output)
273+
274+
// Output: [5 8 13]
275+
}
276+
237277
func ExampleAllowUndefinedVariables() {
238278
code := `name == nil ? "Hello, world!" : sprintf("Hello, %v!", name)`
239279

@@ -866,6 +906,43 @@ func TestExpr_calls_with_nil(t *testing.T) {
866906
require.Equal(t, true, out)
867907
}
868908

909+
func TestConstExpr_error(t *testing.T) {
910+
env := map[string]interface{}{
911+
"divide": func(a, b int) int { return a / b },
912+
}
913+
914+
_, err := expr.Compile(
915+
`1 + divide(1, 0)`,
916+
expr.Env(env),
917+
expr.ConstExpr("divide"),
918+
)
919+
require.Error(t, err)
920+
require.Equal(t, "compile error: integer divide by zero (1:5)\n | 1 + divide(1, 0)\n | ....^", err.Error())
921+
}
922+
923+
func TestConstExpr_error_wrong_type(t *testing.T) {
924+
env := map[string]interface{}{
925+
"divide": 0,
926+
}
927+
928+
_, err := expr.Compile(
929+
`1 + divide(1, 0)`,
930+
expr.Env(env),
931+
expr.ConstExpr("divide"),
932+
)
933+
require.Error(t, err)
934+
require.Equal(t, "const expression \"divide\" must be a function", err.Error())
935+
}
936+
937+
func TestConstExpr_error_no_env(t *testing.T) {
938+
_, err := expr.Compile(
939+
`1 + divide(1, 0)`,
940+
expr.ConstExpr("divide"),
941+
)
942+
require.Error(t, err)
943+
require.Equal(t, "no environment for const expression: divide", err.Error())
944+
}
945+
869946
//
870947
// Mock types
871948
//

0 commit comments

Comments
 (0)