diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 513da3fa6..1ea874713 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -1,7 +1,8 @@ # Getting Started -**Expr** provides a package for evaluating arbitrary expressions as well as type -checking of such expression. +**Expr** is a simple, fast and extensible expression language for Go. It is +designed to be easy to use and integrate into your application. + ## Evaluate @@ -34,22 +35,11 @@ func main() { ## Compile -Usually we want to compile, type check and verify what the expression returns a -boolean (or another type). - -For example, if a user saves an expression from a +Usually, we want to compile, type check and verify that the expression returns a +boolean (or another type). For example, if a user saves an expression from a [web UI](https://antonmedv.github.io/expr/). ```go -package main - -import ( - "fmt" - - "github.com/antonmedv/expr" -) - -func main() { env := map[string]interface{}{ "greet": "Hello, %v!", "names": []string{"world", "you"}, @@ -58,8 +48,8 @@ func main() { code := `sprintf(greet, names[0])` - // Compile the code into a bytecode. This step can be done once - // and program may be reused. Specify an environment for type check. + // Compile the code into a bytecode. This step can be done only once and + // the program may be reused. Specify an environment for the type check. program, err := expr.Compile(code, expr.Env(env)) if err != nil { panic(err) @@ -71,40 +61,28 @@ func main() { } fmt.Print(output) -} ``` -You may use existing types. - -For example, an environment can be a struct. And structs methods can be used as -functions. Expr supports embedded structs and methods defines on them too. +An environment can be a struct and structs methods can be used as +functions. Expr supports embedded structs and methods defined on them too. The struct fields can be renamed by adding struct tags such as `expr:"name"`. ```go -package main - -import ( - "fmt" - "time" - - "github.com/antonmedv/expr" -) - type Env struct { - Tweets []Tweet + Messages []Message `expr:"messages"` } // Methods defined on the struct become functions. func (Env) Format(t time.Time) string { return t.Format(time.RFC822) } -type Tweet struct { +type Message struct { Text string - Date time.Time `expr:"timestamp"` + Date time.Time } func main() { - code := `map(filter(Tweets, {len(.Text) > 0}), {.Text + Format(.timestamp)})` + code := `map(filter(messages, len(.Text) > 0), .Text + Format(.timestamp))` // We can use an empty instance of the struct as an environment. program, err := expr.Compile(code, expr.Env(Env{})) @@ -113,7 +91,7 @@ func main() { } env := Env{ - Tweets: []Tweet{ + Messages: []Message{ {"Oh My God!", time.Now()}, {"How you doin?", time.Now()}, {"Could I be wearing any more clothes?", time.Now()}, @@ -129,4 +107,72 @@ func main() { } ``` +## Configuration + +Expr can be configured to do more things. For example, with [AllowUndefinedVariables](https://pkg.go.dev/github.com/antonmedv/expr#AllowUndefinedVariables) or [AsBool](https://pkg.go.dev/github.com/antonmedv/expr#AsBool) to expect the boolean result from the expression. + +```go +program, err := expr.Compile(code, expr.Env(Env{}), expr.AllowUndefinedVariables(), expr.AsBool()) +``` + +## Functions + +Expr supports any Go functions. For example, you can use `fmt.Sprintf` or methods of your structs. +Also, Expr supports functions configured via [`expr.Function(name, fn[, ...types])`](https://pkg.go.dev/github.com/antonmedv/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) +``` + +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), + ) +``` + * Next: [Operator Overloading](Operator-Overloading.md) diff --git a/docs/Internals.md b/docs/Internals.md index 93f5e7e0c..d3aa8e3f6 100644 --- a/docs/Internals.md +++ b/docs/Internals.md @@ -1,9 +1,7 @@ # Internals -Expr is a stack based virtual machine. It compiles expressions to bytecode and -runs it. - -Compilation is done in a few steps: +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 @@ -12,72 +10,70 @@ Compilation is done in a few steps: - Optimize AST - Compile AST to a bytecode -Compiler has a bunch of optimization which will produce a more optimal program. +The compiler has a bunch of optimization which will produce a more optimal program. ## In array -```js +``` value in ['foo', 'bar', 'baz'] ``` If Expr finds an `in` or `not in` expression with an array, it will be transformed into: -```js +``` value in {"foo": true, "bar": true, "baz": true} ``` ## Constant folding -Arithmetic expressions with constants is computed on compile step and replaced +Arithmetic expressions with constants are computed on compile step and replaced with the result. -```js +``` -(2 - 5) ** 3 - 2 / (+4 - 3) + -2 ``` -Will be compiled to just single number: +Will be compiled into just a single number: -```js +``` 23 ``` ## In range -```js -user.Age in 18. -.32 +``` +user.Age in 18..32 ``` Will be replaced with a binary operator: -```js +``` 18 <= user.Age && user.Age <= 32 ``` ## Const range -```js -1. -.10_000 +``` +1..10_000 ``` -Ranges computed on compile stage, replaced with precreated slices. +Ranges computed on the compile stage, are replaced with pre-created slices. ## Const expr -If some function marked as constant expression with `expr.ConstExpr`. It will be -replaced with result of the call, if all arguments are constants. +If some function is marked as a constant expression with `expr.ConstExpr`. It will be +replaced with the result of the call if all arguments are constants. ```go expr.ConstExpt("fib") ``` -```js +``` fib(42) ``` -Will be replaced with result of `fib(42)` on the compile step. +Will be replaced with the result of `fib`(42)` on the compile step. [ConstExpr Example](https://pkg.go.dev/github.com/antonmedv/expr?tab=doc#ConstExpr) diff --git a/docs/Language-Definition.md b/docs/Language-Definition.md index d2b2912e7..90c0fc90c 100644 --- a/docs/Language-Definition.md +++ b/docs/Language-Definition.md @@ -1,186 +1,186 @@ # Language Definition - - - - - - - - - - - -
Built-in FunctionsOperators
- all()
- any()
- one()
- none()
-
- len()
- map()
- filter()
- count()
-
- matches
- contains
- startsWith
- endsWith
-
- in
- not in
- x..y
- [x:y]
-
- ## Literals -* `true` -* `false` -* `nil` - -### Strings - -Single or double quotes. Unicode sequences (`\uXXXX`) are supported. - -### Numbers - -Integers and floats. - -* `42` -* `3.14` -* `1e6` -* `0x2A` -* `1_000_000` - -### Arrays - -* `[1, 2, 3]` - -Tailing commas are allowed. - -### Maps - -* `{foo: "bar"}` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Boolean + true, false +
Integer + 42, 0x2A +
Float + 0.5, .5 +
String + "foo", 'bar' +
Array + [1, 2, 3] +
Map + {a: 1, b: 2, c: 3} +
Nil + + nil +
-Tailing commas are allowed. ## Operators -### Arithmetic Operators - -* `+` (addition) -* `-` (subtraction) -* `*` (multiplication) -* `/` (division) -* `%` (modulus) -* `^` or `**` (exponent) - -Example: - -``` -x^2 + y^2 -``` - -### Comparison Operators - -* `==` (equal) -* `!=` (not equal) -* `<` (less than) -* `>` (greater than) -* `<=` (less than or equal to) -* `>=` (greater than or equal to) - -### Logical Operators - -* `not` or `!` -* `and` or `&&` -* `or` or `||` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Arithmetic + +, -, *, /, % (modulus), ^ or ** (exponent) +
Comparison + ==, !=, <, >, <=, >= +
Logical + not or !, and or &&, or or || +
Conditional + ?: (ternary) +
Membership + [], ., ?., in +
String + + (concatenation), contains, startsWith, endsWith +
Regex + matches +
Range + .. +
Slice + [:] +
-Example: +Examples: ``` -life < universe || life < everything +user.Age in 18..45 and user.Name not in ["admin", "root"] ``` -### String Operators - -* `+` (concatenation) -* `matches` (regex match) -* `contains` (string contains) -* `startsWith` (has prefix) -* `endsWith` (has suffix) - -Example: - ``` -"hello" matches "h.*" +foo matches "^[A-Z].*" ``` -### Membership Operators - -* `.` (dot) -* `in` (contain) -* `not in` (does not contain) - -Struct fields and map elements can be accessed by using the `.` or the `[]` -syntax. +### Membership Operator -Example: +Fields of structs and items of maps can be accessed with `.` operator +or `[]` operator. Elements of arrays and slices can be accessed with +`[]` operator. Negative indices are supported with `-1` being +the last element. -``` -user.Group in ["human_resources", "marketing"] -``` +The `in` operator can be used to check if an item is in an array or a map. ``` -data["tag-name"] in {foo: 1, bar: 2} +user.Name in list["available-names"] ``` -### Range Operator +#### Optional chaining -* `..` (range) - -Example: +The `?.` operator can be used to access a field of a struct or an item of a map +without checking if the struct or the map is `nil`. If the struct or the map is +`nil`, the result of the expression is `nil`. ``` -user.Age in 18..45 +author?.User?.Name ``` -The range is inclusive: - -``` -1..3 == [1, 2, 3] -``` ### Slice Operator -* `array[:]` (slice) - -Slices can work with arrays or strings. +The slice operator `[:]` can be used to access a slice of an array. -Example: - -Variable `array` is `[1,2,3,4,5]`. +For example, variable `array` is `[1, 2, 3, 4, 5]`: ``` -array[1:4] == [2,3,4] -array[:3] == [1,2,3] -array[3:] == [4,5] +array[1:4] == [2, 3, 4] +array[1:-1] == [2, 3, 4] +array[:3] == [1, 2, 3] +array[3:] == [4, 5] array[:] == array ``` -### Ternary Operator - -* `foo ? 'yes' : 'no'` - -Example: - -``` -user.Age > 30 ? "mature" : "immature" -``` ## Built-in Functions + + + + + + +
+ all()
+ any()
+ one()
+ none()
+
+ map()
+ filter()
+ count()
+
+ len()
+ abs()
+ int()
+ float()
+
+ ### `all(array, predicate)` Returns **true** if all elements satisfies the [predicate](#predicate). @@ -209,10 +209,6 @@ one(Participants, {.Winner}) Returns **true** if _all elements does not_ satisfy the [predicate](#predicate). If the array is empty, returns **true**. -### `len(v)` - -Returns the length of an array, a map or a string. - ### `map(array, predicate)` Returns new array by applying the [predicate](#predicate) to each element of @@ -231,6 +227,26 @@ Equivalent to: len(filter(array, predicate)) ``` +### `len(v)` + +Returns the length of an array, a map or a string. + +### `abs(v)` + +Returns the absolute value of a number. + +### `int(v)` + +Returns the integer value of a number or a string. + +``` +int("123") == 123 +``` + +### `float(v)` + +Returns the float value of a number or a string. + ## Predicate The predicate is an expression that accepts a single argument. To access @@ -246,3 +262,9 @@ omitted `#` symbol (`#.Value` becomes `.Value`). ``` filter(Tweets, {len(.Value) > 280}) ``` + +Braces `{}` can be omitted: + +``` +filter(Tweets, len(.Value) > 280) +``` diff --git a/docs/Operator-Overloading.md b/docs/Operator-Overloading.md index ed5a32d8d..e03027c0a 100644 --- a/docs/Operator-Overloading.md +++ b/docs/Operator-Overloading.md @@ -1,35 +1,23 @@ # Operator Overloading -**Expr** supports operator overloading. For example, you may rewrite a such -expression: +**Expr** supports operator overloading. For example, you may rewrite such expression: -```js +``` Now().Sub(CreatedAt) ``` To use `-` operator: -```js +``` Now() - CreatedAt ``` -To overloading the operator use -[expr.Operator](https://pkg.go.dev/github.com/antonmedv/expr?tab=doc#Operator): +To overload the operator use [Operator](https://pkg.go.dev/github.com/antonmedv/expr?tab=doc#Operator) option: ```go -package main - -import ( - "fmt" - "time" - - "github.com/antonmedv/expr" -) - func main() { code := `Now() - CreatedAt` - // We can define options before compiling. options := []expr.Option{ expr.Env(Env{}), expr.Operator("-", "Sub"), // Replace `-` operator with function `Sub`. @@ -52,15 +40,11 @@ func main() { } type Env struct { - datetime CreatedAt time.Time } -// Functions may be defined on embedded structs as well. -type datetime struct{} - -func (datetime) Now() time.Time { return time.Now() } -func (datetime) Sub(a, b time.Time) time.Duration { return a.Sub(b) } +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 diff --git a/docs/Tips.md b/docs/Tips.md index 8eff776be..3c5180842 100644 --- a/docs/Tips.md +++ b/docs/Tips.md @@ -3,7 +3,7 @@ ## 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%). +In some cases, it can add a small increase in performance (~10%). ```go package main diff --git a/docs/Visitor-and-Patch.md b/docs/Visitor-and-Patch.md index 8a9fa3915..7fe89024e 100644 --- a/docs/Visitor-and-Patch.md +++ b/docs/Visitor-and-Patch.md @@ -1,10 +1,10 @@ # Visitor and Patch The [ast](https://pkg.go.dev/github.com/antonmedv/expr/ast?tab=doc) package -provides `ast.Visitor` interface and `ast.Walk` function. You can use it for -customizing of the AST before compiling. +provides the `ast.Visitor` interface and the `ast.Walk` function. It can be +used to customize the AST before compiling. -For example, if you want to collect all variable names: +For example, to collect all variable names: ```go package main @@ -47,19 +47,9 @@ Specify a visitor to modify the AST with `expr.Patch` function. program, err := expr.Compile(code, expr.Patch(&visitor{})) ``` -For example, we are going to replace expressions `list[-1]` with -`list[len(list)-1]`. +For example, we are going to replace the expression `list[-1]` with the `list[len(list)-1]`. ```go -package main - -import ( - "fmt" - - "github.com/antonmedv/expr" - "github.com/antonmedv/expr/ast" -) - func main() { env := map[string]interface{}{ "list": []int{1, 2, 3}, @@ -105,16 +95,6 @@ Type information is also available. Here is an example, there all `fmt.Stringer` interface automatically converted to `string` type. ```go -package main - -import ( - "fmt" - "reflect" - - "github.com/antonmedv/expr" - "github.com/antonmedv/expr/ast" -) - func main() { code := `Price == "$100"` @@ -152,9 +132,11 @@ func (p *stringerPatcher) Visit(node *ast.Node) { return } if t.Implements(stringer) { - ast.Patch(node, &ast.MethodNode{ - Node: *node, - Method: "String", + ast.Patch(node, &ast.CallNode{ + Callee: &ast.MemberNode{ + Node: *node, + Field: "String", + }, }) }