Skip to content
Open
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
13 changes: 7 additions & 6 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package builtin

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
Expand Down Expand Up @@ -595,13 +596,13 @@ var Builtins = []*Function{
},
{
Name: "first",
Func: func(args ...any) (any, error) {
FuncWithContext: func(ctx context.Context, args ...any) (any, error) {
defer func() {
if r := recover(); r != nil {
return
}
}()
return runtime.Fetch(args[0], 0), nil
return runtime.FromContext(ctx).Fetch(args[0], 0), nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
Expand All @@ -618,13 +619,13 @@ var Builtins = []*Function{
},
{
Name: "last",
Func: func(args ...any) (any, error) {
FuncWithContext: func(ctx context.Context, args ...any) (any, error) {
defer func() {
if r := recover(); r != nil {
return
}
}()
return runtime.Fetch(args[0], -1), nil
return runtime.FromContext(ctx).Fetch(args[0], -1), nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
Expand All @@ -640,8 +641,8 @@ var Builtins = []*Function{
},
},
{
Name: "get",
Func: get,
Name: "get",
FuncWithContext: get,
},
{
Name: "take",
Expand Down
18 changes: 10 additions & 8 deletions builtin/function.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package builtin

import (
"context"
"reflect"
)

type Function struct {
Name string
Fast func(arg any) any
Func func(args ...any) (any, error)
Safe func(args ...any) (any, uint, error)
Types []reflect.Type
Validate func(args []reflect.Type) (reflect.Type, error)
Deref func(i int, arg reflect.Type) bool
Predicate bool
Name string
Fast func(arg any) any
Func func(args ...any) (any, error)
Safe func(args ...any) (any, uint, error)
FuncWithContext func(ctx context.Context, args ...any) (any, error)
Types []reflect.Type
Validate func(args []reflect.Type) (reflect.Type, error)
Deref func(i int, arg reflect.Type) bool
Predicate bool
}

func (f *Function) Type() reflect.Type {
Expand Down
16 changes: 9 additions & 7 deletions builtin/lib.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package builtin

import (
"context"
"fmt"
"math"
"reflect"
Expand Down Expand Up @@ -543,7 +544,7 @@ func flatten(arg reflect.Value, depth int) ([]any, error) {
return ret, nil
}

func get(params ...any) (out any, err error) {
func get(ctx context.Context, params ...any) (out any, err error) {
if len(params) < 2 {
return nil, fmt.Errorf("invalid number of arguments (expected 2, got %d)", len(params))
}
Expand Down Expand Up @@ -597,16 +598,17 @@ func get(params ...any) (out any, err error) {
case reflect.Struct:
fieldName := i.(string)
t := v.Type()
rtCtx := runtime.FromContext(ctx)
field, ok := t.FieldByNameFunc(func(name string) bool {
f, _ := t.FieldByName(name)
switch f.Tag.Get("expr") {
case "-":
tagName, ok := rtCtx.TagName(f.Tag)
if !ok {
return false
case fieldName:
return true
default:
return name == fieldName
}
if tagName != "" {
return tagName == fieldName
}
return name == fieldName
})
if ok && field.IsExported() {
value := v.FieldByIndex(field.Index)
Expand Down
7 changes: 7 additions & 0 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,13 @@ func (v *Checker) checkBuiltinGet(node *ast.BuiltinNode) Nature {
return v.error(node.Arguments[1], "cannot use %s to get an element from %s", prop.String(), base.String())
}
return base.Elem(&v.config.NtCache)
case reflect.Struct, reflect.Ptr:
if s, ok := node.Arguments[1].(*ast.StringNode); ok {
if nt, ok := base.FieldByName(&v.config.NtCache, s.Value); ok {
return nt
}
}
return Nature{}
}
return v.error(node.Arguments[0], "type %v does not support indexing", base.String())
}
Expand Down
6 changes: 6 additions & 0 deletions checker/nature/nature.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,16 @@ type TypeData struct {
// from the Nature type, they only describe. However, when receiving a Nature
// from one of those packages, the cache must be set immediately.
type Cache struct {
tag string
methods map[reflect.Type]*methodset
structs map[reflect.Type]Nature
}

// SetTag ensures the tag is set.
func (c *Cache) SetTag(tag string) {
c.tag = tag
}

// NatureOf returns a Nature describing "i". If "i" is nil then it returns a
// Nature describing the value "nil".
func (c *Cache) NatureOf(i any) Nature {
Expand Down
15 changes: 10 additions & 5 deletions checker/nature/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package nature

import (
"reflect"
"strings"

"github.com/expr-lang/expr/internal/deref"
)

func fieldName(fieldName string, tag reflect.StructTag) (string, bool) {
switch taggedName := tag.Get("expr"); taggedName {
func fieldName(name string, tag reflect.StructTag, tagKey string) (string, bool) {
tagVal := tag.Get(tagKey)
if i := strings.IndexByte(tagVal, ','); i >= 0 {
tagVal = tagVal[:i]
}
switch tagVal {
case "-":
return "", false
case "":
return fieldName, true
return name, true
default:
return taggedName, true
return tagVal, true
}
}

Expand Down Expand Up @@ -59,7 +64,7 @@ func (s *structData) structField(c *Cache, parentEmbed *structData, name string)
if !field.IsExported() {
continue
}
fName, ok := fieldName(field.Name, field.Tag)
fName, ok := fieldName(field.Name, field.Tag, c.tag)
if !ok || fName == "" {
// name can still be empty for a type created at runtime with
// reflect
Expand Down
19 changes: 17 additions & 2 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
debugInfo: make(map[string]string),
}

tag := conf.DefaultTag
if config != nil {
c.ntCache = &c.config.NtCache
tag = config.Tag
} else {
c.ntCache = new(Cache)
}
Expand Down Expand Up @@ -77,6 +79,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
c.functions,
c.debugInfo,
span,
tag,
)
return
}
Expand Down Expand Up @@ -172,8 +175,15 @@ func (c *compiler) addVariable(name string) int {
return c.variables - 1
}

// emitFunction adds builtin.Function.Func to the program.functions and emits call opcode.
// emitFunction adds builtin.Function.Func (or FuncWithContext) to the program and emits a call opcode.
func (c *compiler) emitFunction(fn *builtin.Function, argsLen int) {
if fn.FuncWithContext != nil {
id := c.addConstant(fn.FuncWithContext)
c.debugInfo[fmt.Sprintf("const_%d", id)] = fn.Name
c.emit(OpPush, id)
c.emit(OpCallContext, argsLen)
return
}
switch argsLen {
case 0:
c.emit(OpCall0, c.addFunction(fn.Name, fn.Func))
Expand Down Expand Up @@ -1149,6 +1159,11 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {

if id, ok := builtin.Index[node.Name]; ok {
f := builtin.Builtins[id]
if c.config != nil {
if overridden, ok := c.config.Builtins[node.Name]; ok {
f = overridden
}
}
for i, arg := range node.Arguments {
c.compile(arg)
argType := arg.Type()
Expand All @@ -1171,7 +1186,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpPush, id)
c.debugInfo[fmt.Sprintf("const_%d", id)] = node.Name
c.emit(OpCallSafe, len(node.Arguments))
} else if f.Func != nil {
} else {
c.emitFunction(f, len(node.Arguments))
}
return
Expand Down
16 changes: 15 additions & 1 deletion conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ var (

// DefaultMaxNodes represents default maximum allowed AST nodes by the compiler.
DefaultMaxNodes uint = 1e4

// DefaultTag defines the default tag to use to determine field names. Override
// with the WithTag method.
DefaultTag string = "expr"
)

type FunctionsTable map[string]*builtin.Function
Expand All @@ -36,6 +40,7 @@ type Config struct {
Builtins FunctionsTable
Disabled map[string]bool // disabled builtins
NtCache nature.Cache
Tag string
// DisableIfOperator disables the built-in `if ... { } else { }` operator syntax
// so that users can use a custom function named `if(...)` without conflicts.
// When enabled, the lexer treats `if`/`else` as identifiers and the parser
Expand All @@ -57,9 +62,17 @@ func CreateNew() *Config {
for _, f := range builtin.Builtins {
c.Builtins[f.Name] = f
}
c.SetTag(DefaultTag)
return c
}

// SetTag sets the struct tag key used for field name resolution in expressions.
// It updates the config, the nature cache, and the get() builtin atomically.
func (c *Config) SetTag(tag string) {
c.Tag = tag
c.NtCache.SetTag(tag)
}

// New creates new config with environment.
func New(env any) *Config {
c := CreateNew()
Expand All @@ -77,7 +90,8 @@ func (c *Config) ConstExpr(name string) {
if c.EnvObject == nil {
panic("no environment is specified for ConstExpr()")
}
fn := reflect.ValueOf(runtime.Fetch(c.EnvObject, name))
ctx := runtime.New(c.Tag)
fn := reflect.ValueOf(ctx.Fetch(c.EnvObject, name))
if fn.Kind() != reflect.Func {
panic(fmt.Errorf("const expression %q must be a function", name))
}
Expand Down
9 changes: 9 additions & 0 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ func Operator(operator string, fn ...string) Option {
}
}

// WithTag sets the struct tag key used for field name resolution in expressions.
// Defaults to "expr". Pass "json" to use JSON struct tags, etc.
// Fields tagged with "-" are hidden regardless of which tag is active.
func WithTag(name string) Option {
return func(c *conf.Config) {
c.SetTag(name)
}
}

// ConstExpr defines func expression as constant. If all argument to this function is constants,
// then it can be replaced by result of this func call on compile step.
func ConstExpr(fn string) Option {
Expand Down
Loading
Loading