Skip to content

Commit aad1dca

Browse files
authored
Add ability to return errors from custom functions. (#159)
Also provide initial documentation on using and providing functions.
1 parent da4c23e commit aad1dca

File tree

6 files changed

+323
-6
lines changed

6 files changed

+323
-6
lines changed

checker/checker.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"github.com/antonmedv/expr/parser"
1111
)
1212

13+
var errorType = reflect.TypeOf((*error)(nil)).Elem()
14+
1315
func Check(tree *parser.Tree, config *conf.Config) (reflect.Type, error) {
1416
v := &visitor{
1517
collections: make([]reflect.Type, 0),
@@ -338,8 +340,11 @@ func (v *visitor) FunctionNode(node *ast.FunctionNode) reflect.Type {
338340
if !isInterface(fn) &&
339341
fn.IsVariadic() &&
340342
fn.NumIn() == inputParamsCount &&
341-
fn.NumOut() == 1 &&
342-
fn.Out(0).Kind() == reflect.Interface {
343+
((fn.NumOut() == 1 && // Function with one return value
344+
fn.Out(0).Kind() == reflect.Interface) ||
345+
(fn.NumOut() == 2 && // Function with one return value and an error
346+
fn.Out(0).Kind() == reflect.Interface &&
347+
fn.Out(1) == errorType)) {
343348
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
344349
if rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
345350
node.Fast = true
@@ -380,8 +385,8 @@ func (v *visitor) checkFunc(fn reflect.Type, method bool, node ast.Node, name st
380385
if fn.NumOut() == 0 {
381386
return v.error(node, "func %v doesn't return value", name)
382387
}
383-
if fn.NumOut() != 1 {
384-
return v.error(node, "func %v returns more then one value", name)
388+
if numOut := fn.NumOut(); numOut > 2 {
389+
return v.error(node, "func %v returns more then two values", name)
385390
}
386391

387392
numIn := fn.NumIn()

docs/Custom-Functions.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Custom functions
2+
3+
User can provide custom functions in environment.
4+
This functions can either be defined as functions or as methods.
5+
6+
Functions can be typed, in which case if any of the arguments passed to such function will not match required types - error will be returned.
7+
8+
By default, function need to return at least one value.
9+
Other return values will be silently skipped.
10+
11+
Only exception is if second return value is `error`, in which case returned error will be returned if it is non-nil.
12+
Read about returning errors below.
13+
14+
## Functions
15+
16+
Simple example of custom functions would be to define function in map which will be used as environment:
17+
18+
```go
19+
package main
20+
21+
import (
22+
"fmt"
23+
"github.com/antonmedv/expr"
24+
)
25+
26+
func main() {
27+
env := map[string]interface{}{
28+
"foo": 1,
29+
"double": func(i int) int { return i * 2 },
30+
}
31+
32+
out, err := expr.Eval("double(foo)", env)
33+
34+
if err != nil {
35+
panic(err)
36+
}
37+
fmt.Print(out)
38+
}
39+
```
40+
41+
## Methods
42+
43+
Methods can be defined on type that is provided as environment.
44+
45+
Methods MUST be exported in order to be callable.
46+
47+
```go
48+
package main
49+
50+
import (
51+
"fmt"
52+
"time"
53+
54+
"github.com/antonmedv/expr"
55+
)
56+
57+
type Env struct {
58+
Tweets []Tweet
59+
}
60+
61+
// Methods defined on such struct will be functions.
62+
func (Env) Format(t time.Time) string { return t.Format(time.RFC822) }
63+
64+
type Tweet struct {
65+
Text string
66+
Date time.Time
67+
}
68+
69+
func main() {
70+
code := `map(filter(Tweets, {len(.Text) > 0}), {.Text + Format(.Date)})`
71+
72+
// We can use an empty instance of the struct as an environment.
73+
program, err := expr.Compile(code, expr.Env(Env{}))
74+
if err != nil {
75+
panic(err)
76+
}
77+
78+
env := Env{
79+
Tweets: []Tweet{{"Oh My God!", time.Now()}, {"How you doin?", time.Now()}, {"Could I be wearing any more clothes?", time.Now()}},
80+
}
81+
82+
output, err := expr.Run(program, env)
83+
if err != nil {
84+
panic(err)
85+
}
86+
87+
fmt.Print(output)
88+
}
89+
```
90+
91+
## Fast functions
92+
93+
Fast functions are functions that don't use reflection for calling them.
94+
This improves performance but drops ability to have typed arguments.
95+
96+
Such functions have strict signatures for them:
97+
```go
98+
func(...interface{}) interface{}
99+
```
100+
or
101+
```go
102+
func(...interface{}) (interface{}, error)
103+
```
104+
105+
Methods can also be used as fast functions if they will have signature specified above.
106+
107+
Example:
108+
```go
109+
package main
110+
111+
import (
112+
"fmt"
113+
"github.com/antonmedv/expr"
114+
)
115+
116+
type Env map[string]interface{}
117+
118+
func (Env) FastMethod(...interface{}) interface{} {
119+
return "Hello, "
120+
}
121+
122+
func main() {
123+
env := Env{
124+
"fast_func": func(...interface{}) interface{} { return "world" },
125+
}
126+
127+
out, err := expr.Eval("FastMethod() + fast_func()", env)
128+
129+
if err != nil {
130+
panic(err)
131+
}
132+
fmt.Print(out)
133+
}
134+
```
135+
136+
## Returning errors
137+
138+
Both normal and fast functions can return `error`s as second return value.
139+
In this case if function will return any value and non-nil error - such error will be returned to the caller.
140+
141+
```go
142+
package main
143+
144+
import (
145+
"errors"
146+
"fmt"
147+
"github.com/antonmedv/expr"
148+
)
149+
150+
func main() {
151+
env := map[string]interface{}{
152+
"foo": -1,
153+
"double": func(i int) (int, error) {
154+
if i < 0 {
155+
return 0, errors.New("value cannot be less than zero")
156+
}
157+
return i * 2
158+
},
159+
}
160+
161+
out, err := expr.Eval("double(foo)", env)
162+
163+
// This `err` will be the one returned from `double` function.
164+
// err.Error() == "value cannot be less than zero"
165+
if err != nil {
166+
panic(err)
167+
}
168+
fmt.Print(out)
169+
}
170+
```
171+
172+
* [Contents](README.md)
173+
* Next: [Operator Override](Operator-Override.md)

docs/Getting-Started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ func main() {
112112
```
113113

114114
* [Contents](README.md)
115-
* Next: [Operator Override](Operator-Override.md)
115+
* Next: [Custom functions](Custom-Functions.md)

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* [Getting Started](Getting-Started.md)
44
* [Language Definition](Language-Definition.md)
5+
* [Custom functions](Custom-Functions.md)
56
* [Operator Override](Operator-Override.md)
67
* [Visitor and Patch](Visitor-and-Patch.md)
78
* [Optimizations](Optimizations.md)

vm/vm.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/antonmedv/expr/file"
1010
)
1111

12+
var errorType = reflect.TypeOf((*error)(nil)).Elem()
13+
1214
var (
1315
MemoryBudget int = 1e6
1416
)
@@ -290,6 +292,9 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
290292
}
291293
}
292294
out := FetchFn(env, call.Name).Call(in)
295+
if len(out) == 2 && out[1].Type() == errorType && !out[1].IsNil() {
296+
return nil, out[1].Interface().(error)
297+
}
293298
vm.push(out[0].Interface())
294299

295300
case OpCallFast:
@@ -299,7 +304,15 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
299304
in[i] = vm.pop()
300305
}
301306
fn := FetchFn(env, call.Name).Interface()
302-
vm.push(fn.(func(...interface{}) interface{})(in...))
307+
if typed, ok := fn.(func(...interface{}) interface{}); ok {
308+
vm.push(typed(in...))
309+
} else if typed, ok := fn.(func(...interface{}) (interface{}, error)); ok {
310+
res, err := typed(in...)
311+
if err != nil {
312+
return nil, err
313+
}
314+
vm.push(res)
315+
}
303316

304317
case OpMethod:
305318
call := vm.constants[vm.arg()].(Call)
@@ -315,6 +328,9 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
315328
}
316329
}
317330
out := FetchFn(vm.pop(), call.Name).Call(in)
331+
if len(out) == 2 && out[1].Type() == errorType && !out[1].IsNil() {
332+
return nil, out[1].Interface().(error)
333+
}
318334
vm.push(out[0].Interface())
319335

320336
case OpMethodNilSafe:

0 commit comments

Comments
 (0)