Skip to content
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
13 changes: 9 additions & 4 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/antonmedv/expr/parser"
)

var errorType = reflect.TypeOf((*error)(nil)).Elem()

func Check(tree *parser.Tree, config *conf.Config) (reflect.Type, error) {
v := &visitor{
collections: make([]reflect.Type, 0),
Expand Down Expand Up @@ -334,8 +336,11 @@ func (v *visitor) FunctionNode(node *ast.FunctionNode) reflect.Type {
if !isInterface(fn) &&
fn.IsVariadic() &&
fn.NumIn() == inputParamsCount &&
fn.NumOut() == 1 &&
fn.Out(0).Kind() == reflect.Interface {
((fn.NumOut() == 1 && // Function with one return value
fn.Out(0).Kind() == reflect.Interface) ||
(fn.NumOut() == 2 && // Function with one return value and an error
fn.Out(0).Kind() == reflect.Interface &&
fn.Out(1) == errorType)) {
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
if rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
node.Fast = true
Expand Down Expand Up @@ -373,8 +378,8 @@ func (v *visitor) checkFunc(fn reflect.Type, method bool, node ast.Node, name st
if fn.NumOut() == 0 {
return v.error(node, "func %v doesn't return value", name)
}
if fn.NumOut() != 1 {
return v.error(node, "func %v returns more then one value", name)
if numOut := fn.NumOut(); numOut > 2 {
return v.error(node, "func %v returns more then two values", name)
}

numIn := fn.NumIn()
Expand Down
173 changes: 173 additions & 0 deletions docs/Custom-Functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Custom functions

User can provide custom functions in environment.
This functions can either be defined as functions or as methods.

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.

By default, function need to return at least one value.
Other return values will be silently skipped.

Only exception is if second return value is `error`, in which case returned error will be returned if it is non-nil.
Read about returning errors below.

## Functions

Simple example of custom functions would be to define function in map which will be used as environment:

```go
package main

import (
"fmt"
"github.com/antonmedv/expr"
)

func main() {
env := map[string]interface{}{
"foo": 1,
"double": func(i int) int { return i * 2 },
}

out, err := expr.Eval("double(foo)", env)

if err != nil {
panic(err)
}
fmt.Print(out)
}
```

## Methods

Methods can be defined on type that is provided as environment.

Methods MUST be exported in order to be callable.

```go
package main

import (
"fmt"
"time"

"github.com/antonmedv/expr"
)

type Env struct {
Tweets []Tweet
}

// Methods defined on such struct will be functions.
func (Env) Format(t time.Time) string { return t.Format(time.RFC822) }

type Tweet struct {
Text string
Date time.Time
}

func main() {
code := `map(filter(Tweets, {len(.Text) > 0}), {.Text + Format(.Date)})`

// We can use an empty instance of the struct as an environment.
program, err := expr.Compile(code, expr.Env(Env{}))
if err != nil {
panic(err)
}

env := Env{
Tweets: []Tweet{{"Oh My God!", time.Now()}, {"How you doin?", time.Now()}, {"Could I be wearing any more clothes?", time.Now()}},
}

output, err := expr.Run(program, env)
if err != nil {
panic(err)
}

fmt.Print(output)
}
```

## Fast functions

Fast functions are functions that don't use reflection for calling them.
This improves performance but drops ability to have typed arguments.

Such functions have strict signatures for them:
```go
func(...interface{}) interface{}
```
or
```go
func(...interface{}) (interface{}, error)
```

Methods can also be used as fast functions if they will have signature specified above.

Example:
```go
package main

import (
"fmt"
"github.com/antonmedv/expr"
)

type Env map[string]interface{}

func (Env) FastMethod(...interface{}) interface{} {
return "Hello, "
}

func main() {
env := Env{
"fast_func": func(...interface{}) interface{} { return "world" },
}

out, err := expr.Eval("FastMethod() + fast_func()", env)

if err != nil {
panic(err)
}
fmt.Print(out)
}
```

## Returning errors

Both normal and fast functions can return `error`s as second return value.
In this case if function will return any value and non-nil error - such error will be returned to the caller.

```go
package main

import (
"errors"
"fmt"
"github.com/antonmedv/expr"
)

func main() {
env := map[string]interface{}{
"foo": -1,
"double": func(i int) (int, error) {
if i < 0 {
return 0, errors.New("value cannot be less than zero")
}
return i * 2
},
}

out, err := expr.Eval("double(foo)", env)

// This `err` will be the one returned from `double` function.
// err.Error() == "value cannot be less than zero"
if err != nil {
panic(err)
}
fmt.Print(out)
}
```

* [Contents](README.md)
* Next: [Operator Override](Operator-Override.md)
2 changes: 1 addition & 1 deletion docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ func main() {
```

* [Contents](README.md)
* Next: [Operator Override](Operator-Override.md)
* Next: [Custom functions](Custom-Functions.md)
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* [Getting Started](Getting-Started.md)
* [Language Definition](Language-Definition.md)
* [Custom functions](Custom-Functions.md)
* [Operator Override](Operator-Override.md)
* [Visitor and Patch](Visitor-and-Patch.md)
* [Optimizations](Optimizations.md)
18 changes: 17 additions & 1 deletion vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/antonmedv/expr/file"
)

var errorType = reflect.TypeOf((*error)(nil)).Elem()

var (
MemoryBudget int = 1e6
)
Expand Down Expand Up @@ -282,6 +284,9 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
}
}
out := FetchFn(env, call.Name).Call(in)
if len(out) == 2 && out[1].Type() == errorType && !out[1].IsNil() {
return nil, out[1].Interface().(error)
}
vm.push(out[0].Interface())

case OpCallFast:
Expand All @@ -291,7 +296,15 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
in[i] = vm.pop()
}
fn := FetchFn(env, call.Name).Interface()
vm.push(fn.(func(...interface{}) interface{})(in...))
if typed, ok := fn.(func(...interface{}) interface{}); ok {
vm.push(typed(in...))
} else if typed, ok := fn.(func(...interface{}) (interface{}, error)); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CallFast is a special case. We don't want to change the signature of call fast methods. The idea for call fast funcs is what they are created manually for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't change it here, but rather allowing to add a new one, that also returns an error.

So both variants will work.

res, err := typed(in...)
if err != nil {
return nil, err
}
vm.push(res)
}

case OpMethod:
call := vm.constants[vm.arg()].(Call)
Expand All @@ -307,6 +320,9 @@ func (vm *VM) Run(program *Program, env interface{}) (out interface{}, err error
}
}
out := FetchFn(vm.pop(), call.Name).Call(in)
if len(out) == 2 && out[1].Type() == errorType && !out[1].IsNil() {
return nil, out[1].Interface().(error)
}
vm.push(out[0].Interface())

case OpArray:
Expand Down
Loading