diff --git a/checker/checker.go b/checker/checker.go index ec66daf44..59930ff01 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -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), @@ -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 @@ -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() diff --git a/docs/Custom-Functions.md b/docs/Custom-Functions.md new file mode 100644 index 000000000..133383f77 --- /dev/null +++ b/docs/Custom-Functions.md @@ -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) diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 52f6ee7a2..0ee2a8f59 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -112,4 +112,4 @@ func main() { ``` * [Contents](README.md) -* Next: [Operator Override](Operator-Override.md) +* Next: [Custom functions](Custom-Functions.md) diff --git a/docs/README.md b/docs/README.md index be20a44ea..39713bf61 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) diff --git a/vm/vm.go b/vm/vm.go index 1ce4a72bd..1c8fe4b97 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -9,6 +9,8 @@ import ( "github.com/antonmedv/expr/file" ) +var errorType = reflect.TypeOf((*error)(nil)).Elem() + var ( MemoryBudget int = 1e6 ) @@ -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: @@ -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 { + res, err := typed(in...) + if err != nil { + return nil, err + } + vm.push(res) + } case OpMethod: call := vm.constants[vm.arg()].(Call) @@ -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: diff --git a/vm/vm_test.go b/vm/vm_test.go index c467e809e..8b0146c97 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -1,10 +1,12 @@ package vm_test import ( + "errors" "fmt" "reflect" "testing" + "github.com/antonmedv/expr/ast" "github.com/antonmedv/expr/checker" "github.com/antonmedv/expr/compiler" "github.com/antonmedv/expr/conf" @@ -107,3 +109,123 @@ func TestRun_memory_budget(t *testing.T) { _, err = vm.Run(program, nil) require.Error(t, err) } + +func TestRun_fast_function_with_error(t *testing.T) { + input := `WillError()` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := map[string]interface{}{ + "WillError": func(...interface{}) (interface{}, error) { return 1, errors.New("error") }, + } + funcConf := conf.New(env) + _, err = checker.Check(tree, funcConf) + require.NoError(t, err) + + require.True(t, tree.Node.(*ast.FunctionNode).Fast, "function must be fast") + program, err := compiler.Compile(tree, funcConf) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.EqualError(t, err, "error") + + require.Equal(t, nil, out) +} + +type ErrorEnv struct { + InnerEnv InnerEnv +} +type InnerEnv struct{} + +func (ErrorEnv) WillError() (bool, error) { + return false, errors.New("method error") +} + +func (ErrorEnv) FastError(...interface{}) (interface{}, error) { + return true, nil +} + +func (InnerEnv) WillError() (bool, error) { + return false, errors.New("inner error") +} + +func TestRun_method_with_error(t *testing.T) { + input := `WillError()` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := ErrorEnv{} + funcConf := conf.New(env) + _, err = checker.Check(tree, funcConf) + require.NoError(t, err) + + program, err := compiler.Compile(tree, funcConf) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.EqualError(t, err, "method error") + + require.Equal(t, nil, out) +} +func TestRun_fast_methods(t *testing.T) { + input := `hello() + world()` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := map[string]interface{}{ + "hello": func(...interface{}) interface{} { return "hello " }, + "world": func(...interface{}) interface{} { return "world" }, + } + funcConf := conf.New(env) + _, err = checker.Check(tree, funcConf) + require.NoError(t, err) + + program, err := compiler.Compile(tree, funcConf) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.NoError(t, err) + + require.Equal(t, "hello world", out) +} + +func TestRun_fast_method_with_error(t *testing.T) { + input := `FastError()` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := ErrorEnv{} + funcConf := conf.New(env) + _, err = checker.Check(tree, funcConf) + require.NoError(t, err) + require.True(t, tree.Node.(*ast.FunctionNode).Fast, "method must be fast") + + program, err := compiler.Compile(tree, funcConf) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.NoError(t, err) + + require.Equal(t, true, out) +} + +func TestRun_inner_method_with_error(t *testing.T) { + input := `InnerEnv.WillError()` + + tree, err := parser.Parse(input) + require.NoError(t, err) + + env := ErrorEnv{} + funcConf := conf.New(env) + program, err := compiler.Compile(tree, funcConf) + require.NoError(t, err) + + out, err := vm.Run(program, env) + require.EqualError(t, err, "inner error") + + require.Equal(t, nil, out) +}