Skip to content

Commit

Permalink
Add ExprNative interface. Allows a type to present an expr native/fri…
Browse files Browse the repository at this point in the history
…endly value for evaluation.
  • Loading branch information
rrb3942 committed Nov 1, 2023
1 parent e3c2d0e commit 7e9f0ad
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 13 deletions.
87 changes: 87 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,90 @@ func Benchmark_reduce(b *testing.B) {

require.Equal(b, 5050, out.(int))
}

func Benchmark_nativeAdd(b *testing.B) {
env := make(map[string]any)

env["testOne"] = 1
env["testTwo"] = 2

program, err := expr.Compile("testOne + testTwo", expr.Env(env))
require.NoError(b, err)

var out any
v := vm.VM{}

b.ResetTimer()
for n := 0; n < b.N; n++ {
out, err = v.Run(program, env)
}
b.StopTimer()

require.NoError(b, err)
require.Equal(b, 3, out.(int))
}

func Benchmark_nativeEnabledAdd(b *testing.B) {
env := make(map[string]any)

env["testOne"] = 1
env["testTwo"] = 2

program, err := expr.Compile("testOne + testTwo", expr.ExprNative(true), expr.Env(env))
require.NoError(b, err)

var out any
v := vm.VM{}

b.ResetTimer()
for n := 0; n < b.N; n++ {
out, err = v.Run(program, env)
}
b.StopTimer()

require.NoError(b, err)
require.Equal(b, 3, out.(int))
}

func Benchmark_exprNativeAdd(b *testing.B) {
env := make(map[string]any)

env["testOne"] = &exprNativeInt{MyInt: 1}
env["testTwo"] = &exprNativeInt{MyInt: 2}

program, err := expr.Compile("testOne + testTwo", expr.ExprNative(true), expr.Env(env))
require.NoError(b, err)

var out any
v := vm.VM{}
b.ResetTimer()
for n := 0; n < b.N; n++ {
out, err = v.Run(program, env)
}
b.StopTimer()

require.NoError(b, err)
require.Equal(b, 3, out.(int))
}

func Benchmark_callAdd(b *testing.B) {
env := make(map[string]any)

env["testOne"] = &exprNativeInt{MyInt: 1}
env["testTwo"] = &exprNativeInt{MyInt: 2}

program, err := expr.Compile("testOne.Value() + testTwo.Value()", expr.Env(env))
require.NoError(b, err)

var out any
v := vm.VM{}

b.ResetTimer()
for n := 0; n < b.N; n++ {
out, err = v.Run(program, env)
}
b.StopTimer()

require.NoError(b, err)
require.Equal(b, 3, out.(int))
}
1 change: 1 addition & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
Arguments: c.arguments,
Functions: c.functions,
DebugInfo: c.debugInfo,
ExprNative: config.ExprNative,
}
return
}
Expand Down
3 changes: 2 additions & 1 deletion conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
ExpectAny bool
Optimize bool
Strict bool
ExprNative bool
ConstFns map[string]reflect.Value
Visitors []ast.Visitor
Functions map[string]*ast.Function
Expand Down Expand Up @@ -61,7 +62,7 @@ func (c *Config) WithEnv(env any) {
}

c.Env = env
c.Types = CreateTypesTable(env)
c.Types = CreateTypesTable(env, c.ExprNative)
c.MapEnv = mapEnv
c.DefaultType = mapValueType
c.Strict = true
Expand Down
14 changes: 12 additions & 2 deletions conf/types_table.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package conf

import (
"github.com/antonmedv/expr/vm"
"reflect"
)

