mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Update docs
This commit is contained in:
+99
-120
@@ -1,156 +1,135 @@
|
||||
# Configuration
|
||||
|
||||
Expr can be configured with options. For example, you can pass the environment with variables and functions.
|
||||
## Return type
|
||||
|
||||
## AllowUndefinedVariables()
|
||||
Usually, the return type of expression is anything. But we can instruct type checker to verify the return type of the
|
||||
expression.
|
||||
For example, in filter expressions, we expect the return type to be a boolean.
|
||||
|
||||
This option allows undefined variables in the expression. By default, Expr will return an error
|
||||
if the expression contains undefined variables.
|
||||
```go
|
||||
program, err := expr.Compile(code, expr.AsBool())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`foo + bar`, expr.AllowUndefinedVariables())
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ok := output.(bool) // It is safe to assert the output to bool, if the expression is type checked as bool.
|
||||
```
|
||||
|
||||
## AsBool()
|
||||
If `code` variable for example returns a string, the compiler will return an error.
|
||||
|
||||
This option forces the expression to return a boolean value. If the expression returns a non-boolean value,
|
||||
Expr will return an error.
|
||||
Expr has a few options to specify the return type:
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`Title contains "Hello"`, expr.AsBool())
|
||||
- [expr.AsBool()](https://pkg.go.dev/github.com/expr-lang/expr#AsBool) - expects the return type to be a bool.
|
||||
- [expr.AsInt()](https://pkg.go.dev/github.com/expr-lang/expr#AsInt) - expects the return type to be an int (float64,
|
||||
uint, int32, and other will be cast to int).
|
||||
- [expr.AsInt64()](https://pkg.go.dev/github.com/expr-lang/expr#AsInt64) - expects the return type to be an int64 (
|
||||
float64, uint, int32, and other will be cast to int64).
|
||||
- [expr.AsFloat64()](https://pkg.go.dev/github.com/expr-lang/expr#AsFloat64) - expects the return type to be a float64 (
|
||||
float32 will be cast to float64).
|
||||
- [expr.AsAny()](https://pkg.go.dev/github.com/expr-lang/expr#AsAny) - expects the return type to be anything.
|
||||
- [expr.AsKind(reflect.Kind)](https://pkg.go.dev/github.com/expr-lang/expr#AsKind) - expects the return type to be a
|
||||
specific kind.
|
||||
|
||||
:::tip Warn on any
|
||||
By default, type checker will accept any type, even if the return type is specified. Consider following examples:
|
||||
|
||||
```expr
|
||||
let arr = [1, 2, 3]; arr[0]
|
||||
```
|
||||
|
||||
## AsFloat64()
|
||||
The return type of the expression is `any`. Arrays created in Expr are of type `[]any`. The type checker will not return
|
||||
an error if the return type is specified as `expr.AsInt()`. The output of the expression is `1`, which is an int, but the
|
||||
type checker will not return an error.
|
||||
|
||||
This option forces the expression to return a float64 value. If the expression returns a non-float64 value,
|
||||
Expr will return an error.
|
||||
But we can instruct the type checker to warn us if the return type is `any`. Use [`expr.WarnOnAny()`](https://pkg.go.dev/github.com/expr-lang/expr#WarnOnAny) to enable this behavior.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`42`, expr.AsFloat64())
|
||||
program, err := expr.Compile(code, expr.AsInt(), expr.WarnOnAny())
|
||||
```
|
||||
|
||||
:::note
|
||||
If the expression returns integer value, Expr will convert it to float64.
|
||||
The type checker will return an error if the return type is `any`. We need to modify the expression to return a specific
|
||||
type.
|
||||
|
||||
```expr
|
||||
let arr = [1, 2, 3]; int(arr[0])
|
||||
```
|
||||
:::
|
||||
|
||||
## AsInt()
|
||||
|
||||
This option forces the expression to return an int value. If the expression returns a non-int value,
|
||||
Expr will return an error.
|
||||
## WithContext
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`42`, expr.AsInt())
|
||||
Although the compiled program is guaranteed to be terminated, some user defined functions may not be. For example, if a
|
||||
user defined function calls a remote service, we may want to pass a context to the function.
|
||||
|
||||
This is possible via the [`WithContext`](https://pkg.go.dev/github.com/expr-lang/expr#WithContext) option.
|
||||
|
||||
This option will modify function calls to include the context as the first argument (only if the function signature
|
||||
accepts a context).
|
||||
|
||||
```expr
|
||||
customFunc(42)
|
||||
// will be transformed to
|
||||
customFunc(ctx, 42)
|
||||
```
|
||||
|
||||
:::note
|
||||
If the expression returns a float value, Expr truncates it to int.
|
||||
:::
|
||||
|
||||
## AsInt64()
|
||||
|
||||
Same as `AsInt()` but returns an int64 value.
|
||||
Function `expr.WithContext()` takes the name of context variable. The context variable must be defined in the environment.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`42`, expr.AsInt64())
|
||||
env := map[string]any{
|
||||
"ctx": context.Background(),
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env), expr.WithContext("ctx"))
|
||||
```
|
||||
|
||||
## AsKind()
|
||||
## ConstExpr
|
||||
|
||||
This option forces the expression to return a value of the specified kind.
|
||||
If the expression returns a value of a different kind, Expr will return an error.
|
||||
For some user defined functions, we may want to evaluate the expression at compile time. This is possible via the
|
||||
[`ConstExpr`](https://pkg.go.dev/github.com/expr-lang/expr#ConstExpr) option.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`42`, expr.AsKind(reflect.String))
|
||||
func fib(n int) int {
|
||||
if n <= 1 {
|
||||
return n
|
||||
}
|
||||
return fib(n-1) + fib(n-2)
|
||||
}
|
||||
|
||||
env := map[string]any{
|
||||
"fib": fib,
|
||||
}
|
||||
|
||||
program, err := expr.Compile(`fib(10)`, expr.Env(env), expr.ConstExpr("fib"))
|
||||
```
|
||||
|
||||
## ConstExpr()
|
||||
If all arguments of the function are constants, the function will be evaluated at compile time. The result of the function
|
||||
will be used as a constant in the expression.
|
||||
|
||||
This option tells Expr to treat specified functions as constant expressions.
|
||||
If all arguments of the function are constants, Expr will replace the function call with the result
|
||||
during the compile step.
|
||||
```expr
|
||||
fib(10) // will be transformed to 55 during the compilation
|
||||
fib(12+12) // will be transformed to 267914296 during the compilation
|
||||
fib(x) // will **not** be transformed and will be evaluated at runtime
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Compiler options can be defined as an array:
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`fib(42)`, expr.ConstExpr("fib"))
|
||||
options := []expr.Option{
|
||||
expr.Env(Env{})
|
||||
expr.AsInt(),
|
||||
expr.WarnOnAny(),
|
||||
expr.WithContext("ctx"),
|
||||
expr.ConstExpr("fib"),
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, options...)
|
||||
```
|
||||
|
||||
[ConstExpr Example](https://pkg.go.dev/github.com/expr-lang/expr?tab=doc#ConstExpr)
|
||||
|
||||
## Env()
|
||||
|
||||
This option passes the environment with variables and functions to the expression.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`foo + bar`, expr.Env(Env{}))
|
||||
```
|
||||
|
||||
## Function()
|
||||
|
||||
This option adds a function to the expression.
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
)
|
||||
|
||||
program, err := expr.Compile(`atoi("42")`, atoi)
|
||||
```
|
||||
|
||||
Expr sees the `atoi` function as a function with a variadic number of arguments of type `any` and returns a value of type `any`. But, we can specify the types of arguments and the return value by adding the correct function
|
||||
signature or multiple signatures.
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
new(func(string) int),
|
||||
)
|
||||
```
|
||||
|
||||
Or we can simply reuse the `strconv.Atoi` function.
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
strconv.Atoi,
|
||||
)
|
||||
```
|
||||
|
||||
Here is another example with a few function signatures:
|
||||
|
||||
```go
|
||||
toInt := expr.Function(
|
||||
"toInt",
|
||||
func(params ...any) (any, error) {
|
||||
switch params[0].(type) {
|
||||
case float64:
|
||||
return int(params[0].(float64)), nil
|
||||
case string:
|
||||
return strconv.Atoi(params[0].(string))
|
||||
}
|
||||
return nil, fmt.Errorf("invalid type")
|
||||
},
|
||||
new(func(float64) int),
|
||||
new(func(string) int),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Operator()
|
||||
|
||||
This options defines an [operator overloading](operator-overloading).
|
||||
|
||||
## Optimize()
|
||||
|
||||
This option enables [optimizations](internals.md). By default, Expr will optimize the expression.
|
||||
|
||||
## Patch()
|
||||
|
||||
This option allows you to [patch the expression](visitor-and-patch) before compilation.
|
||||
Full list of available options can be found in the [pkg.go.dev](https://pkg.go.dev/github.com/expr-lang/expr#Option) documentation.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Environment
|
||||
|
||||
The environment is a map or a struct that contains the variables and functions that the expression can access.
|
||||
|
||||
## Struct as Environment
|
||||
|
||||
Let's consider the following example:
|
||||
|
||||
```go
|
||||
type Env struct {
|
||||
UpdatedAt time.Time
|
||||
Posts []Post
|
||||
Map map[string]string `expr:"tags"`
|
||||
}
|
||||
```
|
||||
|
||||
The `Env` struct contains 3 variables that the expression can access: `UpdatedAt`, `Posts`, and `tags`.
|
||||
|
||||
:::info
|
||||
The `expr` tag is used to rename the `Map` field to `tags` variable in the expression.
|
||||
:::
|
||||
|
||||
The `Env` struct can also contain methods. The methods defined on the struct become functions that the expression can
|
||||
call.
|
||||
|
||||
```go
|
||||
func (Env) Format(t time.Time) string {
|
||||
return t.Format(time.RFC822)
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
Methods defined on embedded structs are also accessible.
|
||||
```go
|
||||
type Env struct {
|
||||
Helpers
|
||||
}
|
||||
|
||||
type Helpers struct{}
|
||||
|
||||
func (Helpers) Format(t time.Time) string {
|
||||
return t.Format(time.RFC822)
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
We can use an empty struct `Env{}` to with [expr.Env](https://pkg.go.dev/github.com/expr-lang/expr#Env) to create an environment. Expr will use reflection to find
|
||||
the fields and methods of the struct.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(code, expr.Env(Env{}))
|
||||
```
|
||||
|
||||
Compiler will type check the expression against the environment. After the compilation, we can run the program with the environment.
|
||||
You should use the same type of environment that you passed to the `expr.Env` function.
|
||||
|
||||
```go
|
||||
output, err := expr.Run(program, Env{
|
||||
UpdatedAt: time.Now(),
|
||||
Posts: []Post{{Title: "Hello, World!"}},
|
||||
Map: map[string]string{"tag1": "value1"},
|
||||
})
|
||||
```
|
||||
|
||||
## Map as Environment
|
||||
|
||||
You can also use a map as an environment.
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
"UpdatedAt": time.Time{},
|
||||
"Posts": []Post{},
|
||||
"tags": map[string]string{},
|
||||
"sprintf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env))
|
||||
```
|
||||
|
||||
A map defines variables and functions that the expression can access. The key is the variable name, and the type
|
||||
is the value's type.
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
"object": map[string]any{
|
||||
"field": 42,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Expr will infer the type of the `object` variable as `map[string]any`.
|
||||
|
||||
By default, Expr will return an error if unknown variables are used in the expression.
|
||||
|
||||
You can disable this behavior by passing [`AllowUndefinedVariables`](https://pkg.go.dev/github.com/expr-lang/expr#AllowUndefinedVariables) option to the compiler.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Functions
|
||||
|
||||
Expr comes with a set of [builtin](language-definition.md) functions, but you can also define your own functions.
|
||||
|
||||
The easiest way to define a custom function is to add it to the environment.
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Or you can use functions defined on a struct:
|
||||
|
||||
```go
|
||||
type Env struct{}
|
||||
|
||||
func (Env) Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
If functions are marked with [`ConstExpr`](./configuration.md#constexpr) option, they will be evaluated at compile time.
|
||||
:::
|
||||
|
||||
The best way to define a function from a performance perspective is to use a [`Function`](https://pkg.go.dev/github.com/expr-lang/expr#Function) option.
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
)
|
||||
|
||||
program, err := expr.Compile(`atoi("42")`, atoi)
|
||||
```
|
||||
|
||||
Type checker sees the `atoi` function as a function with a variadic number of arguments of type `any`, and returns
|
||||
a value of type `any`. But, we can specify the types of arguments and the return value by adding the correct function
|
||||
signature or multiple signatures.
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
new(func(string) int),
|
||||
)
|
||||
```
|
||||
|
||||
Or we can simply reuse the strconv.Atoi function as a type:
|
||||
|
||||
```go
|
||||
atoi := expr.Function(
|
||||
"atoi",
|
||||
func(params ...any) (any, error) {
|
||||
return strconv.Atoi(params[0].(string))
|
||||
},
|
||||
// highlight-next-line
|
||||
strconv.Atoi,
|
||||
)
|
||||
```
|
||||
|
||||
It is possible to define multiple signatures for a function:
|
||||
|
||||
```go
|
||||
toInt := expr.Function(
|
||||
"toInt",
|
||||
func(params ...any) (any, error) {
|
||||
switch params[0].(type) {
|
||||
case float64:
|
||||
return int(params[0].(float64)), nil
|
||||
case string:
|
||||
return strconv.Atoi(params[0].(string))
|
||||
}
|
||||
return nil, fmt.Errorf("invalid type")
|
||||
},
|
||||
new(func(float64) int),
|
||||
new(func(string) int),
|
||||
)
|
||||
```
|
||||
+46
-38
@@ -26,20 +26,24 @@ fmt.Print(output) // 4
|
||||
```
|
||||
|
||||
Expr compiles the expression `2 + 2` into a bytecode program. Then we run
|
||||
the program and get the output. The output is `4` as expected.
|
||||
the program and get the output.
|
||||
|
||||
:::tip
|
||||
In performance-critical applications, you can reuse the compiled program. Compiled programs are safe for concurrent use.
|
||||
**Compile once** and run **multiple** times.
|
||||
:::
|
||||
|
||||
The `expr.Compile` function returns a `*vm.Program` and an error. The `expr.Run` function takes a program and an
|
||||
environment. The environment is a map of variables that can be used in the expression. In this example, we use `nil` as
|
||||
an environment because we don't need any variables.
|
||||
|
||||
The `expr.Compile` function returns a `*vm.Program` and an error. The program
|
||||
can be reused between runs. The `expr.Run` function takes a program and an
|
||||
environment. The environment is a map of variables that can be used in the
|
||||
expression. In this example, we use `nil` as an environment because we don't
|
||||
need any variables.
|
||||
|
||||
Now let's pass some variables to the expression:
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
"foo": 100,
|
||||
"bar": 200,
|
||||
"bar": 200,
|
||||
}
|
||||
|
||||
program, err := expr.Compile(`foo + bar`, expr.Env(env))
|
||||
@@ -56,7 +60,9 @@ fmt.Print(output) // 300
|
||||
```
|
||||
|
||||
Why do we need to pass the environment to the `expr.Compile` function? Expr can be used as a type-safe language.
|
||||
Expr can infer the type of the expression and check it against the environment. Here is an example:
|
||||
Expr can infer the type of the expression and check it against the environment.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
@@ -70,73 +76,75 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
Expr can work with any Go types. Here is an example:
|
||||
Expr can work with any Go types:
|
||||
|
||||
```go
|
||||
env := map[string]any{
|
||||
"greet": "Hello, %v!",
|
||||
"names": []string{"world", "you"},
|
||||
"sprintf": fmt.Sprintf,
|
||||
"greet": "Hello, %v!",
|
||||
"names": []string{"world", "you"},
|
||||
"sprintf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
code := `sprintf(greet, names[0])`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output) // Hello, world!
|
||||
```
|
||||
|
||||
Also, Expr can use a struct as an environment. Methods defined on the struct become functions.
|
||||
The struct fields can be renamed with the `expr` tag. Here is an example:
|
||||
The struct fields can be renamed with the `expr` tag.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```go
|
||||
type Env struct {
|
||||
Posts []Post `expr:"posts"`
|
||||
Posts []Post `expr:"posts"`
|
||||
}
|
||||
|
||||
func (Env) Format(t time.Time) string {
|
||||
return t.Format(time.RFC822)
|
||||
return t.Format(time.RFC822)
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Body string
|
||||
Date time.Time
|
||||
Body string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
code := `map(posts, Format(.Date) + ": " + .Body)`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(Env{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
code := `map(posts, Format(.Date) + ": " + .Body)`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(Env{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
env := Env{
|
||||
Posts: []Post{
|
||||
{"Oh My God!", time.Now()},
|
||||
{"How you doin?", time.Now()},
|
||||
{"Could I be wearing any more clothes?", time.Now()},
|
||||
},
|
||||
}
|
||||
env := Env{
|
||||
Posts: []Post{
|
||||
{"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)
|
||||
}
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
fmt.Print(output)
|
||||
}
|
||||
```
|
||||
|
||||
The compiled program can be reused between runs. Here is an example:
|
||||
The compiled program can be reused between runs.
|
||||
|
||||
```go
|
||||
type Env struct {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 386 KiB |
@@ -1,61 +0,0 @@
|
||||
# Internals
|
||||
|
||||
Expr is a stack-based virtual machine. It compiles expressions to bytecode and
|
||||
runs it. The compilation is done in a few steps:
|
||||
|
||||
- Parse expression to AST
|
||||
- Type check AST
|
||||
- Apply operator overloading
|
||||
- Apply custom AST patching
|
||||
- Optimize AST
|
||||
- Compile AST to a bytecode
|
||||
|
||||
The compiler has a bunch of optimization which will produce a more optimal program.
|
||||
|
||||
## In array
|
||||
|
||||
```
|
||||
value in ['foo', 'bar', 'baz']
|
||||
```
|
||||
|
||||
If Expr finds an `in` or `not in` expression with an array, it will be
|
||||
transformed into:
|
||||
|
||||
```
|
||||
value in {"foo": true, "bar": true, "baz": true}
|
||||
```
|
||||
|
||||
## Constant folding
|
||||
|
||||
Arithmetic expressions with constants are computed on compile step and replaced
|
||||
with the result.
|
||||
|
||||
```
|
||||
-(2 - 5) ** 3 - 2 / (+4 - 3) + -2
|
||||
```
|
||||
|
||||
Will be compiled into just a single number:
|
||||
|
||||
```
|
||||
23
|
||||
```
|
||||
|
||||
## In range
|
||||
|
||||
```
|
||||
user.Age in 18..32
|
||||
```
|
||||
|
||||
Will be replaced with a binary operator:
|
||||
|
||||
```
|
||||
18 <= user.Age && user.Age <= 32
|
||||
```
|
||||
|
||||
## Const range
|
||||
|
||||
```
|
||||
1..10_000
|
||||
```
|
||||
|
||||
Ranges computed on the compile stage, are replaced with pre-created slices.
|
||||
@@ -486,7 +486,7 @@ date("2023-08-14 00:00:00", "2006-01-02 15:04:05", "Europe/Zurich")
|
||||
|
||||
Available methods on the date:
|
||||
|
||||
- `Year()` - returns the year
|
||||
- `Year()` - returns the year
|
||||
- `Month()` - returns the month (starting from 1)
|
||||
- `Day()` - returns the day of the month
|
||||
- `Hour()` - returns the hour
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Operator Overloading
|
||||
|
||||
**Expr** supports operator overloading. For example, you may rewrite such expression:
|
||||
|
||||
```
|
||||
Now().Sub(CreatedAt)
|
||||
```
|
||||
|
||||
To use `-` operator:
|
||||
|
||||
```
|
||||
Now() - CreatedAt
|
||||
```
|
||||
|
||||
To overload the operator use [Operator](https://pkg.go.dev/github.com/expr-lang/expr?tab=doc#Operator) option:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
code := `Now() - CreatedAt`
|
||||
|
||||
options := []expr.Option{
|
||||
expr.Env(Env{}),
|
||||
expr.Operator("-", "Sub"), // Replace `-` operator with function `Sub`.
|
||||
}
|
||||
|
||||
program, err := expr.Compile(code, options...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
env := Env{
|
||||
CreatedAt: time.Date(1987, time.November, 24, 20, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(output)
|
||||
}
|
||||
|
||||
type Env struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (Env) Now() time.Time { return time.Now() }
|
||||
func (Env) Sub(a, b time.Time) time.Duration { return a.Sub(b) }
|
||||
```
|
||||
|
||||
**Expr** uses functions from `Env` for operator overloading. If types of
|
||||
operands match types of a function, the operator will be replaced with a
|
||||
function call.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Patch
|
||||
|
||||
Sometimes it may be necessary to modify an expression before the compilation.
|
||||
Expr provides a powerful mechanism to modify the expression using
|
||||
the [`Patch`](https://pkg.go.dev/github.com/expr-lang/expr#Patch) option.
|
||||
|
||||
## Simple example
|
||||
|
||||
Let's start with a simple example. We have an expression that uses a variable `foo`:
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`foo + bar`)
|
||||
```
|
||||
|
||||
We want to replace the `foo` variable with a constant `42`. First, we need to implement a [visitor](./visitor.md):
|
||||
|
||||
```go
|
||||
type FooPatcher struct{}
|
||||
|
||||
func (FooPatcher) Visit(node *ast.Node) {
|
||||
if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == "foo" {
|
||||
ast.Patch(node, &ast.IntegerNode{Value: 42})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we can use the `FooPatcher` to modify the expression:
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(`foo + bar`, expr.Patch(FooPatcher{}))
|
||||
```
|
||||
|
||||
## Advanced example
|
||||
|
||||
Let's consider a more complex example. We have an expression that uses variables `foo` and `bar` of type `Decimal`:
|
||||
|
||||
```go
|
||||
type Decimal struct {
|
||||
Value int
|
||||
}
|
||||
```
|
||||
|
||||
And we want to transform the following expression:
|
||||
|
||||
```expr
|
||||
a + b + c
|
||||
```
|
||||
|
||||
Into functions calls that accept `Decimal` arguments:
|
||||
|
||||
```expr
|
||||
add(add(a, b), c)
|
||||
```
|
||||
|
||||
First, we need to implement a visitor that will transform the expression:
|
||||
|
||||
```go
|
||||
type DecimalPatcher struct{}
|
||||
|
||||
var decimalType = reflect.TypeOf(Decimal{})
|
||||
|
||||
func (DecimalPatcher) Visit(node *ast.Node) {
|
||||
if n, ok := (*node).(*ast.BinaryNode); ok && n.Operator == "+" {
|
||||
if !n.Left.Type().AssignableTo(decimalType) {
|
||||
return // skip, left side is not a Decimal
|
||||
}
|
||||
if !n.Right.Type().AssignableTo(decimalType) {
|
||||
return // skip, right side is not a Decimal
|
||||
}
|
||||
ast.Patch(node, &ast.CallNode{
|
||||
Callee: &ast.IdentifierNode{Value: "add"},
|
||||
Arguments: []ast.Node{n.Left, n.Right},
|
||||
})
|
||||
(*node).SetType(decimalType) // set the type, so patcher can be used multiple times
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We used [Type()](https://pkg.go.dev/github.com/expr-lang/expr/ast#Node.Type) method to get the type of the expression node.
|
||||
The `AssignableTo` method is used to check if the type is `Decimal`. If both sides are `Decimal`, we replace the expression with a function call.
|
||||
|
||||
The important part of this patcher is to set correct types for the nodes. As we constructed a new `CallNode`, it lacks the type information.
|
||||
So after the first patcher run, if we want the patcher to be applied recursively, we need to set the type of the node.
|
||||
|
||||
|
||||
Now we can use the `DecimalPatcher` to modify the expression:
|
||||
|
||||
```go
|
||||
env := map[string]interface{}{
|
||||
"a": Decimal{1},
|
||||
"b": Decimal{2},
|
||||
"c": Decimal{3},
|
||||
"add": func(x, y Decimal) Decimal {
|
||||
return Decimal{x.Value + y.Value}
|
||||
},
|
||||
}
|
||||
|
||||
code := `a + b + c`
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env), expr.Patch(DecimalPatcher{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(output) // Decimal{6}
|
||||
```
|
||||
|
||||
|
||||
:::info
|
||||
Expr comes with already implemented patcher that simplifies operator overloading.
|
||||
|
||||
The `DecimalPatcher` can be replaced with the [Operator](https://pkg.go.dev/github.com/expr-lang/expr#Operator) option.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(code, expr.Env(env), expr.Operator("+", "add"))
|
||||
```
|
||||
|
||||
Operator overloading patcher will check if provided functions (`"add"`) satisfy the operator (`"+"`), and
|
||||
replace the operator with the function call.
|
||||
:::
|
||||
@@ -1,38 +0,0 @@
|
||||
# Tips
|
||||
|
||||
## Reuse VM
|
||||
|
||||
It is possible to reuse a virtual machine between re-runs on the program.
|
||||
In some cases, it can add a small increase in performance (~10%).
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
env := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
}
|
||||
|
||||
program, err := expr.Compile("foo + bar", expr.Env(env))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Reuse this vm instance between runs
|
||||
v := vm.VM{}
|
||||
|
||||
out, err := v.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(out)
|
||||
}
|
||||
```
|
||||
@@ -1,98 +0,0 @@
|
||||
# Visitor and Patch
|
||||
|
||||
The [ast](https://pkg.go.dev/github.com/expr-lang/expr/ast?tab=doc) package
|
||||
provides the `ast.Visitor` interface and the `ast.Walk` function. It can be
|
||||
used to customize the AST before compiling.
|
||||
|
||||
For example, to collect all variable names:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/expr-lang/expr/ast"
|
||||
"github.com/expr-lang/expr/parser"
|
||||
)
|
||||
|
||||
type visitor struct {
|
||||
identifiers []string
|
||||
}
|
||||
|
||||
func (v *visitor) Visit(node *ast.Node) {
|
||||
if n, ok := (*node).(*ast.IdentifierNode); ok {
|
||||
v.identifiers = append(v.identifiers, n.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
tree, err := parser.Parse("foo + bar")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
visitor := &visitor{}
|
||||
ast.Walk(&tree.Node, visitor)
|
||||
|
||||
fmt.Printf("%v", visitor.identifiers) // outputs [foo bar]
|
||||
}
|
||||
```
|
||||
|
||||
## Patch
|
||||
|
||||
Specify a visitor to modify the AST with `expr.Patch` function.
|
||||
|
||||
```go
|
||||
program, err := expr.Compile(code, expr.Patch(&visitor{}))
|
||||
```
|
||||
|
||||
For example, let's pass a context to every function call:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/ast"
|
||||
)
|
||||
|
||||
func main() {
|
||||
env := map[string]interface{}{
|
||||
"foo": func(ctx context.Context, arg int) any {
|
||||
// ...
|
||||
},
|
||||
"ctx": context.Background(),
|
||||
}
|
||||
|
||||
code := `foo(42)` // will be converted to foo(ctx, 42)
|
||||
|
||||
program, err := expr.Compile(code, expr.Env(env), expr.Patch(patcher{}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(output)
|
||||
}
|
||||
|
||||
type patcher struct{}
|
||||
|
||||
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
func (patcher) Visit(node *ast.Node) {
|
||||
callNode, ok := (*node).(*ast.CallNode)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
callNode.Arguments = append([]ast.Node{&ast.IdentifierNode{Value: "ctx"}}, callNode.Arguments...)
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# Visitor
|
||||
|
||||
Expr provides an interface to traverse the AST of the expression before the compilation.
|
||||
The `Visitor` interface allows you to collect information about the expression, modify the expression, or even generate
|
||||
a new expression.
|
||||
|
||||
Let's start with an [ast.Visitor](https://pkg.go.dev/github.com/expr-lang/expr/ast#Visitor) implementation which will
|
||||
collect all variables used in the expression:
|
||||
|
||||
```go
|
||||
type Visitor struct {
|
||||
Identifiers []string
|
||||
}
|
||||
|
||||
func (v *Visitor) Visit(node *ast.Node) {
|
||||
if n, ok := (*node).(*ast.IdentifierNode); ok {
|
||||
v.Identifiers = append(v.Identifiers, n.Value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Full list of available AST nodes can be found in the [ast](https://pkg.go.dev/github.com/expr-lang/expr/ast) documentation.
|
||||
|
||||
Let's parse the expression and use [ast.Walk](https://pkg.go.dev/github.com/expr-lang/expr/ast#Walk) to traverse the AST:
|
||||
|
||||
```go
|
||||
tree, err := parser.Parse(`foo + bar`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
v := &Visitor{}
|
||||
ast.Walk(&tree.Node, v)
|
||||
|
||||
fmt.Println(v.Identifiers) // [foo, bar]
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Although it is possible to access the AST of compiled program, it may be already be modified by patchers, optimizers, etc.
|
||||
|
||||
```go
|
||||
node := program.Node()
|
||||
|
||||
v := &Visitor{}
|
||||
ast.Walk(&node, v)
|
||||
```
|
||||
|
||||
:::
|
||||
Reference in New Issue
Block a user