From 75e007860e76a5e4527ccd509371409b82c2db4d Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Fri, 6 Mar 2020 14:26:58 +0300 Subject: [PATCH] Make type checking non-strict by default --- checker/checker.go | 14 +-- cmd/exe/main.go | 5 +- expr.go | 17 ++- expr_test.go | 249 +++++++++++----------------------------- internal/conf/config.go | 24 ++-- vm/runtime.go | 4 +- vm/vm_test.go | 16 --- 7 files changed, 97 insertions(+), 232 deletions(-) diff --git a/checker/checker.go b/checker/checker.go index 77827f1fe..14a7ac4f4 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -28,8 +28,8 @@ func Check(tree *parser.Tree, config *conf.Config) (t reflect.Type, err error) { v.types = config.Types v.operators = config.Operators v.expect = config.Expect - v.strict = !config.AllowUndefinedVariables - v.defaultType = config.UndefinedVariableType + v.strict = config.Strict + v.defaultType = config.DefaultType } t = v.visit(tree.Node) @@ -37,18 +37,16 @@ func Check(tree *parser.Tree, config *conf.Config) (t reflect.Type, err error) { if v.expect != reflect.Invalid { switch v.expect { case reflect.Int64, reflect.Float64: - if isNumber(t) { - goto okay + if !isNumber(t) { + return nil, fmt.Errorf("expected %v, but got %v", v.expect, t) } default: - if t.Kind() == v.expect { - goto okay + if t.Kind() != v.expect { + return nil, fmt.Errorf("expected %v, but got %v", v.expect, t) } } - return nil, fmt.Errorf("expected %v, but got %v", v.expect, t) } -okay: return } diff --git a/cmd/exe/main.go b/cmd/exe/main.go index 1624cb328..199e72b70 100644 --- a/cmd/exe/main.go +++ b/cmd/exe/main.go @@ -11,7 +11,6 @@ import ( "github.com/antonmedv/expr/ast" "github.com/antonmedv/expr/checker" "github.com/antonmedv/expr/compiler" - "github.com/antonmedv/expr/internal/conf" "github.com/antonmedv/expr/optimizer" "github.com/antonmedv/expr/parser" "github.com/antonmedv/expr/vm" @@ -88,9 +87,7 @@ func printAst() { check(err) if typeCheck { - _, err = checker.Check(tree, &conf.Config{ - AllowUndefinedVariables: true, - }) + _, err = checker.Check(tree, nil) check(err) if opt { diff --git a/expr.go b/expr.go index d77d98d21..bce8263ab 100644 --- a/expr.go +++ b/expr.go @@ -50,10 +50,10 @@ func Env(i interface{}) Option { c.MapEnv = true } else { if reflect.ValueOf(i).Kind() == reflect.Map { - c.UndefinedVariableType = reflect.TypeOf(i).Elem() + c.DefaultType = reflect.TypeOf(i).Elem() } } - c.CheckTypes = true + c.Strict = true c.Types = conf.CreateTypesTable(i) } } @@ -64,8 +64,7 @@ func Env(i interface{}) Option { // runtime.fetch will panic as there is no way to get missing field zero value. func AllowUndefinedVariables() Option { return func(c *conf.Config) { - c.CheckTypes = true - c.AllowUndefinedVariables = true + c.Strict = false } } @@ -124,13 +123,11 @@ func Compile(input string, ops ...Option) (*vm.Program, error) { return nil, err } - if config.CheckTypes { - _, err = checker.Check(tree, config) - if err != nil { - return nil, err - } - checker.PatchOperators(tree, config) + _, err = checker.Check(tree, config) + if err != nil { + return nil, err } + checker.PatchOperators(tree, config) if config.Optimize { optimizer.Optimize(&tree.Node) diff --git a/expr_test.go b/expr_test.go index a4edbf549..5d078344b 100644 --- a/expr_test.go +++ b/expr_test.go @@ -12,7 +12,10 @@ import ( ) func ExampleEval() { - output, err := expr.Eval("'hello world'", nil) + output, err := expr.Eval("greet + name", map[string]interface{}{ + "greet": "Hello, ", + "name": "world!", + }) if err != nil { fmt.Printf("err: %v", err) return @@ -20,97 +23,16 @@ func ExampleEval() { fmt.Printf("%v", output) - // Output: hello world -} - -func ExampleEval_map() { - env := map[string]interface{}{ - "foo": 1, - "bar": []string{"zero", "hello world"}, - "swipe": func(in string) string { - return strings.Replace(in, "world", "user", 1) - }, - } - - output, err := expr.Eval("swipe(bar[foo])", env) - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) - - // Output: hello user -} - -func ExampleEval_map_method() { - env := mockMapEnv{ - "foo": 1, - "bar": []string{"zero", "hello world"}, - } - - program, err := expr.Compile("Swipe(bar[foo])", expr.Env(env)) - if err != nil { - fmt.Printf("%v", err) - return - } - - output, err := expr.Run(program, env) - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) - - // Output: hello user -} - -func ExampleEval_struct() { - type C struct{ C int } - type B struct{ B *C } - type A struct{ A B } - - env := A{B{&C{42}}} - - output, err := expr.Eval("A.B.C", env) - - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) - - // Output: 42 -} - -func ExampleEval_error() { - output, err := expr.Eval("(boo + bar]", nil) - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) - - // Output: unexpected token Bracket("]") (1:11) - // | (boo + bar] - // | ..........^ + // Output: Hello, world! } -func ExampleEval_matches() { - output, err := expr.Eval(`"a" matches "a("`, nil) - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) +func ExampleEval_runtime_error() { + _, err := expr.Eval(`map(1..3, {1 / (# - 3)})`, nil) + fmt.Print(err) - // Output: error parsing regexp: missing closing ): `a(` (1:16) - // | "a" matches "a(" - // | ...............^ + // Output: runtime error: integer divide by zero (1:14) + // | map(1..3, {1 / (# - 3)}) + // | .............^ } func ExampleCompile() { @@ -143,22 +65,30 @@ func ExampleEnv() { type Passengers struct { Adults int } - type Request struct { + type Meta struct { + Tags map[string]string + } + type Env struct { + Meta Segments []*Segment Passengers *Passengers Marker string - Meta map[string]interface{} } - code := `Segments[0].Origin == "MOW" && Passengers.Adults == 2 && Marker == "test" && Meta["accept"]` + code := `all(Segments, {.Origin == "MOW"}) && Passengers.Adults > 0 && Tags["foo"] startsWith "bar"` - program, err := expr.Compile(code, expr.Env(&Request{})) + program, err := expr.Compile(code, expr.Env(Env{})) if err != nil { fmt.Printf("%v", err) return } - request := &Request{ + env := Env{ + Meta: Meta{ + Tags: map[string]string{ + "foo": "bar", + }, + }, Segments: []*Segment{ {Origin: "MOW"}, }, @@ -166,10 +96,9 @@ func ExampleEnv() { Adults: 2, }, Marker: "test", - Meta: map[string]interface{}{"accept": true}, } - output, err := expr.Run(program, request) + output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return @@ -180,80 +109,38 @@ func ExampleEnv() { // Output: true } -func ExampleEnv_with_undefined_variables() { - env := map[string]interface{}{ +func ExampleAsBool() { + env := map[string]int{ "foo": 0, - "bar": 0, - } - - program, err := expr.Compile(`foo + (bar != nil ? bar : 2)`, expr.Env(env)) - if err != nil { - fmt.Printf("%v", err) - return - } - - request := map[string]interface{}{ - "foo": 3, - } - - output, err := expr.Run(program, request) - if err != nil { - fmt.Printf("%v", err) - return - } - - fmt.Printf("%v", output) - - // Output: 5 -} - -func ExampleEnv_allow_undefined_variables() { - env := map[string]string{ - "greet": "", } - program, err := expr.Compile(`greet + name`, expr.Env(env), expr.AllowUndefinedVariables()) + program, err := expr.Compile("foo >= 0", expr.Env(env), expr.AsBool()) if err != nil { fmt.Printf("%v", err) return } - params := map[string]string{ - "greet": "hello, ", - "name": "world", - } - - output, err := expr.Run(program, params) + output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return } - fmt.Printf("%v", output) + fmt.Printf("%v", output.(bool)) - // Output: hello, world + // Output: true } -func ExampleAsBool() { - env := map[string]int{ +func ExampleAsBool_error() { + env := map[string]interface{}{ "foo": 0, } - program, err := expr.Compile("foo >= 0", expr.Env(env), expr.AsBool()) - if err != nil { - fmt.Printf("%v", err) - return - } - - output, err := expr.Run(program, env) - if err != nil { - fmt.Printf("%v", err) - return - } + _, err := expr.Compile("foo + 42", expr.Env(env), expr.AsBool()) - fmt.Printf("%v", output.(bool)) + fmt.Printf("%v", err) - // Output: true + // Output: expected bool, but got int } func ExampleAsFloat64() { @@ -274,12 +161,20 @@ func ExampleAsFloat64() { // Output: 42 } +func ExampleAsFloat64_error() { + _, err := expr.Compile(`!!true`, expr.AsFloat64()) + + fmt.Printf("%v", err) + + // Output: expected float64, but got bool +} + func ExampleAsInt64() { - env := map[string]float64{ - "foo": 3, + env := map[string]interface{}{ + "rating": 5.5, } - program, err := expr.Compile("foo + 2", expr.Env(env), expr.AsInt64()) + program, err := expr.Compile("rating", expr.Env(env), expr.AsInt64()) if err != nil { fmt.Printf("%v", err) return @@ -297,38 +192,38 @@ func ExampleAsInt64() { } func ExampleOperator() { - type Place struct { - Code string - } - type Segment struct { - Origin Place - } - type Helpers struct { - PlaceEq func(p Place, s string) bool - } - type Request struct { - Segments []*Segment - Helpers + code := ` + Now() > CreatedAt && + (Now() - CreatedAt).Hours() > 24 + ` + + type Env struct { + CreatedAt time.Time + Now func() time.Time + Sub func(a, b time.Time) time.Duration + After func(a, b time.Time) bool } - code := `Segments[0].Origin == "MOW" && PlaceEq(Segments[0].Origin, "MOW")` + options := []expr.Option{ + expr.Env(Env{}), + expr.Operator(">", "After"), + expr.Operator("-", "Sub"), + } - program, err := expr.Compile(code, expr.Env(&Request{}), expr.Operator("==", "PlaceEq")) + program, err := expr.Compile(code, options...) if err != nil { fmt.Printf("%v", err) return } - request := &Request{ - Segments: []*Segment{ - {Origin: Place{Code: "MOW"}}, - }, - Helpers: Helpers{PlaceEq: func(p Place, s string) bool { - return p.Code == s - }}, + env := Env{ + CreatedAt: time.Date(2018, 7, 14, 0, 0, 0, 0, time.UTC), + Now: func() time.Time { return time.Now() }, + Sub: func(a, b time.Time) time.Duration { return a.Sub(b) }, + After: func(a, b time.Time) bool { return a.After(b) }, } - output, err := expr.Run(program, request) + output, err := expr.Run(program, env) if err != nil { fmt.Printf("%v", err) return @@ -1085,12 +980,6 @@ type segment struct { Date time.Time } -type mockMapEnv map[string]interface{} - -func (mockMapEnv) Swipe(in string) string { - return strings.Replace(in, "world", "user", 1) -} - type mockMapStringStringEnv map[string]string func (m mockMapStringStringEnv) Split(s, sep string) []string { diff --git a/internal/conf/config.go b/internal/conf/config.go index e7c9f3e28..f5fcb813b 100644 --- a/internal/conf/config.go +++ b/internal/conf/config.go @@ -6,14 +6,13 @@ import ( ) type Config struct { - MapEnv bool - Types TypesTable - CheckTypes bool - Operators OperatorsTable - Expect reflect.Kind - Optimize bool - AllowUndefinedVariables bool - UndefinedVariableType reflect.Type + MapEnv bool + Types TypesTable + Operators OperatorsTable + Expect reflect.Kind + Optimize bool + Strict bool + DefaultType reflect.Type } func New(i interface{}) *Config { @@ -28,10 +27,11 @@ func New(i interface{}) *Config { } return &Config{ - MapEnv: mapEnv, - Types: CreateTypesTable(i), - Optimize: true, - UndefinedVariableType: mapValueType, + MapEnv: mapEnv, + Types: CreateTypesTable(i), + Optimize: true, + Strict: true, + DefaultType: mapValueType, } } diff --git a/vm/runtime.go b/vm/runtime.go index af43f11d6..3d8b2ee72 100644 --- a/vm/runtime.go +++ b/vm/runtime.go @@ -240,7 +240,7 @@ func toInt(a interface{}) int { return int(x) case int: - return int(x) + return x case int8: return int(x) case int16: @@ -282,7 +282,7 @@ func toInt64(a interface{}) int64 { case int32: return int64(x) case int64: - return int64(x) + return x case uint: return int64(x) diff --git a/vm/vm_test.go b/vm/vm_test.go index cdc7f12e0..5e13bd605 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -107,19 +107,3 @@ func TestRun_memory_budget(t *testing.T) { _, err = vm.Run(program, nil) require.Error(t, err) } - -func TestRun_runtime_error(t *testing.T) { - input := `map(1..3, {1/(#-3)})` - - tree, err := parser.Parse(input) - require.NoError(t, err) - - program, err := compiler.Compile(tree, nil) - require.NoError(t, err) - - _, err = vm.Run(program, nil) - require.Error(t, err) - require.Equal(t, `runtime error: integer divide by zero (1:13) - | map(1..3, {1/(#-3)}) - | ............^`, err.Error()) -}