Expand All @@ -20,7 +21,7 @@ type TypesTable map[string]Tag
//
// If map is passed, all items will be treated as variables
// (key as name, value as type).
func CreateTypesTable(i any) TypesTable {
func CreateTypesTable(i any, exprNative bool) TypesTable {
if i == nil {
return nil
}
Expand Down Expand Up @@ -57,7 +58,16 @@ func CreateTypesTable(i any) TypesTable {
if key.String() == "$env" { // Could check for all keywords here
panic("attempt to misuse env keyword as env map key")
}
types[key.String()] = Tag{Type: reflect.TypeOf(value.Interface())}

v := value.Interface()
if exprNative {
if ev, ok := v.(vm.ExprNative); ok {
types[key.String()] = Tag{Type: ev.ExprNativeType()}
continue
}
}

types[key.String()] = Tag{Type: reflect.TypeOf(v)}
}
}

Expand Down
2 changes: 1 addition & 1 deletion docgen/docgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func CreateDoc(i any) *Context {
PkgPath: dereference(reflect.TypeOf(i)).PkgPath(),
}

for name, t := range conf.CreateTypesTable(i) {
for name, t := range conf.CreateTypesTable(i, false) {
if t.Ambiguous {
continue
}
Expand Down
9 changes: 9 additions & 0 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ func Optimize(b bool) Option {
}
}

// ExprNative sets a flag in compiled program teling the runtime to use the value return by ExprNative interface instead of the direct variable
// This option must be passed BEFORE the Env() option during the compile phase or type checking will most likely be broken
// Only applies to map environments
func ExprNative(b bool) Option {
return func(c *conf.Config) {
c.ExprNative = b
}
}

// Patch adds visitor to list of visitors what will be applied before compiling AST to bytecode.
func Patch(visitor ast.Visitor) Option {
return func(c *conf.Config) {
Expand Down
62 changes: 62 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,68 @@ func ExampleAllowUndefinedVariables_zero_value_functions() {
// Output: [foo bar]
}

type exprNativeInt struct {
MyInt int
}

func (n *exprNativeInt) ExprNativeValue() any {
return n.MyInt
}

func (n *exprNativeInt) ExprNativeType() reflect.Type {
return reflect.TypeOf(n.MyInt)
}

func (n *exprNativeInt) Value() int {
return n.MyInt
}

func ExampleExprNative() {
env := make(map[string]any)

env["testOne"] = &exprNativeInt{MyInt: 1}
env["testTwo"] = &exprNativeInt{MyInt: 2}

program, err := expr.Compile("testOne + testTwo", expr.ExprNative(true), 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("%T(%v)", output, output)

// Output: int(3)
}

func ExampleCallAdd() {
env := make(map[string]any)

env["testOne"] = &exprNativeInt{MyInt: 1}
env["testTwo"] = &exprNativeInt{MyInt: 2}

program, err := expr.Compile("testOne.Value() + testTwo.Value()", 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("%T(%v)", output, output)

// Output: int(3)
}

type patcher struct{}

func (p *patcher) Visit(node *ast.Node) {
Expand Down
19 changes: 10 additions & 9 deletions vm/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import (
)

type Program struct {
Node ast.Node
Source *file.Source
Locations []file.Location
Variables []any
Constants []any
Bytecode []Opcode
Arguments []int
Functions []Function
DebugInfo map[string]string
Node ast.Node
Source *file.Source
Locations []file.Location
Variables []any
Constants []any
Bytecode []Opcode
Arguments []int
Functions []Function
DebugInfo map[string]string
ExprNative bool
}

func (program *Program) Disassemble() string {
Expand Down
17 changes: 17 additions & 0 deletions vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type VM struct {
debug bool
step chan struct{}
curr chan int
exprNative bool
memory uint
memoryBudget uint
}
Expand All @@ -47,6 +48,14 @@ type Scope struct {
Acc any
}

// ExprNative is an interface for a type to provide values that expr can use natively
type ExprNative interface {
// ExprNativeValue returns a native value that expr can use directly
ExprNativeValue() any
// ExprNativeType returns the reflect.Type of the type that will be returned by ExprNativeValue
ExprNativeType() reflect.Type
}

func Debug() *VM {
vm := &VM{
debug: true,
Expand Down Expand Up @@ -83,6 +92,7 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
vm.memoryBudget = MemoryBudget
vm.memory = 0
vm.ip = 0
vm.exprNative = program.ExprNative

for vm.ip < len(program.Bytecode) {
if vm.debug {
Expand Down Expand Up @@ -514,6 +524,12 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
}

func (vm *VM) push(value any) {
if vm.exprNative {
if ev, ok := value.(ExprNative); ok {
value = ev.ExprNativeValue()
}
}

vm.stack = append(vm.stack, value)
}

Expand All @@ -524,6 +540,7 @@ func (vm *VM) current() any {
func (vm *VM) pop() any {
value := vm.stack[len(vm.stack)-1]
vm.stack = vm.stack[:len(vm.stack)-1]

return value
}

Expand Down

0 comments on commit 7e9f0ad

Please sign in to comment.