├── .gitattributes
├── .github
├── FUNDING.yml
├── images
│ ├── demo.tape
│ └── out.gif
├── scripts
│ └── coverage.mjs
└── workflows
│ ├── build.yml
│ ├── check.yml
│ ├── diff.yml
│ ├── fuzz.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── ast
├── dump.go
├── find.go
├── find_test.go
├── node.go
├── print.go
├── print_test.go
├── visitor.go
└── visitor_test.go
├── bench_test.go
├── builtin
├── builtin.go
├── builtin_test.go
├── function.go
├── lib.go
├── utils.go
└── validation.go
├── checker
├── checker.go
├── checker_test.go
├── info.go
├── info_test.go
├── nature
│ ├── nature.go
│ └── utils.go
└── types.go
├── compiler
├── compiler.go
└── compiler_test.go
├── conf
├── config.go
└── env.go
├── debug
├── debugger.go
├── go.mod
└── go.sum
├── docgen
├── README.md
├── docgen.go
├── docgen_test.go
└── markdown.go
├── docs
├── configuration.md
├── environment.md
├── functions.md
├── getting-started.md
├── language-definition.md
├── patch.md
└── visitor.md
├── expr.go
├── expr_test.go
├── file
├── error.go
├── location.go
├── source.go
└── source_test.go
├── go.mod
├── internal
├── deref
│ ├── deref.go
│ └── deref_test.go
├── difflib
│ ├── difflib.go
│ └── difflib_test.go
├── spew
│ ├── bypass.go
│ ├── bypasssafe.go
│ ├── common.go
│ ├── common_test.go
│ ├── config.go
│ ├── doc.go
│ ├── dump.go
│ ├── dump_test.go
│ ├── dumpcgo_test.go
│ ├── dumpnocgo_test.go
│ ├── example_test.go
│ ├── format.go
│ ├── format_test.go
│ ├── internal_test.go
│ ├── internalunsafe_test.go
│ ├── spew.go
│ ├── spew_test.go
│ └── testdata
│ │ └── dumpcgo.go
└── testify
│ ├── assert
│ ├── assertion_compare.go
│ ├── assertion_compare_test.go
│ ├── assertion_format.go
│ ├── assertion_format.go.tmpl
│ ├── assertion_forward.go
│ ├── assertion_forward.go.tmpl
│ ├── assertion_order.go
│ ├── assertion_order_test.go
│ ├── assertions.go
│ ├── assertions_test.go
│ ├── doc.go
│ ├── errors.go
│ ├── forward_assertions.go
│ ├── forward_assertions_test.go
│ ├── http_assertions.go
│ ├── http_assertions_test.go
│ └── internal
│ │ └── unsafetests
│ │ ├── doc.go
│ │ └── unsafetests_test.go
│ └── require
│ ├── doc.go
│ ├── forward_requirements.go
│ ├── forward_requirements_test.go
│ ├── require.go
│ ├── require.go.tmpl
│ ├── require_forward.go
│ ├── require_forward.go.tmpl
│ ├── requirements.go
│ └── requirements_test.go
├── optimizer
├── const_expr.go
├── filter_first.go
├── filter_last.go
├── filter_len.go
├── filter_map.go
├── filter_map_test.go
├── fold.go
├── fold_test.go
├── in_array.go
├── in_range.go
├── optimizer.go
├── optimizer_test.go
├── predicate_combination.go
├── sum_array.go
├── sum_array_test.go
├── sum_map.go
└── sum_map_test.go
├── parser
├── lexer
│ ├── lexer.go
│ ├── lexer_test.go
│ ├── state.go
│ ├── token.go
│ └── utils.go
├── operator
│ └── operator.go
├── parser.go
├── parser_test.go
└── utils
│ └── utils.go
├── patcher
├── operator_override.go
├── value
│ ├── bench_test.go
│ ├── value.go
│ ├── value_example_test.go
│ └── value_test.go
├── with_context.go
├── with_context_test.go
├── with_timezone.go
└── with_timezone_test.go
├── repl
├── go.mod
├── go.sum
└── repl.go
├── test
├── bench
│ └── bench_call_test.go
├── coredns
│ ├── coredns.go
│ └── coredns_test.go
├── crowdsec
│ ├── crowdsec.go
│ ├── crowdsec_test.go
│ └── funcs.go
├── deref
│ └── deref_test.go
├── examples
│ ├── examples_test.go
│ └── markdown.go
├── fuzz
│ ├── fuzz_corpus.sh
│ ├── fuzz_corpus.txt
│ ├── fuzz_env.go
│ ├── fuzz_expr.dict
│ ├── fuzz_expr_seed_corpus.zip
│ └── fuzz_test.go
├── gen
│ ├── env.go
│ ├── gen.go
│ ├── gen_test.go
│ └── utils.go
├── interface
│ ├── interface_method_test.go
│ └── interface_test.go
├── issues
│ ├── 461
│ │ └── issue_test.go
│ ├── 688
│ │ └── issue_test.go
│ ├── 723
│ │ └── issue_test.go
│ ├── 730
│ │ └── issue_test.go
│ ├── 739
│ │ └── issue_test.go
│ ├── 756
│ │ └── issue_test.go
│ └── 785
│ │ └── issue_test.go
├── mock
│ └── mock.go
├── operator
│ ├── issues584
│ │ └── issues584_test.go
│ └── operator_test.go
├── patch
│ ├── change_ident_test.go
│ ├── patch_count_test.go
│ ├── patch_test.go
│ └── set_type
│ │ └── set_type_test.go
├── pipes
│ └── pipes_test.go
├── playground
│ ├── data.go
│ └── env.go
└── time
│ └── time_test.go
├── testdata
├── crash.txt
├── crowdsec.json
├── examples.md
└── generated.txt
├── types
├── types.go
└── types_test.go
└── vm
├── debug.go
├── debug_off.go
├── debug_test.go
├── func_types
└── main.go
├── func_types[generated].go
├── opcodes.go
├── program.go
├── program_test.go
├── runtime
├── helpers
│ └── main.go
├── helpers[generated].go
├── helpers_test.go
├── runtime.go
└── sort.go
├── utils.go
├── vm.go
└── vm_test.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | *\[generated\].go linguist-language=txt
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: antonmedv
2 |
--------------------------------------------------------------------------------
/.github/images/demo.tape:
--------------------------------------------------------------------------------
1 | Set Shell zsh
2 | Sleep 500ms
3 | Type "repl"
4 | Enter
5 | Sleep 500ms
6 | Type "1..9 | filter("
7 | Sleep 500ms
8 | Type "# "
9 | Sleep 500ms
10 | Type "% 2 == 0) | map("
11 | Sleep 500ms
12 | Type "# ^ 2"
13 | Sleep 500ms
14 | Type ")"
15 | Enter
16 | Sleep 1s
17 | Type "de"
18 | Sleep 500ms
19 | Type "bug"
20 | Enter
21 | Sleep 1.5s
22 | Enter 50
23 | Escape
24 | Type "OB"
25 | Escape
26 | Type "OB"
27 | Escape
28 | Type "OB"
29 | Escape
30 | Type "OB"
31 | Escape
32 | Type "OB"
33 | Escape
34 | Type "OB"
35 | Escape
36 | Type "OB"
37 | Escape
38 | Type "OB"
39 | Escape
40 | Type "OB"
41 | Escape
42 | Type "OB"
43 | Escape
44 | Type "OB"
45 | Escape
46 | Type "OB"
47 | Escape
48 | Type "OB"
49 | Escape
50 | Type "OB"
51 | Enter
52 | Sleep 1.5s
53 | Escape
54 | Type "OB"
55 | Escape
56 | Type "OB"
57 | Escape
58 | Type "OB"
59 | Escape
60 | Type "OB"
61 | Escape
62 | Type "OB"
63 | Escape
64 | Type "OB"
65 | Escape
66 | Type "OB"
67 | Escape
68 | Type "OB"
69 | Escape
70 | Type "OB"
71 | Escape
72 | Type "OB"
73 | Enter
74 | Sleep 2s
75 | Escape
76 | Type "OB"
77 | Escape
78 | Type "OB"
79 | Ctrl+C
80 | Sleep 1s
81 | Ctrl+D
82 | Ctrl+D
83 |
84 |
--------------------------------------------------------------------------------
/.github/images/out.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/expr-lang/expr/5fbfe7211cd667c622a69bf4609e55f2eade7f63/.github/images/out.gif
--------------------------------------------------------------------------------
/.github/scripts/coverage.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 |
3 | const expected = 90
4 | const exclude = [
5 | 'expr/test', // We do not need to test the test package.
6 | 'checker/mock', // Mocks only used for testing.
7 | 'vm/func_types', // Generated files.
8 | 'vm/runtime/helpers', // Generated files.
9 | 'internal/difflib', // Test dependency. This is vendored dependency, and ideally we also have good tests for it.
10 | 'internal/spew', // Test dependency.
11 | 'internal/testify', // Test dependency.
12 | 'patcher/value', // Contains a lot of repeating code. Ideally we should have a test for it.
13 | 'pro', // Expr Pro is not a part of the main codebase.
14 | ]
15 |
16 | cd(path.resolve(__dirname, '..', '..'))
17 |
18 | await spinner('Running tests', async () => {
19 | await $`go test -coverprofile=coverage.out -coverpkg=github.com/expr-lang/expr/... ./...`
20 | const coverage = fs.readFileSync('coverage.out').toString()
21 | .split('\n')
22 | .filter(line => {
23 | for (const ex of exclude)
24 | if (line.includes(ex)) return false
25 | return true
26 | })
27 | .join('\n')
28 | fs.writeFileSync('coverage.out', coverage)
29 | await $`go tool cover -html=coverage.out -o coverage.html`
30 | })
31 |
32 | const cover = await $({verbose: true})`go tool cover -func=coverage.out`
33 | const total = +cover.stdout.match(/total:\s+\(statements\)\s+(\d+\.\d+)%/)[1]
34 | if (total < expected) {
35 | echo(chalk.red(`Coverage is too low: ${total}% < ${expected}% (expected)`))
36 | process.exit(1)
37 | } else {
38 | echo(`Coverage is good: ${chalk.green(total + '%')} >= ${expected}% (expected)`)
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | go-versions: [ '1.18', '1.22', '1.24' ]
15 | go-arch: [ '386' ]
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Setup Go ${{ matrix.go-version }}
19 | uses: actions/setup-go@v4
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 | - name: Build
23 | run: GOARCH=${{ matrix.go-arch }} go build
24 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: check
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | coverage:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Setup Go 1.18
15 | uses: actions/setup-go@v4
16 | with:
17 | go-version: 1.18
18 | - name: Test
19 | run: npx zx .github/scripts/coverage.mjs
20 |
--------------------------------------------------------------------------------
/.github/workflows/diff.yml:
--------------------------------------------------------------------------------
1 | name: diff
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 | jobs:
8 | bench:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Setup Go 1.18
12 | uses: actions/setup-go@v4
13 | with:
14 | go-version: 1.18
15 | - name: Install benchstat
16 | # NOTE: benchstat@latest requires go 1.23 since 2025-02-14 - this is the last go 1.18 ref
17 | # https://cs.opensource.google/go/x/perf/+/c95ad7d5b636f67d322a7e4832e83103d0fdd292
18 | run: go install golang.org/x/perf/cmd/benchstat@884df5810d2850d775c2cb4885a7ea339128a17d
19 |
20 | - uses: actions/checkout@v3
21 | - name: Benchmark new code
22 | run: go test -bench=. -benchmem -run=^$ -count=10 -timeout=30m | tee /tmp/new.txt
23 |
24 | - name: Checkout master
25 | uses: actions/checkout@v3
26 | with:
27 | ref: master
28 | - name: Benchmark master
29 | run: go test -bench=. -benchmem -run=^$ -count=10 -timeout=30m | tee /tmp/old.txt
30 |
31 | - name: Diff
32 | run: benchstat /tmp/old.txt /tmp/new.txt
33 |
--------------------------------------------------------------------------------
/.github/workflows/fuzz.yml:
--------------------------------------------------------------------------------
1 | name: fuzz
2 | on: [pull_request]
3 | permissions: {}
4 | jobs:
5 | fuzzing:
6 | runs-on: ubuntu-latest
7 | permissions:
8 | security-events: write
9 | steps:
10 | - name: Build Fuzzers
11 | id: build
12 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
13 | with:
14 | oss-fuzz-project-name: 'expr'
15 | language: 'go'
16 | - name: Run Fuzzers
17 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
18 | with:
19 | oss-fuzz-project-name: 'expr'
20 | language: 'go'
21 | fuzz-seconds: 600
22 | output-sarif: true
23 | - name: Upload Crash
24 | uses: actions/upload-artifact@v4
25 | if: failure() && steps.build.outcome == 'success'
26 | with:
27 | name: artifacts
28 | path: ./out/artifacts
29 | - name: Upload Sarif
30 | if: always() && steps.build.outcome == 'success'
31 | uses: github/codeql-action/upload-sarif@v3
32 | with:
33 | # Path to SARIF file relative to the root of the repository
34 | sarif_file: cifuzz-sarif/results.sarif
35 | checkout_path: cifuzz-sarif
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ]
15 | steps:
16 | - uses: actions/checkout@v3
17 | - name: Setup Go ${{ matrix.go-version }}
18 | uses: actions/setup-go@v4
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 | - name: Test
22 | run: go test ./...
23 |
24 | debug:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v3
28 | - name: Setup Go 1.18
29 | uses: actions/setup-go@v4
30 | with:
31 | go-version: 1.18
32 | - name: Test
33 | run: go test -tags=expr_debug -run=TestDebugger -v ./vm
34 |
35 | race:
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v3
39 | - name: Setup Go 1.21
40 | uses: actions/setup-go@v4
41 | with:
42 | go-version: 1.21
43 | - name: Test
44 | run: go test -race .
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.exe~
3 | *.dll
4 | *.so
5 | *.dylib
6 | *.test
7 | *.out
8 | *.html
9 | custom_tests.json
10 | pro/
11 | test/avs/
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Anton Medvedev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Expr is generally backwards compatible with very few exceptions, so we
6 | recommend users to always use the latest version to experience stability,
7 | performance and security.
8 |
9 | We generally backport security issues to a single previous minor version,
10 | unless this is not possible or feasible with a reasonable effort.
11 |
12 | | Version | Supported |
13 | |---------|--------------------|
14 | | 1.x | :white_check_mark: |
15 | | 0.x | :x: |
16 |
17 | ## Reporting a Vulnerability
18 |
19 | If you believe you've discovered a serious vulnerability, please contact the
20 | Expr core team at anton+security@medv.io. We will evaluate your report and if
21 | necessary issue a fix and an advisory. If the issue was previously undisclosed,
22 | we'll also mention your name in the credits.
23 |
--------------------------------------------------------------------------------
/ast/dump.go:
--------------------------------------------------------------------------------
1 | package ast
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "regexp"
7 | )
8 |
9 | func Dump(node Node) string {
10 | return dump(reflect.ValueOf(node), "")
11 | }
12 |
13 | func dump(v reflect.Value, ident string) string {
14 | if !v.IsValid() {
15 | return "nil"
16 | }
17 | t := v.Type()
18 | switch t.Kind() {
19 | case reflect.Struct:
20 | out := t.Name() + "{\n"
21 | for i := 0; i < t.NumField(); i++ {
22 | f := t.Field(i)
23 | if isPrivate(f.Name) {
24 | continue
25 | }
26 | s := v.Field(i)
27 | out += fmt.Sprintf("%v%v: %v,\n", ident+"\t", f.Name, dump(s, ident+"\t"))
28 | }
29 | return out + ident + "}"
30 | case reflect.Slice:
31 | if v.Len() == 0 {
32 | return t.String() + "{}"
33 | }
34 | out := t.String() + "{\n"
35 | for i := 0; i < v.Len(); i++ {
36 | s := v.Index(i)
37 | out += fmt.Sprintf("%v%v,", ident+"\t", dump(s, ident+"\t"))
38 | if i+1 < v.Len() {
39 | out += "\n"
40 | }
41 | }
42 | return out + "\n" + ident + "}"
43 | case reflect.Ptr:
44 | return dump(v.Elem(), ident)
45 | case reflect.Interface:
46 | return dump(reflect.ValueOf(v.Interface()), ident)
47 |
48 | case reflect.String:
49 | return fmt.Sprintf("%q", v)
50 | default:
51 | return fmt.Sprintf("%v", v)
52 | }
53 | }
54 |
55 | var isCapital = regexp.MustCompile("^[A-Z]")
56 |
57 | func isPrivate(s string) bool {
58 | return !isCapital.Match([]byte(s))
59 | }
60 |
--------------------------------------------------------------------------------
/ast/find.go:
--------------------------------------------------------------------------------
1 | package ast
2 |
3 | func Find(node Node, fn func(node Node) bool) Node {
4 | v := &finder{fn: fn}
5 | Walk(&node, v)
6 | return v.node
7 | }
8 |
9 | type finder struct {
10 | node Node
11 | fn func(node Node) bool
12 | }
13 |
14 | func (f *finder) Visit(node *Node) {
15 | if f.fn(*node) {
16 | f.node = *node
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ast/find_test.go:
--------------------------------------------------------------------------------
1 | package ast_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr/ast"
9 | )
10 |
11 | func TestFind(t *testing.T) {
12 | left := &ast.IdentifierNode{
13 | Value: "a",
14 | }
15 | var root ast.Node = &ast.BinaryNode{
16 | Operator: "+",
17 | Left: left,
18 | Right: &ast.IdentifierNode{
19 | Value: "b",
20 | },
21 | }
22 |
23 | x := ast.Find(root, func(node ast.Node) bool {
24 | if n, ok := node.(*ast.IdentifierNode); ok {
25 | return n.Value == "a"
26 | }
27 | return false
28 | })
29 |
30 | require.Equal(t, left, x)
31 | }
32 |
--------------------------------------------------------------------------------
/ast/print_test.go:
--------------------------------------------------------------------------------
1 | package ast_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr/ast"
10 | "github.com/expr-lang/expr/parser"
11 | )
12 |
13 | func TestPrint(t *testing.T) {
14 | tests := []struct {
15 | input string
16 | want string
17 | }{
18 | {`nil`, `nil`},
19 | {`true`, `true`},
20 | {`false`, `false`},
21 | {`1`, `1`},
22 | {`1.1`, `1.1`},
23 | {`"a"`, `"a"`},
24 | {`'a'`, `"a"`},
25 | {`a`, `a`},
26 | {`a.b`, `a.b`},
27 | {`a[0]`, `a[0]`},
28 | {`a["the b"]`, `a["the b"]`},
29 | {`a.b[0]`, `a.b[0]`},
30 | {`a?.b`, `a?.b`},
31 | {`x[0][1]`, `x[0][1]`},
32 | {`x?.[0]?.[1]`, `x?.[0]?.[1]`},
33 | {`-a`, `-a`},
34 | {`!a`, `!a`},
35 | {`not a`, `not a`},
36 | {`a + b`, `a + b`},
37 | {`a + b * c`, `a + b * c`},
38 | {`(a + b) * c`, `(a + b) * c`},
39 | {`a * (b + c)`, `a * (b + c)`},
40 | {`-(a + b) * c`, `-(a + b) * c`},
41 | {`a == b`, `a == b`},
42 | {`a matches b`, `a matches b`},
43 | {`a in b`, `a in b`},
44 | {`a not in b`, `not (a in b)`},
45 | {`a and b`, `a and b`},
46 | {`a or b`, `a or b`},
47 | {`a or b and c`, `a or (b and c)`},
48 | {`a or (b and c)`, `a or (b and c)`},
49 | {`(a or b) and c`, `(a or b) and c`},
50 | {`a ? b : c`, `a ? b : c`},
51 | {`a ? b : c ? d : e`, `a ? b : (c ? d : e)`},
52 | {`(a ? b : c) ? d : e`, `(a ? b : c) ? d : e`},
53 | {`a ? (b ? c : d) : e`, `a ? (b ? c : d) : e`},
54 | {`func()`, `func()`},
55 | {`func(a)`, `func(a)`},
56 | {`func(a, b)`, `func(a, b)`},
57 | {`{}`, `{}`},
58 | {`{a: b}`, `{a: b}`},
59 | {`{a: b, c: d}`, `{a: b, c: d}`},
60 | {`{"a": b, 'c': d}`, `{a: b, c: d}`},
61 | {`{"a": b, c: d}`, `{a: b, c: d}`},
62 | {`{"a": b, 8: 8}`, `{a: b, "8": 8}`},
63 | {`{"9": 9, '8': 8, "foo": d}`, `{"9": 9, "8": 8, foo: d}`},
64 | {`[]`, `[]`},
65 | {`[a]`, `[a]`},
66 | {`[a, b]`, `[a, b]`},
67 | {`len(a)`, `len(a)`},
68 | {`map(a, # > 0)`, `map(a, # > 0)`},
69 | {`map(a, {# > 0})`, `map(a, # > 0)`},
70 | {`map(a, .b)`, `map(a, .b)`},
71 | {`a.b()`, `a.b()`},
72 | {`a.b(c)`, `a.b(c)`},
73 | {`a[1:-1]`, `a[1:-1]`},
74 | {`a[1:]`, `a[1:]`},
75 | {`a[1:]`, `a[1:]`},
76 | {`a[:]`, `a[:]`},
77 | {`(nil ?? 1) > 0`, `(nil ?? 1) > 0`},
78 | {`{("a" + "b"): 42}`, `{("a" + "b"): 42}`},
79 | {`(One == 1 ? true : false) && Two == 2`, `(One == 1 ? true : false) && Two == 2`},
80 | {`not (a == 1 ? b > 1 : b < 2)`, `not (a == 1 ? b > 1 : b < 2)`},
81 | {`(-(1+1)) ** 2`, `(-(1 + 1)) ** 2`},
82 | {`2 ** (-(1+1))`, `2 ** -(1 + 1)`},
83 | {`(2 ** 2) ** 3`, `(2 ** 2) ** 3`},
84 | {`(3 + 5) / (5 % 3)`, `(3 + 5) / (5 % 3)`},
85 | {`(-(1+1)) == 2`, `-(1 + 1) == 2`},
86 | {`if true { 1 } else { 2 }`, `true ? 1 : 2`},
87 | }
88 |
89 | for _, tt := range tests {
90 | t.Run(tt.input, func(t *testing.T) {
91 | tree, err := parser.Parse(tt.input)
92 | require.NoError(t, err)
93 | assert.Equal(t, tt.want, tree.Node.String())
94 | })
95 | }
96 | }
97 |
98 | func TestPrint_MemberNode(t *testing.T) {
99 | node := &ast.MemberNode{
100 | Node: &ast.IdentifierNode{
101 | Value: "a",
102 | },
103 | Property: &ast.StringNode{Value: "b c"},
104 | Optional: true,
105 | }
106 | require.Equal(t, `a?.["b c"]`, node.String())
107 | }
108 |
109 | func TestPrint_ConstantNode(t *testing.T) {
110 | tests := []struct {
111 | input any
112 | want string
113 | }{
114 | {nil, `nil`},
115 | {true, `true`},
116 | {false, `false`},
117 | {1, `1`},
118 | {1.1, `1.1`},
119 | {"a", `"a"`},
120 | {[]int{1, 2, 3}, `[1,2,3]`},
121 | {map[string]int{"a": 1}, `{"a":1}`},
122 | }
123 |
124 | for _, tt := range tests {
125 | t.Run(tt.want, func(t *testing.T) {
126 | node := &ast.ConstantNode{
127 | Value: tt.input,
128 | }
129 | require.Equal(t, tt.want, node.String())
130 | })
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/ast/visitor.go:
--------------------------------------------------------------------------------
1 | package ast
2 |
3 | import "fmt"
4 |
5 | type Visitor interface {
6 | Visit(node *Node)
7 | }
8 |
9 | func Walk(node *Node, v Visitor) {
10 | if *node == nil {
11 | return
12 | }
13 | switch n := (*node).(type) {
14 | case *NilNode:
15 | case *IdentifierNode:
16 | case *IntegerNode:
17 | case *FloatNode:
18 | case *BoolNode:
19 | case *StringNode:
20 | case *ConstantNode:
21 | case *UnaryNode:
22 | Walk(&n.Node, v)
23 | case *BinaryNode:
24 | Walk(&n.Left, v)
25 | Walk(&n.Right, v)
26 | case *ChainNode:
27 | Walk(&n.Node, v)
28 | case *MemberNode:
29 | Walk(&n.Node, v)
30 | Walk(&n.Property, v)
31 | case *SliceNode:
32 | Walk(&n.Node, v)
33 | if n.From != nil {
34 | Walk(&n.From, v)
35 | }
36 | if n.To != nil {
37 | Walk(&n.To, v)
38 | }
39 | case *CallNode:
40 | Walk(&n.Callee, v)
41 | for i := range n.Arguments {
42 | Walk(&n.Arguments[i], v)
43 | }
44 | case *BuiltinNode:
45 | for i := range n.Arguments {
46 | Walk(&n.Arguments[i], v)
47 | }
48 | case *PredicateNode:
49 | Walk(&n.Node, v)
50 | case *PointerNode:
51 | case *VariableDeclaratorNode:
52 | Walk(&n.Value, v)
53 | Walk(&n.Expr, v)
54 | case *SequenceNode:
55 | for i := range n.Nodes {
56 | Walk(&n.Nodes[i], v)
57 | }
58 | case *ConditionalNode:
59 | Walk(&n.Cond, v)
60 | Walk(&n.Exp1, v)
61 | Walk(&n.Exp2, v)
62 | case *ArrayNode:
63 | for i := range n.Nodes {
64 | Walk(&n.Nodes[i], v)
65 | }
66 | case *MapNode:
67 | for i := range n.Pairs {
68 | Walk(&n.Pairs[i], v)
69 | }
70 | case *PairNode:
71 | Walk(&n.Key, v)
72 | Walk(&n.Value, v)
73 | default:
74 | panic(fmt.Sprintf("undefined node type (%T)", node))
75 | }
76 |
77 | v.Visit(node)
78 | }
79 |
--------------------------------------------------------------------------------
/ast/visitor_test.go:
--------------------------------------------------------------------------------
1 | package ast_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 |
8 | "github.com/expr-lang/expr/ast"
9 | )
10 |
11 | type visitor struct {
12 | identifiers []string
13 | }
14 |
15 | func (v *visitor) Visit(node *ast.Node) {
16 | if n, ok := (*node).(*ast.IdentifierNode); ok {
17 | v.identifiers = append(v.identifiers, n.Value)
18 | }
19 | }
20 |
21 | func TestWalk(t *testing.T) {
22 | var node ast.Node
23 | node = &ast.BinaryNode{
24 | Operator: "+",
25 | Left: &ast.IdentifierNode{Value: "foo"},
26 | Right: &ast.IdentifierNode{Value: "bar"},
27 | }
28 |
29 | visitor := &visitor{}
30 | ast.Walk(&node, visitor)
31 | assert.Equal(t, []string{"foo", "bar"}, visitor.identifiers)
32 | }
33 |
34 | type patcher struct{}
35 |
36 | func (p *patcher) Visit(node *ast.Node) {
37 | if _, ok := (*node).(*ast.IdentifierNode); ok {
38 | *node = &ast.NilNode{}
39 | }
40 | }
41 |
42 | func TestWalk_patch(t *testing.T) {
43 | var node ast.Node
44 | node = &ast.BinaryNode{
45 | Operator: "+",
46 | Left: &ast.IdentifierNode{Value: "foo"},
47 | Right: &ast.IdentifierNode{Value: "bar"},
48 | }
49 |
50 | patcher := &patcher{}
51 | ast.Walk(&node, patcher)
52 | assert.IsType(t, &ast.NilNode{}, node.(*ast.BinaryNode).Left)
53 | assert.IsType(t, &ast.NilNode{}, node.(*ast.BinaryNode).Right)
54 | }
55 |
--------------------------------------------------------------------------------
/builtin/function.go:
--------------------------------------------------------------------------------
1 | package builtin
2 |
3 | import (
4 | "reflect"
5 | )
6 |
7 | type Function struct {
8 | Name string
9 | Fast func(arg any) any
10 | Func func(args ...any) (any, error)
11 | Safe func(args ...any) (any, uint, error)
12 | Types []reflect.Type
13 | Validate func(args []reflect.Type) (reflect.Type, error)
14 | Deref func(i int, arg reflect.Type) bool
15 | Predicate bool
16 | }
17 |
18 | func (f *Function) Type() reflect.Type {
19 | if len(f.Types) > 0 {
20 | return f.Types[0]
21 | }
22 | return reflect.TypeOf(f.Func)
23 | }
24 |
--------------------------------------------------------------------------------
/builtin/utils.go:
--------------------------------------------------------------------------------
1 | package builtin
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "time"
7 |
8 | "github.com/expr-lang/expr/internal/deref"
9 | )
10 |
11 | var (
12 | anyType = reflect.TypeOf(new(any)).Elem()
13 | integerType = reflect.TypeOf(0)
14 | floatType = reflect.TypeOf(float64(0))
15 | arrayType = reflect.TypeOf([]any{})
16 | mapType = reflect.TypeOf(map[any]any{})
17 | timeType = reflect.TypeOf(new(time.Time)).Elem()
18 | locationType = reflect.TypeOf(new(time.Location))
19 | )
20 |
21 | func kind(t reflect.Type) reflect.Kind {
22 | if t == nil {
23 | return reflect.Invalid
24 | }
25 | t = deref.Type(t)
26 | return t.Kind()
27 | }
28 |
29 | func types(types ...any) []reflect.Type {
30 | ts := make([]reflect.Type, len(types))
31 | for i, t := range types {
32 | t := reflect.TypeOf(t)
33 | if t.Kind() == reflect.Ptr {
34 | t = t.Elem()
35 | }
36 | if t.Kind() != reflect.Func {
37 | panic("not a function")
38 | }
39 | ts[i] = t
40 | }
41 | return ts
42 | }
43 |
44 | func toInt(val any) (int, error) {
45 | switch v := val.(type) {
46 | case int:
47 | return v, nil
48 | case int8:
49 | return int(v), nil
50 | case int16:
51 | return int(v), nil
52 | case int32:
53 | return int(v), nil
54 | case int64:
55 | return int(v), nil
56 | case uint:
57 | return int(v), nil
58 | case uint8:
59 | return int(v), nil
60 | case uint16:
61 | return int(v), nil
62 | case uint32:
63 | return int(v), nil
64 | case uint64:
65 | return int(v), nil
66 | default:
67 | return 0, fmt.Errorf("cannot use %T as argument (type int)", val)
68 | }
69 | }
70 |
71 | func bitFunc(name string, fn func(x, y int) (any, error)) *Function {
72 | return &Function{
73 | Name: name,
74 | Func: func(args ...any) (any, error) {
75 | if len(args) != 2 {
76 | return nil, fmt.Errorf("invalid number of arguments for %s (expected 2, got %d)", name, len(args))
77 | }
78 | x, err := toInt(args[0])
79 | if err != nil {
80 | return nil, fmt.Errorf("%v to call %s", err, name)
81 | }
82 | y, err := toInt(args[1])
83 | if err != nil {
84 | return nil, fmt.Errorf("%v to call %s", err, name)
85 | }
86 | return fn(x, y)
87 | },
88 | Types: types(new(func(int, int) int)),
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/builtin/validation.go:
--------------------------------------------------------------------------------
1 | package builtin
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/expr-lang/expr/internal/deref"
8 | )
9 |
10 | func validateAggregateFunc(name string, args []reflect.Type) (reflect.Type, error) {
11 | switch len(args) {
12 | case 0:
13 | return anyType, fmt.Errorf("not enough arguments to call %s", name)
14 | default:
15 | for _, arg := range args {
16 | switch kind(deref.Type(arg)) {
17 | case reflect.Interface, reflect.Array, reflect.Slice:
18 | return anyType, nil
19 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
20 | default:
21 | return anyType, fmt.Errorf("invalid argument for %s (type %s)", name, arg)
22 | }
23 | }
24 | return args[0], nil
25 | }
26 | }
27 |
28 | func validateRoundFunc(name string, args []reflect.Type) (reflect.Type, error) {
29 | if len(args) != 1 {
30 | return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
31 | }
32 | switch kind(args[0]) {
33 | case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface:
34 | return floatType, nil
35 | default:
36 | return anyType, fmt.Errorf("invalid argument for %s (type %s)", name, args[0])
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/checker/info.go:
--------------------------------------------------------------------------------
1 | package checker
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/expr-lang/expr/ast"
7 | . "github.com/expr-lang/expr/checker/nature"
8 | "github.com/expr-lang/expr/vm"
9 | )
10 |
11 | func FieldIndex(env Nature, node ast.Node) (bool, []int, string) {
12 | switch n := node.(type) {
13 | case *ast.IdentifierNode:
14 | if env.Kind() == reflect.Struct {
15 | if field, ok := env.Get(n.Value); ok && len(field.FieldIndex) > 0 {
16 | return true, field.FieldIndex, n.Value
17 | }
18 | }
19 | case *ast.MemberNode:
20 | base := n.Node.Nature()
21 | base = base.Deref()
22 | if base.Kind() == reflect.Struct {
23 | if prop, ok := n.Property.(*ast.StringNode); ok {
24 | name := prop.Value
25 | if field, ok := base.FieldByName(name); ok {
26 | return true, field.FieldIndex, name
27 | }
28 | }
29 | }
30 | }
31 | return false, nil, ""
32 | }
33 |
34 | func MethodIndex(env Nature, node ast.Node) (bool, int, string) {
35 | switch n := node.(type) {
36 | case *ast.IdentifierNode:
37 | if env.Kind() == reflect.Struct {
38 | if m, ok := env.Get(n.Value); ok {
39 | return m.Method, m.MethodIndex, n.Value
40 | }
41 | }
42 | case *ast.MemberNode:
43 | if name, ok := n.Property.(*ast.StringNode); ok {
44 | base := n.Node.Type()
45 | if base != nil && base.Kind() != reflect.Interface {
46 | if m, ok := base.MethodByName(name.Value); ok {
47 | return true, m.Index, name.Value
48 | }
49 | }
50 | }
51 | }
52 | return false, 0, ""
53 | }
54 |
55 | func TypedFuncIndex(fn reflect.Type, method bool) (int, bool) {
56 | if fn == nil {
57 | return 0, false
58 | }
59 | if fn.Kind() != reflect.Func {
60 | return 0, false
61 | }
62 | // OnCallTyped doesn't work for functions with variadic arguments.
63 | if fn.IsVariadic() {
64 | return 0, false
65 | }
66 | // OnCallTyped doesn't work named function, like `type MyFunc func() int`.
67 | if fn.PkgPath() != "" { // If PkgPath() is not empty, it means that function is named.
68 | return 0, false
69 | }
70 |
71 | fnNumIn := fn.NumIn()
72 | fnInOffset := 0
73 | if method {
74 | fnNumIn--
75 | fnInOffset = 1
76 | }
77 |
78 | funcTypes:
79 | for i := range vm.FuncTypes {
80 | if i == 0 {
81 | continue
82 | }
83 | typed := reflect.ValueOf(vm.FuncTypes[i]).Elem().Type()
84 | if typed.Kind() != reflect.Func {
85 | continue
86 | }
87 | if typed.NumOut() != fn.NumOut() {
88 | continue
89 | }
90 | for j := 0; j < typed.NumOut(); j++ {
91 | if typed.Out(j) != fn.Out(j) {
92 | continue funcTypes
93 | }
94 | }
95 | if typed.NumIn() != fnNumIn {
96 | continue
97 | }
98 | for j := 0; j < typed.NumIn(); j++ {
99 | if typed.In(j) != fn.In(j+fnInOffset) {
100 | continue funcTypes
101 | }
102 | }
103 | return i, true
104 | }
105 | return 0, false
106 | }
107 |
108 | func IsFastFunc(fn reflect.Type, method bool) bool {
109 | if fn == nil {
110 | return false
111 | }
112 | if fn.Kind() != reflect.Func {
113 | return false
114 | }
115 | numIn := 1
116 | if method {
117 | numIn = 2
118 | }
119 | if fn.IsVariadic() &&
120 | fn.NumIn() == numIn &&
121 | fn.NumOut() == 1 &&
122 | fn.Out(0).Kind() == reflect.Interface {
123 | rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
124 | if kind(rest) == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
125 | return true
126 | }
127 | }
128 | return false
129 | }
130 |
--------------------------------------------------------------------------------
/checker/info_test.go:
--------------------------------------------------------------------------------
1 | package checker_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 |
8 | "github.com/expr-lang/expr/internal/testify/require"
9 |
10 | "github.com/expr-lang/expr/checker"
11 | "github.com/expr-lang/expr/test/mock"
12 | )
13 |
14 | func TestTypedFuncIndex(t *testing.T) {
15 | fn := func() time.Duration {
16 | return 1 * time.Second
17 | }
18 | index, ok := checker.TypedFuncIndex(reflect.TypeOf(fn), false)
19 | require.True(t, ok)
20 | require.Equal(t, 1, index)
21 | }
22 |
23 | func TestTypedFuncIndex_excludes_named_functions(t *testing.T) {
24 | var fn mock.MyFunc
25 |
26 | _, ok := checker.TypedFuncIndex(reflect.TypeOf(fn), false)
27 | require.False(t, ok)
28 | }
29 |
--------------------------------------------------------------------------------
/checker/nature/utils.go:
--------------------------------------------------------------------------------
1 | package nature
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/expr-lang/expr/internal/deref"
7 | )
8 |
9 | func fieldName(field reflect.StructField) string {
10 | if taggedName := field.Tag.Get("expr"); taggedName != "" {
11 | return taggedName
12 | }
13 | return field.Name
14 | }
15 |
16 | func fetchField(t reflect.Type, name string) (reflect.StructField, bool) {
17 | // If t is not a struct, early return.
18 | if t.Kind() != reflect.Struct {
19 | return reflect.StructField{}, false
20 | }
21 |
22 | // First check all structs fields.
23 | for i := 0; i < t.NumField(); i++ {
24 | field := t.Field(i)
25 | // Search all fields, even embedded structs.
26 | if fieldName(field) == name {
27 | return field, true
28 | }
29 | }
30 |
31 | // Second check fields of embedded structs.
32 | for i := 0; i < t.NumField(); i++ {
33 | anon := t.Field(i)
34 | if anon.Anonymous {
35 | anonType := anon.Type
36 | if anonType.Kind() == reflect.Pointer {
37 | anonType = anonType.Elem()
38 | }
39 | if field, ok := fetchField(anonType, name); ok {
40 | field.Index = append(anon.Index, field.Index...)
41 | return field, true
42 | }
43 | }
44 | }
45 |
46 | return reflect.StructField{}, false
47 | }
48 |
49 | func StructFields(t reflect.Type) map[string]Nature {
50 | table := make(map[string]Nature)
51 |
52 | t = deref.Type(t)
53 | if t == nil {
54 | return table
55 | }
56 |
57 | switch t.Kind() {
58 | case reflect.Struct:
59 | for i := 0; i < t.NumField(); i++ {
60 | f := t.Field(i)
61 |
62 | if f.Anonymous {
63 | for name, typ := range StructFields(f.Type) {
64 | if _, ok := table[name]; ok {
65 | continue
66 | }
67 | typ.FieldIndex = append(f.Index, typ.FieldIndex...)
68 | table[name] = typ
69 | }
70 | }
71 |
72 | table[fieldName(f)] = Nature{
73 | Type: f.Type,
74 | FieldIndex: f.Index,
75 | }
76 |
77 | }
78 | }
79 |
80 | return table
81 | }
82 |
--------------------------------------------------------------------------------
/checker/types.go:
--------------------------------------------------------------------------------
1 | package checker
2 |
3 | import (
4 | "reflect"
5 | "time"
6 |
7 | . "github.com/expr-lang/expr/checker/nature"
8 | )
9 |
10 | var (
11 | unknown = Nature{}
12 | nilNature = Nature{Nil: true}
13 | boolNature = Nature{Type: reflect.TypeOf(true)}
14 | integerNature = Nature{Type: reflect.TypeOf(0)}
15 | floatNature = Nature{Type: reflect.TypeOf(float64(0))}
16 | stringNature = Nature{Type: reflect.TypeOf("")}
17 | arrayNature = Nature{Type: reflect.TypeOf([]any{})}
18 | mapNature = Nature{Type: reflect.TypeOf(map[string]any{})}
19 | timeNature = Nature{Type: reflect.TypeOf(time.Time{})}
20 | durationNature = Nature{Type: reflect.TypeOf(time.Duration(0))}
21 | )
22 |
23 | var (
24 | anyType = reflect.TypeOf(new(any)).Elem()
25 | timeType = reflect.TypeOf(time.Time{})
26 | durationType = reflect.TypeOf(time.Duration(0))
27 | arrayType = reflect.TypeOf([]any{})
28 | )
29 |
30 | func arrayOf(nt Nature) Nature {
31 | return Nature{
32 | Type: arrayType,
33 | ArrayOf: &nt,
34 | }
35 | }
36 |
37 | func isNil(nt Nature) bool {
38 | return nt.Nil
39 | }
40 |
41 | func combined(l, r Nature) Nature {
42 | if isUnknown(l) || isUnknown(r) {
43 | return unknown
44 | }
45 | if isFloat(l) || isFloat(r) {
46 | return floatNature
47 | }
48 | return integerNature
49 | }
50 |
51 | func anyOf(nt Nature, fns ...func(Nature) bool) bool {
52 | for _, fn := range fns {
53 | if fn(nt) {
54 | return true
55 | }
56 | }
57 | return false
58 | }
59 |
60 | func or(l, r Nature, fns ...func(Nature) bool) bool {
61 | if isUnknown(l) && isUnknown(r) {
62 | return true
63 | }
64 | if isUnknown(l) && anyOf(r, fns...) {
65 | return true
66 | }
67 | if isUnknown(r) && anyOf(l, fns...) {
68 | return true
69 | }
70 | return false
71 | }
72 |
73 | func isUnknown(nt Nature) bool {
74 | return nt.IsUnknown()
75 | }
76 |
77 | func isInteger(nt Nature) bool {
78 | switch nt.Kind() {
79 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
80 | fallthrough
81 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
82 | return nt.PkgPath() == ""
83 | }
84 | return false
85 | }
86 |
87 | func isFloat(nt Nature) bool {
88 | switch nt.Kind() {
89 | case reflect.Float32, reflect.Float64:
90 | return nt.PkgPath() == ""
91 | }
92 | return false
93 | }
94 |
95 | func isNumber(nt Nature) bool {
96 | return isInteger(nt) || isFloat(nt)
97 | }
98 |
99 | func isTime(nt Nature) bool {
100 | switch nt.Type {
101 | case timeType:
102 | return true
103 | }
104 | return false
105 | }
106 |
107 | func isDuration(nt Nature) bool {
108 | switch nt.Type {
109 | case durationType:
110 | return true
111 | }
112 | return false
113 | }
114 |
115 | func isBool(nt Nature) bool {
116 | switch nt.Kind() {
117 | case reflect.Bool:
118 | return true
119 | }
120 | return false
121 | }
122 |
123 | func isString(nt Nature) bool {
124 | switch nt.Kind() {
125 | case reflect.String:
126 | return true
127 | }
128 | return false
129 | }
130 |
131 | func isArray(nt Nature) bool {
132 | switch nt.Kind() {
133 | case reflect.Slice, reflect.Array:
134 | return true
135 | }
136 | return false
137 | }
138 |
139 | func isMap(nt Nature) bool {
140 | switch nt.Kind() {
141 | case reflect.Map:
142 | return true
143 | }
144 | return false
145 | }
146 |
147 | func isStruct(nt Nature) bool {
148 | switch nt.Kind() {
149 | case reflect.Struct:
150 | return true
151 | }
152 | return false
153 | }
154 |
155 | func isFunc(nt Nature) bool {
156 | switch nt.Kind() {
157 | case reflect.Func:
158 | return true
159 | }
160 | return false
161 | }
162 |
163 | func kind(t reflect.Type) reflect.Kind {
164 | if t == nil {
165 | return reflect.Invalid
166 | }
167 | return t.Kind()
168 | }
169 |
170 | func isComparable(l, r Nature) bool {
171 | if isUnknown(l) || isUnknown(r) {
172 | return true
173 | }
174 | if isNil(l) || isNil(r) {
175 | return true
176 | }
177 | if isNumber(l) && isNumber(r) {
178 | return true
179 | }
180 | if isDuration(l) && isDuration(r) {
181 | return true
182 | }
183 | if isTime(l) && isTime(r) {
184 | return true
185 | }
186 | if isArray(l) && isArray(r) {
187 | return true
188 | }
189 | return l.AssignableTo(r)
190 | }
191 |
--------------------------------------------------------------------------------
/conf/config.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/expr-lang/expr/ast"
8 | "github.com/expr-lang/expr/builtin"
9 | "github.com/expr-lang/expr/checker/nature"
10 | "github.com/expr-lang/expr/vm/runtime"
11 | )
12 |
13 | const (
14 | // DefaultMemoryBudget represents an upper limit of memory usage
15 | DefaultMemoryBudget uint = 1e6
16 |
17 | // DefaultMaxNodes represents an upper limit of AST nodes
18 | DefaultMaxNodes uint = 10000
19 | )
20 |
21 | type FunctionsTable map[string]*builtin.Function
22 |
23 | type Config struct {
24 | EnvObject any
25 | Env nature.Nature
26 | Expect reflect.Kind
27 | ExpectAny bool
28 | Optimize bool
29 | Strict bool
30 | Profile bool
31 | MaxNodes uint
32 | MemoryBudget uint
33 | ConstFns map[string]reflect.Value
34 | Visitors []ast.Visitor
35 | Functions FunctionsTable
36 | Builtins FunctionsTable
37 | Disabled map[string]bool // disabled builtins
38 | }
39 |
40 | // CreateNew creates new config with default values.
41 | func CreateNew() *Config {
42 | c := &Config{
43 | Optimize: true,
44 | MaxNodes: DefaultMaxNodes,
45 | MemoryBudget: DefaultMemoryBudget,
46 | ConstFns: make(map[string]reflect.Value),
47 | Functions: make(map[string]*builtin.Function),
48 | Builtins: make(map[string]*builtin.Function),
49 | Disabled: make(map[string]bool),
50 | }
51 | for _, f := range builtin.Builtins {
52 | c.Builtins[f.Name] = f
53 | }
54 | return c
55 | }
56 |
57 | // New creates new config with environment.
58 | func New(env any) *Config {
59 | c := CreateNew()
60 | c.WithEnv(env)
61 | return c
62 | }
63 |
64 | func (c *Config) WithEnv(env any) {
65 | c.EnvObject = env
66 | c.Env = Env(env)
67 | c.Strict = c.Env.Strict
68 | }
69 |
70 | func (c *Config) ConstExpr(name string) {
71 | if c.EnvObject == nil {
72 | panic("no environment is specified for ConstExpr()")
73 | }
74 | fn := reflect.ValueOf(runtime.Fetch(c.EnvObject, name))
75 | if fn.Kind() != reflect.Func {
76 | panic(fmt.Errorf("const expression %q must be a function", name))
77 | }
78 | c.ConstFns[name] = fn
79 | }
80 |
81 | type Checker interface {
82 | Check()
83 | }
84 |
85 | func (c *Config) Check() {
86 | for _, v := range c.Visitors {
87 | if c, ok := v.(Checker); ok {
88 | c.Check()
89 | }
90 | }
91 | }
92 |
93 | func (c *Config) IsOverridden(name string) bool {
94 | if _, ok := c.Functions[name]; ok {
95 | return true
96 | }
97 | if _, ok := c.Env.Get(name); ok {
98 | return true
99 | }
100 | return false
101 | }
102 |
--------------------------------------------------------------------------------
/conf/env.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | . "github.com/expr-lang/expr/checker/nature"
8 | "github.com/expr-lang/expr/internal/deref"
9 | "github.com/expr-lang/expr/types"
10 | )
11 |
12 | func Env(env any) Nature {
13 | if env == nil {
14 | return Nature{
15 | Type: reflect.TypeOf(map[string]any{}),
16 | Strict: true,
17 | }
18 | }
19 |
20 | switch env := env.(type) {
21 | case types.Map:
22 | return env.Nature()
23 | }
24 |
25 | v := reflect.ValueOf(env)
26 | d := deref.Value(v)
27 |
28 | switch d.Kind() {
29 | case reflect.Struct:
30 | return Nature{
31 | Type: v.Type(),
32 | Strict: true,
33 | }
34 |
35 | case reflect.Map:
36 | n := Nature{
37 | Type: v.Type(),
38 | Fields: make(map[string]Nature, v.Len()),
39 | Strict: true,
40 | }
41 |
42 | for _, key := range v.MapKeys() {
43 | elem := v.MapIndex(key)
44 | if !elem.IsValid() || !elem.CanInterface() {
45 | panic(fmt.Sprintf("invalid map value: %s", key))
46 | }
47 |
48 | face := elem.Interface()
49 |
50 | switch face := face.(type) {
51 | case types.Map:
52 | n.Fields[key.String()] = face.Nature()
53 |
54 | default:
55 | if face == nil {
56 | n.Fields[key.String()] = Nature{Nil: true}
57 | continue
58 | }
59 | n.Fields[key.String()] = Nature{Type: reflect.TypeOf(face)}
60 | }
61 |
62 | }
63 |
64 | return n
65 | }
66 |
67 | panic(fmt.Sprintf("unknown type %T", env))
68 | }
69 |
--------------------------------------------------------------------------------
/debug/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/expr-lang/expr/debug
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/expr-lang/expr v0.0.0
7 | github.com/gdamore/tcell/v2 v2.6.0
8 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703
9 | )
10 |
11 | require (
12 | github.com/gdamore/encoding v1.0.0 // indirect
13 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
14 | github.com/mattn/go-runewidth v0.0.15 // indirect
15 | github.com/rivo/uniseg v0.4.4 // indirect
16 | golang.org/x/sys v0.11.0 // indirect
17 | golang.org/x/term v0.11.0 // indirect
18 | golang.org/x/text v0.12.0 // indirect
19 | )
20 |
21 | replace github.com/expr-lang/expr => ../
22 |
--------------------------------------------------------------------------------
/debug/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
2 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
3 | github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
4 | github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
5 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
6 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
7 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
8 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
9 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
10 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 h1:ZyM/+FYnpbZsFWuCohniM56kRoHRB4r5EuIzXEYkpxo=
11 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE=
12 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
13 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
14 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
15 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
16 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
18 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
19 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
23 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
32 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
34 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
35 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
36 | golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
37 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
39 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
40 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
41 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
42 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
43 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
46 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
47 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
48 |
--------------------------------------------------------------------------------
/docgen/README.md:
--------------------------------------------------------------------------------
1 | # DocGen
2 |
3 | This package provides documentation generator with JSON or Markdown output.
4 |
5 | ## Usage
6 |
7 | Create a file and put next code into it.
8 |
9 | ```go
10 | package main
11 |
12 | import (
13 | "encoding/json"
14 | "fmt"
15 |
16 | "github.com/expr-lang/expr/docgen"
17 | )
18 |
19 | func main() {
20 | // TODO: Replace env with your own types.
21 | doc := docgen.CreateDoc(env)
22 |
23 | buf, err := json.MarshalIndent(doc, "", " ")
24 | if err != nil {
25 | panic(err)
26 | }
27 | fmt.Println(string(buf))
28 | }
29 | ```
30 |
31 | Run `go run your_file.go`. Documentation will be printed in JSON format.
32 |
33 | ## Markdown
34 |
35 | To generate markdown documentation:
36 |
37 | ```go
38 | package main
39 |
40 | import "github.com/expr-lang/expr/docgen"
41 |
42 | func main() {
43 | // TODO: Replace env with your own types.
44 | doc := docgen.CreateDoc(env)
45 |
46 | print(doc.Markdown())
47 | }
48 | ```
49 |
--------------------------------------------------------------------------------
/docgen/markdown.go:
--------------------------------------------------------------------------------
1 | package docgen
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 | )
8 |
9 | func (c *Context) Markdown() string {
10 | var variables []string
11 | for name := range c.Variables {
12 | variables = append(variables, string(name))
13 | }
14 |
15 | var types []string
16 | for name := range c.Types {
17 | types = append(types, string(name))
18 | }
19 |
20 | sort.Strings(variables)
21 | sort.Strings(types)
22 |
23 | out := `### Variables
24 | | Name | Type |
25 | |------|------|
26 | `
27 | for _, name := range variables {
28 | v := c.Variables[Identifier(name)]
29 | if v.Kind == "func" {
30 | continue
31 | }
32 | if v.Kind == "operator" {
33 | continue
34 | }
35 | out += fmt.Sprintf("| %v | %v |\n", name, link(v))
36 | }
37 |
38 | out += `
39 | ### Functions
40 | | Name | Return type |
41 | |------|-------------|
42 | `
43 | for _, name := range variables {
44 | v := c.Variables[Identifier(name)]
45 | if v.Kind == "func" {
46 | args := make([]string, len(v.Arguments))
47 | for i, arg := range v.Arguments {
48 | args[i] = link(arg)
49 | }
50 | out += fmt.Sprintf("| %v(%v) | %v |\n", name, strings.Join(args, ", "), link(v.Return))
51 | }
52 | }
53 |
54 | out += "\n### Types\n"
55 | for _, name := range types {
56 | t := c.Types[TypeName(name)]
57 | out += fmt.Sprintf("#### %v\n", name)
58 | out += fields(t)
59 | out += "\n"
60 | }
61 |
62 | return out
63 | }
64 |
65 | func link(t *Type) string {
66 | if t == nil {
67 | return "nil"
68 | }
69 | if t.Name != "" {
70 | return fmt.Sprintf("[%v](#%v)", t.Name, t.Name)
71 | }
72 | if t.Kind == "array" {
73 | return fmt.Sprintf("array(%v)", link(t.Type))
74 | }
75 | if t.Kind == "map" {
76 | return fmt.Sprintf("map(%v => %v)", link(t.Key), link(t.Type))
77 | }
78 | return fmt.Sprintf("`%v`", t.Kind)
79 | }
80 |
81 | func fields(t *Type) string {
82 | var fields []string
83 | for field := range t.Fields {
84 | fields = append(fields, string(field))
85 | }
86 | sort.Strings(fields)
87 |
88 | out := ""
89 | foundFields := false
90 | for _, name := range fields {
91 | v := t.Fields[Identifier(name)]
92 | if v.Kind != "func" {
93 | if !foundFields {
94 | out += "| Field | Type |\n|---|---|\n"
95 | }
96 | foundFields = true
97 |
98 | out += fmt.Sprintf("| %v | %v |\n", name, link(v))
99 | }
100 | }
101 | foundMethod := false
102 | for _, name := range fields {
103 | v := t.Fields[Identifier(name)]
104 | if v.Kind == "func" {
105 | if !foundMethod {
106 | out += "\n| Method | Returns |\n|---|---|\n"
107 | }
108 | foundMethod = true
109 |
110 | args := make([]string, len(v.Arguments))
111 | for i, arg := range v.Arguments {
112 | args[i] = link(arg)
113 | }
114 | out += fmt.Sprintf("| %v(%v) | %v |\n", name, strings.Join(args, ", "), link(v.Return))
115 | }
116 | }
117 | return out
118 | }
119 |
--------------------------------------------------------------------------------
/docs/environment.md:
--------------------------------------------------------------------------------
1 | # Environment
2 |
3 | The environment is a map or a struct that contains the variables and functions that the expression can access.
4 |
5 | ## Struct as Environment
6 |
7 | Let's consider the following example:
8 |
9 | ```go
10 | type Env struct {
11 | UpdatedAt time.Time
12 | Posts []Post
13 | Map map[string]string `expr:"tags"`
14 | }
15 | ```
16 |
17 | The `Env` struct contains 3 variables that the expression can access: `UpdatedAt`, `Posts`, and `tags`.
18 |
19 | :::info
20 | The `expr` tag is used to rename the `Map` field to `tags` variable in the expression.
21 | :::
22 |
23 | The `Env` struct can also contain methods. The methods defined on the struct become functions that the expression can
24 | call.
25 |
26 | ```go
27 | func (Env) Format(t time.Time) string {
28 | return t.Format(time.RFC822)
29 | }
30 | ```
31 |
32 | :::tip
33 | Methods defined on embedded structs are also accessible.
34 | ```go
35 | type Env struct {
36 | Helpers
37 | }
38 |
39 | type Helpers struct{}
40 |
41 | func (Helpers) Format(t time.Time) string {
42 | return t.Format(time.RFC822)
43 | }
44 | ```
45 | :::
46 |
47 | 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
48 | the fields and methods of the struct.
49 |
50 | ```go
51 | program, err := expr.Compile(code, expr.Env(Env{}))
52 | ```
53 |
54 | Compiler will type check the expression against the environment. After the compilation, we can run the program with the environment.
55 | You should use the same type of environment that you passed to the `expr.Env` function.
56 |
57 | ```go
58 | output, err := expr.Run(program, Env{
59 | UpdatedAt: time.Now(),
60 | Posts: []Post{{Title: "Hello, World!"}},
61 | Map: map[string]string{"tag1": "value1"},
62 | })
63 | ```
64 |
65 | ## Map as Environment
66 |
67 | You can also use a map as an environment.
68 |
69 | ```go
70 | env := map[string]any{
71 | "UpdatedAt": time.Time{},
72 | "Posts": []Post{},
73 | "tags": map[string]string{},
74 | "sprintf": fmt.Sprintf,
75 | }
76 |
77 | program, err := expr.Compile(code, expr.Env(env))
78 | ```
79 |
80 | A map defines variables and functions that the expression can access. The key is the variable name, and the type
81 | is the value's type.
82 |
83 | ```go
84 | env := map[string]any{
85 | "object": map[string]any{
86 | "field": 42,
87 | },
88 | "struct": struct {
89 | Field int `expr:"field"`
90 | }{42},
91 | }
92 | ```
93 |
94 | Expr will infer the type of the `object` variable as `map[string]any`.
95 | Accessing fields of the `object` and `struct` variables will return the following results.
96 |
97 | ```expr
98 | object.field // 42
99 | object.unknown // nil (no error)
100 |
101 | struct.field // 42
102 | struct.unknown // error (unknown field)
103 |
104 | foobar // error (unknown variable)
105 | ```
106 |
107 | :::note
108 | The `foobar` variable is not defined in the environment.
109 | By default, Expr will return an error if unknown variables are used in the expression.
110 | You can disable this behavior by passing [`AllowUndefinedVariables`](https://pkg.go.dev/github.com/expr-lang/expr#AllowUndefinedVariables) option to the compiler.
111 | :::
112 |
--------------------------------------------------------------------------------
/docs/functions.md:
--------------------------------------------------------------------------------
1 | # Functions
2 |
3 | Expr comes with a set of [builtin](language-definition.md) functions, but you can also define your own functions.
4 |
5 | The easiest way to define a custom function is to add it to the environment.
6 |
7 | ```go
8 | env := map[string]any{
9 | "add": func(a, b int) int {
10 | return a + b
11 | },
12 | }
13 | ```
14 |
15 | Or you can use functions defined on a struct:
16 |
17 | ```go
18 | type Env struct{}
19 |
20 | func (Env) Add(a, b int) int {
21 | return a + b
22 | }
23 | ```
24 |
25 | :::info
26 | If functions are marked with [`ConstExpr`](./configuration.md#constexpr) option, they will be evaluated at compile time.
27 | :::
28 |
29 | 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.
30 |
31 | ```go
32 | atoi := expr.Function(
33 | "atoi",
34 | func(params ...any) (any, error) {
35 | return strconv.Atoi(params[0].(string))
36 | },
37 | )
38 |
39 | program, err := expr.Compile(`atoi("42")`, atoi)
40 | ```
41 |
42 | Type checker sees the `atoi` function as a function with a variadic number of arguments of type `any`, and returns
43 | a value of type `any`. But, we can specify the types of arguments and the return value by adding the correct function
44 | signature or multiple signatures.
45 |
46 | ```go
47 | atoi := expr.Function(
48 | "atoi",
49 | func(params ...any) (any, error) {
50 | return strconv.Atoi(params[0].(string))
51 | },
52 | // highlight-next-line
53 | new(func(string) int),
54 | )
55 | ```
56 |
57 | Or we can simply reuse the strconv.Atoi function as a type:
58 |
59 | ```go
60 | atoi := expr.Function(
61 | "atoi",
62 | func(params ...any) (any, error) {
63 | return strconv.Atoi(params[0].(string))
64 | },
65 | // highlight-next-line
66 | strconv.Atoi,
67 | )
68 | ```
69 |
70 | It is possible to define multiple signatures for a function:
71 |
72 | ```go
73 | toInt := expr.Function(
74 | "toInt",
75 | func(params ...any) (any, error) {
76 | switch params[0].(type) {
77 | case float64:
78 | return int(params[0].(float64)), nil
79 | case string:
80 | return strconv.Atoi(params[0].(string))
81 | }
82 | return nil, fmt.Errorf("invalid type")
83 | },
84 | // highlight-start
85 | new(func(float64) int),
86 | new(func(string) int),
87 | // highlight-end
88 | )
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | **Expr** is a simple, fast and extensible expression language for Go. It is
4 | designed to be easy to use and integrate into your Go application. Let's delve
5 | deeper into its core features:
6 |
7 | - **Memory safe** - Designed to prevent vulnerabilities like buffer overflows and memory leaks.
8 | - **Type safe** - Enforces strict type rules, aligning with Go's type system.
9 | - **Terminating** - Ensures every expression evaluation cannot loop indefinitely.
10 | - **Side effects free** - Evaluations won't modify global states or variables.
11 |
12 | Let's start with a simple example:
13 |
14 | ```go
15 | program, err := expr.Compile(`2 + 2`)
16 | if err != nil {
17 | panic(err)
18 | }
19 |
20 | output, err := expr.Run(program, nil)
21 | if err != nil {
22 | panic(err)
23 | }
24 |
25 | fmt.Print(output) // 4
26 | ```
27 |
28 | Expr compiles the expression `2 + 2` into a bytecode program. Then we run
29 | the program and get the output.
30 |
31 | :::tip
32 | In performance-critical applications, you can reuse the compiled program. Compiled programs are safe for concurrent use.
33 | **Compile once** and run **multiple** times.
34 | :::
35 |
36 | The `expr.Compile` function returns a `*vm.Program` and an error. The `expr.Run` function takes a program and an
37 | environment. The environment is a map of variables that can be used in the expression. In this example, we use `nil` as
38 | an environment because we don't need any variables.
39 |
40 |
41 | Now let's pass some variables to the expression:
42 |
43 | ```go
44 | env := map[string]any{
45 | "foo": 100,
46 | "bar": 200,
47 | }
48 |
49 | program, err := expr.Compile(`foo + bar`, expr.Env(env))
50 | if err != nil {
51 | panic(err)
52 | }
53 |
54 | output, err := expr.Run(program, env)
55 | if err != nil {
56 | panic(err)
57 | }
58 |
59 | fmt.Print(output) // 300
60 | ```
61 |
62 | Why do we need to pass the environment to the `expr.Compile` function? Expr can be used as a type-safe language.
63 | Expr can infer the type of the expression and check it against the environment.
64 |
65 | Here is an example:
66 |
67 | ```go
68 | env := map[string]any{
69 | "name": "Anton",
70 | "age": 35,
71 | }
72 |
73 | program, err := expr.Compile(`name + age`, expr.Env(env))
74 | if err != nil {
75 | // highlight-next-line
76 | panic(err) // Will panic with "invalid operation: string + int"
77 | }
78 | ```
79 |
80 | Expr can work with any Go types:
81 |
82 | ```go
83 | env := map[string]any{
84 | "greet": "Hello, %v!",
85 | "names": []string{"world", "you"},
86 | "sprintf": fmt.Sprintf,
87 | }
88 |
89 | program, err := expr.Compile(`sprintf(greet, names[0])`, expr.Env(env))
90 | if err != nil {
91 | panic(err)
92 | }
93 |
94 | output, err := expr.Run(program, env)
95 | if err != nil {
96 | panic(err)
97 | }
98 |
99 | fmt.Print(output) // Hello, world!
100 | ```
101 |
102 | Also, Expr can use a struct as an environment. Here is an example:
103 |
104 | ```go
105 | type Env struct {
106 | Posts []Post `expr:"posts"`
107 | }
108 |
109 | func (Env) Format(t time.Time) string { // Methods defined on the struct become functions.
110 | return t.Format(time.RFC822)
111 | }
112 |
113 | type Post struct {
114 | Body string
115 | Date time.Time
116 | }
117 |
118 | func main() {
119 | code := `map(posts, Format(.Date) + ": " + .Body)`
120 |
121 | program, err := expr.Compile(code, expr.Env(Env{})) // Pass the struct as an environment.
122 | if err != nil {
123 | panic(err)
124 | }
125 |
126 | env := Env{
127 | Posts: []Post{
128 | {"Oh My God!", time.Now()},
129 | {"How you doin?", time.Now()},
130 | {"Could I be wearing any more clothes?", time.Now()},
131 | },
132 | }
133 |
134 | output, err := expr.Run(program, env)
135 | if err != nil {
136 | panic(err)
137 | }
138 |
139 | fmt.Print(output)
140 | }
141 | ```
142 |
143 | The compiled program can be reused between runs.
144 |
145 | ```go
146 | type Env struct {
147 | X int
148 | Y int
149 | }
150 |
151 | program, err := expr.Compile(`X + Y`, expr.Env(Env{}))
152 | if err != nil {
153 | panic(err)
154 | }
155 |
156 | output, err := expr.Run(program, Env{1, 2})
157 | if err != nil {
158 | panic(err)
159 | }
160 |
161 | fmt.Print(output) // 3
162 |
163 | output, err = expr.Run(program, Env{3, 4})
164 | if err != nil {
165 | panic(err)
166 | }
167 |
168 | fmt.Print(output) // 7
169 | ```
170 |
171 | :::info Eval = Compile + Run
172 | For one-off expressions, you can use the `expr.Eval` function. It compiles and runs the expression in one step.
173 | ```go
174 | output, err := expr.Eval(`2 + 2`, env)
175 | ```
176 | :::
177 |
--------------------------------------------------------------------------------
/docs/patch.md:
--------------------------------------------------------------------------------
1 | # Patch
2 |
3 | Sometimes it may be necessary to modify an expression before the compilation.
4 | For example, you may want to replace a variable with a constant, transform an expression into a function call,
5 | or even modify the expression to use a different operator.
6 |
7 | ## Simple example
8 |
9 | Let's start with a simple example. We have an expression that uses a variable `foo`:
10 |
11 | ```go
12 | program, err := expr.Compile(`foo + bar`)
13 | ```
14 |
15 | We want to replace the `foo` variable with a constant `42`. First, we need to implement a [visitor](./visitor.md):
16 |
17 | ```go
18 | type FooPatcher struct{}
19 |
20 | func (FooPatcher) Visit(node *ast.Node) {
21 | if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == "foo" {
22 | // highlight-next-line
23 | ast.Patch(node, &ast.IntegerNode{Value: 42})
24 | }
25 | }
26 | ```
27 |
28 | 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.
29 |
30 | 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:
31 |
32 | ```go
33 | program, err := expr.Compile(`foo + bar`, expr.Patch(FooPatcher{}))
34 | ```
35 |
36 | ## Advanced example
37 |
38 | Let's consider a more complex example. We have an expression that uses variables `foo` and `bar` of type `Decimal`:
39 |
40 | ```go
41 | type Decimal struct {
42 | Value int
43 | }
44 | ```
45 |
46 | And we want to transform the following expression:
47 |
48 | ```expr
49 | a + b + c
50 | ```
51 |
52 | Into functions calls that accept `Decimal` arguments:
53 |
54 | ```expr
55 | add(add(a, b), c)
56 | ```
57 |
58 | First, we need to implement a visitor that will transform the expression:
59 |
60 | ```go
61 | type DecimalPatcher struct{}
62 |
63 | var decimalType = reflect.TypeOf(Decimal{})
64 |
65 | func (DecimalPatcher) Visit(node *ast.Node) {
66 | if n, ok := (*node).(*ast.BinaryNode); ok && n.Operator == "+" {
67 |
68 | if !n.Left.Type().AssignableTo(decimalType) {
69 | return // skip, left side is not a Decimal
70 | }
71 |
72 | if !n.Right.Type().AssignableTo(decimalType) {
73 | return // skip, right side is not a Decimal
74 | }
75 |
76 | // highlight-start
77 | callNode := &ast.CallNode{
78 | Callee: &ast.IdentifierNode{Value: "add"},
79 | Arguments: []ast.Node{n.Left, n.Right},
80 | }
81 | ast.Patch(node, callNode)
82 | // highlight-end
83 |
84 | (*node).SetType(decimalType) // set the type, so the patcher can be applied recursively
85 | }
86 | }
87 | ```
88 |
89 | We used [Type()](https://pkg.go.dev/github.com/expr-lang/expr/ast#Node.Type) method to get the type of the expression node.
90 | 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.
91 |
92 | 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.
93 | So after the first patcher run, if we want the patcher to be applied recursively, we need to set the type of the node.
94 |
95 |
96 | Now we can use the `DecimalPatcher` to modify the expression:
97 |
98 | ```go
99 | env := map[string]interface{}{
100 | "a": Decimal{1},
101 | "b": Decimal{2},
102 | "c": Decimal{3},
103 | "add": func(x, y Decimal) Decimal {
104 | return Decimal{x.Value + y.Value}
105 | },
106 | }
107 |
108 | code := `a + b + c`
109 |
110 | // highlight-next-line
111 | program, err := expr.Compile(code, expr.Env(env), expr.Patch(DecimalPatcher{}))
112 | if err != nil {
113 | panic(err)
114 | }
115 |
116 | output, err := expr.Run(program, env)
117 | if err != nil {
118 | panic(err)
119 | }
120 |
121 | fmt.Println(output) // Decimal{6}
122 | ```
123 |
124 |
125 | :::info
126 | Expr comes with already implemented patcher that simplifies operator overloading.
127 |
128 | The `DecimalPatcher` can be replaced with the [Operator](https://pkg.go.dev/github.com/expr-lang/expr#Operator) option.
129 |
130 | ```go
131 | program, err := expr.Compile(code, expr.Env(env), expr.Operator("+", "add"))
132 | ```
133 |
134 | Operator overloading patcher will check if provided functions (`"add"`) satisfy the operator (`"+"`), and
135 | replace the operator with the function call.
136 | :::
137 |
--------------------------------------------------------------------------------
/docs/visitor.md:
--------------------------------------------------------------------------------
1 | # Visitor
2 |
3 | Expr provides an interface to traverse the AST of the expression before the compilation.
4 | The `Visitor` interface allows you to collect information about the expression, modify the expression, or even generate
5 | a new expression.
6 |
7 | Let's start with an [ast.Visitor](https://pkg.go.dev/github.com/expr-lang/expr/ast#Visitor) implementation which will
8 | collect all variables used in the expression.
9 |
10 | Visitor must implement a single method `Visit(*ast.Node)`, which will be called for each node in the AST.
11 |
12 | ```go
13 | type Visitor struct {
14 | Identifiers []string
15 | }
16 |
17 | func (v *Visitor) Visit(node *ast.Node) {
18 | if n, ok := (*node).(*ast.IdentifierNode); ok {
19 | v.Identifiers = append(v.Identifiers, n.Value)
20 | }
21 | }
22 | ```
23 |
24 | Full list of available AST nodes can be found in the [ast](https://pkg.go.dev/github.com/expr-lang/expr/ast) documentation.
25 |
26 | Let's parse the expression and use [ast.Walk](https://pkg.go.dev/github.com/expr-lang/expr/ast#Walk) to traverse the AST:
27 |
28 | ```go
29 | tree, err := parser.Parse(`foo + bar`)
30 | if err != nil {
31 | panic(err)
32 | }
33 |
34 | v := &Visitor{}
35 | // highlight-next-line
36 | ast.Walk(&tree.Node, v)
37 |
38 | fmt.Println(v.Identifiers) // [foo, bar]
39 | ```
40 |
41 | :::note
42 |
43 | Although it is possible to access the AST of compiled program, it may be already be modified by patchers, optimizers, etc.
44 |
45 | ```go
46 | program, err := expr.Compile(`foo + bar`)
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | // highlight-next-line
52 | node := program.Node()
53 |
54 | v := &Visitor{}
55 | ast.Walk(&node, v)
56 | ```
57 |
58 | :::
59 |
--------------------------------------------------------------------------------
/file/error.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "unicode/utf8"
7 | )
8 |
9 | type Error struct {
10 | Location
11 | Line int `json:"line"`
12 | Column int `json:"column"`
13 | Message string `json:"message"`
14 | Snippet string `json:"snippet"`
15 | Prev error `json:"prev"`
16 | }
17 |
18 | func (e *Error) Error() string {
19 | return e.format()
20 | }
21 |
22 | func (e *Error) Bind(source Source) *Error {
23 | e.Line = 1
24 | for i, r := range source {
25 | if i == e.From {
26 | break
27 | }
28 | if r == '\n' {
29 | e.Line++
30 | e.Column = 0
31 | } else {
32 | e.Column++
33 | }
34 | }
35 | if snippet, found := source.Snippet(e.Line); found {
36 | snippet := strings.Replace(snippet, "\t", " ", -1)
37 | srcLine := "\n | " + snippet
38 | var bytes = []byte(snippet)
39 | var indLine = "\n | "
40 | for i := 0; i < e.Column && len(bytes) > 0; i++ {
41 | _, sz := utf8.DecodeRune(bytes)
42 | bytes = bytes[sz:]
43 | if sz > 1 {
44 | goto noind
45 | } else {
46 | indLine += "."
47 | }
48 | }
49 | if _, sz := utf8.DecodeRune(bytes); sz > 1 {
50 | goto noind
51 | } else {
52 | indLine += "^"
53 | }
54 | srcLine += indLine
55 |
56 | noind:
57 | e.Snippet = srcLine
58 | }
59 | return e
60 | }
61 |
62 | func (e *Error) Unwrap() error {
63 | return e.Prev
64 | }
65 |
66 | func (e *Error) Wrap(err error) {
67 | e.Prev = err
68 | }
69 |
70 | func (e *Error) format() string {
71 | if e.Snippet == "" {
72 | return e.Message
73 | }
74 | return fmt.Sprintf(
75 | "%s (%d:%d)%s",
76 | e.Message,
77 | e.Line,
78 | e.Column+1, // add one to the 0-based column for display
79 | e.Snippet,
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/file/location.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | type Location struct {
4 | From int `json:"from"`
5 | To int `json:"to"`
6 | }
7 |
--------------------------------------------------------------------------------
/file/source.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "strings"
5 | "unicode/utf8"
6 | )
7 |
8 | type Source []rune
9 |
10 | func NewSource(contents string) Source {
11 | return []rune(contents)
12 | }
13 |
14 | func (s Source) String() string {
15 | return string(s)
16 | }
17 |
18 | func (s Source) Snippet(line int) (string, bool) {
19 | if s == nil {
20 | return "", false
21 | }
22 | lines := strings.Split(string(s), "\n")
23 | lineOffsets := make([]int, len(lines))
24 | var offset int
25 | for i, line := range lines {
26 | offset = offset + utf8.RuneCountInString(line) + 1
27 | lineOffsets[i] = offset
28 | }
29 | charStart, found := getLineOffset(lineOffsets, line)
30 | if !found || len(s) == 0 {
31 | return "", false
32 | }
33 | charEnd, found := getLineOffset(lineOffsets, line+1)
34 | if found {
35 | return string(s[charStart : charEnd-1]), true
36 | }
37 | return string(s[charStart:]), true
38 | }
39 |
40 | func getLineOffset(lineOffsets []int, line int) (int, bool) {
41 | if line == 1 {
42 | return 0, true
43 | } else if line > 1 && line <= len(lineOffsets) {
44 | offset := lineOffsets[line-2]
45 | return offset, true
46 | }
47 | return -1, false
48 | }
49 |
--------------------------------------------------------------------------------
/file/source_test.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | const (
8 | unexpectedSnippet = "%s got snippet '%s', want '%v'"
9 | snippetNotFound = "%s snippet not found, wanted '%v'"
10 | snippetFound = "%s snippet found at Line %d, wanted none"
11 | )
12 |
13 | func TestStringSource_SnippetMultiLine(t *testing.T) {
14 | source := NewSource("hello\nworld\nmy\nbub\n")
15 | if str, found := source.Snippet(1); !found {
16 | t.Errorf(snippetNotFound, t.Name(), 1)
17 | } else if str != "hello" {
18 | t.Errorf(unexpectedSnippet, t.Name(), str, "hello")
19 | }
20 | if str2, found := source.Snippet(2); !found {
21 | t.Errorf(snippetNotFound, t.Name(), 2)
22 | } else if str2 != "world" {
23 | t.Errorf(unexpectedSnippet, t.Name(), str2, "world")
24 | }
25 | if str3, found := source.Snippet(3); !found {
26 | t.Errorf(snippetNotFound, t.Name(), 3)
27 | } else if str3 != "my" {
28 | t.Errorf(unexpectedSnippet, t.Name(), str3, "my")
29 | }
30 | if str4, found := source.Snippet(4); !found {
31 | t.Errorf(snippetNotFound, t.Name(), 4)
32 | } else if str4 != "bub" {
33 | t.Errorf(unexpectedSnippet, t.Name(), str4, "bub")
34 | }
35 | if str5, found := source.Snippet(5); !found {
36 | t.Errorf(snippetNotFound, t.Name(), 5)
37 | } else if str5 != "" {
38 | t.Errorf(unexpectedSnippet, t.Name(), str5, "")
39 | }
40 | }
41 |
42 | func TestStringSource_SnippetSingleLine(t *testing.T) {
43 | source := NewSource("hello, world")
44 | if str, found := source.Snippet(1); !found {
45 | t.Errorf(snippetNotFound, t.Name(), 1)
46 | } else if str != "hello, world" {
47 | t.Errorf(unexpectedSnippet, t.Name(), str, "hello, world")
48 | }
49 | if str2, found := source.Snippet(2); found {
50 | t.Errorf(snippetFound, t.Name(), 2)
51 | } else if str2 != "" {
52 | t.Errorf(unexpectedSnippet, t.Name(), str2, "")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/expr-lang/expr
2 |
3 | go 1.18
4 |
--------------------------------------------------------------------------------
/internal/deref/deref.go:
--------------------------------------------------------------------------------
1 | package deref
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | )
7 |
8 | func Interface(p any) any {
9 | if p == nil {
10 | return nil
11 | }
12 |
13 | v := reflect.ValueOf(p)
14 |
15 | for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
16 | if v.IsNil() {
17 | return nil
18 | }
19 | v = v.Elem()
20 | }
21 |
22 | if v.IsValid() {
23 | return v.Interface()
24 | }
25 |
26 | panic(fmt.Sprintf("cannot dereference %v", p))
27 | }
28 |
29 | func Type(t reflect.Type) reflect.Type {
30 | if t == nil {
31 | return nil
32 | }
33 | for t.Kind() == reflect.Ptr {
34 | t = t.Elem()
35 | }
36 | return t
37 | }
38 |
39 | func Value(v reflect.Value) reflect.Value {
40 | for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
41 | if v.IsNil() {
42 | return v
43 | }
44 | v = v.Elem()
45 | }
46 | return v
47 | }
48 |
--------------------------------------------------------------------------------
/internal/deref/deref_test.go:
--------------------------------------------------------------------------------
1 | package deref_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/internal/testify/assert"
8 |
9 | "github.com/expr-lang/expr/internal/deref"
10 | )
11 |
12 | func TestDeref(t *testing.T) {
13 | a := uint(42)
14 | b := &a
15 | c := &b
16 | d := &c
17 |
18 | got := deref.Interface(d)
19 | assert.Equal(t, uint(42), got)
20 | }
21 |
22 | func TestDeref_mix_ptr_with_interface(t *testing.T) {
23 | a := uint(42)
24 | var b any = &a
25 | var c any = &b
26 | d := &c
27 |
28 | got := deref.Interface(d)
29 | assert.Equal(t, uint(42), got)
30 | }
31 |
32 | func TestDeref_nil(t *testing.T) {
33 | var a *int
34 | assert.Nil(t, deref.Interface(a))
35 | assert.Nil(t, deref.Interface(nil))
36 | }
37 |
38 | func TestType(t *testing.T) {
39 | a := uint(42)
40 | b := &a
41 | c := &b
42 | d := &c
43 |
44 | dt := deref.Type(reflect.TypeOf(d))
45 | assert.Equal(t, reflect.Uint, dt.Kind())
46 | }
47 |
48 | func TestType_two_ptr_with_interface(t *testing.T) {
49 | a := uint(42)
50 | var b any = &a
51 |
52 | dt := deref.Type(reflect.TypeOf(b))
53 | assert.Equal(t, reflect.Uint, dt.Kind())
54 |
55 | }
56 |
57 | func TestType_three_ptr_with_interface(t *testing.T) {
58 | a := uint(42)
59 | var b any = &a
60 | var c any = &b
61 |
62 | dt := deref.Type(reflect.TypeOf(c))
63 | assert.Equal(t, reflect.Interface, dt.Kind())
64 | }
65 |
66 | func TestType_nil(t *testing.T) {
67 | assert.Nil(t, deref.Type(nil))
68 | }
69 |
70 | func TestValue(t *testing.T) {
71 | a := uint(42)
72 | b := &a
73 | c := &b
74 | d := &c
75 |
76 | got := deref.Value(reflect.ValueOf(d))
77 | assert.Equal(t, uint(42), got.Interface())
78 | }
79 |
80 | func TestValue_two_ptr_with_interface(t *testing.T) {
81 | a := uint(42)
82 | var b any = &a
83 |
84 | got := deref.Value(reflect.ValueOf(b))
85 | assert.Equal(t, uint(42), got.Interface())
86 | }
87 |
88 | func TestValue_three_ptr_with_interface(t *testing.T) {
89 | a := uint(42)
90 | var b any = &a
91 | c := &b
92 |
93 | got := deref.Value(reflect.ValueOf(c))
94 | assert.Equal(t, uint(42), got.Interface())
95 | }
96 |
97 | func TestValue_nil(t *testing.T) {
98 | got := deref.Value(reflect.ValueOf(nil))
99 | assert.False(t, got.IsValid())
100 | }
101 |
102 | func TestValue_nil_in_chain(t *testing.T) {
103 | var a any = nil
104 | var b any = &a
105 | c := &b
106 |
107 | got := deref.Value(reflect.ValueOf(c))
108 | assert.True(t, got.IsValid())
109 | assert.True(t, got.IsNil())
110 | assert.Nil(t, got.Interface())
111 | }
112 |
--------------------------------------------------------------------------------
/internal/spew/bypasssafe.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015-2016 Dave Collins
2 | //
3 | // Permission to use, copy, modify, and distribute this software for any
4 | // purpose with or without fee is hereby granted, provided that the above
5 | // copyright notice and this permission notice appear in all copies.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 | // NOTE: Due to the following build constraints, this file will only be compiled
16 | // when the code is running on Google App Engine, compiled by GopherJS, or
17 | // "-tags safe" is added to the go build command line. The "disableunsafe"
18 | // tag is deprecated and thus should not be used.
19 | //go:build js || appengine || safe || disableunsafe || !go1.4
20 | // +build js appengine safe disableunsafe !go1.4
21 |
22 | package spew
23 |
24 | import "reflect"
25 |
26 | const (
27 | // UnsafeDisabled is a build-time constant which specifies whether or
28 | // not access to the unsafe package is available.
29 | UnsafeDisabled = true
30 | )
31 |
32 | // unsafeReflectValue typically converts the passed reflect.Value into a one
33 | // that bypasses the typical safety restrictions preventing access to
34 | // unaddressable and unexported data. However, doing this relies on access to
35 | // the unsafe package. This is a stub version which simply returns the passed
36 | // reflect.Value when the unsafe package is not available.
37 | func unsafeReflectValue(v reflect.Value) reflect.Value {
38 | return v
39 | }
40 |
--------------------------------------------------------------------------------
/internal/spew/dumpcgo_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013-2016 Dave Collins
2 | //
3 | // Permission to use, copy, modify, and distribute this software for any
4 | // purpose with or without fee is hereby granted, provided that the above
5 | // copyright notice and this permission notice appear in all copies.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 | // NOTE: Due to the following build constraints, this file will only be compiled
16 | // when both cgo is supported and "-tags testcgo" is added to the go test
17 | // command line. This means the cgo tests are only added (and hence run) when
18 | // specifically requested. This configuration is used because spew itself
19 | // does not require cgo to run even though it does handle certain cgo types
20 | // specially. Rather than forcing all clients to require cgo and an external
21 | // C compiler just to run the tests, this scheme makes them optional.
22 | //go:build cgo && testcgo
23 | // +build cgo,testcgo
24 |
25 | package spew_test
26 |
27 | import (
28 | "fmt"
29 |
30 | "github.com/expr-lang/expr/internal/spew/testdata"
31 | )
32 |
33 | func addCgoDumpTests() {
34 | // C char pointer.
35 | v := testdata.GetCgoCharPointer()
36 | nv := testdata.GetCgoNullCharPointer()
37 | pv := &v
38 | vcAddr := fmt.Sprintf("%p", v)
39 | vAddr := fmt.Sprintf("%p", pv)
40 | pvAddr := fmt.Sprintf("%p", &pv)
41 | vt := "*testdata._Ctype_char"
42 | vs := "116"
43 | addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n")
44 | addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n")
45 | addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")\n")
46 | addDumpTest(nv, "("+vt+")()\n")
47 |
48 | // C char array.
49 | v2, v2l, v2c := testdata.GetCgoCharArray()
50 | v2Len := fmt.Sprintf("%d", v2l)
51 | v2Cap := fmt.Sprintf("%d", v2c)
52 | v2t := "[6]testdata._Ctype_char"
53 | v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " +
54 | "{\n 00000000 74 65 73 74 32 00 " +
55 | " |test2.|\n}"
56 | addDumpTest(v2, "("+v2t+") "+v2s+"\n")
57 |
58 | // C unsigned char array.
59 | v3, v3l, v3c := testdata.GetCgoUnsignedCharArray()
60 | v3Len := fmt.Sprintf("%d", v3l)
61 | v3Cap := fmt.Sprintf("%d", v3c)
62 | v3t := "[6]testdata._Ctype_unsignedchar"
63 | v3t2 := "[6]testdata._Ctype_uchar"
64 | v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
65 | "{\n 00000000 74 65 73 74 33 00 " +
66 | " |test3.|\n}"
67 | addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n")
68 |
69 | // C signed char array.
70 | v4, v4l, v4c := testdata.GetCgoSignedCharArray()
71 | v4Len := fmt.Sprintf("%d", v4l)
72 | v4Cap := fmt.Sprintf("%d", v4c)
73 | v4t := "[6]testdata._Ctype_schar"
74 | v4t2 := "testdata._Ctype_schar"
75 | v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
76 | "{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 +
77 | ") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 +
78 | ") 0\n}"
79 | addDumpTest(v4, "("+v4t+") "+v4s+"\n")
80 |
81 | // C uint8_t array.
82 | v5, v5l, v5c := testdata.GetCgoUint8tArray()
83 | v5Len := fmt.Sprintf("%d", v5l)
84 | v5Cap := fmt.Sprintf("%d", v5c)
85 | v5t := "[6]testdata._Ctype_uint8_t"
86 | v5t2 := "[6]testdata._Ctype_uchar"
87 | v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
88 | "{\n 00000000 74 65 73 74 35 00 " +
89 | " |test5.|\n}"
90 | addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n")
91 |
92 | // C typedefed unsigned char array.
93 | v6, v6l, v6c := testdata.GetCgoTypedefedUnsignedCharArray()
94 | v6Len := fmt.Sprintf("%d", v6l)
95 | v6Cap := fmt.Sprintf("%d", v6c)
96 | v6t := "[6]testdata._Ctype_custom_uchar_t"
97 | v6t2 := "[6]testdata._Ctype_uchar"
98 | v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
99 | "{\n 00000000 74 65 73 74 36 00 " +
100 | " |test6.|\n}"
101 | addDumpTest(v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s+"\n")
102 | }
103 |
--------------------------------------------------------------------------------
/internal/spew/dumpnocgo_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 Dave Collins
2 | //
3 | // Permission to use, copy, modify, and distribute this software for any
4 | // purpose with or without fee is hereby granted, provided that the above
5 | // copyright notice and this permission notice appear in all copies.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 | // NOTE: Due to the following build constraints, this file will only be compiled
16 | // when either cgo is not supported or "-tags testcgo" is not added to the go
17 | // test command line. This file intentionally does not setup any cgo tests in
18 | // this scenario.
19 | //go:build !cgo || !testcgo
20 | // +build !cgo !testcgo
21 |
22 | package spew_test
23 |
24 | func addCgoDumpTests() {
25 | // Don't add any tests for cgo since this file is only compiled when
26 | // there should not be any cgo tests.
27 | }
28 |
--------------------------------------------------------------------------------
/internal/spew/internal_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013-2016 Dave Collins
3 | *
4 | * Permission to use, copy, modify, and distribute this software for any
5 | * purpose with or without fee is hereby granted, provided that the above
6 | * copyright notice and this permission notice appear in all copies.
7 | *
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 | */
16 |
17 | /*
18 | This test file is part of the spew package rather than the spew_test
19 | package because it needs access to internals to properly test certain cases
20 | which are not possible via the public interface since they should never happen.
21 | */
22 |
23 | package spew
24 |
25 | import (
26 | "bytes"
27 | "reflect"
28 | "testing"
29 | )
30 |
31 | // dummyFmtState implements a fake fmt.State to use for testing invalid
32 | // reflect.Value handling. This is necessary because the fmt package catches
33 | // invalid values before invoking the formatter on them.
34 | type dummyFmtState struct {
35 | bytes.Buffer
36 | }
37 |
38 | func (dfs *dummyFmtState) Flag(f int) bool {
39 | return f == int('+')
40 | }
41 |
42 | func (dfs *dummyFmtState) Precision() (int, bool) {
43 | return 0, false
44 | }
45 |
46 | func (dfs *dummyFmtState) Width() (int, bool) {
47 | return 0, false
48 | }
49 |
50 | // TestInvalidReflectValue ensures the dump and formatter code handles an
51 | // invalid reflect value properly. This needs access to internal state since it
52 | // should never happen in real code and therefore can't be tested via the public
53 | // API.
54 | func TestInvalidReflectValue(t *testing.T) {
55 | i := 1
56 |
57 | // Dump invalid reflect value.
58 | v := new(reflect.Value)
59 | buf := new(bytes.Buffer)
60 | d := dumpState{w: buf, cs: &Config}
61 | d.dump(*v)
62 | s := buf.String()
63 | want := ""
64 | if s != want {
65 | t.Errorf("InvalidReflectValue #%d\n got: %s want: %s", i, s, want)
66 | }
67 | i++
68 |
69 | // Formatter invalid reflect value.
70 | buf2 := new(dummyFmtState)
71 | f := formatState{value: *v, cs: &Config, fs: buf2}
72 | f.format(*v)
73 | s = buf2.String()
74 | want = ""
75 | if s != want {
76 | t.Errorf("InvalidReflectValue #%d got: %s want: %s", i, s, want)
77 | }
78 | }
79 |
80 | // SortValues makes the internal sortValues function available to the test
81 | // package.
82 | func SortValues(values []reflect.Value, cs *ConfigState) {
83 | sortValues(values, cs)
84 | }
85 |
--------------------------------------------------------------------------------
/internal/spew/internalunsafe_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013-2016 Dave Collins
2 |
3 | // Permission to use, copy, modify, and distribute this software for any
4 | // purpose with or without fee is hereby granted, provided that the above
5 | // copyright notice and this permission notice appear in all copies.
6 |
7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 | // NOTE: Due to the following build constraints, this file will only be compiled
16 | // when the code is not running on Google App Engine, compiled by GopherJS, and
17 | // "-tags safe" is not added to the go build command line. The "disableunsafe"
18 | // tag is deprecated and thus should not be used.
19 | //go:build !js && !appengine && !safe && !disableunsafe && go1.4
20 | // +build !js,!appengine,!safe,!disableunsafe,go1.4
21 |
22 | /*
23 | This test file is part of the spew package rather than the spew_test
24 | package because it needs access to internals to properly test certain cases
25 | which are not possible via the public interface since they should never happen.
26 | */
27 |
28 | package spew
29 |
30 | import (
31 | "bytes"
32 | "reflect"
33 | "testing"
34 | )
35 |
36 | // changeKind uses unsafe to intentionally change the kind of a reflect.Value to
37 | // the maximum kind value which does not exist. This is needed to test the
38 | // fallback code which punts to the standard fmt library for new types that
39 | // might get added to the language.
40 | func changeKind(v *reflect.Value, readOnly bool) {
41 | flags := flagField(v)
42 | if readOnly {
43 | *flags |= flagRO
44 | } else {
45 | *flags &^= flagRO
46 | }
47 | *flags |= flagKindMask
48 | }
49 |
50 | // TestAddedReflectValue tests functionality of the dump and formatter code which
51 | // falls back to the standard fmt library for new types that might get added to
52 | // the language.
53 | func TestAddedReflectValue(t *testing.T) {
54 | i := 1
55 |
56 | // Dump using a reflect.Value that is exported.
57 | v := reflect.ValueOf(int8(5))
58 | changeKind(&v, false)
59 | buf := new(bytes.Buffer)
60 | d := dumpState{w: buf, cs: &Config}
61 | d.dump(v)
62 | s := buf.String()
63 | want := "(int8) 5"
64 | if s != want {
65 | t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
66 | }
67 | i++
68 |
69 | // Dump using a reflect.Value that is not exported.
70 | changeKind(&v, true)
71 | buf.Reset()
72 | d.dump(v)
73 | s = buf.String()
74 | want = "(int8) "
75 | if s != want {
76 | t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want)
77 | }
78 | i++
79 |
80 | // Formatter using a reflect.Value that is exported.
81 | changeKind(&v, false)
82 | buf2 := new(dummyFmtState)
83 | f := formatState{value: v, cs: &Config, fs: buf2}
84 | f.format(v)
85 | s = buf2.String()
86 | want = "5"
87 | if s != want {
88 | t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
89 | }
90 | i++
91 |
92 | // Formatter using a reflect.Value that is not exported.
93 | changeKind(&v, true)
94 | buf2.Reset()
95 | f = formatState{value: v, cs: &Config, fs: buf2}
96 | f.format(v)
97 | s = buf2.String()
98 | want = ""
99 | if s != want {
100 | t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/internal/spew/testdata/dumpcgo.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 Dave Collins
2 | //
3 | // Permission to use, copy, modify, and distribute this software for any
4 | // purpose with or without fee is hereby granted, provided that the above
5 | // copyright notice and this permission notice appear in all copies.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 | // NOTE: Due to the following build constraints, this file will only be compiled
16 | // when both cgo is supported and "-tags testcgo" is added to the go test
17 | // command line. This code should really only be in the dumpcgo_test.go file,
18 | // but unfortunately Go will not allow cgo in test files, so this is a
19 | // workaround to allow cgo types to be tested. This configuration is used
20 | // because spew itself does not require cgo to run even though it does handle
21 | // certain cgo types specially. Rather than forcing all clients to require cgo
22 | // and an external C compiler just to run the tests, this scheme makes them
23 | // optional.
24 | //go:build cgo && testcgo
25 | // +build cgo,testcgo
26 |
27 | package testdata
28 |
29 | /*
30 | #include
31 | typedef unsigned char custom_uchar_t;
32 |
33 | char *ncp = 0;
34 | char *cp = "test";
35 | char ca[6] = {'t', 'e', 's', 't', '2', '\0'};
36 | unsigned char uca[6] = {'t', 'e', 's', 't', '3', '\0'};
37 | signed char sca[6] = {'t', 'e', 's', 't', '4', '\0'};
38 | uint8_t ui8ta[6] = {'t', 'e', 's', 't', '5', '\0'};
39 | custom_uchar_t tuca[6] = {'t', 'e', 's', 't', '6', '\0'};
40 | */
41 | import "C"
42 |
43 | // GetCgoNullCharPointer returns a null char pointer via cgo. This is only
44 | // used for tests.
45 | func GetCgoNullCharPointer() interface{} {
46 | return C.ncp
47 | }
48 |
49 | // GetCgoCharPointer returns a char pointer via cgo. This is only used for
50 | // tests.
51 | func GetCgoCharPointer() interface{} {
52 | return C.cp
53 | }
54 |
55 | // GetCgoCharArray returns a char array via cgo and the array's len and cap.
56 | // This is only used for tests.
57 | func GetCgoCharArray() (interface{}, int, int) {
58 | return C.ca, len(C.ca), cap(C.ca)
59 | }
60 |
61 | // GetCgoUnsignedCharArray returns an unsigned char array via cgo and the
62 | // array's len and cap. This is only used for tests.
63 | func GetCgoUnsignedCharArray() (interface{}, int, int) {
64 | return C.uca, len(C.uca), cap(C.uca)
65 | }
66 |
67 | // GetCgoSignedCharArray returns a signed char array via cgo and the array's len
68 | // and cap. This is only used for tests.
69 | func GetCgoSignedCharArray() (interface{}, int, int) {
70 | return C.sca, len(C.sca), cap(C.sca)
71 | }
72 |
73 | // GetCgoUint8tArray returns a uint8_t array via cgo and the array's len and
74 | // cap. This is only used for tests.
75 | func GetCgoUint8tArray() (interface{}, int, int) {
76 | return C.ui8ta, len(C.ui8ta), cap(C.ui8ta)
77 | }
78 |
79 | // GetCgoTypedefedUnsignedCharArray returns a typedefed unsigned char array via
80 | // cgo and the array's len and cap. This is only used for tests.
81 | func GetCgoTypedefedUnsignedCharArray() (interface{}, int, int) {
82 | return C.tuca, len(C.tuca), cap(C.tuca)
83 | }
84 |
--------------------------------------------------------------------------------
/internal/testify/assert/assertion_format.go.tmpl:
--------------------------------------------------------------------------------
1 | {{.CommentFormat}}
2 | func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool {
3 | if h, ok := t.(tHelper); ok { h.Helper() }
4 | return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}})
5 | }
6 |
--------------------------------------------------------------------------------
/internal/testify/assert/assertion_forward.go.tmpl:
--------------------------------------------------------------------------------
1 | {{.CommentWithoutT "a"}}
2 | func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool {
3 | if h, ok := a.t.(tHelper); ok { h.Helper() }
4 | return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
5 | }
6 |
--------------------------------------------------------------------------------
/internal/testify/assert/assertion_order.go:
--------------------------------------------------------------------------------
1 | package assert
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | )
7 |
8 | // isOrdered checks that collection contains orderable elements.
9 | func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool {
10 | objKind := reflect.TypeOf(object).Kind()
11 | if objKind != reflect.Slice && objKind != reflect.Array {
12 | return false
13 | }
14 |
15 | objValue := reflect.ValueOf(object)
16 | objLen := objValue.Len()
17 |
18 | if objLen <= 1 {
19 | return true
20 | }
21 |
22 | value := objValue.Index(0)
23 | valueInterface := value.Interface()
24 | firstValueKind := value.Kind()
25 |
26 | for i := 1; i < objLen; i++ {
27 | prevValue := value
28 | prevValueInterface := valueInterface
29 |
30 | value = objValue.Index(i)
31 | valueInterface = value.Interface()
32 |
33 | compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind)
34 |
35 | if !isComparable {
36 | return Fail(t, fmt.Sprintf("Can not compare type \"%s\" and \"%s\"", reflect.TypeOf(value), reflect.TypeOf(prevValue)), msgAndArgs...)
37 | }
38 |
39 | if !containsValue(allowedComparesResults, compareResult) {
40 | return Fail(t, fmt.Sprintf(failMessage, prevValue, value), msgAndArgs...)
41 | }
42 | }
43 |
44 | return true
45 | }
46 |
47 | // IsIncreasing asserts that the collection is increasing
48 | //
49 | // assert.IsIncreasing(t, []int{1, 2, 3})
50 | // assert.IsIncreasing(t, []float{1, 2})
51 | // assert.IsIncreasing(t, []string{"a", "b"})
52 | func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
53 | return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...)
54 | }
55 |
56 | // IsNonIncreasing asserts that the collection is not increasing
57 | //
58 | // assert.IsNonIncreasing(t, []int{2, 1, 1})
59 | // assert.IsNonIncreasing(t, []float{2, 1})
60 | // assert.IsNonIncreasing(t, []string{"b", "a"})
61 | func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
62 | return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...)
63 | }
64 |
65 | // IsDecreasing asserts that the collection is decreasing
66 | //
67 | // assert.IsDecreasing(t, []int{2, 1, 0})
68 | // assert.IsDecreasing(t, []float{2, 1})
69 | // assert.IsDecreasing(t, []string{"b", "a"})
70 | func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
71 | return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...)
72 | }
73 |
74 | // IsNonDecreasing asserts that the collection is not decreasing
75 | //
76 | // assert.IsNonDecreasing(t, []int{1, 1, 2})
77 | // assert.IsNonDecreasing(t, []float{1, 2})
78 | // assert.IsNonDecreasing(t, []string{"a", "b"})
79 | func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
80 | return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...)
81 | }
82 |
--------------------------------------------------------------------------------
/internal/testify/assert/doc.go:
--------------------------------------------------------------------------------
1 | // Package assert provides a set of comprehensive testing tools for use with the normal Go testing system.
2 | //
3 | // # Example Usage
4 | //
5 | // The following is a complete example using assert in a standard test function:
6 | //
7 | // import (
8 | // "testing"
9 | // "github.com/expr-lang/expr/internal/testify/assert"
10 | // )
11 | //
12 | // func TestSomething(t *testing.T) {
13 | //
14 | // var a string = "Hello"
15 | // var b string = "Hello"
16 | //
17 | // assert.Equal(t, a, b, "The two words should be the same.")
18 | //
19 | // }
20 | //
21 | // if you assert many times, use the format below:
22 | //
23 | // import (
24 | // "testing"
25 | // "github.com/expr-lang/expr/internal/testify/assert"
26 | // )
27 | //
28 | // func TestSomething(t *testing.T) {
29 | // assert := assert.New(t)
30 | //
31 | // var a string = "Hello"
32 | // var b string = "Hello"
33 | //
34 | // assert.Equal(a, b, "The two words should be the same.")
35 | // }
36 | //
37 | // # Assertions
38 | //
39 | // Assertions allow you to easily write test code, and are global funcs in the `assert` package.
40 | // All assertion functions take, as the first argument, the `*testing.T` object provided by the
41 | // testing framework. This allows the assertion funcs to write the failings and other details to
42 | // the correct place.
43 | //
44 | // Every assertion function also takes an optional string message as the final argument,
45 | // allowing custom error messages to be appended to the message the assertion method outputs.
46 | package assert
47 |
--------------------------------------------------------------------------------
/internal/testify/assert/errors.go:
--------------------------------------------------------------------------------
1 | package assert
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | // AnError is an error instance useful for testing. If the code does not care
8 | // about error specifics, and only needs to return the error for example, this
9 | // error should be used to make the test code more readable.
10 | var AnError = errors.New("assert.AnError general error for testing")
11 |
--------------------------------------------------------------------------------
/internal/testify/assert/forward_assertions.go:
--------------------------------------------------------------------------------
1 | package assert
2 |
3 | // Assertions provides assertion methods around the
4 | // TestingT interface.
5 | type Assertions struct {
6 | t TestingT
7 | }
8 |
9 | // New makes a new Assertions object for the specified TestingT.
10 | func New(t TestingT) *Assertions {
11 | return &Assertions{
12 | t: t,
13 | }
14 | }
15 |
16 | //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs"
17 |
--------------------------------------------------------------------------------
/internal/testify/assert/internal/unsafetests/doc.go:
--------------------------------------------------------------------------------
1 | // This package exists just to isolate tests that reference the [unsafe] package.
2 | //
3 | // The tests in this package are totally safe.
4 | package unsafetests
5 |
--------------------------------------------------------------------------------
/internal/testify/assert/internal/unsafetests/unsafetests_test.go:
--------------------------------------------------------------------------------
1 | package unsafetests_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "unsafe"
7 |
8 | "github.com/expr-lang/expr/internal/testify/assert"
9 | )
10 |
11 | type ignoreTestingT struct{}
12 |
13 | var _ assert.TestingT = ignoreTestingT{}
14 |
15 | func (ignoreTestingT) Helper() {}
16 |
17 | func (ignoreTestingT) Errorf(format string, args ...interface{}) {
18 | // Run the formatting, but ignore the result
19 | msg := fmt.Sprintf(format, args...)
20 | _ = msg
21 | }
22 |
23 | func TestUnsafePointers(t *testing.T) {
24 | var ignore ignoreTestingT
25 |
26 | assert.True(t, assert.Nil(t, unsafe.Pointer(nil), "unsafe.Pointer(nil) is nil"))
27 | assert.False(t, assert.NotNil(ignore, unsafe.Pointer(nil), "unsafe.Pointer(nil) is nil"))
28 |
29 | assert.True(t, assert.Nil(t, unsafe.Pointer((*int)(nil)), "unsafe.Pointer((*int)(nil)) is nil"))
30 | assert.False(t, assert.NotNil(ignore, unsafe.Pointer((*int)(nil)), "unsafe.Pointer((*int)(nil)) is nil"))
31 |
32 | assert.False(t, assert.Nil(ignore, unsafe.Pointer(new(int)), "unsafe.Pointer(new(int)) is NOT nil"))
33 | assert.True(t, assert.NotNil(t, unsafe.Pointer(new(int)), "unsafe.Pointer(new(int)) is NOT nil"))
34 | }
35 |
--------------------------------------------------------------------------------
/internal/testify/require/doc.go:
--------------------------------------------------------------------------------
1 | // Package require implements the same assertions as the `assert` package but
2 | // stops test execution when a test fails.
3 | //
4 | // # Example Usage
5 | //
6 | // The following is a complete example using require in a standard test function:
7 | //
8 | // import (
9 | // "testing"
10 | // "github.com/expr-lang/expr/internal/testify/require"
11 | // )
12 | //
13 | // func TestSomething(t *testing.T) {
14 | //
15 | // var a string = "Hello"
16 | // var b string = "Hello"
17 | //
18 | // require.Equal(t, a, b, "The two words should be the same.")
19 | //
20 | // }
21 | //
22 | // # Assertions
23 | //
24 | // The `require` package have same global functions as in the `assert` package,
25 | // but instead of returning a boolean result they call `t.FailNow()`.
26 | //
27 | // Every assertion function also takes an optional string message as the final argument,
28 | // allowing custom error messages to be appended to the message the assertion method outputs.
29 | package require
30 |
--------------------------------------------------------------------------------
/internal/testify/require/forward_requirements.go:
--------------------------------------------------------------------------------
1 | package require
2 |
3 | // Assertions provides assertion methods around the
4 | // TestingT interface.
5 | type Assertions struct {
6 | t TestingT
7 | }
8 |
9 | // New makes a new Assertions object for the specified TestingT.
10 | func New(t TestingT) *Assertions {
11 | return &Assertions{
12 | t: t,
13 | }
14 | }
15 |
16 | //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require_forward.go.tmpl -include-format-funcs"
17 |
--------------------------------------------------------------------------------
/internal/testify/require/require.go.tmpl:
--------------------------------------------------------------------------------
1 | {{.Comment}}
2 | func {{.DocInfo.Name}}(t TestingT, {{.Params}}) {
3 | if h, ok := t.(tHelper); ok { h.Helper() }
4 | if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return }
5 | t.FailNow()
6 | }
7 |
--------------------------------------------------------------------------------
/internal/testify/require/require_forward.go.tmpl:
--------------------------------------------------------------------------------
1 | {{.CommentWithoutT "a"}}
2 | func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) {
3 | if h, ok := a.t.(tHelper); ok { h.Helper() }
4 | {{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
5 | }
6 |
--------------------------------------------------------------------------------
/internal/testify/require/requirements.go:
--------------------------------------------------------------------------------
1 | package require
2 |
3 | // TestingT is an interface wrapper around *testing.T
4 | type TestingT interface {
5 | Errorf(format string, args ...interface{})
6 | FailNow()
7 | }
8 |
9 | type tHelper = interface {
10 | Helper()
11 | }
12 |
13 | // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful
14 | // for table driven tests.
15 | type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{})
16 |
17 | // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful
18 | // for table driven tests.
19 | type ValueAssertionFunc func(TestingT, interface{}, ...interface{})
20 |
21 | // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful
22 | // for table driven tests.
23 | type BoolAssertionFunc func(TestingT, bool, ...interface{})
24 |
25 | // ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful
26 | // for table driven tests.
27 | type ErrorAssertionFunc func(TestingT, error, ...interface{})
28 |
29 | //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs"
30 |
--------------------------------------------------------------------------------
/optimizer/const_expr.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strings"
7 |
8 | . "github.com/expr-lang/expr/ast"
9 | "github.com/expr-lang/expr/file"
10 | )
11 |
12 | var errorType = reflect.TypeOf((*error)(nil)).Elem()
13 |
14 | type constExpr struct {
15 | applied bool
16 | err error
17 | fns map[string]reflect.Value
18 | }
19 |
20 | func (c *constExpr) Visit(node *Node) {
21 | defer func() {
22 | if r := recover(); r != nil {
23 | msg := fmt.Sprintf("%v", r)
24 | // Make message more actual, it's a runtime error, but at compile step.
25 | msg = strings.Replace(msg, "runtime error:", "compile error:", 1)
26 | c.err = &file.Error{
27 | Location: (*node).Location(),
28 | Message: msg,
29 | }
30 | }
31 | }()
32 |
33 | if call, ok := (*node).(*CallNode); ok {
34 | if name, ok := call.Callee.(*IdentifierNode); ok {
35 | fn, ok := c.fns[name.Value]
36 | if ok {
37 | in := make([]reflect.Value, len(call.Arguments))
38 | for i := 0; i < len(call.Arguments); i++ {
39 | arg := call.Arguments[i]
40 | var param any
41 |
42 | switch a := arg.(type) {
43 | case *NilNode:
44 | param = nil
45 | case *IntegerNode:
46 | param = a.Value
47 | case *FloatNode:
48 | param = a.Value
49 | case *BoolNode:
50 | param = a.Value
51 | case *StringNode:
52 | param = a.Value
53 | case *ConstantNode:
54 | param = a.Value
55 |
56 | default:
57 | return // Const expr optimization not applicable.
58 | }
59 |
60 | if param == nil && reflect.TypeOf(param) == nil {
61 | // In case of nil value and nil type use this hack,
62 | // otherwise reflect.Call will panic on zero value.
63 | in[i] = reflect.ValueOf(¶m).Elem()
64 | } else {
65 | in[i] = reflect.ValueOf(param)
66 | }
67 | }
68 |
69 | out := fn.Call(in)
70 | value := out[0].Interface()
71 | if len(out) == 2 && out[1].Type() == errorType && !out[1].IsNil() {
72 | c.err = out[1].Interface().(error)
73 | return
74 | }
75 | constNode := &ConstantNode{Value: value}
76 | patchWithType(node, constNode)
77 | c.applied = true
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/optimizer/filter_first.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | )
6 |
7 | type filterFirst struct{}
8 |
9 | func (*filterFirst) Visit(node *Node) {
10 | if member, ok := (*node).(*MemberNode); ok && member.Property != nil && !member.Optional {
11 | if prop, ok := member.Property.(*IntegerNode); ok && prop.Value == 0 {
12 | if filter, ok := member.Node.(*BuiltinNode); ok &&
13 | filter.Name == "filter" &&
14 | len(filter.Arguments) == 2 {
15 | patchCopyType(node, &BuiltinNode{
16 | Name: "find",
17 | Arguments: filter.Arguments,
18 | Throws: true, // to match the behavior of filter()[0]
19 | Map: filter.Map,
20 | })
21 | }
22 | }
23 | }
24 | if first, ok := (*node).(*BuiltinNode); ok &&
25 | first.Name == "first" &&
26 | len(first.Arguments) == 1 {
27 | if filter, ok := first.Arguments[0].(*BuiltinNode); ok &&
28 | filter.Name == "filter" &&
29 | len(filter.Arguments) == 2 {
30 | patchCopyType(node, &BuiltinNode{
31 | Name: "find",
32 | Arguments: filter.Arguments,
33 | Throws: false, // as first() will return nil if not found
34 | Map: filter.Map,
35 | })
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/optimizer/filter_last.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | )
6 |
7 | type filterLast struct{}
8 |
9 | func (*filterLast) Visit(node *Node) {
10 | if member, ok := (*node).(*MemberNode); ok && member.Property != nil && !member.Optional {
11 | if prop, ok := member.Property.(*IntegerNode); ok && prop.Value == -1 {
12 | if filter, ok := member.Node.(*BuiltinNode); ok &&
13 | filter.Name == "filter" &&
14 | len(filter.Arguments) == 2 {
15 | patchCopyType(node, &BuiltinNode{
16 | Name: "findLast",
17 | Arguments: filter.Arguments,
18 | Throws: true, // to match the behavior of filter()[-1]
19 | Map: filter.Map,
20 | })
21 | }
22 | }
23 | }
24 | if first, ok := (*node).(*BuiltinNode); ok &&
25 | first.Name == "last" &&
26 | len(first.Arguments) == 1 {
27 | if filter, ok := first.Arguments[0].(*BuiltinNode); ok &&
28 | filter.Name == "filter" &&
29 | len(filter.Arguments) == 2 {
30 | patchCopyType(node, &BuiltinNode{
31 | Name: "findLast",
32 | Arguments: filter.Arguments,
33 | Throws: false, // as last() will return nil if not found
34 | Map: filter.Map,
35 | })
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/optimizer/filter_len.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | )
6 |
7 | type filterLen struct{}
8 |
9 | func (*filterLen) Visit(node *Node) {
10 | if ln, ok := (*node).(*BuiltinNode); ok &&
11 | ln.Name == "len" &&
12 | len(ln.Arguments) == 1 {
13 | if filter, ok := ln.Arguments[0].(*BuiltinNode); ok &&
14 | filter.Name == "filter" &&
15 | len(filter.Arguments) == 2 {
16 | patchCopyType(node, &BuiltinNode{
17 | Name: "count",
18 | Arguments: filter.Arguments,
19 | })
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/optimizer/filter_map.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | )
6 |
7 | type filterMap struct{}
8 |
9 | func (*filterMap) Visit(node *Node) {
10 | if mapBuiltin, ok := (*node).(*BuiltinNode); ok &&
11 | mapBuiltin.Name == "map" &&
12 | len(mapBuiltin.Arguments) == 2 &&
13 | Find(mapBuiltin.Arguments[1], isIndexPointer) == nil {
14 | if predicate, ok := mapBuiltin.Arguments[1].(*PredicateNode); ok {
15 | if filter, ok := mapBuiltin.Arguments[0].(*BuiltinNode); ok &&
16 | filter.Name == "filter" &&
17 | filter.Map == nil /* not already optimized */ {
18 | patchCopyType(node, &BuiltinNode{
19 | Name: "filter",
20 | Arguments: filter.Arguments,
21 | Map: predicate.Node,
22 | })
23 | }
24 | }
25 | }
26 | }
27 |
28 | func isIndexPointer(node Node) bool {
29 | if pointer, ok := node.(*PointerNode); ok && pointer.Name == "index" {
30 | return true
31 | }
32 | return false
33 | }
34 |
--------------------------------------------------------------------------------
/optimizer/filter_map_test.go:
--------------------------------------------------------------------------------
1 | package optimizer_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/expr-lang/expr/ast"
7 | "github.com/expr-lang/expr/internal/testify/assert"
8 | "github.com/expr-lang/expr/internal/testify/require"
9 | "github.com/expr-lang/expr/optimizer"
10 | "github.com/expr-lang/expr/parser"
11 | )
12 |
13 | func TestOptimize_filter_map(t *testing.T) {
14 | tree, err := parser.Parse(`map(filter(users, .Name == "Bob"), .Age)`)
15 | require.NoError(t, err)
16 |
17 | err = optimizer.Optimize(&tree.Node, nil)
18 | require.NoError(t, err)
19 |
20 | expected := &BuiltinNode{
21 | Name: "filter",
22 | Arguments: []Node{
23 | &IdentifierNode{Value: "users"},
24 | &PredicateNode{
25 | Node: &BinaryNode{
26 | Operator: "==",
27 | Left: &MemberNode{
28 | Node: &PointerNode{},
29 | Property: &StringNode{Value: "Name"},
30 | },
31 | Right: &StringNode{Value: "Bob"},
32 | },
33 | },
34 | },
35 | Map: &MemberNode{
36 | Node: &PointerNode{},
37 | Property: &StringNode{Value: "Age"},
38 | },
39 | }
40 |
41 | assert.Equal(t, Dump(expected), Dump(tree.Node))
42 | }
43 |
44 | func TestOptimize_filter_map_with_index_pointer(t *testing.T) {
45 | tree, err := parser.Parse(`map(filter(users, true), #index)`)
46 | require.NoError(t, err)
47 |
48 | err = optimizer.Optimize(&tree.Node, nil)
49 | require.NoError(t, err)
50 |
51 | expected := &BuiltinNode{
52 | Name: "map",
53 | Arguments: []Node{
54 | &BuiltinNode{
55 | Name: "filter",
56 | Arguments: []Node{
57 | &IdentifierNode{Value: "users"},
58 | &PredicateNode{
59 | Node: &BoolNode{Value: true},
60 | },
61 | },
62 | Throws: false,
63 | Map: nil,
64 | },
65 | &PredicateNode{
66 | Node: &PointerNode{Name: "index"},
67 | },
68 | },
69 | Throws: false,
70 | Map: nil,
71 | }
72 |
73 | assert.Equal(t, Dump(expected), Dump(tree.Node))
74 | }
75 |
76 | func TestOptimize_filter_map_with_index_pointer_with_index_pointer_in_first_argument(t *testing.T) {
77 | tree, err := parser.Parse(`1..2 | map(map(filter([#index], true), 42))`)
78 | require.NoError(t, err)
79 |
80 | err = optimizer.Optimize(&tree.Node, nil)
81 | require.NoError(t, err)
82 |
83 | expected := &BuiltinNode{
84 | Name: "map",
85 | Arguments: []Node{
86 | &BinaryNode{
87 | Operator: "..",
88 | Left: &IntegerNode{Value: 1},
89 | Right: &IntegerNode{Value: 2},
90 | },
91 | &PredicateNode{
92 | Node: &BuiltinNode{
93 | Name: "filter",
94 | Arguments: []Node{
95 | &ArrayNode{
96 | Nodes: []Node{
97 | &PointerNode{Name: "index"},
98 | },
99 | },
100 | &PredicateNode{
101 | Node: &BoolNode{Value: true},
102 | },
103 | },
104 | Throws: false,
105 | Map: &IntegerNode{Value: 42},
106 | },
107 | },
108 | },
109 | Throws: false,
110 | Map: nil,
111 | }
112 |
113 | assert.Equal(t, Dump(expected), Dump(tree.Node))
114 | }
115 |
--------------------------------------------------------------------------------
/optimizer/fold_test.go:
--------------------------------------------------------------------------------
1 | package optimizer_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/ast"
8 | "github.com/expr-lang/expr/internal/testify/assert"
9 | "github.com/expr-lang/expr/internal/testify/require"
10 | "github.com/expr-lang/expr/optimizer"
11 | "github.com/expr-lang/expr/parser"
12 | )
13 |
14 | func TestOptimize_constant_folding(t *testing.T) {
15 | tree, err := parser.Parse(`[1,2,3][5*5-25]`)
16 | require.NoError(t, err)
17 |
18 | err = optimizer.Optimize(&tree.Node, nil)
19 | require.NoError(t, err)
20 |
21 | expected := &ast.MemberNode{
22 | Node: &ast.ConstantNode{Value: []any{1, 2, 3}},
23 | Property: &ast.IntegerNode{Value: 0},
24 | }
25 |
26 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
27 | }
28 |
29 | func TestOptimize_constant_folding_with_floats(t *testing.T) {
30 | tree, err := parser.Parse(`1 + 2.0 * ((1.0 * 2) / 2) - 0`)
31 | require.NoError(t, err)
32 |
33 | err = optimizer.Optimize(&tree.Node, nil)
34 | require.NoError(t, err)
35 |
36 | expected := &ast.FloatNode{Value: 3.0}
37 |
38 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
39 | assert.Equal(t, reflect.Float64, tree.Node.Type().Kind())
40 | }
41 |
42 | func TestOptimize_constant_folding_with_bools(t *testing.T) {
43 | tree, err := parser.Parse(`(true and false) or (true or false) or (false and false) or (true and (true == false))`)
44 | require.NoError(t, err)
45 |
46 | err = optimizer.Optimize(&tree.Node, nil)
47 | require.NoError(t, err)
48 |
49 | expected := &ast.BoolNode{Value: true}
50 |
51 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
52 | }
53 |
54 | func TestOptimize_constant_folding_filter_filter(t *testing.T) {
55 | tree, err := parser.Parse(`filter(filter(1..2, true), true)`)
56 | require.NoError(t, err)
57 |
58 | err = optimizer.Optimize(&tree.Node, nil)
59 | require.NoError(t, err)
60 |
61 | expected := &ast.BuiltinNode{
62 | Name: "filter",
63 | Arguments: []ast.Node{
64 | &ast.BinaryNode{
65 | Operator: "..",
66 | Left: &ast.IntegerNode{
67 | Value: 1,
68 | },
69 | Right: &ast.IntegerNode{
70 | Value: 2,
71 | },
72 | },
73 | &ast.BoolNode{
74 | Value: true,
75 | },
76 | },
77 | Throws: false,
78 | Map: nil,
79 | }
80 |
81 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
82 | }
83 |
--------------------------------------------------------------------------------
/optimizer/in_array.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | "reflect"
5 |
6 | . "github.com/expr-lang/expr/ast"
7 | )
8 |
9 | type inArray struct{}
10 |
11 | func (*inArray) Visit(node *Node) {
12 | switch n := (*node).(type) {
13 | case *BinaryNode:
14 | if n.Operator == "in" {
15 | if array, ok := n.Right.(*ArrayNode); ok {
16 | if len(array.Nodes) > 0 {
17 | t := n.Left.Type()
18 | if t == nil || t.Kind() != reflect.Int {
19 | // This optimization can be only performed if left side is int type,
20 | // as runtime.in func uses reflect.Map.MapIndex and keys of map must,
21 | // be same as checked value type.
22 | goto string
23 | }
24 |
25 | for _, a := range array.Nodes {
26 | if _, ok := a.(*IntegerNode); !ok {
27 | goto string
28 | }
29 | }
30 | {
31 | value := make(map[int]struct{})
32 | for _, a := range array.Nodes {
33 | value[a.(*IntegerNode).Value] = struct{}{}
34 | }
35 | m := &ConstantNode{Value: value}
36 | m.SetType(reflect.TypeOf(value))
37 | patchCopyType(node, &BinaryNode{
38 | Operator: n.Operator,
39 | Left: n.Left,
40 | Right: m,
41 | })
42 | }
43 |
44 | string:
45 | for _, a := range array.Nodes {
46 | if _, ok := a.(*StringNode); !ok {
47 | return
48 | }
49 | }
50 | {
51 | value := make(map[string]struct{})
52 | for _, a := range array.Nodes {
53 | value[a.(*StringNode).Value] = struct{}{}
54 | }
55 | m := &ConstantNode{Value: value}
56 | m.SetType(reflect.TypeOf(value))
57 | patchCopyType(node, &BinaryNode{
58 | Operator: n.Operator,
59 | Left: n.Left,
60 | Right: m,
61 | })
62 | }
63 |
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/optimizer/in_range.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | "reflect"
5 |
6 | . "github.com/expr-lang/expr/ast"
7 | )
8 |
9 | type inRange struct{}
10 |
11 | func (*inRange) Visit(node *Node) {
12 | switch n := (*node).(type) {
13 | case *BinaryNode:
14 | if n.Operator == "in" {
15 | t := n.Left.Type()
16 | if t == nil {
17 | return
18 | }
19 | if t.Kind() != reflect.Int {
20 | return
21 | }
22 | if rangeOp, ok := n.Right.(*BinaryNode); ok && rangeOp.Operator == ".." {
23 | if from, ok := rangeOp.Left.(*IntegerNode); ok {
24 | if to, ok := rangeOp.Right.(*IntegerNode); ok {
25 | patchCopyType(node, &BinaryNode{
26 | Operator: "and",
27 | Left: &BinaryNode{
28 | Operator: ">=",
29 | Left: n.Left,
30 | Right: from,
31 | },
32 | Right: &BinaryNode{
33 | Operator: "<=",
34 | Left: n.Left,
35 | Right: to,
36 | },
37 | })
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/optimizer/optimizer.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | . "github.com/expr-lang/expr/ast"
8 | "github.com/expr-lang/expr/conf"
9 | )
10 |
11 | func Optimize(node *Node, config *conf.Config) error {
12 | Walk(node, &inArray{})
13 | for limit := 1000; limit >= 0; limit-- {
14 | fold := &fold{}
15 | Walk(node, fold)
16 | if fold.err != nil {
17 | return fold.err
18 | }
19 | if !fold.applied {
20 | break
21 | }
22 | }
23 | if config != nil && len(config.ConstFns) > 0 {
24 | for limit := 100; limit >= 0; limit-- {
25 | constExpr := &constExpr{
26 | fns: config.ConstFns,
27 | }
28 | Walk(node, constExpr)
29 | if constExpr.err != nil {
30 | return constExpr.err
31 | }
32 | if !constExpr.applied {
33 | break
34 | }
35 | }
36 | }
37 | Walk(node, &inRange{})
38 | Walk(node, &filterMap{})
39 | Walk(node, &filterLen{})
40 | Walk(node, &filterLast{})
41 | Walk(node, &filterFirst{})
42 | Walk(node, &predicateCombination{})
43 | Walk(node, &sumArray{})
44 | Walk(node, &sumMap{})
45 | return nil
46 | }
47 |
48 | var (
49 | boolType = reflect.TypeOf(true)
50 | integerType = reflect.TypeOf(0)
51 | floatType = reflect.TypeOf(float64(0))
52 | stringType = reflect.TypeOf("")
53 | )
54 |
55 | func patchWithType(node *Node, newNode Node) {
56 | switch n := newNode.(type) {
57 | case *BoolNode:
58 | newNode.SetType(boolType)
59 | case *IntegerNode:
60 | newNode.SetType(integerType)
61 | case *FloatNode:
62 | newNode.SetType(floatType)
63 | case *StringNode:
64 | newNode.SetType(stringType)
65 | case *ConstantNode:
66 | newNode.SetType(reflect.TypeOf(n.Value))
67 | case *BinaryNode:
68 | newNode.SetType(n.Type())
69 | default:
70 | panic(fmt.Sprintf("unknown type %T", newNode))
71 | }
72 | Patch(node, newNode)
73 | }
74 |
75 | func patchCopyType(node *Node, newNode Node) {
76 | t := (*node).Type()
77 | newNode.SetType(t)
78 | Patch(node, newNode)
79 | }
80 |
--------------------------------------------------------------------------------
/optimizer/predicate_combination.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | "github.com/expr-lang/expr/parser/operator"
6 | )
7 |
8 | /*
9 | predicateCombination is a visitor that combines multiple predicate calls into a single call.
10 | For example, the following expression:
11 |
12 | all(x, x > 1) && all(x, x < 10) -> all(x, x > 1 && x < 10)
13 | any(x, x > 1) || any(x, x < 10) -> any(x, x > 1 || x < 10)
14 | none(x, x > 1) && none(x, x < 10) -> none(x, x > 1 || x < 10)
15 | */
16 | type predicateCombination struct{}
17 |
18 | func (v *predicateCombination) Visit(node *Node) {
19 | if op, ok := (*node).(*BinaryNode); ok && operator.IsBoolean(op.Operator) {
20 | if left, ok := op.Left.(*BuiltinNode); ok {
21 | if combinedOp, ok := combinedOperator(left.Name, op.Operator); ok {
22 | if right, ok := op.Right.(*BuiltinNode); ok && right.Name == left.Name {
23 | if left.Arguments[0].Type() == right.Arguments[0].Type() && left.Arguments[0].String() == right.Arguments[0].String() {
24 | predicate := &PredicateNode{
25 | Node: &BinaryNode{
26 | Operator: combinedOp,
27 | Left: left.Arguments[1].(*PredicateNode).Node,
28 | Right: right.Arguments[1].(*PredicateNode).Node,
29 | },
30 | }
31 | v.Visit(&predicate.Node)
32 | patchCopyType(node, &BuiltinNode{
33 | Name: left.Name,
34 | Arguments: []Node{
35 | left.Arguments[0],
36 | predicate,
37 | },
38 | })
39 | }
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
46 | func combinedOperator(fn, op string) (string, bool) {
47 | switch {
48 | case fn == "all" && (op == "and" || op == "&&"):
49 | return op, true
50 | case fn == "any" && (op == "or" || op == "||"):
51 | return op, true
52 | case fn == "none" && (op == "and" || op == "&&"):
53 | switch op {
54 | case "and":
55 | return "or", true
56 | case "&&":
57 | return "||", true
58 | }
59 | }
60 | return "", false
61 | }
62 |
--------------------------------------------------------------------------------
/optimizer/sum_array.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | "fmt"
5 |
6 | . "github.com/expr-lang/expr/ast"
7 | )
8 |
9 | type sumArray struct{}
10 |
11 | func (*sumArray) Visit(node *Node) {
12 | if sumBuiltin, ok := (*node).(*BuiltinNode); ok &&
13 | sumBuiltin.Name == "sum" &&
14 | len(sumBuiltin.Arguments) == 1 {
15 | if array, ok := sumBuiltin.Arguments[0].(*ArrayNode); ok &&
16 | len(array.Nodes) >= 2 {
17 | patchCopyType(node, sumArrayFold(array))
18 | }
19 | }
20 | }
21 |
22 | func sumArrayFold(array *ArrayNode) *BinaryNode {
23 | if len(array.Nodes) > 2 {
24 | return &BinaryNode{
25 | Operator: "+",
26 | Left: array.Nodes[0],
27 | Right: sumArrayFold(&ArrayNode{Nodes: array.Nodes[1:]}),
28 | }
29 | } else if len(array.Nodes) == 2 {
30 | return &BinaryNode{
31 | Operator: "+",
32 | Left: array.Nodes[0],
33 | Right: array.Nodes[1],
34 | }
35 | }
36 | panic(fmt.Errorf("sumArrayFold: invalid array length %d", len(array.Nodes)))
37 | }
38 |
--------------------------------------------------------------------------------
/optimizer/sum_array_test.go:
--------------------------------------------------------------------------------
1 | package optimizer_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | "github.com/expr-lang/expr/ast"
11 | "github.com/expr-lang/expr/optimizer"
12 | "github.com/expr-lang/expr/parser"
13 | "github.com/expr-lang/expr/vm"
14 | )
15 |
16 | func BenchmarkSumArray(b *testing.B) {
17 | env := map[string]any{
18 | "a": 1,
19 | "b": 2,
20 | "c": 3,
21 | "d": 4,
22 | }
23 |
24 | program, err := expr.Compile(`sum([a, b, c, d])`, expr.Env(env))
25 | require.NoError(b, err)
26 |
27 | var out any
28 | b.ResetTimer()
29 | for n := 0; n < b.N; n++ {
30 | out, err = vm.Run(program, env)
31 | }
32 | b.StopTimer()
33 |
34 | require.NoError(b, err)
35 | require.Equal(b, 10, out)
36 | }
37 |
38 | func TestOptimize_sum_array(t *testing.T) {
39 | tree, err := parser.Parse(`sum([a, b])`)
40 | require.NoError(t, err)
41 |
42 | err = optimizer.Optimize(&tree.Node, nil)
43 | require.NoError(t, err)
44 |
45 | expected := &ast.BinaryNode{
46 | Operator: "+",
47 | Left: &ast.IdentifierNode{Value: "a"},
48 | Right: &ast.IdentifierNode{Value: "b"},
49 | }
50 |
51 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
52 | }
53 |
54 | func TestOptimize_sum_array_3(t *testing.T) {
55 | tree, err := parser.Parse(`sum([a, b, c])`)
56 | require.NoError(t, err)
57 |
58 | err = optimizer.Optimize(&tree.Node, nil)
59 | require.NoError(t, err)
60 |
61 | expected := &ast.BinaryNode{
62 | Operator: "+",
63 | Left: &ast.IdentifierNode{Value: "a"},
64 | Right: &ast.BinaryNode{
65 | Operator: "+",
66 | Left: &ast.IdentifierNode{Value: "b"},
67 | Right: &ast.IdentifierNode{Value: "c"},
68 | },
69 | }
70 |
71 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
72 | }
73 |
--------------------------------------------------------------------------------
/optimizer/sum_map.go:
--------------------------------------------------------------------------------
1 | package optimizer
2 |
3 | import (
4 | . "github.com/expr-lang/expr/ast"
5 | )
6 |
7 | type sumMap struct{}
8 |
9 | func (*sumMap) Visit(node *Node) {
10 | if sumBuiltin, ok := (*node).(*BuiltinNode); ok &&
11 | sumBuiltin.Name == "sum" &&
12 | len(sumBuiltin.Arguments) == 1 {
13 | if mapBuiltin, ok := sumBuiltin.Arguments[0].(*BuiltinNode); ok &&
14 | mapBuiltin.Name == "map" &&
15 | len(mapBuiltin.Arguments) == 2 {
16 | patchCopyType(node, &BuiltinNode{
17 | Name: "sum",
18 | Arguments: []Node{
19 | mapBuiltin.Arguments[0],
20 | mapBuiltin.Arguments[1],
21 | },
22 | })
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/optimizer/sum_map_test.go:
--------------------------------------------------------------------------------
1 | package optimizer_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr/ast"
10 | "github.com/expr-lang/expr/optimizer"
11 | "github.com/expr-lang/expr/parser"
12 | )
13 |
14 | func TestOptimize_sum_map(t *testing.T) {
15 | tree, err := parser.Parse(`sum(map(users, {.Age}))`)
16 | require.NoError(t, err)
17 |
18 | err = optimizer.Optimize(&tree.Node, nil)
19 | require.NoError(t, err)
20 |
21 | expected := &ast.BuiltinNode{
22 | Name: "sum",
23 | Arguments: []ast.Node{
24 | &ast.IdentifierNode{Value: "users"},
25 | &ast.PredicateNode{
26 | Node: &ast.MemberNode{
27 | Node: &ast.PointerNode{},
28 | Property: &ast.StringNode{Value: "Age"},
29 | },
30 | },
31 | },
32 | }
33 |
34 | assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
35 | }
36 |
--------------------------------------------------------------------------------
/parser/lexer/token.go:
--------------------------------------------------------------------------------
1 | package lexer
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/expr-lang/expr/file"
7 | )
8 |
9 | type Kind string
10 |
11 | const (
12 | Identifier Kind = "Identifier"
13 | Number Kind = "Number"
14 | String Kind = "String"
15 | Operator Kind = "Operator"
16 | Bracket Kind = "Bracket"
17 | EOF Kind = "EOF"
18 | )
19 |
20 | type Token struct {
21 | file.Location
22 | Kind Kind
23 | Value string
24 | }
25 |
26 | func (t Token) String() string {
27 | if t.Value == "" {
28 | return string(t.Kind)
29 | }
30 | return fmt.Sprintf("%s(%#v)", t.Kind, t.Value)
31 | }
32 |
33 | func (t Token) Is(kind Kind, values ...string) bool {
34 | if len(values) == 0 {
35 | return kind == t.Kind
36 | }
37 |
38 | for _, v := range values {
39 | if v == t.Value {
40 | goto found
41 | }
42 | }
43 | return false
44 |
45 | found:
46 | return kind == t.Kind
47 | }
48 |
--------------------------------------------------------------------------------
/parser/operator/operator.go:
--------------------------------------------------------------------------------
1 | package operator
2 |
3 | type Associativity int
4 |
5 | const (
6 | Left Associativity = iota + 1
7 | Right
8 | )
9 |
10 | type Operator struct {
11 | Precedence int
12 | Associativity Associativity
13 | }
14 |
15 | func Less(a, b string) bool {
16 | return Binary[a].Precedence < Binary[b].Precedence
17 | }
18 |
19 | func IsBoolean(op string) bool {
20 | return op == "and" || op == "or" || op == "&&" || op == "||"
21 | }
22 |
23 | func AllowedNegateSuffix(op string) bool {
24 | switch op {
25 | case "contains", "matches", "startsWith", "endsWith", "in":
26 | return true
27 | default:
28 | return false
29 | }
30 | }
31 |
32 | var Unary = map[string]Operator{
33 | "not": {50, Left},
34 | "!": {50, Left},
35 | "-": {90, Left},
36 | "+": {90, Left},
37 | }
38 |
39 | var Binary = map[string]Operator{
40 | "|": {0, Left},
41 | "or": {10, Left},
42 | "||": {10, Left},
43 | "and": {15, Left},
44 | "&&": {15, Left},
45 | "==": {20, Left},
46 | "!=": {20, Left},
47 | "<": {20, Left},
48 | ">": {20, Left},
49 | ">=": {20, Left},
50 | "<=": {20, Left},
51 | "in": {20, Left},
52 | "matches": {20, Left},
53 | "contains": {20, Left},
54 | "startsWith": {20, Left},
55 | "endsWith": {20, Left},
56 | "..": {25, Left},
57 | "+": {30, Left},
58 | "-": {30, Left},
59 | "*": {60, Left},
60 | "/": {60, Left},
61 | "%": {60, Left},
62 | "**": {100, Right},
63 | "^": {100, Right},
64 | "??": {500, Left},
65 | }
66 |
67 | func IsComparison(op string) bool {
68 | return op == "<" || op == ">" || op == ">=" || op == "<="
69 | }
70 |
--------------------------------------------------------------------------------
/parser/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "unicode"
5 | "unicode/utf8"
6 | )
7 |
8 | func IsValidIdentifier(str string) bool {
9 | if len(str) == 0 {
10 | return false
11 | }
12 | h, w := utf8.DecodeRuneInString(str)
13 | if !IsAlphabetic(h) {
14 | return false
15 | }
16 | for _, r := range str[w:] {
17 | if !IsAlphaNumeric(r) {
18 | return false
19 | }
20 | }
21 | return true
22 | }
23 |
24 | func IsSpace(r rune) bool {
25 | return unicode.IsSpace(r)
26 | }
27 |
28 | func IsAlphaNumeric(r rune) bool {
29 | return IsAlphabetic(r) || unicode.IsDigit(r)
30 | }
31 |
32 | func IsAlphabetic(r rune) bool {
33 | return r == '_' || r == '$' || unicode.IsLetter(r)
34 | }
35 |
--------------------------------------------------------------------------------
/patcher/operator_override.go:
--------------------------------------------------------------------------------
1 | package patcher
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/expr-lang/expr/ast"
8 | "github.com/expr-lang/expr/builtin"
9 | "github.com/expr-lang/expr/checker/nature"
10 | "github.com/expr-lang/expr/conf"
11 | )
12 |
13 | type OperatorOverloading struct {
14 | Operator string // Operator token to overload.
15 | Overloads []string // List of function names to replace operator with.
16 | Env *nature.Nature // Env type.
17 | Functions conf.FunctionsTable // Env functions.
18 | applied bool // Flag to indicate if any changes were made to the tree.
19 | }
20 |
21 | func (p *OperatorOverloading) Visit(node *ast.Node) {
22 | binaryNode, ok := (*node).(*ast.BinaryNode)
23 | if !ok {
24 | return
25 | }
26 |
27 | if binaryNode.Operator != p.Operator {
28 | return
29 | }
30 |
31 | leftType := binaryNode.Left.Type()
32 | rightType := binaryNode.Right.Type()
33 |
34 | ret, fn, ok := p.FindSuitableOperatorOverload(leftType, rightType)
35 | if ok {
36 | newNode := &ast.CallNode{
37 | Callee: &ast.IdentifierNode{Value: fn},
38 | Arguments: []ast.Node{binaryNode.Left, binaryNode.Right},
39 | }
40 | newNode.SetType(ret)
41 | ast.Patch(node, newNode)
42 | p.applied = true
43 | }
44 | }
45 |
46 | // Tracking must be reset before every walk over the AST tree
47 | func (p *OperatorOverloading) Reset() {
48 | p.applied = false
49 | }
50 |
51 | func (p *OperatorOverloading) ShouldRepeat() bool {
52 | return p.applied
53 | }
54 |
55 | func (p *OperatorOverloading) FindSuitableOperatorOverload(l, r reflect.Type) (reflect.Type, string, bool) {
56 | t, fn, ok := p.findSuitableOperatorOverloadInFunctions(l, r)
57 | if !ok {
58 | t, fn, ok = p.findSuitableOperatorOverloadInTypes(l, r)
59 | }
60 | return t, fn, ok
61 | }
62 |
63 | func (p *OperatorOverloading) findSuitableOperatorOverloadInTypes(l, r reflect.Type) (reflect.Type, string, bool) {
64 | for _, fn := range p.Overloads {
65 | fnType, ok := p.Env.Get(fn)
66 | if !ok {
67 | continue
68 | }
69 | firstInIndex := 0
70 | if fnType.Method {
71 | firstInIndex = 1 // As first argument to method is receiver.
72 | }
73 | ret, done := checkTypeSuits(fnType.Type, l, r, firstInIndex)
74 | if done {
75 | return ret, fn, true
76 | }
77 | }
78 | return nil, "", false
79 | }
80 |
81 | func (p *OperatorOverloading) findSuitableOperatorOverloadInFunctions(l, r reflect.Type) (reflect.Type, string, bool) {
82 | for _, fn := range p.Overloads {
83 | fnType, ok := p.Functions[fn]
84 | if !ok {
85 | continue
86 | }
87 | firstInIndex := 0
88 | for _, overload := range fnType.Types {
89 | ret, done := checkTypeSuits(overload, l, r, firstInIndex)
90 | if done {
91 | return ret, fn, true
92 | }
93 | }
94 | }
95 | return nil, "", false
96 | }
97 |
98 | func checkTypeSuits(t reflect.Type, l reflect.Type, r reflect.Type, firstInIndex int) (reflect.Type, bool) {
99 | firstArgType := t.In(firstInIndex)
100 | secondArgType := t.In(firstInIndex + 1)
101 |
102 | firstArgumentFit := l == firstArgType || (firstArgType.Kind() == reflect.Interface && (l == nil || l.Implements(firstArgType)))
103 | secondArgumentFit := r == secondArgType || (secondArgType.Kind() == reflect.Interface && (r == nil || r.Implements(secondArgType)))
104 | if firstArgumentFit && secondArgumentFit {
105 | return t.Out(0), true
106 | }
107 | return nil, false
108 | }
109 |
110 | func (p *OperatorOverloading) Check() {
111 | for _, fn := range p.Overloads {
112 | fnType, foundType := p.Env.Get(fn)
113 | fnFunc, foundFunc := p.Functions[fn]
114 | if !foundFunc && (!foundType || fnType.Type.Kind() != reflect.Func) {
115 | panic(fmt.Errorf("function %s for %s operator does not exist in the environment", fn, p.Operator))
116 | }
117 |
118 | if foundType {
119 | checkType(fnType, fn, p.Operator)
120 | }
121 |
122 | if foundFunc {
123 | checkFunc(fnFunc, fn, p.Operator)
124 | }
125 | }
126 | }
127 |
128 | func checkType(fnType nature.Nature, fn string, operator string) {
129 | requiredNumIn := 2
130 | if fnType.Method {
131 | requiredNumIn = 3 // As first argument of method is receiver.
132 | }
133 | if fnType.Type.NumIn() != requiredNumIn || fnType.Type.NumOut() != 1 {
134 | panic(fmt.Errorf("function %s for %s operator does not have a correct signature", fn, operator))
135 | }
136 | }
137 |
138 | func checkFunc(fn *builtin.Function, name string, operator string) {
139 | if len(fn.Types) == 0 {
140 | panic(fmt.Errorf("function %q for %q operator misses types", name, operator))
141 | }
142 | for _, t := range fn.Types {
143 | if t.NumIn() != 2 || t.NumOut() != 1 {
144 | panic(fmt.Errorf("function %q for %q operator does not have a correct signature", name, operator))
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/patcher/value/bench_test.go:
--------------------------------------------------------------------------------
1 | package value
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr"
9 | "github.com/expr-lang/expr/vm"
10 | )
11 |
12 | func Benchmark_valueAdd(b *testing.B) {
13 | env := make(map[string]any)
14 | env["ValueOne"] = &customInt{1}
15 | env["ValueTwo"] = &customInt{2}
16 |
17 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
18 | require.NoError(b, err)
19 |
20 | var out any
21 | v := vm.VM{}
22 |
23 | b.ResetTimer()
24 | for n := 0; n < b.N; n++ {
25 | out, err = v.Run(program, env)
26 | }
27 | b.StopTimer()
28 |
29 | require.NoError(b, err)
30 | require.Equal(b, 3, out.(int))
31 | }
32 |
33 | func Benchmark_valueUntypedAdd(b *testing.B) {
34 | env := make(map[string]any)
35 | env["ValueOne"] = &customUntypedInt{1}
36 | env["ValueTwo"] = &customUntypedInt{2}
37 |
38 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
39 | require.NoError(b, err)
40 |
41 | var out any
42 | v := vm.VM{}
43 |
44 | b.ResetTimer()
45 | for n := 0; n < b.N; n++ {
46 | out, err = v.Run(program, env)
47 | }
48 | b.StopTimer()
49 |
50 | require.NoError(b, err)
51 | require.Equal(b, 3, out.(int))
52 | }
53 |
54 | func Benchmark_valueTypedAdd(b *testing.B) {
55 | env := make(map[string]any)
56 | env["ValueOne"] = &customTypedInt{1}
57 | env["ValueTwo"] = &customTypedInt{2}
58 |
59 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
60 | require.NoError(b, err)
61 |
62 | var out any
63 | v := vm.VM{}
64 |
65 | b.ResetTimer()
66 | for n := 0; n < b.N; n++ {
67 | out, err = v.Run(program, env)
68 | }
69 | b.StopTimer()
70 |
71 | require.NoError(b, err)
72 | require.Equal(b, 3, out.(int))
73 | }
74 |
--------------------------------------------------------------------------------
/patcher/value/value_example_test.go:
--------------------------------------------------------------------------------
1 | package value_test
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/patcher/value"
8 | "github.com/expr-lang/expr/vm"
9 | )
10 |
11 | type myInt struct {
12 | Int int
13 | }
14 |
15 | func (v *myInt) AsInt() int {
16 | return v.Int
17 | }
18 |
19 | func (v *myInt) AsAny() any {
20 | return v.Int
21 | }
22 |
23 | func ExampleAnyValuer() {
24 | env := make(map[string]any)
25 | env["ValueOne"] = &myInt{1}
26 | env["ValueTwo"] = &myInt{2}
27 |
28 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), value.ValueGetter)
29 |
30 | if err != nil {
31 | panic(err)
32 | }
33 |
34 | out, err := vm.Run(program, env)
35 |
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | fmt.Println(out)
41 | // Output: 3
42 | }
43 |
--------------------------------------------------------------------------------
/patcher/value/value_test.go:
--------------------------------------------------------------------------------
1 | package value
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr"
9 | "github.com/expr-lang/expr/vm"
10 | )
11 |
12 | type customInt struct {
13 | Int int
14 | }
15 |
16 | func (v *customInt) AsInt() int {
17 | return v.Int
18 | }
19 |
20 | func (v *customInt) AsAny() any {
21 | return v.Int
22 | }
23 |
24 | type customTypedInt struct {
25 | Int int
26 | }
27 |
28 | func (v *customTypedInt) AsInt() int {
29 | return v.Int
30 | }
31 |
32 | type customUntypedInt struct {
33 | Int int
34 | }
35 |
36 | func (v *customUntypedInt) AsAny() any {
37 | return v.Int
38 | }
39 |
40 | type customString struct {
41 | String string
42 | }
43 |
44 | func (v *customString) AsString() string {
45 | return v.String
46 | }
47 |
48 | func (v *customString) AsAny() any {
49 | return v.String
50 | }
51 |
52 | type customTypedString struct {
53 | String string
54 | }
55 |
56 | func (v *customTypedString) AsString() string {
57 | return v.String
58 | }
59 |
60 | type customUntypedString struct {
61 | String string
62 | }
63 |
64 | func (v *customUntypedString) AsAny() any {
65 | return v.String
66 | }
67 |
68 | type customTypedArray struct {
69 | Array []any
70 | }
71 |
72 | func (v *customTypedArray) AsArray() []any {
73 | return v.Array
74 | }
75 |
76 | type customTypedMap struct {
77 | Map map[string]any
78 | }
79 |
80 | func (v *customTypedMap) AsMap() map[string]any {
81 | return v.Map
82 | }
83 |
84 | func Test_valueAddInt(t *testing.T) {
85 | env := make(map[string]any)
86 | env["ValueOne"] = &customInt{1}
87 | env["ValueTwo"] = &customInt{2}
88 |
89 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
90 | require.NoError(t, err)
91 | out, err := vm.Run(program, env)
92 |
93 | require.NoError(t, err)
94 | require.Equal(t, 3, out.(int))
95 | }
96 |
97 | func Test_valueUntypedAddInt(t *testing.T) {
98 | env := make(map[string]any)
99 | env["ValueOne"] = &customUntypedInt{1}
100 | env["ValueTwo"] = &customUntypedInt{2}
101 |
102 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
103 | require.NoError(t, err)
104 |
105 | out, err := vm.Run(program, env)
106 |
107 | require.NoError(t, err)
108 | require.Equal(t, 3, out.(int))
109 | }
110 |
111 | func Test_valueTypedAddInt(t *testing.T) {
112 | env := make(map[string]any)
113 | env["ValueOne"] = &customTypedInt{1}
114 | env["ValueTwo"] = &customTypedInt{2}
115 |
116 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
117 | require.NoError(t, err)
118 |
119 | out, err := vm.Run(program, env)
120 |
121 | require.NoError(t, err)
122 | require.Equal(t, 3, out.(int))
123 | }
124 |
125 | func Test_valueTypedAddMismatch(t *testing.T) {
126 | env := make(map[string]any)
127 | env["ValueOne"] = &customTypedInt{1}
128 | env["ValueTwo"] = &customTypedString{"test"}
129 |
130 | _, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
131 | require.Error(t, err)
132 | }
133 |
134 | func Test_valueUntypedAddMismatch(t *testing.T) {
135 | env := make(map[string]any)
136 | env["ValueOne"] = &customUntypedInt{1}
137 | env["ValueTwo"] = &customUntypedString{"test"}
138 |
139 | program, err := expr.Compile("ValueOne + ValueTwo", expr.Env(env), ValueGetter)
140 | require.NoError(t, err)
141 |
142 | _, err = vm.Run(program, env)
143 |
144 | require.Error(t, err)
145 | }
146 |
147 | func Test_valueTypedArray(t *testing.T) {
148 | env := make(map[string]any)
149 | env["ValueOne"] = &customTypedArray{[]any{1, 2}}
150 |
151 | program, err := expr.Compile("ValueOne[0] + ValueOne[1]", expr.Env(env), ValueGetter)
152 | require.NoError(t, err)
153 |
154 | out, err := vm.Run(program, env)
155 |
156 | require.NoError(t, err)
157 | require.Equal(t, 3, out.(int))
158 | }
159 |
160 | func Test_valueTypedMap(t *testing.T) {
161 | env := make(map[string]any)
162 | env["ValueOne"] = &customTypedMap{map[string]any{"one": 1, "two": 2}}
163 |
164 | program, err := expr.Compile("ValueOne.one + ValueOne.two", expr.Env(env), ValueGetter)
165 | require.NoError(t, err)
166 |
167 | out, err := vm.Run(program, env)
168 |
169 | require.NoError(t, err)
170 | require.Equal(t, 3, out.(int))
171 | }
172 |
--------------------------------------------------------------------------------
/patcher/with_context.go:
--------------------------------------------------------------------------------
1 | package patcher
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/expr-lang/expr/ast"
7 | )
8 |
9 | // WithContext adds WithContext.Name argument to all functions calls with a context.Context argument.
10 | type WithContext struct {
11 | Name string
12 | }
13 |
14 | // Visit adds WithContext.Name argument to all functions calls with a context.Context argument.
15 | func (w WithContext) Visit(node *ast.Node) {
16 | switch call := (*node).(type) {
17 | case *ast.CallNode:
18 | fn := call.Callee.Type()
19 | if fn == nil {
20 | return
21 | }
22 | if fn.Kind() != reflect.Func {
23 | return
24 | }
25 | switch fn.NumIn() {
26 | case 0:
27 | return
28 | case 1:
29 | if fn.In(0).String() != "context.Context" {
30 | return
31 | }
32 | default:
33 | if fn.In(0).String() != "context.Context" &&
34 | fn.In(1).String() != "context.Context" {
35 | return
36 | }
37 | }
38 | ast.Patch(node, &ast.CallNode{
39 | Callee: call.Callee,
40 | Arguments: append([]ast.Node{
41 | &ast.IdentifierNode{Value: w.Name},
42 | }, call.Arguments...),
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/patcher/with_context_test.go:
--------------------------------------------------------------------------------
1 | package patcher_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | "github.com/expr-lang/expr/patcher"
11 | )
12 |
13 | func TestWithContext(t *testing.T) {
14 | env := map[string]any{
15 | "fn": func(ctx context.Context, a int) int {
16 | return ctx.Value("value").(int) + a
17 | },
18 | "ctx": context.TODO(),
19 | }
20 |
21 | withContext := patcher.WithContext{Name: "ctx"}
22 |
23 | program, err := expr.Compile(`fn(40)`, expr.Env(env), expr.Patch(withContext))
24 | require.NoError(t, err)
25 |
26 | ctx := context.WithValue(context.Background(), "value", 2)
27 | env["ctx"] = ctx
28 |
29 | output, err := expr.Run(program, env)
30 | require.NoError(t, err)
31 | require.Equal(t, 42, output)
32 | }
33 |
34 | func TestWithContext_with_env_Function(t *testing.T) {
35 | env := map[string]any{
36 | "ctx": context.TODO(),
37 | }
38 |
39 | fn := expr.Function("fn",
40 | func(params ...any) (any, error) {
41 | ctx := params[0].(context.Context)
42 | a := params[1].(int)
43 |
44 | return ctx.Value("value").(int) + a, nil
45 | },
46 | new(func(context.Context, int) int),
47 | )
48 |
49 | program, err := expr.Compile(
50 | `fn(40)`,
51 | expr.Env(env),
52 | expr.WithContext("ctx"),
53 | fn,
54 | )
55 | require.NoError(t, err)
56 |
57 | ctx := context.WithValue(context.Background(), "value", 2)
58 | env["ctx"] = ctx
59 |
60 | output, err := expr.Run(program, env)
61 | require.NoError(t, err)
62 | require.Equal(t, 42, output)
63 | }
64 |
65 | type testEnvContext struct {
66 | Context context.Context `expr:"ctx"`
67 | }
68 |
69 | func (testEnvContext) Fn(ctx context.Context, a int) int {
70 | return ctx.Value("value").(int) + a
71 | }
72 |
73 | func TestWithContext_env_struct(t *testing.T) {
74 | withContext := patcher.WithContext{Name: "ctx"}
75 |
76 | program, err := expr.Compile(`Fn(40)`, expr.Env(testEnvContext{}), expr.Patch(withContext))
77 | require.NoError(t, err)
78 |
79 | ctx := context.WithValue(context.Background(), "value", 2)
80 | env := testEnvContext{
81 | Context: ctx,
82 | }
83 |
84 | output, err := expr.Run(program, env)
85 | require.NoError(t, err)
86 | require.Equal(t, 42, output)
87 | }
88 |
89 | type TestFoo struct {
90 | contextValue int
91 | }
92 |
93 | func (f *TestFoo) GetValue(a int) int64 {
94 | return int64(f.contextValue + a)
95 | }
96 |
97 | func TestWithContext_with_env_method_chain(t *testing.T) {
98 | env := map[string]any{
99 | "ctx": context.TODO(),
100 | }
101 |
102 | fn := expr.Function("fn",
103 | func(params ...any) (any, error) {
104 | ctx := params[0].(context.Context)
105 |
106 | contextValue := ctx.Value("value").(int)
107 |
108 | return &TestFoo{
109 | contextValue: contextValue,
110 | }, nil
111 | },
112 | new(func(context.Context) *TestFoo),
113 | )
114 |
115 | program, err := expr.Compile(
116 | `fn().GetValue(40)`,
117 | expr.Env(env),
118 | expr.WithContext("ctx"),
119 | fn,
120 | expr.AsInt64(),
121 | )
122 | require.NoError(t, err)
123 |
124 | ctx := context.WithValue(context.Background(), "value", 2)
125 | env["ctx"] = ctx
126 |
127 | output, err := expr.Run(program, env)
128 | require.NoError(t, err)
129 | require.Equal(t, int64(42), output)
130 | }
131 |
132 | func TestWithContext_issue529(t *testing.T) {
133 | env := map[string]any{
134 | "ctx": context.Background(),
135 | "foo": func(ctx context.Context, n int) int {
136 | if ctx == nil {
137 | panic("wanted a context")
138 | }
139 | return n + 1
140 | },
141 | }
142 | options := []expr.Option{
143 | expr.Env(env),
144 | expr.WithContext("ctx"),
145 | }
146 |
147 | code := "foo(0) | foo()"
148 | program, err := expr.Compile(code, options...)
149 | require.NoError(t, err)
150 |
151 | out, err := expr.Run(program, env)
152 | require.NoError(t, err)
153 | require.Equal(t, 2, out)
154 | }
155 |
--------------------------------------------------------------------------------
/patcher/with_timezone.go:
--------------------------------------------------------------------------------
1 | package patcher
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/expr-lang/expr/ast"
7 | )
8 |
9 | // WithTimezone passes Location to date() and now() functions.
10 | type WithTimezone struct {
11 | Location *time.Location
12 | }
13 |
14 | func (t WithTimezone) Visit(node *ast.Node) {
15 | if btin, ok := (*node).(*ast.BuiltinNode); ok {
16 | switch btin.Name {
17 | case "date", "now":
18 | loc := &ast.ConstantNode{Value: t.Location}
19 | ast.Patch(node, &ast.BuiltinNode{
20 | Name: btin.Name,
21 | Arguments: append([]ast.Node{loc}, btin.Arguments...),
22 | })
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/patcher/with_timezone_test.go:
--------------------------------------------------------------------------------
1 | package patcher_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | )
11 |
12 | func TestWithTimezone_date(t *testing.T) {
13 | program, err := expr.Compile(`date("2024-05-07 23:00:00")`, expr.Timezone("Europe/Zurich"))
14 | require.NoError(t, err)
15 |
16 | out, err := expr.Run(program, nil)
17 | require.NoError(t, err)
18 | require.Equal(t, "2024-05-07T23:00:00+02:00", out.(time.Time).Format(time.RFC3339))
19 | }
20 |
21 | func TestWithTimezone_now(t *testing.T) {
22 | program, err := expr.Compile(`now()`, expr.Timezone("Asia/Kamchatka"))
23 | require.NoError(t, err)
24 |
25 | out, err := expr.Run(program, nil)
26 | require.NoError(t, err)
27 | require.Equal(t, "Asia/Kamchatka", out.(time.Time).Location().String())
28 | }
29 |
--------------------------------------------------------------------------------
/repl/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/expr-lang/expr/repl
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf
7 | github.com/expr-lang/expr v1.13.0
8 | github.com/expr-lang/expr/debug v0.0.0
9 | )
10 |
11 | require (
12 | github.com/chzyer/test v1.0.0 // indirect
13 | github.com/gdamore/encoding v1.0.0 // indirect
14 | github.com/gdamore/tcell/v2 v2.6.0 // indirect
15 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
16 | github.com/mattn/go-runewidth v0.0.15 // indirect
17 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 // indirect
18 | github.com/rivo/uniseg v0.4.4 // indirect
19 | golang.org/x/sys v0.11.0 // indirect
20 | golang.org/x/term v0.11.0 // indirect
21 | golang.org/x/text v0.12.0 // indirect
22 | )
23 |
24 | replace github.com/expr-lang/expr => ../
25 |
26 | replace github.com/expr-lang/expr/debug => ../debug
27 |
--------------------------------------------------------------------------------
/repl/repl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "strings"
8 |
9 | "github.com/expr-lang/expr/test/fuzz"
10 |
11 | "github.com/bettercap/readline"
12 |
13 | "github.com/expr-lang/expr"
14 | "github.com/expr-lang/expr/builtin"
15 | "github.com/expr-lang/expr/debug"
16 | "github.com/expr-lang/expr/vm"
17 | )
18 |
19 | var keywords = []string{
20 | // Commands:
21 | "exit", "opcodes", "debug", "mem",
22 |
23 | // Operators:
24 | "and", "or", "in", "not", "not in",
25 | "contains", "matches", "startsWith", "endsWith",
26 | }
27 |
28 | func main() {
29 | env := fuzz.NewEnv()
30 | for name := range env {
31 | keywords = append(keywords, name)
32 | }
33 | fn := fuzz.Func()
34 | keywords = append(keywords, "fn")
35 | home, err := os.UserHomeDir()
36 | if err != nil {
37 | panic(err)
38 | }
39 | rl, err := readline.NewEx(&readline.Config{
40 | Prompt: "❯ ",
41 | AutoComplete: completer{append(builtin.Names, keywords...)},
42 | HistoryFile: home + "/.expr_history",
43 | })
44 | if err != nil {
45 | panic(err)
46 | }
47 | defer rl.Close()
48 |
49 | var memUsage uint64
50 | var program *vm.Program
51 |
52 | for {
53 | line, err := rl.Readline()
54 | if err != nil { // io.EOF when Ctrl-D is pressed
55 | break
56 | }
57 | line = strings.TrimSpace(line)
58 |
59 | switch line {
60 | case "":
61 | continue
62 |
63 | case "exit":
64 | return
65 |
66 | case "mem":
67 | fmt.Printf("memory usage: %s\n", humanizeBytes(memUsage))
68 | continue
69 |
70 | case "opcodes":
71 | if program == nil {
72 | fmt.Println("no program")
73 | continue
74 | }
75 | fmt.Println(program.Disassemble())
76 | continue
77 |
78 | case "debug":
79 | if program == nil {
80 | fmt.Println("no program")
81 | continue
82 | }
83 | debug.StartDebugger(program, env)
84 | continue
85 | }
86 |
87 | program, err = expr.Compile(line, expr.Env(env), fn)
88 | if err != nil {
89 | fmt.Printf("compile error: %s\n", err)
90 | continue
91 | }
92 |
93 | start := memoryUsage()
94 | output, err := expr.Run(program, env)
95 | if err != nil {
96 | fmt.Printf("runtime error: %s\n", err)
97 | continue
98 | }
99 | memUsage = memoryUsage() - start
100 |
101 | fmt.Println(output)
102 | }
103 | }
104 |
105 | type completer struct {
106 | words []string
107 | }
108 |
109 | func (c completer) Do(line []rune, pos int) ([][]rune, int) {
110 | var lastWord string
111 | for i := pos - 1; i >= 0; i-- {
112 | if line[i] == ' ' {
113 | break
114 | }
115 | lastWord = string(line[i]) + lastWord
116 | }
117 |
118 | var words [][]rune
119 | for _, word := range c.words {
120 | if strings.HasPrefix(word, lastWord) {
121 | words = append(words, []rune(strings.TrimPrefix(word, lastWord)))
122 | }
123 | }
124 |
125 | return words, len(lastWord)
126 | }
127 |
128 | func memoryUsage() uint64 {
129 | var m runtime.MemStats
130 | runtime.ReadMemStats(&m)
131 | return m.Alloc
132 | }
133 |
134 | func humanizeBytes(b uint64) string {
135 | const unit = 1024
136 | if b < unit {
137 | return fmt.Sprintf("%d B", b)
138 | }
139 | div, exp := uint64(unit), 0
140 | for n := b / unit; n >= unit; n /= unit {
141 | div *= unit
142 | exp++
143 | }
144 | return fmt.Sprintf("%.2f %ciB",
145 | float64(b)/float64(div), "KMGTPE"[exp])
146 | }
147 |
--------------------------------------------------------------------------------
/test/bench/bench_call_test.go:
--------------------------------------------------------------------------------
1 | package bench_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | "github.com/expr-lang/expr/vm"
9 | )
10 |
11 | type Env struct {
12 | Fn func() bool
13 | }
14 |
15 | func BenchmarkCall_callTyped(b *testing.B) {
16 | code := `Fn()`
17 |
18 | p, err := expr.Compile(code, expr.Env(Env{}))
19 | require.NoError(b, err)
20 | require.Equal(b, p.Bytecode[1], vm.OpCallTyped)
21 |
22 | env := Env{
23 | Fn: func() bool {
24 | return true
25 | },
26 | }
27 |
28 | var out any
29 |
30 | b.ResetTimer()
31 | for n := 0; n < b.N; n++ {
32 | program, _ := expr.Compile(code, expr.Env(env))
33 | out, err = vm.Run(program, env)
34 | }
35 | b.StopTimer()
36 |
37 | require.NoError(b, err)
38 | require.True(b, out.(bool))
39 | }
40 |
41 | func BenchmarkCall_eval(b *testing.B) {
42 | code := `Fn()`
43 |
44 | p, err := expr.Compile(code)
45 | require.NoError(b, err)
46 | require.Equal(b, p.Bytecode[1], vm.OpCall)
47 |
48 | env := Env{
49 | Fn: func() bool {
50 | return true
51 | },
52 | }
53 |
54 | var out any
55 | b.ResetTimer()
56 | for n := 0; n < b.N; n++ {
57 | out, err = expr.Eval(code, env)
58 | }
59 | b.StopTimer()
60 |
61 | require.NoError(b, err)
62 | require.True(b, out.(bool))
63 | }
64 |
--------------------------------------------------------------------------------
/test/coredns/coredns.go:
--------------------------------------------------------------------------------
1 | package coredns
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 | )
8 |
9 | func DefaultEnv(ctx context.Context, state *Request) map[string]any {
10 | return map[string]any{
11 | "incidr": func(ipStr, cidrStr string) (bool, error) {
12 | ip := net.ParseIP(ipStr)
13 | if ip == nil {
14 | return false, errors.New("first argument is not an IP address")
15 | }
16 | _, cidr, err := net.ParseCIDR(cidrStr)
17 | if err != nil {
18 | return false, err
19 | }
20 | return cidr.Contains(ip), nil
21 | },
22 | "metadata": func(label string) string {
23 | return ""
24 | },
25 | "type": state.Type,
26 | "name": state.Name,
27 | "class": state.Class,
28 | "proto": state.Proto,
29 | "size": state.Len,
30 | "client_ip": state.IP,
31 | "port": state.Port,
32 | "id": func() int { return int(state.Req.Id) },
33 | "opcode": func() int { return state.Req.Opcode },
34 | "do": state.Do,
35 | "bufsize": state.Size,
36 | "server_ip": state.LocalIP,
37 | "server_port": state.LocalPort,
38 | }
39 | }
40 |
41 | type Request struct {
42 | Req *Msg
43 | W ResponseWriter
44 | Zone string
45 | }
46 |
47 | func (r *Request) NewWithQuestion(name string, typ uint16) Request {
48 | return Request{}
49 | }
50 |
51 | func (r *Request) IP() string {
52 | return ""
53 | }
54 |
55 | func (r *Request) LocalIP() string {
56 | return ""
57 | }
58 |
59 | func (r *Request) Port() string {
60 | return ""
61 | }
62 |
63 | func (r *Request) LocalPort() string {
64 | return ""
65 | }
66 |
67 | func (r *Request) RemoteAddr() string { return r.W.RemoteAddr().String() }
68 |
69 | func (r *Request) LocalAddr() string { return r.W.LocalAddr().String() }
70 |
71 | func (r *Request) Proto() string {
72 | return "udp"
73 | }
74 |
75 | func (r *Request) Family() int {
76 | return 2
77 | }
78 |
79 | func (r *Request) Do() bool {
80 | return true
81 | }
82 |
83 | func (r *Request) Len() int { return 0 }
84 |
85 | func (r *Request) Size() int {
86 | return 0
87 | }
88 |
89 | func (r *Request) SizeAndDo(m *Msg) bool {
90 | return true
91 | }
92 |
93 | func (r *Request) Scrub(reply *Msg) *Msg {
94 | return reply
95 | }
96 |
97 | func (r *Request) Type() string {
98 | return ""
99 | }
100 |
101 | func (r *Request) QType() uint16 {
102 | return 0
103 | }
104 |
105 | func (r *Request) Name() string {
106 | return "."
107 | }
108 |
109 | func (r *Request) QName() string {
110 | return "."
111 | }
112 |
113 | func (r *Request) Class() string {
114 | return ""
115 | }
116 |
117 | func (r *Request) QClass() uint16 {
118 | return 0
119 | }
120 |
121 | func (r *Request) Clear() {
122 | }
123 |
124 | func (r *Request) Match(reply *Msg) bool {
125 | return true
126 | }
127 |
128 | type Msg struct {
129 | MsgHdr
130 | Compress bool `json:"-"`
131 | Question []Question
132 | Answer []RR
133 | Ns []RR
134 | Extra []RR
135 | }
136 |
137 | type MsgHdr struct {
138 | Id uint16
139 | Response bool
140 | Opcode int
141 | Authoritative bool
142 | Truncated bool
143 | RecursionDesired bool
144 | RecursionAvailable bool
145 | Zero bool
146 | AuthenticatedData bool
147 | CheckingDisabled bool
148 | Rcode int
149 | }
150 |
151 | type Question struct {
152 | Name string `dns:"cdomain-name"`
153 | Qtype uint16
154 | Qclass uint16
155 | }
156 |
157 | type RR interface {
158 | Header() *RR_Header
159 | String() string
160 | }
161 |
162 | type RR_Header struct {
163 | Name string `dns:"cdomain-name"`
164 | Rrtype uint16
165 | Class uint16
166 | Ttl uint32
167 | Rdlength uint16
168 | }
169 |
170 | type ResponseWriter interface {
171 | LocalAddr() net.Addr
172 | RemoteAddr() net.Addr
173 | WriteMsg(*Msg) error
174 | Write([]byte) (int, error)
175 | Close() error
176 | TsigStatus() error
177 | TsigTimersOnly(bool)
178 | Hijack()
179 | }
180 |
--------------------------------------------------------------------------------
/test/coredns/coredns_test.go:
--------------------------------------------------------------------------------
1 | package coredns_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/internal/testify/assert"
8 |
9 | "github.com/expr-lang/expr"
10 | "github.com/expr-lang/expr/test/coredns"
11 | )
12 |
13 | func TestCoreDNS(t *testing.T) {
14 | env := coredns.DefaultEnv(context.Background(), &coredns.Request{})
15 |
16 | tests := []struct {
17 | input string
18 | }{
19 | {`metadata('geoip/city/name') == 'Exampleshire'`},
20 | {`(type() == 'A' && name() == 'example.com') || client_ip() == '1.2.3.4'`},
21 | {`name() matches '^abc\\..*\\.example\\.com\\.$'`},
22 | {`type() in ['A', 'AAAA']`},
23 | {`incidr(client_ip(), '192.168.0.0/16')`},
24 | {`incidr(client_ip(), '127.0.0.0/24')`},
25 | }
26 |
27 | for _, test := range tests {
28 | t.Run(test.input, func(t *testing.T) {
29 | _, err := expr.Compile(test.input, expr.Env(env), expr.DisableBuiltin("type"))
30 | assert.NoError(t, err)
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/crowdsec/crowdsec_test.go:
--------------------------------------------------------------------------------
1 | package crowdsec_test
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "testing"
7 |
8 | "github.com/expr-lang/expr/internal/testify/require"
9 |
10 | "github.com/expr-lang/expr"
11 | "github.com/expr-lang/expr/test/crowdsec"
12 | )
13 |
14 | func TestCrowdsec(t *testing.T) {
15 | b, err := os.ReadFile("../../testdata/crowdsec.json")
16 | require.NoError(t, err)
17 |
18 | var examples []string
19 | err = json.Unmarshal(b, &examples)
20 | require.NoError(t, err)
21 |
22 | env := map[string]any{
23 | "evt": &crowdsec.Event{},
24 | }
25 |
26 | var opt = []expr.Option{
27 | expr.Env(env),
28 | }
29 | for _, fn := range crowdsec.CustomFunctions {
30 | opt = append(
31 | opt,
32 | expr.Function(
33 | fn.Name,
34 | func(params ...any) (any, error) {
35 | return nil, nil
36 | },
37 | fn.Func...,
38 | ),
39 | )
40 | }
41 |
42 | for _, line := range examples {
43 | t.Run(line, func(t *testing.T) {
44 | _, err = expr.Compile(line, opt...)
45 | require.NoError(t, err)
46 | })
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/examples/examples_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr"
8 | "github.com/expr-lang/expr/internal/testify/require"
9 | )
10 |
11 | var examples []CodeBlock
12 |
13 | func init() {
14 | b, err := os.ReadFile("../../testdata/examples.md")
15 | if err != nil {
16 | panic(err)
17 | }
18 | examples = extractCodeBlocksWithTitle(string(b))
19 | }
20 |
21 | func TestExamples(t *testing.T) {
22 | for _, code := range examples {
23 | code := code
24 | t.Run(code.Title, func(t *testing.T) {
25 | program, err := expr.Compile(code.Content, expr.Env(nil))
26 | require.NoError(t, err)
27 |
28 | _, err = expr.Run(program, nil)
29 | require.NoError(t, err)
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/examples/markdown.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // CodeBlock holds the optional title and content of a code block.
8 | type CodeBlock struct {
9 | Title string
10 | Content string
11 | }
12 |
13 | func extractCodeBlocksWithTitle(markdown string) []CodeBlock {
14 | var blocks []CodeBlock
15 | var currentBlock []string
16 | var currentTitle string
17 | inBlock := false
18 |
19 | // Split the markdown into lines.
20 | lines := strings.Split(markdown, "\n")
21 | for i, line := range lines {
22 | trimmed := strings.TrimSpace(line)
23 | // Check if the line starts with a code block fence.
24 | if strings.HasPrefix(trimmed, "```") {
25 | // If already inside a code block, this marks its end.
26 | if inBlock {
27 | blocks = append(blocks, CodeBlock{
28 | Title: currentTitle,
29 | Content: strings.Join(currentBlock, "\n"),
30 | })
31 | currentBlock = nil
32 | inBlock = false
33 | currentTitle = ""
34 | } else {
35 | // Not in a block: starting a new code block.
36 | // Look backwards for the closest non-empty line that is not a code fence.
37 | title := ""
38 | for j := i - 1; j >= 0; j-- {
39 | prev := strings.TrimSpace(lines[j])
40 | if prev == "" || strings.HasPrefix(prev, "```") {
41 | continue
42 | }
43 | title = prev
44 | break
45 | }
46 | currentTitle = title
47 | inBlock = true
48 | }
49 | // Skip the fence line.
50 | continue
51 | }
52 |
53 | // If inside a code block, add the line.
54 | if inBlock {
55 | currentBlock = append(currentBlock, line)
56 | }
57 | }
58 | return blocks
59 | }
60 |
--------------------------------------------------------------------------------
/test/fuzz/fuzz_corpus.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | inputfile=./fuzz_corpus.txt
4 | zipname=fuzz_expr_seed_corpus.zip
5 |
6 | if [ ! -f "$inputfile" ]; then
7 | echo "Error: File $inputfile not found!"
8 | exit 1
9 | fi
10 |
11 | lineno=1
12 | while IFS= read -r line; do
13 | echo "$line" >"file_${lineno}.txt"
14 | ((lineno++))
15 | done <"$inputfile"
16 |
17 | zip "$zipname" file_*.txt
18 |
19 | rm file_*.txt
20 |
21 | echo "Done!"
22 |
--------------------------------------------------------------------------------
/test/fuzz/fuzz_env.go:
--------------------------------------------------------------------------------
1 | package fuzz
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/expr-lang/expr"
7 | )
8 |
9 | func NewEnv() map[string]any {
10 | return map[string]any{
11 | "ok": true,
12 | "f64": .5,
13 | "f32": float32(.5),
14 | "i": 1,
15 | "i64": int64(1),
16 | "i32": int32(1),
17 | "array": []int{1, 2, 3, 4, 5},
18 | "list": []Foo{{"bar"}, {"baz"}},
19 | "foo": Foo{"bar"},
20 | "add": func(a, b int) int { return a + b },
21 | "div": func(a, b int) int { return a / b },
22 | "half": func(a float64) float64 { return a / 2 },
23 | "score": func(a int, x ...int) int {
24 | s := a
25 | for _, n := range x {
26 | s += n
27 | }
28 | return s
29 | },
30 | "greet": func(name string) string { return "Hello, " + name },
31 | }
32 | }
33 |
34 | func Func() expr.Option {
35 | return expr.Function("fn", func(params ...any) (any, error) {
36 | return fmt.Sprintf("fn(%v)", params), nil
37 | })
38 | }
39 |
40 | type Foo struct {
41 | Bar string
42 | }
43 |
44 | func (f Foo) String() string {
45 | return "foo"
46 | }
47 |
48 | func (f Foo) Qux(s string) string {
49 | return f.Bar + s
50 | }
51 |
--------------------------------------------------------------------------------
/test/fuzz/fuzz_expr.dict:
--------------------------------------------------------------------------------
1 | "{"
2 | "}"
3 | ","
4 | "["
5 | "]"
6 | "("
7 | ")"
8 | ":"
9 | "'"
10 | "\""
11 | "0.1"
12 | "1.2"
13 | "2.3"
14 | "-3.4"
15 | "-4.5"
16 | "-5.6"
17 | "1e2"
18 | "2e3"
19 | "-3e4"
20 | "-4e5"
21 | "0"
22 | "1"
23 | "2"
24 | "-3"
25 | "-4"
26 | "0x"
27 |
28 | "true"
29 | "false"
30 | "not"
31 | "nil"
32 |
33 | "String"
34 |
35 | "ok"
36 | "f64"
37 | "f32"
38 | "i"
39 | "i64"
40 | "i32"
41 | "array"
42 | "list"
43 | "foo"
44 | "add"
45 | "div"
46 | "half"
47 | "score"
48 | "greet"
49 | "Foo"
50 | "Bar"
51 | "Qux"
52 |
53 | "all"
54 | "none"
55 | "any"
56 | "one"
57 | "filter"
58 | "map"
59 | "count"
60 | "find"
61 | "findIndex"
62 | "findLast"
63 | "findLastIndex"
64 | "len"
65 | "type"
66 | "abs"
67 | "int"
68 | "float"
69 | "string"
70 | "trim"
71 | "trimPrefix"
72 | "trimSuffix"
73 | "upper"
74 | "lower"
75 | "split"
76 | "splitAfter"
77 | "replace"
78 | "repeat"
79 | "join"
80 | "indexOf"
81 | "lastIndexOf"
82 | "hasPrefix"
83 | "hasSuffix"
84 | "max"
85 | "min"
86 | "toJSON"
87 | "fromJSON"
88 | "toBase64"
89 | "fromBase64"
90 | "now"
91 | "duration"
92 | "date"
93 | "first"
94 | "last"
95 | "get"
96 | "keys"
97 | "values"
98 | "sort"
99 | "sortBy"
100 |
--------------------------------------------------------------------------------
/test/fuzz/fuzz_expr_seed_corpus.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/expr-lang/expr/5fbfe7211cd667c622a69bf4609e55f2eade7f63/test/fuzz/fuzz_expr_seed_corpus.zip
--------------------------------------------------------------------------------
/test/fuzz/fuzz_test.go:
--------------------------------------------------------------------------------
1 | package fuzz
2 |
3 | import (
4 | _ "embed"
5 | "regexp"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/expr-lang/expr"
10 | )
11 |
12 | //go:embed fuzz_corpus.txt
13 | var fuzzCorpus string
14 |
15 | func FuzzExpr(f *testing.F) {
16 | corpus := strings.Split(strings.TrimSpace(fuzzCorpus), "\n")
17 | for _, s := range corpus {
18 | f.Add(s)
19 | }
20 |
21 | skip := []*regexp.Regexp{
22 | regexp.MustCompile(`cannot fetch .* from .*`),
23 | regexp.MustCompile(`cannot get .* from .*`),
24 | regexp.MustCompile(`cannot slice`),
25 | regexp.MustCompile(`slice index out of range`),
26 | regexp.MustCompile(`error parsing regexp`),
27 | regexp.MustCompile(`integer divide by zero`),
28 | regexp.MustCompile(`interface conversion`),
29 | regexp.MustCompile(`invalid argument for .*`),
30 | regexp.MustCompile(`invalid character`),
31 | regexp.MustCompile(`invalid operation`),
32 | regexp.MustCompile(`invalid duration`),
33 | regexp.MustCompile(`time: missing unit in duration`),
34 | regexp.MustCompile(`time: unknown unit .* in duration`),
35 | regexp.MustCompile(`unknown time zone`),
36 | regexp.MustCompile(`json: unsupported value`),
37 | regexp.MustCompile(`unexpected end of JSON input`),
38 | regexp.MustCompile(`memory budget exceeded`),
39 | regexp.MustCompile(`using interface \{} as type .*`),
40 | regexp.MustCompile(`reflect.Value.MapIndex: value of type .* is not assignable to type .*`),
41 | regexp.MustCompile(`reflect: Call using .* as type .*`),
42 | regexp.MustCompile(`reflect: Call with too few input arguments`),
43 | regexp.MustCompile(`reflect: call of reflect.Value.Call on .* Value`),
44 | regexp.MustCompile(`reflect: call of reflect.Value.Index on map Value`),
45 | regexp.MustCompile(`reflect: call of reflect.Value.Len on .* Value`),
46 | regexp.MustCompile(`reflect: string index out of range`),
47 | regexp.MustCompile(`strings: negative Repeat count`),
48 | regexp.MustCompile(`strings: illegal bytes to escape`),
49 | regexp.MustCompile(`operator "in" not defined on int`),
50 | regexp.MustCompile(`invalid date .*`),
51 | regexp.MustCompile(`cannot parse .* as .*`),
52 | regexp.MustCompile(`operator "in" not defined on .*`),
53 | regexp.MustCompile(`cannot sum .*`),
54 | regexp.MustCompile(`index out of range: .* \(array length is .*\)`),
55 | regexp.MustCompile(`cannot use as argument \(type .*\) to call .*`),
56 | regexp.MustCompile(`illegal base64 data at input byte .*`),
57 | }
58 |
59 | env := NewEnv()
60 | fn := Func()
61 |
62 | f.Fuzz(func(t *testing.T, code string) {
63 | if len(code) > 1000 {
64 | t.Skip("too long code")
65 | }
66 |
67 | program, err := expr.Compile(code, expr.Env(env), fn)
68 | if err != nil {
69 | t.Skipf("compile error: %s", err)
70 | }
71 |
72 | _, err = expr.Run(program, env)
73 | if err != nil {
74 | for _, r := range skip {
75 | if r.MatchString(err.Error()) {
76 | t.Skipf("skip error: %s", err)
77 | return
78 | }
79 | }
80 | t.Errorf("%s", err)
81 | }
82 | })
83 | }
84 |
--------------------------------------------------------------------------------
/test/gen/env.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var Env = map[string]any{
4 | "ok": true,
5 | "i": 1,
6 | "str": "str",
7 | "f64": .5,
8 | "array": []int{1, 2, 3, 4, 5},
9 | "foo": Foo{"foo"},
10 | "list": []Foo{{"bar"}, {"baz"}},
11 | "add": func(a, b int) int { return a + b },
12 | "greet": func(name string) string { return "Hello, " + name },
13 | }
14 |
15 | type Foo struct {
16 | Bar string
17 | }
18 |
19 | func (f Foo) String() string {
20 | return "foo"
21 | }
22 |
--------------------------------------------------------------------------------
/test/gen/gen_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "flag"
6 | "os"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/expr-lang/expr"
11 | "github.com/expr-lang/expr/internal/testify/require"
12 | )
13 |
14 | var updateFlag = flag.Bool("update", false, "Drop failing lines from examples.txt")
15 |
16 | func TestGenerated(t *testing.T) {
17 | flag.Parse()
18 |
19 | b, err := os.ReadFile("../../testdata/generated.txt")
20 | require.NoError(t, err)
21 |
22 | examples := strings.TrimSpace(string(b))
23 | var validLines []string
24 |
25 | for _, line := range strings.Split(examples, "\n") {
26 | line := line
27 | t.Run(line, func(t *testing.T) {
28 | program, err := expr.Compile(line, expr.Env(Env))
29 | if err != nil {
30 | if !*updateFlag {
31 | t.Errorf("Compilation failed: %v", err)
32 | }
33 | return
34 | }
35 |
36 | _, err = expr.Run(program, Env)
37 | if err != nil {
38 | if !*updateFlag {
39 | t.Errorf("Execution failed: %v", err)
40 | }
41 | return
42 | }
43 |
44 | validLines = append(validLines, line)
45 | })
46 | }
47 |
48 | if *updateFlag {
49 | file, err := os.Create("../../testdata/examples.txt")
50 | if err != nil {
51 | t.Fatalf("Failed to update examples.txt: %v", err)
52 | }
53 | defer func(file *os.File) {
54 | _ = file.Close()
55 | }(file)
56 |
57 | writer := bufio.NewWriter(file)
58 | for _, line := range validLines {
59 | _, err := writer.WriteString(line + "\n")
60 | if err != nil {
61 | t.Fatalf("Failed to write to examples.txt: %v", err)
62 | }
63 | }
64 | _ = writer.Flush()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/test/gen/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "math/rand"
5 | )
6 |
7 | func maybe() bool {
8 | return rand.Intn(2) == 0
9 | }
10 |
11 | type list[T any] []element[T]
12 |
13 | type element[T any] struct {
14 | value T
15 | weight int
16 | }
17 |
18 | func oneOf[T any](cases []element[T]) T {
19 | total := 0
20 | for _, c := range cases {
21 | total += c.weight
22 | }
23 | r := rand.Intn(total)
24 | for _, c := range cases {
25 | if r < c.weight {
26 | return c.value
27 | }
28 | r -= c.weight
29 | }
30 | return cases[0].value
31 | }
32 |
33 | func random[T any](array []T) T {
34 | return array[rand.Intn(len(array))]
35 | }
36 |
--------------------------------------------------------------------------------
/test/interface/interface_method_test.go:
--------------------------------------------------------------------------------
1 | package interface_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | )
11 |
12 | type Bar interface {
13 | Bar() int
14 | }
15 |
16 | type FooImpl struct{}
17 |
18 | func (f FooImpl) Foo() Bar {
19 | return BarImpl{}
20 | }
21 |
22 | type BarImpl struct{}
23 |
24 | // Aba is a special method that is not part of the Bar interface,
25 | // but is used to test that the correct method is called. "Aba" name
26 | // is chosen to be before "Bar" in the alphabet.
27 | func (b BarImpl) Aba() bool {
28 | return true
29 | }
30 |
31 | func (b BarImpl) Bar() int {
32 | return 42
33 | }
34 |
35 | func TestInterfaceMethod(t *testing.T) {
36 | require.True(t, BarImpl{}.Aba())
37 | require.True(t, BarImpl{}.Bar() == 42)
38 |
39 | env := map[string]any{
40 | "var": FooImpl{},
41 | }
42 | p, err := expr.Compile(`var.Foo().Bar()`, expr.Env(env))
43 | assert.NoError(t, err)
44 |
45 | out, err := expr.Run(p, env)
46 | assert.NoError(t, err)
47 | assert.Equal(t, 42, out)
48 | }
49 |
--------------------------------------------------------------------------------
/test/interface/interface_test.go:
--------------------------------------------------------------------------------
1 | package interface_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/assert"
8 | )
9 |
10 | type StoreInterface interface {
11 | Get(string) int
12 | }
13 |
14 | type StoreImpt struct{}
15 |
16 | func (f StoreImpt) Get(s string) int {
17 | return 42
18 | }
19 |
20 | func (f StoreImpt) Set(s string, i int) bool {
21 | return true
22 | }
23 |
24 | type Env struct {
25 | Store StoreInterface `expr:"store"`
26 | }
27 |
28 | func TestInterfaceHide(t *testing.T) {
29 | var env Env
30 | p, err := expr.Compile(`store.Get("foo")`, expr.Env(env))
31 | assert.NoError(t, err)
32 |
33 | out, err := expr.Run(p, Env{Store: StoreImpt{}})
34 | assert.NoError(t, err)
35 | assert.Equal(t, 42, out)
36 |
37 | _, err = expr.Compile(`store.Set("foo", 100)`, expr.Env(env))
38 | assert.Error(t, err)
39 | assert.Contains(t, err.Error(), "type interface_test.StoreInterface has no method Set")
40 | }
41 |
--------------------------------------------------------------------------------
/test/issues/461/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | func TestIssue461(t *testing.T) {
11 | type EnvStr string
12 | type EnvField struct {
13 | S EnvStr
14 | Str string
15 | }
16 | type Env struct {
17 | S EnvStr
18 | Str string
19 | EnvField EnvField
20 | }
21 | var tests = []struct {
22 | input string
23 | env Env
24 | want bool
25 | err string
26 | }{
27 | {
28 | input: "Str == S",
29 | env: Env{S: "string", Str: "string"},
30 | err: "invalid operation: == (mismatched types string and issue_test.EnvStr)",
31 | },
32 | {
33 | input: "Str == Str",
34 | env: Env{Str: "string"},
35 | want: true,
36 | },
37 | {
38 | input: "S == S",
39 | env: Env{Str: "string"},
40 | want: true,
41 | },
42 | {
43 | input: `Str == "string"`,
44 | env: Env{Str: "string"},
45 | want: true,
46 | },
47 | {
48 | input: `S == "string"`,
49 | env: Env{Str: "string"},
50 | err: "invalid operation: == (mismatched types issue_test.EnvStr and string)",
51 | },
52 | {
53 | input: "EnvField.Str == EnvField.S",
54 | env: Env{EnvField: EnvField{S: "string", Str: "string"}},
55 | err: "invalid operation: == (mismatched types string and issue_test.EnvStr)",
56 | },
57 | {
58 | input: "EnvField.Str == EnvField.Str",
59 | env: Env{EnvField: EnvField{Str: "string"}},
60 | want: true,
61 | },
62 | {
63 | input: "EnvField.S == EnvField.S",
64 | env: Env{EnvField: EnvField{Str: "string"}},
65 | want: true,
66 | },
67 | {
68 | input: `EnvField.Str == "string"`,
69 | env: Env{EnvField: EnvField{Str: "string"}},
70 | want: true,
71 | },
72 | {
73 | input: `EnvField.S == "string"`,
74 | env: Env{EnvField: EnvField{Str: "string"}},
75 | err: "invalid operation: == (mismatched types issue_test.EnvStr and string)",
76 | },
77 | }
78 |
79 | for _, tt := range tests {
80 | t.Run(tt.input, func(t *testing.T) {
81 | program, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AsBool())
82 |
83 | if tt.err != "" {
84 | require.Error(t, err)
85 | require.Contains(t, err.Error(), tt.err)
86 | } else {
87 | out, err := expr.Run(program, tt.env)
88 | require.NoError(t, err)
89 | require.Equal(t, tt.want, out)
90 | }
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/test/issues/688/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | type Foo interface {
11 | Add(a int, b *int) int
12 | }
13 |
14 | type FooImpl struct {
15 | }
16 |
17 | func (*FooImpl) Add(a int, b *int) int {
18 | return 0
19 | }
20 |
21 | type Env struct {
22 | Foo Foo `expr:"foo"`
23 | }
24 |
25 | func (Env) Any(x any) any {
26 | return x
27 | }
28 |
29 | func TestNoInterfaceMethodWithNil(t *testing.T) {
30 | program, err := expr.Compile(`foo.Add(1, nil)`)
31 | require.NoError(t, err)
32 |
33 | _, err = expr.Run(program, Env{Foo: &FooImpl{}})
34 | require.NoError(t, err)
35 | }
36 |
37 | func TestNoInterfaceMethodWithNil_with_env(t *testing.T) {
38 | program, err := expr.Compile(`foo.Add(1, nil)`, expr.Env(Env{}))
39 | require.NoError(t, err)
40 |
41 | _, err = expr.Run(program, Env{Foo: &FooImpl{}})
42 | require.NoError(t, err)
43 | }
44 |
45 | func TestNoInterfaceMethodWithNil_with_any(t *testing.T) {
46 | program, err := expr.Compile(`Any(nil)`, expr.Env(Env{}))
47 | require.NoError(t, err)
48 |
49 | out, err := expr.Run(program, Env{Foo: &FooImpl{}})
50 | require.NoError(t, err)
51 | require.Equal(t, nil, out)
52 | }
53 |
--------------------------------------------------------------------------------
/test/issues/723/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | func TestIssue723(t *testing.T) {
11 | emptyMap := make(map[string]string)
12 | env := map[string]interface{}{
13 | "empty_map": emptyMap,
14 | }
15 |
16 | code := `get(empty_map, "non_existing_key")`
17 |
18 | program, err := expr.Compile(code, expr.Env(env))
19 | require.NoError(t, err)
20 |
21 | output, err := expr.Run(program, env)
22 | require.NoError(t, err)
23 | require.Equal(t, nil, output)
24 | }
25 |
--------------------------------------------------------------------------------
/test/issues/730/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | type ModeEnum int
11 |
12 | const (
13 | ModeEnumA ModeEnum = 1
14 | )
15 |
16 | type Env struct {
17 | Mode *ModeEnum
18 | }
19 |
20 | func TestIssue730(t *testing.T) {
21 | code := `int(Mode) == 1`
22 |
23 | tmp := ModeEnumA
24 |
25 | env := map[string]any{
26 | "Mode": &tmp,
27 | }
28 |
29 | program, err := expr.Compile(code, expr.Env(env))
30 | require.NoError(t, err)
31 |
32 | output, err := expr.Run(program, env)
33 | require.NoError(t, err)
34 | require.True(t, output.(bool))
35 | }
36 |
37 | func TestIssue730_warn_about_different_types(t *testing.T) {
38 | code := `Mode == 1`
39 |
40 | _, err := expr.Compile(code, expr.Env(Env{}))
41 | require.Error(t, err)
42 | require.Contains(t, err.Error(), "invalid operation: == (mismatched types issue_test.ModeEnum and int)")
43 | }
44 |
45 | func TestIssue730_eval(t *testing.T) {
46 | code := `Mode == 1`
47 |
48 | tmp := ModeEnumA
49 |
50 | env := map[string]any{
51 | "Mode": &tmp,
52 | }
53 |
54 | // Golang also does not allow this:
55 | // _ = ModeEnumA == int(1) // will not compile
56 |
57 | out, err := expr.Eval(code, env)
58 | require.NoError(t, err)
59 | require.False(t, out.(bool))
60 | }
61 |
--------------------------------------------------------------------------------
/test/issues/739/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | func TestIssue739(t *testing.T) {
11 | jsonString := `{"Num": 1}`
12 | env := map[string]any{
13 | "aJSONString": &jsonString,
14 | }
15 |
16 | result, err := expr.Eval("fromJSON(aJSONString)", env)
17 | require.NoError(t, err)
18 | require.Contains(t, result, "Num")
19 | }
20 |
--------------------------------------------------------------------------------
/test/issues/756/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr"
8 | "github.com/expr-lang/expr/internal/testify/require"
9 | )
10 |
11 | type X struct{}
12 |
13 | func (x *X) HelloCtx(ctx context.Context, text string) error {
14 | return nil
15 | }
16 |
17 | func TestIssue756(t *testing.T) {
18 | env := map[string]any{
19 | "_goctx_": context.TODO(),
20 | "_g_": map[string]*X{
21 | "rpc": {},
22 | },
23 | "text": "еуче",
24 | }
25 | exprStr := `let v = _g_.rpc.HelloCtx(text); v`
26 | program, err := expr.Compile(exprStr, expr.Env(env), expr.WithContext("_goctx_"))
27 | require.NoError(t, err)
28 |
29 | _, err = expr.Run(program, env)
30 | require.NoError(t, err)
31 | }
32 |
--------------------------------------------------------------------------------
/test/issues/785/issue_test.go:
--------------------------------------------------------------------------------
1 | package issue_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr"
7 | "github.com/expr-lang/expr/internal/testify/require"
8 | )
9 |
10 | func TestIssue785(t *testing.T) {
11 | emptyMap := map[string]any{}
12 |
13 | env := map[string]interface{}{
14 | "empty_map": emptyMap,
15 | }
16 |
17 | {
18 | code := `get(empty_map, "non_existing_key") | get("some_key") | get("another_key") | get("yet_another_key") | get("last_key")`
19 |
20 | program, err := expr.Compile(code, expr.Env(env))
21 | require.NoError(t, err)
22 |
23 | output, err := expr.Run(program, env)
24 | require.NoError(t, err)
25 | require.Equal(t, nil, output)
26 | }
27 |
28 | {
29 | code := `{} | get("non_existing_key") | get("some_key") | get("another_key") | get("yet_another_key") | get("last_key")`
30 |
31 | program, err := expr.Compile(code, expr.Env(env))
32 | require.NoError(t, err)
33 |
34 | output, err := expr.Run(program, env)
35 | require.NoError(t, err)
36 | require.Equal(t, nil, output)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/operator/issues584/issues584_test.go:
--------------------------------------------------------------------------------
1 | package issues584_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 |
8 | "github.com/expr-lang/expr"
9 | )
10 |
11 | type Env struct{}
12 |
13 | type Program struct {
14 | }
15 |
16 | func (p *Program) Foo() Value {
17 | return func(e *Env) float64 {
18 | return 5
19 | }
20 | }
21 |
22 | func (p *Program) Bar() Value {
23 | return func(e *Env) float64 {
24 | return 100
25 | }
26 | }
27 |
28 | func (p *Program) AndCondition(a, b Condition) Conditions {
29 | return Conditions{a, b}
30 | }
31 |
32 | func (p *Program) AndConditions(a Conditions, b Condition) Conditions {
33 | return append(a, b)
34 | }
35 |
36 | func (p *Program) ValueGreaterThan_float(v Value, i float64) Condition {
37 | return func(e *Env) bool {
38 | realized := v(e)
39 | return realized > i
40 | }
41 | }
42 |
43 | func (p *Program) ValueLessThan_float(v Value, i float64) Condition {
44 | return func(e *Env) bool {
45 | realized := v(e)
46 | return realized < i
47 | }
48 | }
49 |
50 | type Condition func(e *Env) bool
51 | type Conditions []Condition
52 |
53 | type Value func(e *Env) float64
54 |
55 | func TestIssue584(t *testing.T) {
56 | code := `Foo() > 1.5 and Bar() < 200.0`
57 |
58 | p := &Program{}
59 |
60 | opt := []expr.Option{
61 | expr.Env(p),
62 | expr.Operator("and", "AndCondition", "AndConditions"),
63 | expr.Operator(">", "ValueGreaterThan_float"),
64 | expr.Operator("<", "ValueLessThan_float"),
65 | }
66 |
67 | program, err := expr.Compile(code, opt...)
68 | assert.Nil(t, err)
69 |
70 | state, err := expr.Run(program, p)
71 | assert.Nil(t, err)
72 | assert.NotNil(t, state)
73 | }
74 |
--------------------------------------------------------------------------------
/test/patch/change_ident_test.go:
--------------------------------------------------------------------------------
1 | package patch_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr"
9 | "github.com/expr-lang/expr/ast"
10 | "github.com/expr-lang/expr/vm"
11 | "github.com/expr-lang/expr/vm/runtime"
12 | )
13 |
14 | func TestPatch_change_ident(t *testing.T) {
15 | program, err := expr.Compile(
16 | `foo`,
17 | expr.Env(Env{}),
18 | expr.Patch(changeIdent{}),
19 | )
20 | require.NoError(t, err)
21 |
22 | expected := &vm.Program{
23 | Bytecode: []vm.Opcode{
24 | vm.OpLoadField,
25 | },
26 | Arguments: []int{
27 | 0,
28 | },
29 | Constants: []any{
30 | &runtime.Field{
31 | Path: []string{"bar"},
32 | Index: []int{1},
33 | },
34 | },
35 | }
36 |
37 | require.Equal(t, expected.Disassemble(), program.Disassemble())
38 | }
39 |
40 | type Env struct {
41 | Foo int `expr:"foo"`
42 | Bar int `expr:"bar"`
43 | }
44 |
45 | type changeIdent struct{}
46 |
47 | func (changeIdent) Visit(node *ast.Node) {
48 | id, ok := (*node).(*ast.IdentifierNode)
49 | if !ok {
50 | return
51 | }
52 | if id.Value == "foo" {
53 | // A correct way to patch the node:
54 | //
55 | // newNode := &ast.IdentifierNode{Value: "bar"}
56 | // ast.Patch(node, newNode)
57 | //
58 | // But we can do it in a wrong way:
59 | id.Value = "bar"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/patch/patch_count_test.go:
--------------------------------------------------------------------------------
1 | package patch_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr"
9 | "github.com/expr-lang/expr/ast"
10 | "github.com/expr-lang/expr/test/mock"
11 | )
12 |
13 | // This patcher tracks how many nodes it patches which can
14 | // be used to verify if it was run too many times or not at all
15 | type countingPatcher struct {
16 | PatchCount int
17 | }
18 |
19 | func (c *countingPatcher) Visit(node *ast.Node) {
20 | switch (*node).(type) {
21 | case *ast.IntegerNode:
22 | c.PatchCount++
23 | }
24 | }
25 |
26 | // Test over a simple expression
27 | func TestPatch_Count(t *testing.T) {
28 | patcher := countingPatcher{}
29 |
30 | _, err := expr.Compile(
31 | `5 + 5`,
32 | expr.Env(mock.Env{}),
33 | expr.Patch(&patcher),
34 | )
35 | require.NoError(t, err)
36 |
37 | require.Equal(t, 2, patcher.PatchCount, "Patcher run an unexpected number of times during compile")
38 | }
39 |
40 | // Test with operator overloading
41 | func TestPatchOperator_Count(t *testing.T) {
42 | patcher := countingPatcher{}
43 |
44 | _, err := expr.Compile(
45 | `5 + 5`,
46 | expr.Env(mock.Env{}),
47 | expr.Patch(&patcher),
48 | expr.Operator("+", "_intAdd"),
49 | expr.Function(
50 | "_intAdd",
51 | func(params ...any) (any, error) {
52 | return params[0].(int) + params[1].(int), nil
53 | },
54 | new(func(int, int) int),
55 | ),
56 | )
57 |
58 | require.NoError(t, err)
59 |
60 | require.Equal(t, 2, patcher.PatchCount, "Patcher run an unexpected number of times during compile")
61 | }
62 |
--------------------------------------------------------------------------------
/test/patch/patch_test.go:
--------------------------------------------------------------------------------
1 | package patch_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 |
8 | "github.com/expr-lang/expr"
9 | "github.com/expr-lang/expr/ast"
10 | "github.com/expr-lang/expr/test/mock"
11 | )
12 |
13 | type lengthPatcher struct{}
14 |
15 | func (p *lengthPatcher) Visit(node *ast.Node) {
16 | switch n := (*node).(type) {
17 | case *ast.MemberNode:
18 | if prop, ok := n.Property.(*ast.StringNode); ok && prop.Value == "length" {
19 | ast.Patch(node, &ast.BuiltinNode{
20 | Name: "len",
21 | Arguments: []ast.Node{n.Node},
22 | })
23 | }
24 | }
25 | }
26 |
27 | func TestPatch_length(t *testing.T) {
28 | program, err := expr.Compile(
29 | `String.length == 5`,
30 | expr.Env(mock.Env{}),
31 | expr.Patch(&lengthPatcher{}),
32 | )
33 | require.NoError(t, err)
34 |
35 | env := mock.Env{String: "hello"}
36 | output, err := expr.Run(program, env)
37 | require.NoError(t, err)
38 | require.Equal(t, true, output)
39 | }
40 |
--------------------------------------------------------------------------------
/test/patch/set_type/set_type_test.go:
--------------------------------------------------------------------------------
1 | package set_type_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | "github.com/expr-lang/expr/ast"
11 | )
12 |
13 | func TestPatch_SetType(t *testing.T) {
14 | _, err := expr.Compile(
15 | `Value + "string"`,
16 | expr.Env(Env{}),
17 | expr.Function(
18 | "getValue",
19 | func(params ...any) (any, error) {
20 | return params[0].(Value).Int, nil
21 | },
22 | // We can set function type right here,
23 | // but we want to check what SetType in
24 | // getValuePatcher will take an effect.
25 | ),
26 | expr.Patch(getValuePatcher{}),
27 | )
28 | require.Error(t, err)
29 | }
30 |
31 | type Value struct {
32 | Int int
33 | }
34 |
35 | type Env struct {
36 | Value Value
37 | }
38 |
39 | var valueType = reflect.TypeOf((*Value)(nil)).Elem()
40 |
41 | type getValuePatcher struct{}
42 |
43 | func (getValuePatcher) Visit(node *ast.Node) {
44 | id, ok := (*node).(*ast.IdentifierNode)
45 | if !ok {
46 | return
47 | }
48 | if id.Type() == valueType {
49 | newNode := &ast.CallNode{
50 | Callee: &ast.IdentifierNode{Value: "getValue"},
51 | Arguments: []ast.Node{id},
52 | }
53 | newNode.SetType(reflect.TypeOf(0))
54 | ast.Patch(node, newNode)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/pipes/pipes_test.go:
--------------------------------------------------------------------------------
1 | package pipes_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/internal/testify/require"
8 |
9 | "github.com/expr-lang/expr"
10 | )
11 |
12 | func TestPipes(t *testing.T) {
13 | env := map[string]any{
14 | "sprintf": fmt.Sprintf,
15 | }
16 |
17 | tests := []struct {
18 | input string
19 | want any
20 | }{
21 | {
22 | `-1 | abs()`,
23 | 1,
24 | },
25 | {
26 | `"%s bar %d" | sprintf("foo", -42 | abs())`,
27 | "foo bar 42",
28 | },
29 | {
30 | `[] | first() ?? "foo"`,
31 | "foo",
32 | },
33 | {
34 | `"a" | upper() + "B" | lower()`,
35 | "ab",
36 | },
37 | }
38 |
39 | for _, test := range tests {
40 | t.Run(test.input, func(t *testing.T) {
41 | program, err := expr.Compile(test.input, expr.Env(env))
42 | require.NoError(t, err)
43 |
44 | out, err := expr.Run(program, env)
45 | require.NoError(t, err)
46 | require.Equal(t, test.want, out)
47 | })
48 | }
49 | }
50 |
51 | func TestPipes_map_filter(t *testing.T) {
52 | program, err := expr.Compile(`1..9 | map(# + 1) | filter(# % 2 == 0)`)
53 | require.NoError(t, err)
54 |
55 | out, err := expr.Run(program, nil)
56 | require.NoError(t, err)
57 | require.Equal(t, []any{2, 4, 6, 8, 10}, out)
58 | }
59 |
--------------------------------------------------------------------------------
/test/playground/data.go:
--------------------------------------------------------------------------------
1 | package playground
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func ExampleData() Blog {
9 | profileJohn := UserProfile{
10 | Birthday: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
11 | Biography: "A passionate writer about Go.",
12 | Website: "https://johndoe.com",
13 | }
14 |
15 | profileJane := UserProfile{
16 | Birthday: time.Date(1985, 2, 15, 0, 0, 0, 0, time.UTC),
17 | Biography: "A web developer and writer.",
18 | Website: "https://jane.com",
19 | }
20 |
21 | authorJohn := Author{
22 | ID: 1,
23 | FirstName: "John",
24 | LastName: "Doe",
25 | Email: "john.doe@example.com",
26 | Profile: profileJohn,
27 | }
28 |
29 | authorJane := Author{
30 | ID: 2,
31 | FirstName: "Jane",
32 | LastName: "Smith",
33 | Email: "jane.smith@example.com",
34 | Profile: profileJane,
35 | }
36 |
37 | posts := []Post{
38 | {
39 | ID: 1,
40 | Title: "Understanding Golang",
41 | Content: "Go is an open-source programming language...",
42 | PublishDate: time.Now().AddDate(0, -1, 0),
43 | Author: authorJohn,
44 | Comments: generateComments(3),
45 | Tags: []string{"Go", "Programming"},
46 | Likes: 100,
47 | },
48 | {
49 | ID: 2,
50 | Title: "Exploring Python",
51 | Content: "Python is versatile...",
52 | PublishDate: time.Now().AddDate(0, -2, 0),
53 | Author: authorJane,
54 | Comments: generateComments(4),
55 | Tags: []string{"Python", "Development"},
56 | Likes: 150,
57 | },
58 | {
59 | ID: 3,
60 | Title: "Web Development Basics",
61 | Content: "The world of web development...",
62 | PublishDate: time.Now().AddDate(0, -3, 0),
63 | Author: authorJane,
64 | Comments: generateComments(5),
65 | Tags: []string{"Web", "HTML", "CSS"},
66 | Likes: 125,
67 | },
68 | {
69 | ID: 4,
70 | Title: "Machine Learning in a Nutshell",
71 | Content: "ML is revolutionizing industries...",
72 | PublishDate: time.Now().AddDate(0, -5, 0),
73 | Author: authorJohn,
74 | Comments: generateComments(6),
75 | Tags: []string{"ML", "AI"},
76 | Likes: 200,
77 | },
78 | {
79 | ID: 5,
80 | Title: "JavaScript: The Good Parts",
81 | Content: "JavaScript powers the web...",
82 | PublishDate: time.Now().AddDate(0, -4, 0),
83 | Author: authorJane,
84 | Comments: generateComments(3),
85 | Tags: []string{"JavaScript", "Web"},
86 | Likes: 170,
87 | },
88 | }
89 |
90 | blog := Blog{
91 | Posts: make([]Post, len(posts)),
92 | Authors: map[int]Author{authorJohn.ID: authorJohn, authorJane.ID: authorJane},
93 | TotalViews: 10000,
94 | TotalPosts: len(posts),
95 | TotalLikes: 0,
96 | }
97 |
98 | for i, post := range posts {
99 | blog.Posts[i] = post
100 | blog.TotalLikes += post.Likes
101 | }
102 |
103 | return blog
104 | }
105 |
106 | func generateComments(count int) []Comment {
107 | var comments []Comment
108 | for i := 1; i <= count; i++ {
109 | comment := Comment{
110 | ID: i,
111 | AuthorName: fmt.Sprintf("Commenter %d", i),
112 | Content: fmt.Sprintf("This is comment %d!", i),
113 | CommentDate: time.Now().AddDate(0, 0, -i),
114 | Upvotes: i * 5,
115 | }
116 | comments = append(comments, comment)
117 | }
118 | return comments
119 | }
120 |
--------------------------------------------------------------------------------
/test/playground/env.go:
--------------------------------------------------------------------------------
1 | package playground
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type UserProfile struct {
8 | Birthday time.Time
9 | Biography string
10 | Website string
11 | }
12 |
13 | func (u UserProfile) Age() int {
14 | return time.Now().Year() - u.Birthday.Year()
15 | }
16 |
17 | type Author struct {
18 | ID int
19 | FirstName string
20 | LastName string
21 | Email string
22 | Profile UserProfile
23 | }
24 |
25 | func (a Author) FullName() string {
26 | return a.FirstName + " " + a.LastName
27 | }
28 |
29 | func (a Author) IsAdmin() bool {
30 | return a.ID == 1
31 | }
32 |
33 | type Post struct {
34 | ID int
35 | Title string
36 | Content string
37 | PublishDate time.Time
38 | Author Author
39 | Comments []Comment
40 | Tags []string
41 | Likes int
42 | }
43 |
44 | func (p Post) Published() bool {
45 | return !p.PublishDate.IsZero()
46 | }
47 |
48 | func (p Post) After(date time.Time) bool {
49 | return p.PublishDate.After(date)
50 | }
51 |
52 | func (p Post) Before(date time.Time) bool {
53 | return p.PublishDate.Before(date)
54 | }
55 |
56 | func (p Post) Compare(other Post) int {
57 | if p.PublishDate.Before(other.PublishDate) {
58 | return -1
59 | } else if p.PublishDate.After(other.PublishDate) {
60 | return 1
61 | }
62 | return 0
63 | }
64 |
65 | func (p Post) Equal(other Post) bool {
66 | return p.Compare(other) == 0
67 | }
68 |
69 | func (p Post) IsZero() bool {
70 | return p.ID == 0 && p.Title == "" && p.Content == "" && p.PublishDate.IsZero()
71 | }
72 |
73 | type Comment struct {
74 | ID int
75 | AuthorName string
76 | Content string
77 | CommentDate time.Time
78 | Upvotes int
79 | }
80 |
81 | func (c Comment) Upvoted() bool {
82 | return c.Upvotes > 0
83 | }
84 |
85 | func (c Comment) AuthorEmail() string {
86 | return c.AuthorName + "@example.com"
87 | }
88 |
89 | type Blog struct {
90 | Posts []Post
91 | Authors map[int]Author
92 | TotalViews int
93 | TotalPosts int
94 | TotalLikes int
95 | }
96 |
97 | func (b Blog) RecentPosts() []Post {
98 | var posts []Post
99 | for _, post := range b.Posts {
100 | if post.Published() {
101 | posts = append(posts, post)
102 | }
103 | }
104 | return posts
105 | }
106 |
107 | func (b Blog) PopularPosts() []Post {
108 | var posts []Post
109 | for _, post := range b.Posts {
110 | if post.Likes > 150 {
111 | posts = append(posts, post)
112 | }
113 | }
114 | return posts
115 | }
116 |
117 | func (b Blog) TotalUpvotes() int {
118 | var upvotes int
119 | for _, post := range b.Posts {
120 | for _, comment := range post.Comments {
121 | upvotes += comment.Upvotes
122 | }
123 | }
124 | return upvotes
125 | }
126 |
127 | func (b Blog) TotalComments() int {
128 | var comments int
129 | for _, post := range b.Posts {
130 | comments += len(post.Comments)
131 | }
132 | return comments
133 | }
134 |
135 | func (Blog) Add(a, b float64) float64 {
136 | return a + b
137 | }
138 |
139 | func (Blog) Sub(a, b float64) float64 {
140 | return a - b
141 | }
142 |
143 | func (Blog) Title(post Post) string {
144 | return post.Title
145 | }
146 |
147 | func (Blog) HasTag(post Post, tag string) bool {
148 | for _, t := range post.Tags {
149 | if t == tag {
150 | return true
151 | }
152 | }
153 | return false
154 | }
155 |
156 | func (Blog) IsAdmin(author Author) bool {
157 | return author.IsAdmin()
158 | }
159 |
160 | func (Blog) IsZero(post Post) bool {
161 | return post.IsZero()
162 | }
163 |
164 | func (Blog) WithID(posts []Post, id int) Post {
165 | for _, post := range posts {
166 | if post.ID == id {
167 | return post
168 | }
169 | }
170 | return Post{}
171 | }
172 |
--------------------------------------------------------------------------------
/testdata/crash.txt:
--------------------------------------------------------------------------------
1 | '
--------------------------------------------------------------------------------
/testdata/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | Character Frequency Grouping
4 |
5 | ```
6 | let char = "0";
7 | 1..100000
8 | | map(string(#))
9 | | groupBy(split(#, char) | len() - 1)
10 | | toPairs()
11 | | map({{
12 | count: #[0],
13 | len: len(#[1]),
14 | examples: [first(#[1]), last(#[1])],
15 | }})
16 | | sortBy(.len, 'desc')
17 | ```
18 |
19 | Log Filtering and Aggregation
20 |
21 | ```
22 | let logs = [
23 | {timestamp: date("2023-08-14 08:30:00"), message: "User logged in", level: "info"},
24 | {timestamp: date("2023-08-14 09:00:00"), message: "Error processing payment", level: "error"},
25 | {timestamp: date("2023-08-14 10:15:00"), message: "User logged out", level: "info"},
26 | {timestamp: date("2023-08-14 11:00:00"), message: "Error connecting to database", level: "error"}
27 | ];
28 |
29 | logs
30 | | filter(.level == "error")
31 | | map({{
32 | time: string(.timestamp),
33 | detail: .message
34 | }})
35 | | sortBy(.time)
36 | ```
37 |
38 | Financial Data Analysis and Summary
39 |
40 | ```
41 | let accounts = [
42 | {name: "Alice", balance: 1234.56, transactions: [100, -50, 200]},
43 | {name: "Bob", balance: 2345.67, transactions: [-200, 300, -150]},
44 | {name: "Charlie", balance: 3456.78, transactions: [400, -100, 50]}
45 | ];
46 |
47 | {
48 | totalBalance: sum(accounts, .balance),
49 | averageBalance: mean(map(accounts, .balance)),
50 | totalTransactions: reduce(accounts, #acc + len(.transactions), 0),
51 | accounts: map(accounts, {{
52 | name: .name,
53 | final: .balance + sum(.transactions),
54 | transactionCount: len(.transactions)
55 | }})
56 | }
57 | ```
58 |
59 | Bitwise Operations and Flags Decoding
60 |
61 | ```
62 | let flags = [
63 | {name: "read", value: 0b0001},
64 | {name: "write", value: 0b0010},
65 | {name: "execute", value: 0b0100},
66 | {name: "admin", value: 0b1000}
67 | ];
68 |
69 | let userPermissions = 0b1011;
70 |
71 | flags
72 | | filter(userPermissions | bitand(.value) != 0)
73 | | map(.name)
74 | ```
75 |
76 | Nested Predicates with Optional Chaining
77 |
78 | ```
79 | let users = [
80 | {id: 1, name: "Alice", posts: [{title: "Hello World", content: "Short post"}, {title: "Another Post", content: "This is a bit longer post"}]},
81 | {id: 2, name: "Bob", posts: nil},
82 | {id: 3, name: "Charlie", posts: [{title: "Quick Update", content: "Update content"}]}
83 | ];
84 |
85 | users
86 | | filter(
87 | // Check if any post has content length greater than 10.
88 | any(.posts ?? [], len(.content) > 10)
89 | )
90 | | map({{name: .name, postCount: len(.posts ?? [])}})
91 | ```
92 |
93 | String Manipulation and Validation
94 |
95 | ```
96 | " Apple, banana, Apple, orange, banana, kiwi "
97 | | trim()
98 | | split(",")
99 | | map(trim(#))
100 | | map(lower(#))
101 | | uniq()
102 | | sort()
103 | | join(", ")
104 | ```
105 |
106 | Date Difference
107 |
108 | ```
109 | let startDate = date("2023-01-01");
110 | let endDate = date("2023-12-31");
111 | let diff = endDate - startDate;
112 | {
113 | daysBetween: diff.Hours() / 24,
114 | hoursBetween: diff.Hours()
115 | }
116 | ```
117 |
118 | Phone number filtering
119 |
120 | ```
121 | let phone = filter(split("123-456-78901", ""), # in map(0..9, string(#)))[:10];
122 | join(concat(["("], phone[:3], [")"], phone[3:6], ["-"], phone[6:]))
123 | ```
124 |
125 | Prime numbers
126 |
127 | ```
128 | 2..1000 | filter(let N = #; none(2..N-1, N % # == 0))
129 | ```
130 |
--------------------------------------------------------------------------------
/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strings"
7 |
8 | . "github.com/expr-lang/expr/checker/nature"
9 | )
10 |
11 | // Type is a type that can be used to represent a value.
12 | type Type interface {
13 | Nature() Nature
14 | Equal(Type) bool
15 | String() string
16 | }
17 |
18 | var (
19 | Int = TypeOf(0)
20 | Int8 = TypeOf(int8(0))
21 | Int16 = TypeOf(int16(0))
22 | Int32 = TypeOf(int32(0))
23 | Int64 = TypeOf(int64(0))
24 | Uint = TypeOf(uint(0))
25 | Uint8 = TypeOf(uint8(0))
26 | Uint16 = TypeOf(uint16(0))
27 | Uint32 = TypeOf(uint32(0))
28 | Uint64 = TypeOf(uint64(0))
29 | Float = TypeOf(float32(0))
30 | Float64 = TypeOf(float64(0))
31 | String = TypeOf("")
32 | Bool = TypeOf(true)
33 | Nil = nilType{}
34 | Any = anyType{}
35 | )
36 |
37 | func TypeOf(v any) Type {
38 | if v == nil {
39 | return Nil
40 | }
41 | return rtype{t: reflect.TypeOf(v)}
42 | }
43 |
44 | type anyType struct{}
45 |
46 | func (anyType) Nature() Nature {
47 | return Nature{Type: nil}
48 | }
49 |
50 | func (anyType) Equal(t Type) bool {
51 | return true
52 | }
53 |
54 | func (anyType) String() string {
55 | return "any"
56 | }
57 |
58 | type nilType struct{}
59 |
60 | func (nilType) Nature() Nature {
61 | return Nature{Nil: true}
62 | }
63 |
64 | func (nilType) Equal(t Type) bool {
65 | if t == Any {
66 | return true
67 | }
68 | return t == Nil
69 | }
70 |
71 | func (nilType) String() string {
72 | return "nil"
73 | }
74 |
75 | type rtype struct {
76 | t reflect.Type
77 | }
78 |
79 | func (r rtype) Nature() Nature {
80 | return Nature{Type: r.t}
81 | }
82 |
83 | func (r rtype) Equal(t Type) bool {
84 | if t == Any {
85 | return true
86 | }
87 | if rt, ok := t.(rtype); ok {
88 | return r.t.String() == rt.t.String()
89 | }
90 | return false
91 | }
92 |
93 | func (r rtype) String() string {
94 | return r.t.String()
95 | }
96 |
97 | // Map represents a map[string]any type with defined keys.
98 | type Map map[string]Type
99 |
100 | const Extra = "[[__extra_keys__]]"
101 |
102 | func (m Map) Nature() Nature {
103 | nt := Nature{
104 | Type: reflect.TypeOf(map[string]any{}),
105 | Fields: make(map[string]Nature, len(m)),
106 | Strict: true,
107 | }
108 | for k, v := range m {
109 | if k == Extra {
110 | nt.Strict = false
111 | natureOfDefaultValue := v.Nature()
112 | nt.DefaultMapValue = &natureOfDefaultValue
113 | continue
114 | }
115 | nt.Fields[k] = v.Nature()
116 | }
117 | return nt
118 | }
119 |
120 | func (m Map) Equal(t Type) bool {
121 | if t == Any {
122 | return true
123 | }
124 | mt, ok := t.(Map)
125 | if !ok {
126 | return false
127 | }
128 | if len(m) != len(mt) {
129 | return false
130 | }
131 | for k, v := range m {
132 | if !v.Equal(mt[k]) {
133 | return false
134 | }
135 | }
136 | return true
137 | }
138 |
139 | func (m Map) String() string {
140 | pairs := make([]string, 0, len(m))
141 | for k, v := range m {
142 | pairs = append(pairs, fmt.Sprintf("%s: %s", k, v.String()))
143 | }
144 | return fmt.Sprintf("Map{%s}", strings.Join(pairs, ", "))
145 | }
146 |
147 | // Array returns a type that represents an array of the given type.
148 | func Array(of Type) Type {
149 | return array{of}
150 | }
151 |
152 | type array struct {
153 | of Type
154 | }
155 |
156 | func (a array) Nature() Nature {
157 | of := a.of.Nature()
158 | return Nature{
159 | Type: reflect.TypeOf([]any{}),
160 | Fields: make(map[string]Nature, 1),
161 | ArrayOf: &of,
162 | }
163 | }
164 |
165 | func (a array) Equal(t Type) bool {
166 | if t == Any {
167 | return true
168 | }
169 | at, ok := t.(array)
170 | if !ok {
171 | return false
172 | }
173 | if a.of.Equal(at.of) {
174 | return true
175 | }
176 | return false
177 | }
178 |
179 | func (a array) String() string {
180 | return fmt.Sprintf("Array{%s}", a.of.String())
181 | }
182 |
--------------------------------------------------------------------------------
/types/types_test.go:
--------------------------------------------------------------------------------
1 | package types_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/require"
7 | . "github.com/expr-lang/expr/types"
8 | )
9 |
10 | func TestType_Equal(t *testing.T) {
11 | tests := []struct {
12 | index string // Index added for IDEA to show green test marker per test.
13 | a, b Type
14 | want bool
15 | }{
16 | {"1", Int, Int, true},
17 | {"2", Int, Int8, false},
18 | {"3", Int, Uint, false},
19 | {"4", Int, Float, false},
20 | {"5", Int, String, false},
21 | {"6", Int, Bool, false},
22 | {"7", Int, Nil, false},
23 | {"8", Int, Array(Int), false},
24 | {"9", Int, Map{"foo": Int}, false},
25 | {"11", Int, Array(Int), false},
26 | {"12", Array(Int), Array(Int), true},
27 | {"13", Array(Int), Array(Float), false},
28 | {"14", Map{"foo": Int}, Map{"foo": Int}, true},
29 | {"15", Map{"foo": Int}, Map{"foo": Float}, false},
30 | {"19", Map{"foo": Map{"bar": Int}}, Map{"foo": Map{"bar": Int}}, true},
31 | {"20", Map{"foo": Map{"bar": Int}}, Map{"foo": Map{"bar": Float}}, false},
32 | {"21", Any, Any, true},
33 | {"22", Any, Int, true},
34 | {"23", Int, Any, true},
35 | {"24", Any, Map{"foo": Int}, true},
36 | {"25", Map{"foo": Int}, Any, true},
37 | {"28", Any, Array(Int), true},
38 | {"29", Array(Int), Any, true},
39 | }
40 |
41 | for _, tt := range tests {
42 | t.Run(tt.index, func(t *testing.T) {
43 | if tt.want {
44 | require.True(t, tt.a.Equal(tt.b), tt.a.String()+" == "+tt.b.String())
45 | } else {
46 | require.False(t, tt.a.Equal(tt.b), tt.a.String()+" == "+tt.b.String())
47 | }
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/vm/debug.go:
--------------------------------------------------------------------------------
1 | //go:build expr_debug
2 | // +build expr_debug
3 |
4 | package vm
5 |
6 | const debug = true
7 |
--------------------------------------------------------------------------------
/vm/debug_off.go:
--------------------------------------------------------------------------------
1 | //go:build !expr_debug
2 | // +build !expr_debug
3 |
4 | package vm
5 |
6 | const debug = false
7 |
--------------------------------------------------------------------------------
/vm/debug_test.go:
--------------------------------------------------------------------------------
1 | //go:build expr_debug
2 | // +build expr_debug
3 |
4 | package vm_test
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/expr-lang/expr/internal/testify/require"
10 |
11 | "github.com/expr-lang/expr/compiler"
12 | "github.com/expr-lang/expr/parser"
13 | "github.com/expr-lang/expr/vm"
14 | )
15 |
16 | func TestDebugger(t *testing.T) {
17 | input := `[1, 2]`
18 |
19 | node, err := parser.Parse(input)
20 | require.NoError(t, err)
21 |
22 | program, err := compiler.Compile(node, nil)
23 | require.NoError(t, err)
24 |
25 | debug := vm.Debug()
26 | go func() {
27 | debug.Step()
28 | debug.Step()
29 | debug.Step()
30 | debug.Step()
31 | }()
32 | go func() {
33 | for range debug.Position() {
34 | }
35 | }()
36 |
37 | _, err = debug.Run(program, nil)
38 | require.NoError(t, err)
39 | require.Len(t, debug.Stack, 0)
40 | require.Nil(t, debug.Scopes)
41 | }
42 |
--------------------------------------------------------------------------------
/vm/func_types/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "go/format"
7 | "reflect"
8 | "strings"
9 | "text/template"
10 | . "time"
11 | )
12 |
13 | // Keep sorted.
14 | var types = []any{
15 | nil,
16 | new(func() Duration),
17 | new(func() Month),
18 | new(func() Time),
19 | new(func() Weekday),
20 | new(func() []any),
21 | new(func() []byte),
22 | new(func() any),
23 | new(func() bool),
24 | new(func() byte),
25 | new(func() float32),
26 | new(func() float64),
27 | new(func() int),
28 | new(func() int16),
29 | new(func() int32),
30 | new(func() int64),
31 | new(func() int8),
32 | new(func() map[string]any),
33 | new(func() rune),
34 | new(func() string),
35 | new(func() uint),
36 | new(func() uint16),
37 | new(func() uint32),
38 | new(func() uint64),
39 | new(func() uint8),
40 | new(func(Duration) Duration),
41 | new(func(Duration) Time),
42 | new(func(Time) Duration),
43 | new(func(Time) bool),
44 | new(func([]any) []any),
45 | new(func([]any) any),
46 | new(func([]any) map[string]any),
47 | new(func([]any, string) string),
48 | new(func([]byte) string),
49 | new(func([]string, string) string),
50 | new(func(any) []any),
51 | new(func(any) any),
52 | new(func(any) bool),
53 | new(func(any) float64),
54 | new(func(any) int),
55 | new(func(any) map[string]any),
56 | new(func(any) string),
57 | new(func(any, any) []any),
58 | new(func(any, any) any),
59 | new(func(any, any) bool),
60 | new(func(any, any) string),
61 | new(func(bool) bool),
62 | new(func(bool) float64),
63 | new(func(bool) int),
64 | new(func(bool) string),
65 | new(func(bool, bool) bool),
66 | new(func(float32) float64),
67 | new(func(float64) bool),
68 | new(func(float64) float32),
69 | new(func(float64) float64),
70 | new(func(float64) int),
71 | new(func(float64) string),
72 | new(func(float64, float64) bool),
73 | new(func(int) bool),
74 | new(func(int) float64),
75 | new(func(int) int),
76 | new(func(int) string),
77 | new(func(int, int) bool),
78 | new(func(int, int) int),
79 | new(func(int, int) string),
80 | new(func(int16) int32),
81 | new(func(int32) float64),
82 | new(func(int32) int),
83 | new(func(int32) int64),
84 | new(func(int64) Time),
85 | new(func(int8) int),
86 | new(func(int8) int16),
87 | new(func(string) []byte),
88 | new(func(string) []string),
89 | new(func(string) bool),
90 | new(func(string) float64),
91 | new(func(string) int),
92 | new(func(string) string),
93 | new(func(string, byte) int),
94 | new(func(string, int) int),
95 | new(func(string, rune) int),
96 | new(func(string, string) bool),
97 | new(func(string, string) string),
98 | new(func(uint) float64),
99 | new(func(uint) int),
100 | new(func(uint) uint),
101 | new(func(uint16) uint),
102 | new(func(uint32) uint64),
103 | new(func(uint64) float64),
104 | new(func(uint64) int64),
105 | new(func(uint8) byte),
106 | }
107 |
108 | func main() {
109 | data := struct {
110 | Index string
111 | Code string
112 | }{}
113 |
114 | for i, t := range types {
115 | if i == 0 {
116 | continue
117 | }
118 | fn := reflect.ValueOf(t).Elem().Type()
119 | data.Index += fmt.Sprintf("%v: new(%v),\n", i, fn)
120 | data.Code += fmt.Sprintf("case %d:\n", i)
121 | args := make([]string, fn.NumIn())
122 | for j := fn.NumIn() - 1; j >= 0; j-- {
123 | cast := fmt.Sprintf(".(%v)", fn.In(j))
124 | if fn.In(j).Kind() == reflect.Interface {
125 | cast = ""
126 | }
127 | data.Code += fmt.Sprintf("arg%v := vm.pop()%v\n", j+1, cast)
128 | args[j] = fmt.Sprintf("arg%v", j+1)
129 | }
130 | data.Code += fmt.Sprintf("return fn.(%v)(%v)\n", fn, strings.Join(args, ", "))
131 | }
132 |
133 | var b bytes.Buffer
134 | err := template.Must(
135 | template.New("func_types").
136 | Parse(source),
137 | ).Execute(&b, data)
138 | if err != nil {
139 | panic(err)
140 | }
141 |
142 | formatted, err := format.Source(b.Bytes())
143 | if err != nil {
144 | panic(err)
145 | }
146 | fmt.Print(string(formatted))
147 | }
148 |
149 | const source = `// Code generated by vm/func_types/main.go. DO NOT EDIT.
150 |
151 | package vm
152 |
153 | import (
154 | "fmt"
155 | "time"
156 | )
157 |
158 | var FuncTypes = []any{
159 | {{ .Index }}
160 | }
161 |
162 | func (vm *VM) call(fn any, kind int) any {
163 | switch kind {
164 | {{ .Code }}
165 | }
166 | panic(fmt.Sprintf("unknown function kind (%v)", kind))
167 | }
168 | `
169 |
--------------------------------------------------------------------------------
/vm/opcodes.go:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | type Opcode byte
4 |
5 | const (
6 | OpInvalid Opcode = iota
7 | OpPush
8 | OpInt
9 | OpPop
10 | OpStore
11 | OpLoadVar
12 | OpLoadConst
13 | OpLoadField
14 | OpLoadFast
15 | OpLoadMethod
16 | OpLoadFunc
17 | OpLoadEnv
18 | OpFetch
19 | OpFetchField
20 | OpMethod
21 | OpTrue
22 | OpFalse
23 | OpNil
24 | OpNegate
25 | OpNot
26 | OpEqual
27 | OpEqualInt
28 | OpEqualString
29 | OpJump
30 | OpJumpIfTrue
31 | OpJumpIfFalse
32 | OpJumpIfNil
33 | OpJumpIfNotNil
34 | OpJumpIfEnd
35 | OpJumpBackward
36 | OpIn
37 | OpLess
38 | OpMore
39 | OpLessOrEqual
40 | OpMoreOrEqual
41 | OpAdd
42 | OpSubtract
43 | OpMultiply
44 | OpDivide
45 | OpModulo
46 | OpExponent
47 | OpRange
48 | OpMatches
49 | OpMatchesConst
50 | OpContains
51 | OpStartsWith
52 | OpEndsWith
53 | OpSlice
54 | OpCall
55 | OpCall0
56 | OpCall1
57 | OpCall2
58 | OpCall3
59 | OpCallN
60 | OpCallFast
61 | OpCallSafe
62 | OpCallTyped
63 | OpCallBuiltin1
64 | OpArray
65 | OpMap
66 | OpLen
67 | OpCast
68 | OpDeref
69 | OpIncrementIndex
70 | OpDecrementIndex
71 | OpIncrementCount
72 | OpGetIndex
73 | OpGetCount
74 | OpGetLen
75 | OpGetAcc
76 | OpSetAcc
77 | OpSetIndex
78 | OpPointer
79 | OpThrow
80 | OpCreate
81 | OpGroupBy
82 | OpSortBy
83 | OpSort
84 | OpProfileStart
85 | OpProfileEnd
86 | OpBegin
87 | OpEnd // This opcode must be at the end of this list.
88 | )
89 |
--------------------------------------------------------------------------------
/vm/program_test.go:
--------------------------------------------------------------------------------
1 | package vm_test
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/expr-lang/expr/vm"
8 | )
9 |
10 | func TestProgram_Disassemble(t *testing.T) {
11 | for op := vm.OpPush; op < vm.OpEnd; op++ {
12 | program := vm.Program{
13 | Constants: []any{1, 2},
14 | Bytecode: []vm.Opcode{op},
15 | Arguments: []int{1},
16 | }
17 | d := program.Disassemble()
18 | if strings.Contains(d, "(unknown)") {
19 | t.Errorf("cannot disassemble all opcodes")
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/vm/runtime/helpers_test.go:
--------------------------------------------------------------------------------
1 | package runtime_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/expr-lang/expr/internal/testify/assert"
7 |
8 | "github.com/expr-lang/expr/vm/runtime"
9 | )
10 |
11 | var tests = []struct {
12 | name string
13 | a, b any
14 | want bool
15 | }{
16 | {"int == int", 42, 42, true},
17 | {"int != int", 42, 33, false},
18 | {"int == int8", 42, int8(42), true},
19 | {"int == int16", 42, int16(42), true},
20 | {"int == int32", 42, int32(42), true},
21 | {"int == int64", 42, int64(42), true},
22 | {"float == float", 42.0, 42.0, true},
23 | {"float != float", 42.0, 33.0, false},
24 | {"float == int", 42.0, 42, true},
25 | {"float != int", 42.0, 33, false},
26 | {"string == string", "foo", "foo", true},
27 | {"string != string", "foo", "bar", false},
28 | {"bool == bool", true, true, true},
29 | {"bool != bool", true, false, false},
30 | {"[]any == []int", []any{1, 2, 3}, []int{1, 2, 3}, true},
31 | {"[]any != []int", []any{1, 2, 3}, []int{1, 2, 99}, false},
32 | {"deep []any == []any", []any{[]int{1}, 2, []any{"3"}}, []any{[]any{1}, 2, []string{"3"}}, true},
33 | {"deep []any != []any", []any{[]int{1}, 2, []any{"3", "42"}}, []any{[]any{1}, 2, []string{"3"}}, false},
34 | {"map[string]any == map[string]any", map[string]any{"a": 1}, map[string]any{"a": 1}, true},
35 | {"map[string]any != map[string]any", map[string]any{"a": 1}, map[string]any{"a": 1, "b": 2}, false},
36 | }
37 |
38 | func TestEqual(t *testing.T) {
39 | for _, tt := range tests {
40 | t.Run(tt.name, func(t *testing.T) {
41 | got := runtime.Equal(tt.a, tt.b)
42 | assert.Equal(t, tt.want, got, "Equal(%v, %v) = %v; want %v", tt.a, tt.b, got, tt.want)
43 | got = runtime.Equal(tt.b, tt.a)
44 | assert.Equal(t, tt.want, got, "Equal(%v, %v) = %v; want %v", tt.b, tt.a, got, tt.want)
45 | })
46 | }
47 |
48 | }
49 |
50 | func BenchmarkEqual(b *testing.B) {
51 | for _, tt := range tests {
52 | b.Run(tt.name, func(b *testing.B) {
53 | for i := 0; i < b.N; i++ {
54 | runtime.Equal(tt.a, tt.b)
55 | }
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/vm/runtime/sort.go:
--------------------------------------------------------------------------------
1 | package runtime
2 |
3 | type SortBy struct {
4 | Desc bool
5 | Array []any
6 | Values []any
7 | }
8 |
9 | func (s *SortBy) Len() int {
10 | return len(s.Array)
11 | }
12 |
13 | func (s *SortBy) Swap(i, j int) {
14 | s.Array[i], s.Array[j] = s.Array[j], s.Array[i]
15 | s.Values[i], s.Values[j] = s.Values[j], s.Values[i]
16 | }
17 |
18 | func (s *SortBy) Less(i, j int) bool {
19 | a, b := s.Values[i], s.Values[j]
20 | if s.Desc {
21 | return Less(b, a)
22 | }
23 | return Less(a, b)
24 | }
25 |
26 | type Sort struct {
27 | Desc bool
28 | Array []any
29 | }
30 |
31 | func (s *Sort) Len() int {
32 | return len(s.Array)
33 | }
34 |
35 | func (s *Sort) Swap(i, j int) {
36 | s.Array[i], s.Array[j] = s.Array[j], s.Array[i]
37 | }
38 |
39 | func (s *Sort) Less(i, j int) bool {
40 | a, b := s.Array[i], s.Array[j]
41 | if s.Desc {
42 | return Less(b, a)
43 | }
44 | return Less(a, b)
45 | }
46 |
--------------------------------------------------------------------------------
/vm/utils.go:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import (
4 | "reflect"
5 | "time"
6 | )
7 |
8 | type (
9 | Function = func(params ...any) (any, error)
10 | SafeFunction = func(params ...any) (any, uint, error)
11 | )
12 |
13 | var (
14 | errorType = reflect.TypeOf((*error)(nil)).Elem()
15 | )
16 |
17 | type Scope struct {
18 | Array reflect.Value
19 | Index int
20 | Len int
21 | Count int
22 | Acc any
23 | }
24 |
25 | type groupBy = map[any][]any
26 |
27 | type Span struct {
28 | Name string `json:"name"`
29 | Expression string `json:"expression"`
30 | Duration int64 `json:"duration"`
31 | Children []*Span `json:"children"`
32 | start time.Time
33 | }
34 |
35 | func GetSpan(program *Program) *Span {
36 | return program.span
37 | }
38 |
--------------------------------------------------------------------------------