├── .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 | --------------------------------------------------------------------------------