Skip to content
Closed
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
7 changes: 3 additions & 4 deletions builtin/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,11 +560,10 @@ func get(params ...any) (out any, err error) {
}

// Methods can be defined on any type.
if v.NumMethod() > 0 {
if runtime.MethodByNameHook != nil && v.NumMethod() > 0 {
if methodName, ok := i.(string); ok {
method := v.MethodByName(methodName)
if method.IsValid() {
return method.Interface(), nil
if m, ok := runtime.MethodByNameHook(v, methodName); ok {
return m, nil
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions checker/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
}
case *ast.MemberNode:
if name, ok := n.Property.(*ast.StringNode); ok {
base := n.Node.Type()
if base != nil && base.Kind() != reflect.Interface {
if m, ok := base.MethodByName(name.Value); ok {
return true, m.Index, name.Value
base := n.Node.Nature()
if base != nil && base.Kind != reflect.Interface {
if m, ok := base.MethodByName(c, name.Value); ok && m.TypeData != nil {
return true, m.MethodIndex, name.Value
}
}
}
Expand Down
15 changes: 14 additions & 1 deletion checker/nature/nature.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func ArrayFromType(c *Cache, t reflect.Type) Nature {
}

func (n *Nature) IsAny(c *Cache) bool {
return n.Type != nil && n.Kind == reflect.Interface && n.NumMethods(c) == 0
return n.Type != nil && n.Kind == reflect.Interface && n.Type.NumMethod() == 0
}

func (n *Nature) IsUnknown(c *Cache) bool {
Expand Down Expand Up @@ -294,7 +294,20 @@ func (n *Nature) NumMethods(c *Cache) int {
return 0
}

// MethodByNameHook, when non-nil, looks up a method on a Nature by name. If
// nil, expressions must use Function-registered callables instead.
var MethodByNameHook func(c *Cache, n *Nature, name string) (Nature, bool)

func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
if MethodByNameHook == nil {
return Nature{}, false
}
return MethodByNameHook(c, n, name)
}

// LookupMethod is the reference implementation of MethodByNameHook. It
// transitively reaches reflect.Type.Method via the methodset cache.
func LookupMethod(c *Cache, n *Nature, name string) (Nature, bool) {
if s := n.getMethodset(c); s != nil {
if m := s.method(c, name); m != nil {
return m.nature, true
Expand Down
9 changes: 6 additions & 3 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,12 @@ func (c *compiler) CallNode(node *ast.CallNode) {
switch callee := node.Callee.(type) {
case *ast.MemberNode:
if prop, ok := callee.Property.(*ast.StringNode); ok {
if _, ok = callee.Node.Type().MethodByName(prop.Value); ok && callee.Node.Type().Kind() != reflect.Interface {
fnInOffset = 1
fnNumIn--
base := callee.Node.Nature()
if base != nil && base.Kind != reflect.Interface {
if _, ok := base.MethodByName(c.ntCache, prop.Value); ok {
fnInOffset = 1
fnNumIn--
}
}
}
case *ast.IdentifierNode:
Expand Down
28 changes: 2 additions & 26 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/optimizer"
"github.com/expr-lang/expr/parser"
"github.com/expr-lang/expr/patcher"
"github.com/expr-lang/expr/vm"
)
Expand All @@ -25,7 +24,8 @@ type Option func(c *conf.Config)
// If struct is passed, all fields will be treated as variables,
// as well as all fields of embedded structs and struct itself.
// If map is passed, all items will be treated as variables.
// Methods defined on this type will be available as functions.
// Methods defined on this type will be available as functions,
// unless imported through github.com/expr-lang/expr/static.
func Env(env any) Option {
return func(c *conf.Config) {
c.WithEnv(env)
Expand Down Expand Up @@ -264,27 +264,3 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
func Run(program *vm.Program, env any) (any, error) {
return vm.Run(program, env)
}

// Eval parses, compiles and runs given input.
func Eval(input string, env any) (any, error) {
if _, ok := env.(Option); ok {
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
}

tree, err := parser.Parse(input)
if err != nil {
return nil, err
}

program, err := compiler.Compile(tree, nil)
if err != nil {
return nil, err
}

output, err := Run(program, env)
if err != nil {
return nil, err
}

return output, nil
}
71 changes: 71 additions & 0 deletions reflectmethods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package expr

import (
"fmt"
"reflect"

"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/compiler"
"github.com/expr-lang/expr/parser"
"github.com/expr-lang/expr/vm/runtime"
)

// This file installs the method-dispatch hooks that the shared packages
// (vm/runtime, checker/nature, builtin) consult to perform reflective method
// lookups. The hooks are installed only when the parent expr package is
// imported.
//
// All four reflect.* method-resolution call sites that the linker treats as
// REFLECTMETHOD live exclusively in this file (or in functions reachable
// only from this file's hooks):
//
// - reflect.Value.MethodByName in fetchMethodByName
// - reflect.Value.Method in fetchMethodIndexed
// - reflect.Type.Method in nature.LookupMethod (transitively)
// - reflect.Type.MethodByName not used

func init() {
runtime.MethodByNameHook = fetchMethodByName
runtime.MethodIndexedHook = fetchMethodIndexed
nature.MethodByNameHook = nature.LookupMethod
}

func fetchMethodByName(v reflect.Value, name string) (any, bool) {
method := v.MethodByName(name)
if method.IsValid() {
return method.Interface(), true
}
return nil, false
}

func fetchMethodIndexed(v reflect.Value, index int) (any, bool) {
method := v.Method(index)
if method.IsValid() {
return method.Interface(), true
}
return nil, false
}

// Eval parses, compiles and runs given input.
func Eval(input string, env any) (any, error) {
if _, ok := env.(Option); ok {
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
}

tree, err := parser.Parse(input)
if err != nil {
return nil, err
}

program, err := compiler.Compile(tree, nil)
if err != nil {
return nil, err
}

output, err := Run(program, env)
if err != nil {
return nil, err
}

return output, nil
}
11 changes: 11 additions & 0 deletions static/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package expr (imported as github.com/expr-lang/expr/static) is a
// dead-code-elimination-friendly entry point for expr.
//
// It exposes the same API as the parent expr, without the `Eval()` function. It
// does not trigger the Go linker's REFLECTMETHOD analysis and methods of every
// reachable type can be eliminated by dead-code elimination. Calling a method
// defined on the Env type does NOT work. Compilation fails with "unknown name
// <method>". To expose a function, use the Function option.
package expr

//go:generate cp ../expr.go expr.go
Loading
Loading