This commit is contained in:
Anton Medvedev
2018-07-14 21:47:46 +03:00
parent ce2a54e239
commit cfcc41a5e7
3 changed files with 826 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
package expr
import (
"fmt"
"math"
"regexp"
)
type evaluator interface {
eval(env interface{}) (interface{}, error)
}
// Eval parses and evaluates given input.
func Eval(input string, env interface{}) (node interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
node, err = Parse(input)
if err != nil {
return nil, err
}
return Run(node, env)
}
// Run evaluates given ast.
func Run(node Node, env interface{}) (interface{}, error) {
if e, ok := node.(evaluator); ok {
return e.eval(env)
}
return nil, fmt.Errorf("implement evaluator for %T", node)
}
// eval functions
func (n nilNode) eval(env interface{}) (interface{}, error) {
return nil, nil
}
func (n identifierNode) eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n numberNode) eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n boolNode) eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n textNode) eval(env interface{}) (interface{}, error) {
return n.value, nil
}
func (n nameNode) eval(env interface{}) (interface{}, error) {
return extract(env, n.name)
}
func (n unaryNode) eval(env interface{}) (interface{}, error) {
val, err := Run(n.node, env)
if err != nil {
return nil, err
}
switch n.operator {
case "not", "!":
if !isBool(val) {
return nil, fmt.Errorf("negation of non-bool type %T", val)
}
return !toBool(val), nil
}
v, err := cast(val)
if err != nil {
return nil, err
}
switch n.operator {
case "-":
return -v, nil
case "+":
return +v, nil
}
return nil, fmt.Errorf("implement unary %q operator", n.operator)
}
func (n binaryNode) eval(env interface{}) (interface{}, error) {
left, err := Run(n.left, env)
if err != nil {
return nil, err
}
switch n.operator {
case "or", "||":
if !isBool(left) {
return nil, fmt.Errorf("non-bool value in cond (%T)", left)
}
if toBool(left) {
return true, nil
}
right, err := Run(n.right, env)
if err != nil {
return nil, err
}
if !isBool(right) {
return nil, fmt.Errorf("non-bool value in cond (%T)", right)
}
return toBool(right), nil
case "and", "&&":
if !isBool(left) {
return nil, fmt.Errorf("non-bool value in cond (%T)", left)
}
if toBool(left) {
right, err := Run(n.right, env)
if err != nil {
return nil, err
}
if !isBool(right) {
return nil, fmt.Errorf("non-bool value in cond (%T)", right)
}
return toBool(right), nil
}
return false, nil
}
right, err := Run(n.right, env)
if err != nil {
return nil, err
}
switch n.operator {
case "==":
return equal(left, right), nil
case "!=":
return !equal(left, right), nil
case "in":
ok, err := contains(left, right)
if err != nil {
return nil, err
}
return ok, nil
case "not in":
ok, err := contains(left, right)
if err != nil {
return nil, err
}
return !ok, nil
case "matches":
if isText(left) && isText(right) {
matched, err := regexp.MatchString(toText(right), toText(left))
if err != nil {
return nil, err
}
return matched, nil
}
return nil, fmt.Errorf("operator matches not defined on (%T, %T)", left, right)
case "~":
if isText(left) && isText(right) {
return toText(left) + toText(right), nil
}
return nil, fmt.Errorf("operator ~ not defined on (%T, %T)", left, right)
}
// Next goes operators on numbers
l, err := cast(left)
if err != nil {
return nil, err
}
r, err := cast(right)
if err != nil {
return nil, err
}
switch n.operator {
case "|":
return int(l) | int(r), nil
case "^":
return int(l) ^ int(r), nil
case "&":
return int(l) & int(r), nil
case "<":
return l < r, nil
case ">":
return l > r, nil
case ">=":
return l >= r, nil
case "<=":
return l <= r, nil
case "+":
return l + r, nil
case "-":
return l - r, nil
case "*":
return l * r, nil
case "/":
div := r
if div == 0 {
return nil, fmt.Errorf("division by zero")
}
return l / div, nil
case "%":
numerator := int64(l)
denominator := int64(r)
if denominator == 0 {
return nil, fmt.Errorf("division by zero")
}
return float64(numerator % denominator), nil
case "**":
return math.Pow(l, r), nil
case "..":
return makeRange(int64(l), int64(r)), nil
}
return nil, fmt.Errorf("implement %q operator", n.operator)
}
func makeRange(min, max int64) []float64 {
a := make([]float64, max-min+1)
for i := range a {
a[i] = float64(min + int64(i))
}
return a
}
func (n propertyNode) eval(env interface{}) (interface{}, error) {
v, err := Run(n.node, env)
if err != nil {
return nil, err
}
p, err := Run(n.property, env)
if err != nil {
return nil, err
}
return extract(v, p)
}
func (n methodNode) eval(env interface{}) (interface{}, error) {
v, err := Run(n.node, env)
if err != nil {
return nil, err
}
method, err := extract(v, n.property.value)
if err != nil {
return nil, err
}
return call(n.property.value, method, n.arguments, env)
}
func (n functionNode) eval(env interface{}) (interface{}, error) {
fn, err := extract(env, n.name)
if err != nil {
return nil, err
}
return call(n.name, fn, n.arguments, env)
}
func (n conditionalNode) eval(env interface{}) (interface{}, error) {
cond, err := Run(n.cond, env)
if err != nil {
return nil, err
}
// If
if !isBool(cond) {
return nil, fmt.Errorf("non-bool value used in cond (%T)", cond)
}
// Then
if toBool(cond) {
a, err := Run(n.exp1, env)
if err != nil {
return nil, err
}
return a, nil
}
// Else
b, err := Run(n.exp2, env)
if err != nil {
return nil, err
}
return b, nil
}
func (n arrayNode) eval(env interface{}) (interface{}, error) {
array := make([]interface{}, 0)
for _, node := range n.nodes {
val, err := Run(node, env)
if err != nil {
return nil, err
}
array = append(array, val)
}
return array, nil
}
func (n mapNode) eval(env interface{}) (interface{}, error) {
m := make(map[interface{}]interface{})
for _, pair := range n.pairs {
key, err := Run(pair.key, env)
if err != nil {
return nil, err
}
value, err := Run(pair.value, env)
if err != nil {
return nil, err
}
m[key] = value
}
return m, nil
}
+343
View File
@@ -0,0 +1,343 @@
package expr
import (
"fmt"
"reflect"
"strings"
"testing"
)
type evalTest struct {
input string
env interface{}
expected interface{}
}
var evalTests = []evalTest{
{
"foo",
map[string]int{"foo": 33},
33,
},
{
"foo == bar",
map[string]interface{}{"foo": 1, "bar": 1},
true,
},
{
"foo || (bar && !false && true)",
map[string]interface{}{"foo": false, "bar": true},
true,
},
{
"foo && bar",
map[string]interface{}{"foo": false, "bar": true},
false,
},
{
"!foo && bar",
map[string]interface{}{"foo": false, "bar": true},
true,
},
{
"2+2==4",
nil,
true,
},
{
"2+3",
nil,
float64(5),
},
{
"5-2",
nil,
float64(3),
},
{
"2*3",
nil,
float64(6),
},
{
"6/2",
nil,
float64(3),
},
{
"8%3",
nil,
float64(2),
},
{
"2**4",
nil,
float64(16),
},
{
"2**4",
nil,
float64(16),
},
{
"-(2-5)**3-2/(+4-3)+-2",
nil,
float64(23),
},
{
`"hello" ~ ' ' ~ "world"`,
nil,
"hello world",
},
{
"+0 == -0",
nil,
true,
},
{
"1 < 2 and 3 > 2",
nil,
true,
},
{
"!(1 != 1) && 2 >= 2 && 3 <= 3",
nil,
true,
},
{
"[1, 02, 1e3, 1.2e-4]",
nil,
[]interface{}{float64(1), float64(2), float64(1000), float64(0.00012)},
},
{
"1..5",
nil,
[]float64{1, 2, 3, 4, 5},
},
{
"{foo: 1}",
nil,
map[interface{}]interface{}{"foo": float64(1)},
},
{
`{foo: "bar"}.foo`,
nil,
"bar",
},
{
`Foo.Bar`,
struct{ Foo struct{ Bar bool } }{Foo: struct{ Bar bool }{Bar: true}},
true,
},
{
"foo[2]",
map[string]interface{}{"foo": []rune{'a', 'b', 'c'}},
'c',
},
{
"[1, 2, 3][2/2]",
nil,
float64(2),
},
{
`[true][A]`,
struct{ A int }{0},
true,
},
{
`A-1`,
struct{ A int }{1},
float64(0),
},
{
`A == 0`,
struct{ A uint8 }{0},
true,
},
{
`A == B`,
struct {
A uint8
B float64
}{1, 1},
true,
},
{
`5 in 0..9`,
nil,
true,
},
{
`"1" in ["1", "2"]`,
nil,
true,
},
{
`"0" not in ["1", "2"]`,
nil,
true,
},
{
`A == nil`,
struct{ A interface{} }{nil},
true,
},
{
"foo['bar'].baz",
map[string]interface{}{"foo": map[string]interface{}{"bar": map[string]interface{}{"baz": true}}},
true,
},
{
`60 & 13`,
nil,
12,
},
{
`60 ^ 13`,
nil,
49,
},
{
`60 | 13`,
nil,
61,
},
{
`"seafood" matches "foo.*"`,
nil,
true,
},
{
`not ("seafood" matches "[0-9]+") ? "a" : "b"`,
nil,
"a",
},
{
`foo.bar("world")`,
map[string]interface{}{"foo": map[string]interface{}{"bar": func(in string) string { return "hello " + in }}},
"hello world",
},
{
`foo.bar()`,
map[string]interface{}{"foo": map[string]interface{}{"bar": func() {}}},
nil,
},
{
`foo("world")`,
map[string]interface{}{"foo": func(in string) string { return "hello " + in }},
"hello world",
},
}
type evalErrorTest struct {
input string
env interface{}
err string
}
var evalErrorTests = []evalErrorTest{
{
"bar",
map[string]int{"foo": 1},
`can't get "bar"`,
},
{
`"foo" ~ nil`,
nil,
`operator ~ not defined on (string, <nil>)`,
},
{
"foo['bar'].baz",
map[string]interface{}{"foo": nil},
`can't get "bar" from <nil>`,
},
{
`"seafood" matches "a(b"`,
nil,
`error parsing regexp:`,
},
{
`0 ? 1 : 2`,
nil,
`non-bool value used in cond`,
},
{
`foo()`,
map[string]interface{}{"foo": func() (int, int) { return 0, 1 }},
`func "foo" must return only one value`,
},
{
`foo()`,
map[string]interface{}{"foo": nil},
`reflect: call of reflect.Value.Call on zero Value`,
},
}
func TestEval(t *testing.T) {
for _, test := range evalTests {
actual, err := Eval(test.input, test.env)
if err != nil {
t.Errorf("%s:\n%v", test.input, err)
continue
}
if !reflect.DeepEqual(actual, test.expected) {
t.Errorf("%s:\ngot\n\t%#v\nexpected\n\t%#v", test.input, actual, test.expected)
}
}
}
func TestEvalError(t *testing.T) {
for _, test := range evalErrorTests {
_, err := Eval(test.input, test.env)
if err == nil {
err = fmt.Errorf("<nil>")
}
if !strings.HasPrefix(err.Error(), test.err) {
t.Errorf("%s:\ngot\n\t%+v\nexpected\n\t%v", test.input, err.Error(), test.err)
}
}
}
func TestEvalComplex(t *testing.T) {
type cookie struct {
Key string
Value string
}
type user struct {
UserAgent string
Cookies []cookie
}
type request struct {
User user
}
p := map[string]interface{}{
"Request": request{user{
Cookies: []cookie{{"origin", "www"}},
UserAgent: "Mozilla 1",
}},
"Values": func(xs []cookie) []string {
vs := make([]string, 0)
for _, x := range xs {
vs = append(vs, x.Value)
}
return vs
},
}
input := `Request.User.UserAgent matches "Mozilla" && "www" in Values(Request.User.Cookies)`
node, err := Parse(input, Names("Request"), Funcs("Values"))
if err != nil {
t.Fatal(err)
}
actual, err := Run(node, p)
if err != nil {
t.Fatal(err)
}
expected := true
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("TestEvalComplex:\ngot\n\t%#v\nexpected:\n\t%#v", actual, expected)
}
}
+139
View File
@@ -0,0 +1,139 @@
package expr
import (
"fmt"
"reflect"
)
func isBool(val interface{}) bool {
return val != nil && reflect.TypeOf(val).Kind() == reflect.Bool
}
func toBool(val interface{}) bool {
return reflect.ValueOf(val).Bool()
}
func isText(val interface{}) bool {
return val != nil && reflect.TypeOf(val).Kind() == reflect.String
}
func toText(val interface{}) string {
return reflect.ValueOf(val).String()
}
func equal(left, right interface{}) bool {
if isNumber(left) && canBeNumber(right) {
right, _ := cast(right)
return left == right
} else if canBeNumber(left) && isNumber(right) {
left, _ := cast(left)
return left == right
} else {
return reflect.DeepEqual(left, right)
}
}
func isNumber(val interface{}) bool {
return val != nil && reflect.TypeOf(val).Kind() == reflect.Float64
}
func cast(v interface{}) (float64, error) {
if v != nil {
switch reflect.TypeOf(v).Kind() {
case reflect.Float32, reflect.Float64:
return v.(float64), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(reflect.ValueOf(v).Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(reflect.ValueOf(v).Uint()), nil // TODO: Check if uint64 fits into float64.
}
}
return 0, fmt.Errorf("can't cast %T to float64", v)
}
func canBeNumber(v interface{}) bool {
if v != nil {
switch reflect.TypeOf(v).Kind() {
case reflect.Float32, reflect.Float64:
fallthrough
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fallthrough
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
}
}
return false
}
func extract(from interface{}, it interface{}) (interface{}, error) {
if from != nil {
switch reflect.TypeOf(from).Kind() {
case reflect.Array, reflect.Slice, reflect.String:
i, err := cast(it)
if err != nil {
return nil, err
}
value := reflect.ValueOf(from).Index(int(i))
if value.IsValid() && value.CanInterface() {
return value.Interface(), nil
}
case reflect.Map:
value := reflect.ValueOf(from).MapIndex(reflect.ValueOf(it))
if value.IsValid() && value.CanInterface() {
return value.Interface(), nil
}
case reflect.Struct:
value := reflect.ValueOf(from).FieldByName(reflect.ValueOf(it).String())
if value.IsValid() && value.CanInterface() {
return value.Interface(), nil
}
}
}
return nil, fmt.Errorf("can't get %q from %T", it, from)
}
func contains(needle interface{}, array interface{}) (bool, error) {
if array != nil {
value := reflect.ValueOf(array)
switch reflect.TypeOf(array).Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < value.Len(); i++ {
value := value.Index(i)
if value.IsValid() && value.CanInterface() {
if equal(value.Interface(), needle) {
return true, nil
}
}
}
return false, nil
}
return false, fmt.Errorf("operator in not defined on %T", array)
}
return false, nil
}
func call(name string, fn interface{}, arguments []Node, env interface{}) (interface{}, error) {
in := make([]reflect.Value, 0)
for _, arg := range arguments {
a, err := Run(arg, env)
if err != nil {
return nil, err
}
in = append(in, reflect.ValueOf(a))
}
out := reflect.ValueOf(fn).Call(in)
if len(out) == 0 {
return nil, nil
} else if len(out) > 1 {
return nil, fmt.Errorf("func %q must return only one value", name)
}
if out[0].IsValid() && out[0].CanInterface() {
return out[0].Interface(), nil
}
return nil, nil
}