Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: expr-lang/expr
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.17.0
Choose a base ref
...
head repository: expr-lang/expr
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.17.1
Choose a head ref
  • 4 commits
  • 10 files changed
  • 1 contributor

Commits on Mar 16, 2025

  1. Update language-definition.md

    antonmedv committed Mar 16, 2025

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    marekdedic Marek Dědič
    Copy the full SHA
    1598c62 View commit details

Commits on Mar 17, 2025

  1. Update SECURITY.md

    antonmedv authored Mar 17, 2025

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    marekdedic Marek Dědič
    Copy the full SHA
    8419ba4 View commit details
  2. Update docs

    antonmedv committed Mar 17, 2025

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    marekdedic Marek Dědič
    Copy the full SHA
    0f99b19 View commit details

Commits on Mar 18, 2025

  1. Fix parsing of variable declaration nodes combined with sequence node (

    …#773)
    
    Fixes #772
    antonmedv authored Mar 18, 2025

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    marekdedic Marek Dědič
    Copy the full SHA
    e135bda View commit details
Showing with 135 additions and 62 deletions.
  1. +2 −5 SECURITY.md
  2. +14 −15 docs/configuration.md
  3. +18 −2 docs/environment.md
  4. +3 −0 docs/functions.md
  5. +6 −10 docs/getting-started.md
  6. +8 −0 docs/language-definition.md
  7. +17 −6 docs/patch.md
  8. +11 −2 docs/visitor.md
  9. +3 −3 parser/parser.go
  10. +53 −19 parser/parser_test.go
7 changes: 2 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -11,11 +11,8 @@ unless this is not possible or feasible with a reasonable effort.

| Version | Supported |
|---------|--------------------|
| 1.16 | :white_check_mark: |
| 1.15 | :white_check_mark: |
| 1.14 | :white_check_mark: |
| 1.13 | :white_check_mark: |
| < 1.13 | :x: |
| 1.x | :white_check_mark: |
| 0.x | :x: |

## Reporting a Vulnerability

29 changes: 14 additions & 15 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -6,15 +6,15 @@ Usually, the return type of expression is anything. But we can instruct type che
expression.
For example, in filter expressions, we expect the return type to be a boolean.

```go
```go
program, err := expr.Compile(code, expr.AsBool())
if err != nil {
panic(err)
panic(err)
}

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

ok := output.(bool) // It is safe to assert the output to bool, if the expression is type checked as bool.
@@ -82,6 +82,9 @@ Function `expr.WithContext()` takes the name of context variable. The context va
```go
env := map[string]any{
"ctx": context.Background(),
"customFunc": func(ctx context.Context, a int) int {
return a
},
}

program, err := expr.Compile(code, expr.Env(env), expr.WithContext("ctx"))
@@ -116,20 +119,16 @@ fib(12+12) // will be transformed to 267914296 during the compilation
fib(x) // will **not** be transformed and will be evaluated at runtime
```

## Options
## Timezone

Compiler options can be defined as an array:
By default, the timezone is set to `time.Local`. We can change the timezone via the [`Timezone`](https://pkg.go.dev/github.com/expr-lang/expr#Timezone) option.

```go
options := []expr.Option{
expr.Env(Env{})
expr.AsInt(),
expr.WarnOnAny(),
expr.WithContext("ctx"),
expr.ConstExpr("fib"),
}

program, err := expr.Compile(code, options...)
program, err := expr.Compile(code, expr.Timezone(time.UTC))
```

Full list of available options can be found in the [pkg.go.dev](https://pkg.go.dev/github.com/expr-lang/expr#Option) documentation.
The timezone is used for the following functions:
```expr
date("2024-11-23 12:00:00") // parses the date in the specified timezone
now() // returns the current time in the specified timezone
```
20 changes: 18 additions & 2 deletions docs/environment.md
Original file line number Diff line number Diff line change
@@ -84,12 +84,28 @@ is the value's type.
env := map[string]any{
"object": map[string]any{
"field": 42,
},
},
"struct": struct {
Field int `expr:"field"`
}{42},
}
```

Expr will infer the type of the `object` variable as `map[string]any`.
Accessing fields of the `object` and `struct` variables will return the following results.

By default, Expr will return an error if unknown variables are used in the expression.
```expr
object.field // 42
object.unknown // nil (no error)
struct.field // 42
struct.unknown // error (unknown field)
foobar // error (unknown variable)
```

:::note
The `foobar` variable is not defined in the environment.
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.
:::
3 changes: 3 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ atoi := expr.Function(
func(params ...any) (any, error) {
return strconv.Atoi(params[0].(string))
},
// highlight-next-line
new(func(string) int),
)
```
@@ -80,7 +81,9 @@ toInt := expr.Function(
}
return nil, fmt.Errorf("invalid type")
},
// highlight-start
new(func(float64) int),
new(func(string) int),
// highlight-end
)
```
16 changes: 6 additions & 10 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ env := map[string]any{

program, err := expr.Compile(`name + age`, expr.Env(env))
if err != nil {
// highlight-next-line
panic(err) // Will panic with "invalid operation: string + int"
}
```
@@ -85,9 +86,7 @@ env := map[string]any{
"sprintf": fmt.Sprintf,
}

