Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package mexpr

import "testing"

func BenchmarkInternals(b *testing.B) {
b.Run("lexer-complex", func(b *testing.B) {
b.ReportAllocs()
expression := `foo.bar / (1 * 1024 * 1024) >= 1.0 and "v" in baz and baz.length > 3 and arr[2:].length == 1`
for n := 0; n < b.N; n++ {
l := lexer{expression: expression}
for {
tok, err := l.Next()
if err != nil {
b.Fatal(err)
}
if tok.Type == TokenEOF {
break
}
}
}
})

b.Run("resolve-lazy-value-non-function", func(b *testing.B) {
b.ReportAllocs()
input := map[string]any{"foo": "bar"}
for n := 0; n < b.N; n++ {
if _, ok := resolveLazyValue(input); ok {
b.Fatal("unexpected lazy value")
}
}
})

b.Run("resolve-lazy-value-number-func", func(b *testing.B) {
b.ReportAllocs()
input := func() int { return 42 }
for n := 0; n < b.N; n++ {
out, ok := resolveLazyValue(input)
if !ok || out.(int) != 42 {
b.Fatalf("unexpected lazy value result: %v %v", out, ok)
}
}
})

b.Run("deep-equal-number", func(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
if !deepEqual(1, 1.0) {
b.Fatal("expected equal numbers")
}
}
})
}
88 changes: 88 additions & 0 deletions conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,95 @@ func normalize(v any) any {
return v
}

func normalizedNumber(v any) (float64, bool) {
switch n := v.(type) {
case int:
return float64(n), true
case int8:
return float64(n), true
case int16:
return float64(n), true
case int32:
return float64(n), true
case int64:
return float64(n), true
case uint:
return float64(n), true
case uint8:
return float64(n), true
case uint16:
return float64(n), true
case uint32:
return float64(n), true
case uint64:
return float64(n), true
case float32:
return float64(n), true
case float64:
return n, true
case func() int:
return float64(n()), true
case func() int8:
return float64(n()), true
case func() int16:
return float64(n()), true
case func() int32:
return float64(n()), true
case func() int64:
return float64(n()), true
case func() uint:
return float64(n()), true
case func() uint8:
return float64(n()), true
case func() uint16:
return float64(n()), true
case func() uint32:
return float64(n()), true
case func() uint64:
return float64(n()), true
case func() float32:
return float64(n()), true
case func() float64:
return n(), true
}
return 0, false
}

func normalizedString(v any) (string, bool) {
switch s := v.(type) {
case string:
return s, true
case []byte:
return string(s), true
case func() string:
return s(), true
}
return "", false
}

func normalizedBool(v any) (bool, bool) {
switch b := v.(type) {
case bool:
return b, true
case func() bool:
return b(), true
}
return false, false
}

// deepEqual returns whether two values are deeply equal.
func deepEqual(left, right any) bool {
if leftNum, ok := normalizedNumber(left); ok {
rightNum, ok := normalizedNumber(right)
return ok && leftNum == rightNum
}
if leftStr, ok := normalizedString(left); ok {
rightStr, ok := normalizedString(right)
return ok && leftStr == rightStr
}
if leftBool, ok := normalizedBool(left); ok {
rightBool, ok := normalizedBool(right)
return ok && leftBool == rightBool
}
return recursiveDeepEqual(left, right)
}
31 changes: 27 additions & 4 deletions expr.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
// Package mexpr provides a simple expression parser.
package mexpr

func parseInterpreterOptions(options []InterpreterOption) (bool, bool) {
strict := false
unquoted := false
for _, opt := range options {
switch opt {
case StrictMode:
strict = true
case UnquotedStrings:
unquoted = true
}
}
return strict, unquoted
}

// Parse an expression and return the abstract syntax tree. If `types` is
// passed, it should be a set of representative example values for the input
// which will be used to type check the expression against.
func Parse(expression string, types any, options ...InterpreterOption) (*Node, Error) {
l := NewLexer(expression)
p := NewParser(l)
l := lexer{expression: expression}
p := parser{lexer: &l}
ast, err := p.Parse()
if err != nil {
return nil, err
Expand All @@ -22,13 +36,22 @@ func Parse(expression string, types any, options ...InterpreterOption) (*Node, E
// TypeCheck will take a parsed AST and type check against the given input
// structure with representative example values.
func TypeCheck(ast *Node, types any, options ...InterpreterOption) Error {
i := NewTypeChecker(ast, options...)
_, unquoted := parseInterpreterOptions(options)
i := typeChecker{
ast: ast,
unquoted: unquoted,
}
return i.Run(types)
}

// Run executes an AST with the given input and returns the output.
func Run(ast *Node, input any, options ...InterpreterOption) (any, Error) {
i := NewInterpreter(ast, options...)
strict, unquoted := parseInterpreterOptions(options)
i := interpreter{
ast: ast,
strict: strict,
unquoted: unquoted,
}
return i.Run(input)
}

Expand Down
45 changes: 45 additions & 0 deletions functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,51 @@ func getFunctionSchema(v any) (*schema, bool) {
}

func resolveLazyValue(v any) (any, bool) {
switch fn := v.(type) {
case nil:
return nil, false
case bool, string, []byte:
return nil, false
case int, int8, int16, int32, int64:
return nil, false
case uint, uint8, uint16, uint32, uint64:
return nil, false
case float32, float64:
return nil, false
case []any, []int, []float64, []string:
return nil, false
case map[string]any, map[any]any:
return nil, false
case func() bool:
return fn(), true
case func() int:
return fn(), true
case func() int8:
return fn(), true
case func() int16:
return fn(), true
case func() int32:
return fn(), true
case func() int64:
return fn(), true
case func() uint:
return fn(), true
case func() uint8:
return fn(), true
case func() uint16:
return fn(), true
case func() uint32:
return fn(), true
case func() uint64:
return fn(), true
case func() float32:
return fn(), true
case func() float64:
return fn(), true
case func() string:
return fn(), true
}

s, ok := getFunctionSchema(v)
if !ok || len(s.parameters) != 0 {
return nil, false
Expand Down
Loading
Loading