code := `sprintf(greet, names[0])`

program, err := expr.Compile(code, expr.Env(env))
program, err := expr.Compile(`sprintf(greet, names[0])`, expr.Env(env))
if err != nil {
panic(err)
}
@@ -100,17 +99,14 @@ if err != nil {
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:
Also, Expr can use a struct as an environment. Here is an example:

```go
type Env struct {
Posts []Post `expr:"posts"`
}

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

@@ -122,7 +118,7 @@ type Post struct {
func main() {
code := `map(posts, Format(.Date) + ": " + .Body)`

program, err := expr.Compile(code, expr.Env(Env{}))
program, err := expr.Compile(code, expr.Env(Env{})) // Pass the struct as an environment.
if err != nil {
panic(err)
}
@@ -172,7 +168,7 @@ if err != nil {
fmt.Print(output) // 7
```

:::tip
:::info Eval = Compile + Run
For one-off expressions, you can use the `expr.Eval` function. It compiles and runs the expression in one step.
```go
output, err := expr.Eval(`2 + 2`, env)
8 changes: 8 additions & 0 deletions docs/language-definition.md
Original file line number Diff line number Diff line change
@@ -697,6 +697,14 @@ Flattens given array into one-dimensional array.
flatten([1, 2, [3, 4]]) == [1, 2, 3, 4]
```

### uniq(array) {#uniq}

Removes duplicates from an array.

```expr
uniq([1, 2, 3, 2, 1]) == [1, 2, 3]
```

### join(array[, delimiter]) {#join}

Joins an array of strings into a single string with the given delimiter.
23 changes: 17 additions & 6 deletions docs/patch.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 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.
For example, you may want to replace a variable with a constant, transform an expression into a function call,
or even modify the expression to use a different operator.

## Simple example

@@ -19,12 +19,15 @@ type FooPatcher struct{}

func (FooPatcher) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == "foo" {
// highlight-next-line
ast.Patch(node, &ast.IntegerNode{Value: 42})
}
}
```

Now we can use the `FooPatcher` to modify the expression:
We used the [ast.Patch](https://pkg.go.dev/github.com/expr-lang/expr/ast#Patch) function to replace the `foo` variable with an integer node.

Now we can use the `FooPatcher` to modify the expression on compilation via the [expr.Patch](https://pkg.go.dev/github.com/expr-lang/expr#Patch) option:

```go
program, err := expr.Compile(`foo + bar`, expr.Patch(FooPatcher{}))
@@ -61,17 +64,24 @@ 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{

// highlight-start
callNode := &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
}
ast.Patch(node, callNode)
// highlight-end

(*node).SetType(decimalType) // set the type, so the patcher can be applied recursively
}
}
```
@@ -97,6 +107,7 @@ env := map[string]interface{}{

code := `a + b + c`

// highlight-next-line
program, err := expr.Compile(code, expr.Env(env), expr.Patch(DecimalPatcher{}))
if err != nil {
panic(err)
13 changes: 11 additions & 2 deletions docs/visitor.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Visitor

Expr provides an interface to traverse the AST of the expression before the compilation.
Expr provides an interface to traverse the <span title="Abstract Syntax Tree" style={{borderBottom: "1px dotted currentColor"}}>AST</span> 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:
collect all variables used in the expression.

Visitor must implement a single method `Visit(*ast.Node)`, which will be called for each node in the AST.

```go
type Visitor struct {
@@ -30,6 +32,7 @@ if err != nil {
}

v := &Visitor{}
// highlight-next-line
ast.Walk(&tree.Node, v)

fmt.Println(v.Identifiers) // [foo, bar]
@@ -40,6 +43,12 @@ fmt.Println(v.Identifiers) // [foo, bar]
Although it is possible to access the AST of compiled program, it may be already be modified by patchers, optimizers, etc.

```go
program, err := expr.Compile(`foo + bar`)
if err != nil {
panic(err)
}

// highlight-next-line
node := program.Node()

v := &Visitor{}
6 changes: 3 additions & 3 deletions parser/parser.go
Original file line number Diff line number Diff line change
@@ -323,13 +323,13 @@ func (p *parser) parseConditional(node Node) Node {
p.next()

if !p.current.Is(Operator, ":") {
expr1 = p.parseSequenceExpression()
expr1 = p.parseExpression(0)
p.expect(Operator, ":")
expr2 = p.parseSequenceExpression()
expr2 = p.parseExpression(0)
} else {
p.next()
expr1 = node
expr2 = p.parseSequenceExpression()
expr2 = p.parseExpression(0)
}

node = p.createNode(&ConditionalNode{
Loading