Improve Performance of the Type System (#824)

* export the checker and ensure no methods or fields are exported

* make Checker reusable

* add PatchAndCheck method to Checker, and reuse it in visitors

* avoid unnecessary map and consolidate structs

* make all Nature methods use pointer-receiver

* added cache for type and nature information

* optimize type and nature checks

* more aggressively cache some partial results and add cheap checks

* cache reused values

* reduce copying in Nature by breaking down optional fields

* enforce use of cache

* improve ergonomics of FromType and NatureOf

* improve struct method fetching

* make cache private to Nature

* first iteration improving struct reflect caching

* cleaned up struct fields data

* improve caching of functions data

* simplify checker code

* simplify and remove dead code

* add checker benchmarks

* address PR comments and minor improvements

* rename from SetCache to Bind

* make the benchmarks more fair

* fix benchmark option not being applied

* move Cache to be argument instead of field

* merge FuncData into Optional

* rename Optional to TypeData

* simplify struct logic

* cache builtin numeric types

---------

Co-authored-by: Anton Medvedev <[email protected]>
This commit is contained in:
Diego Augusto Molina
2025-09-18 14:38:27 +02:00
committed by GitHub
co-authored by Anton Medvedev
parent fd562c5c2a
commit a60d994e25
18 changed files with 1389 additions and 955 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ func (n *base) Type() reflect.Type {
// SetType sets the type of the node.
func (n *base) SetType(t reflect.Type) {
n.nature.Type = t
n.nature = nature.FromType(t)
}
// NilNode represents nil.
+497 -467
View File
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
package checker_test
import (
"runtime"
"testing"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/parser"
)
func BenchmarkChecker(b *testing.B) {
cases := []struct {
name, input string
}{
{"function calls", `
func(
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
),
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
),
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
)
)
`},
{"unary and binary operations", `
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1
`},
{"deep struct access", `
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a
`},
}
f := func(params ...any) (any, error) { return nil, nil }
env := map[string]any{
"a": new(recursive),
"b": true,
"func": f,
}
config := conf.New(env)
expr.Function("func", f, f)(config)
expr.ConstExpr("func")(config)
for _, c := range cases {
batchSize := 100_000
if batchSize > b.N {
batchSize = b.N
}
trees := make([]*parser.Tree, 0, batchSize)
for i := 0; i < batchSize; i++ {
tree, err := parser.ParseWithConfig(c.input, config)
if err != nil {
b.Fatal(err)
}
trees = append(trees, tree)
}
runtime.GC() // try to cleanup the mess from the initialization
b.Run("name="+c.name, func(b *testing.B) {
var err error
for i := 0; i < b.N; i++ {
j := i
if j < 0 || j >= len(trees) {
b.StopTimer()
invalidateTrees(trees...)
j = 0
b.StartTimer()
}
_, err = checker.Check(trees[j], config)
}
b.StopTimer()
if err != nil {
b.Fatal(err)
}
})
}
}
type visitorFunc func(*ast.Node)
func (f visitorFunc) Visit(node *ast.Node) { f(node) }
func invalidateTrees(trees ...*parser.Tree) {
for _, tree := range trees {
ast.Walk(&tree.Node, visitorFunc(func(node *ast.Node) {
(*node).SetNature(nature.Nature{})
}))
}
}
type recursive struct {
Inner *recursive `expr:"a"`
}
+15 -8
View File
@@ -136,6 +136,7 @@ func TestCheck(t *testing.T) {
{"(Embed).EmbedPointerEmbedInt > 0"},
}
c := new(checker.Checker)
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var err error
@@ -145,7 +146,7 @@ func TestCheck(t *testing.T) {
config := conf.New(mock.Env{})
expr.AsBool()(config)
_, err = checker.Check(tree, config)
_, err = c.Check(tree, config)
assert.NoError(t, err)
})
}
@@ -682,12 +683,13 @@ invalid operation: + (mismatched types int and bool) (1:6)
},
}
c := new(checker.Checker)
for _, tt := range errorTests {
t.Run(tt.code, func(t *testing.T) {
tree, err := parser.Parse(tt.code)
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(mock.Env{}))
_, err = c.Check(tree, conf.New(mock.Env{}))
if err == nil {
err = fmt.Errorf("<nil>")
}
@@ -839,6 +841,7 @@ func TestCheck_TypeWeights(t *testing.T) {
"Float32": float32(11),
"Float64": float64(12),
}
c := new(checker.Checker)
for a := range types {
for b := range types {
tree, err := parser.Parse(fmt.Sprintf("%s + %s", a, b))
@@ -846,7 +849,7 @@ func TestCheck_TypeWeights(t *testing.T) {
config := conf.New(types)
_, err = checker.Check(tree, config)
_, err = c.Check(tree, config)
require.NoError(t, err)
}
}
@@ -926,6 +929,7 @@ func TestCheck_Function_types_are_checked(t *testing.T) {
config := conf.CreateNew()
add(config)
c := new(checker.Checker)
tests := []string{
"add(1)",
@@ -938,7 +942,7 @@ func TestCheck_Function_types_are_checked(t *testing.T) {
tree, err := parser.Parse(test)
require.NoError(t, err)
_, err = checker.Check(tree, config)
_, err = c.Check(tree, config)
require.NoError(t, err)
require.Equal(t, reflect.Int, tree.Node.Type().Kind())
})
@@ -948,7 +952,7 @@ func TestCheck_Function_types_are_checked(t *testing.T) {
tree, err := parser.Parse("add(1, '2')")
require.NoError(t, err)
_, err = checker.Check(tree, config)
_, err = c.Check(tree, config)
require.Error(t, err)
require.Equal(t, "cannot use string as argument (type int) to call add (1:8)\n | add(1, '2')\n | .......^", err.Error())
})
@@ -1046,12 +1050,13 @@ func TestCheck_env_keyword(t *testing.T) {
{`$env[name]`, reflect.Interface},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
rtype, err := checker.Check(tree, conf.New(env))
rtype, err := c.Check(tree, conf.New(env))
require.NoError(t, err)
require.True(t, rtype.Kind() == test.want, fmt.Sprintf("expected %s, got %s", test.want, rtype.Kind()))
})
@@ -1067,12 +1072,13 @@ func TestCheck_builtin_without_call(t *testing.T) {
{`string.A`, "type func(interface {}) string has no field A (1:8)\n | string.A\n | .......^"},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(nil))
_, err = c.Check(tree, conf.New(nil))
require.Error(t, err)
require.Equal(t, test.err, err.Error())
})
@@ -1134,13 +1140,14 @@ func TestCheck_types(t *testing.T) {
{`arr | filter(.value contains "a") | filter(.value == 0)`, `invalid operation: == (mismatched types string and int)`},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.code, func(t *testing.T) {
tree, err := parser.Parse(test.code)
require.NoError(t, err)
config := conf.New(env)
_, err = checker.Check(tree, config)
_, err = c.Check(tree, config)
if test.err == noerr {
require.NoError(t, err)
} else {
+11 -14
View File
@@ -8,22 +8,19 @@ import (
"github.com/expr-lang/expr/vm"
)
func FieldIndex(env Nature, node ast.Node) (bool, []int, string) {
func FieldIndex(c *Cache, env Nature, node ast.Node) (bool, []int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if env.Kind() == reflect.Struct {
if field, ok := env.Get(n.Value); ok && len(field.FieldIndex) > 0 {
return true, field.FieldIndex, n.Value
}
if idx, ok := env.FieldIndex(c, n.Value); ok {
return true, idx, n.Value
}
case *ast.MemberNode:
base := n.Node.Nature()
base = base.Deref()
if base.Kind() == reflect.Struct {
base = base.Deref(c)
if base.Kind == reflect.Struct {
if prop, ok := n.Property.(*ast.StringNode); ok {
name := prop.Value
if field, ok := base.FieldByName(name); ok {
return true, field.FieldIndex, name
if idx, ok := base.FieldIndex(c, prop.Value); ok {
return true, idx, prop.Value
}
}
}
@@ -31,11 +28,11 @@ func FieldIndex(env Nature, node ast.Node) (bool, []int, string) {
return false, nil, ""
}
func MethodIndex(env Nature, node ast.Node) (bool, int, string) {
func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if env.Kind() == reflect.Struct {
if m, ok := env.Get(n.Value); ok {
if env.Kind == reflect.Struct {
if m, ok := env.Get(c, n.Value); ok && m.TypeData != nil {
return m.Method, m.MethodIndex, n.Value
}
}
@@ -121,7 +118,7 @@ func IsFastFunc(fn reflect.Type, method bool) bool {
fn.NumOut() == 1 &&
fn.Out(0).Kind() == reflect.Interface {
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
if kind(rest) == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
if rest != nil && rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
return true
}
}
+464 -158
View File
@@ -1,247 +1,450 @@
package nature
import (
"fmt"
"reflect"
"time"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/internal/deref"
)
var (
unknown = Nature{}
intType = reflect.TypeOf(0)
floatType = reflect.TypeOf(float64(0))
arrayType = reflect.TypeOf([]any{})
timeType = reflect.TypeOf(time.Time{})
durationType = reflect.TypeOf(time.Duration(0))
builtinInt = map[reflect.Type]struct{}{
reflect.TypeOf(int(0)): {},
reflect.TypeOf(int8(0)): {},
reflect.TypeOf(int16(0)): {},
reflect.TypeOf(int32(0)): {},
reflect.TypeOf(int64(0)): {},
reflect.TypeOf(uintptr(0)): {},
reflect.TypeOf(uint(0)): {},
reflect.TypeOf(uint8(0)): {},
reflect.TypeOf(uint16(0)): {},
reflect.TypeOf(uint32(0)): {},
reflect.TypeOf(uint64(0)): {},
}
builtinFloat = map[reflect.Type]struct{}{
reflect.TypeOf(float32(0)): {},
reflect.TypeOf(float64(0)): {},
}
)
type NatureCheck int
const (
_ NatureCheck = iota
BoolCheck
StringCheck
IntegerCheck
NumberCheck
MapCheck
ArrayCheck
TimeCheck
DurationCheck
)
type Nature struct {
Type reflect.Type // Type of the value. If nil, then value is unknown.
Func *builtin.Function // Used to pass function type from callee to CallNode.
ArrayOf *Nature // Elem nature of array type (usually Type is []any, but ArrayOf can be any nature).
PredicateOut *Nature // Out nature of predicate.
// The order of the fields matter, check alignment before making changes.
Type reflect.Type // Type of the value. If nil, then value is unknown.
Kind reflect.Kind // Kind of the value.
*TypeData
// Ref is a reference used for multiple, disjoint purposes. When the Nature
// is for a:
// - Predicate: then Ref is the nature of the Out of the predicate.
// - Array-like types: then Ref is the Elem nature of array type (usually Type is []any, but ArrayOf can be any nature).
Ref *Nature
Nil bool // If value is nil.
Strict bool // If map is types.StrictMap.
Method bool // If value retrieved from method. Usually used to determine amount of in arguments.
IsInteger bool // If it's a builtin integer or unsigned integer type.
IsFloat bool // If it's a builtin float type.
}
type TypeData struct {
methodset *methodset // optional to avoid the map in *Cache
*structData
// map-only data
Fields map[string]Nature // Fields of map type.
DefaultMapValue *Nature // Default value of map type.
Strict bool // If map is types.StrictMap.
Nil bool // If value is nil.
Method bool // If value retrieved from method. Usually used to determine amount of in arguments.
// callable-only data
Func *builtin.Function // Used to pass function type from callee to CallNode.
MethodIndex int // Index of method in type.
FieldIndex []int // Index of field in type.
inElem, outZero *Nature
numIn, numOut int
isVariadic bool
isVariadicSet bool
numInSet bool
numOutSet bool
}
func (n Nature) IsAny() bool {
return n.Kind() == reflect.Interface && n.NumMethods() == 0
// Cache is a shared cache of type information. It is only used in the stages
// where type information becomes relevant, so packages like ast, parser, types,
// and lexer do not need to use the cache because they don't need any service
// 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 {
methods map[reflect.Type]*methodset
structs map[reflect.Type]Nature
}
func (n Nature) IsUnknown() bool {
switch {
case n.Type == nil && !n.Nil:
return true
case n.IsAny():
return true
// 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 {
// reflect.TypeOf(nil) returns nil, but in FromType we want to differentiate
// what nil means for us
if i == nil {
return Nature{Nil: true}
}
return false
return c.FromType(reflect.TypeOf(i))
}
func (n Nature) String() string {
// FromType returns a Nature describing a value of type "t". If "t" is nil then
// it returns a Nature describing an unknown value.
func (c *Cache) FromType(t reflect.Type) Nature {
if t == nil {
return Nature{}
}
var td *TypeData
var isInteger, isFloat bool
k := t.Kind()
switch k {
case reflect.Struct:
// c can be nil when we call the package function FromType, which uses a
// nil *Cache to call this method.
if c != nil {
return c.getStruct(t)
}
fallthrough
case reflect.Func:
td = new(TypeData)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
_, isInteger = builtinInt[t]
case reflect.Float32, reflect.Float64:
_, isFloat = builtinFloat[t]
}
return Nature{
Type: t,
Kind: k,
TypeData: td,
IsInteger: isInteger,
IsFloat: isFloat,
}
}
func (c *Cache) getStruct(t reflect.Type) Nature {
if c.structs == nil {
c.structs = map[reflect.Type]Nature{}
} else if nt, ok := c.structs[t]; ok {
return nt
}
nt := Nature{
Type: t,
Kind: reflect.Struct,
TypeData: &TypeData{
structData: &structData{
rType: t,
numField: t.NumField(),
anonIdx: -1, // do not lookup embedded fields yet
},
},
}
c.structs[t] = nt
return nt
}
func (c *Cache) getMethodset(t reflect.Type, k reflect.Kind) *methodset {
if t == nil || c == nil {
return nil
}
if c.methods == nil {
c.methods = map[reflect.Type]*methodset{
t: nil,
}
} else if s, ok := c.methods[t]; ok {
return s
}
numMethod := t.NumMethod()
if numMethod < 1 {
c.methods[t] = nil // negative cache
return nil
}
s := &methodset{
rType: t,
kind: k,
numMethod: numMethod,
}
c.methods[t] = s
return s
}
// NatureOf calls NatureOf on a nil *Cache. See the comment on Cache.
func NatureOf(i any) Nature {
var c *Cache
return c.NatureOf(i)
}
// FromType calls FromType on a nil *Cache. See the comment on Cache.
func FromType(t reflect.Type) Nature {
var c *Cache
return c.FromType(t)
}
func ArrayFromType(c *Cache, t reflect.Type) Nature {
elem := c.FromType(t)
nt := c.FromType(arrayType)
nt.Ref = &elem
return nt
}
func (n *Nature) IsAny(c *Cache) bool {
return n.Type != nil && n.Kind == reflect.Interface && n.NumMethods(c) == 0
}
func (n *Nature) IsUnknown(c *Cache) bool {
return n.Type == nil && !n.Nil || n.IsAny(c)
}
func (n *Nature) String() string {
if n.Type != nil {
return n.Type.String()
}
return "unknown"
}
func (n Nature) Deref() Nature {
if n.Type != nil {
n.Type = deref.Type(n.Type)
func (n *Nature) Deref(c *Cache) Nature {
t, _, changed := deref.TypeKind(n.Type, n.Kind)
if !changed {
return *n
}
return n
return c.FromType(t)
}
func (n Nature) Kind() reflect.Kind {
if n.Type != nil {
return n.Type.Kind()
func (n *Nature) Key(c *Cache) Nature {
if n.Kind == reflect.Map {
return c.FromType(n.Type.Key())
}
return reflect.Invalid
return Nature{}
}
func (n Nature) Key() Nature {
if n.Kind() == reflect.Map {
return Nature{Type: n.Type.Key()}
}
return unknown
}
func (n Nature) Elem() Nature {
switch n.Kind() {
func (n *Nature) Elem(c *Cache) Nature {
switch n.Kind {
case reflect.Ptr:
return Nature{Type: n.Type.Elem()}
return c.FromType(n.Type.Elem())
case reflect.Map:
if n.DefaultMapValue != nil {
if n.TypeData != nil && n.DefaultMapValue != nil {
return *n.DefaultMapValue
}
return Nature{Type: n.Type.Elem()}
case reflect.Array, reflect.Slice:
if n.ArrayOf != nil {
return *n.ArrayOf
return c.FromType(n.Type.Elem())
case reflect.Slice, reflect.Array:
if n.Ref != nil {
return *n.Ref
}
return Nature{Type: n.Type.Elem()}
return c.FromType(n.Type.Elem())
}
return unknown
return Nature{}
}
func (n Nature) AssignableTo(nt Nature) bool {
func (n *Nature) AssignableTo(nt Nature) bool {
if n.Nil {
// Untyped nil is assignable to any interface, but implements only the empty interface.
if nt.IsAny() {
switch nt.Kind {
case reflect.Pointer, reflect.Interface, reflect.Chan, reflect.Func,
reflect.Map, reflect.Slice:
// nil can be assigned to these kinds
return true
}
}
if n.Type == nil || nt.Type == nil {
if n.Type == nil || nt.Type == nil ||
n.Kind != nt.Kind && nt.Kind != reflect.Interface {
return false
}
return n.Type.AssignableTo(nt.Type)
}
func (n Nature) NumMethods() int {
if n.Type == nil {
return 0
func (n *Nature) getMethodset(c *Cache) *methodset {
if n.TypeData != nil && n.TypeData.methodset != nil {
return n.TypeData.methodset
}
return n.Type.NumMethod()
s := c.getMethodset(n.Type, n.Kind)
if n.TypeData != nil {
n.TypeData.methodset = s // cache locally if possible
}
return s
}
func (n Nature) MethodByName(name string) (Nature, bool) {
if n.Type == nil {
return unknown, false
}
method, ok := n.Type.MethodByName(name)
if !ok {
return unknown, false
}
if n.Type.Kind() == reflect.Interface {
// In case of interface type method will not have a receiver,
// and to prevent checker decreasing numbers of in arguments
// return method type as not method (second argument is false).
// Also, we can not use m.Index here, because it will be
// different indexes for different types which implement
// the same interface.
return Nature{Type: method.Type}, true
} else {
return Nature{
Type: method.Type,
Method: true,
MethodIndex: method.Index,
}, true
func (n *Nature) NumMethods(c *Cache) int {
if s := n.getMethodset(c); s != nil {
return s.numMethod
}
return 0
}
func (n Nature) NumIn() int {
if n.Type == nil {
return 0
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
if s := n.getMethodset(c); s != nil {
if m := s.method(c, name); m != nil {
return m.nature, true
}
}
return n.Type.NumIn()
return Nature{}, false
}
func (n Nature) In(i int) Nature {
if n.Type == nil {
return unknown
func (n *Nature) NumIn() int {
if n.numInSet {
return n.numIn
}
return Nature{Type: n.Type.In(i)}
n.numInSet = true
n.numIn = n.Type.NumIn()
return n.numIn
}
func (n Nature) NumOut() int {
if n.Type == nil {
return 0
func (n *Nature) InElem(c *Cache, i int) Nature {
if n.inElem == nil {
n2 := c.FromType(n.Type.In(i))
n2 = n2.Elem(c)
n.inElem = &n2
}
return n.Type.NumOut()
return *n.inElem
}
func (n Nature) Out(i int) Nature {
if n.Type == nil {
return unknown
}
return Nature{Type: n.Type.Out(i)}
func (n *Nature) In(c *Cache, i int) Nature {
return c.FromType(n.Type.In(i))
}
func (n Nature) IsVariadic() bool {
if n.Type == nil {
return false
}
return n.Type.IsVariadic()
}
func (n Nature) FieldByName(name string) (Nature, bool) {
if n.Type == nil {
return unknown, false
}
field, ok := fetchField(n.Type, name)
return Nature{Type: field.Type, FieldIndex: field.Index}, ok
}
func (n Nature) PkgPath() string {
if n.Type == nil {
return ""
}
return n.Type.PkgPath()
}
func (n Nature) IsFastMap() bool {
if n.Type == nil {
return false
}
if n.Type.Kind() == reflect.Map &&
n.Type.Key().Kind() == reflect.String &&
n.Type.Elem().Kind() == reflect.Interface {
return true
func (n *Nature) IsFirstArgUnknown(c *Cache) bool {
if n.Type != nil {
n2 := c.FromType(n.Type.In(0))
return n2.IsUnknown(c)
}
return false
}
func (n Nature) Get(name string) (Nature, bool) {
if n.Type == nil {
return unknown, false
func (n *Nature) NumOut() int {
if n.numOutSet {
return n.numOut
}
if m, ok := n.MethodByName(name); ok {
return m, true
}
t := deref.Type(n.Type)
switch t.Kind() {
case reflect.Struct:
if f, ok := fetchField(t, name); ok {
return Nature{
Type: f.Type,
FieldIndex: f.Index,
}, true
}
case reflect.Map:
if f, ok := n.Fields[name]; ok {
return f, true
}
}
return unknown, false
n.numOutSet = true
n.numOut = n.Type.NumOut()
return n.numOut
}
func (n Nature) All() map[string]Nature {
func (n *Nature) Out(c *Cache, i int) Nature {
if i != 0 {
return n.out(c, i)
}
if n.outZero != nil {
return *n.outZero
}
nt := n.out(c, 0)
n.outZero = &nt
return nt
}
func (n *Nature) out(c *Cache, i int) Nature {
if n.Type == nil {
return Nature{}
}
return c.FromType(n.Type.Out(i))
}
func (n *Nature) IsVariadic() bool {
if n.isVariadicSet {
return n.isVariadic
}
n.isVariadicSet = true
n.isVariadic = n.Type.IsVariadic()
return n.isVariadic
}
func (n *Nature) FieldByName(c *Cache, name string) (Nature, bool) {
if n.Kind != reflect.Struct {
return Nature{}, false
}
var sd *structData
if n.TypeData != nil && n.structData != nil {
sd = n.structData
} else {
sd = c.getStruct(n.Type).structData
}
if sf := sd.structField(c, nil, name); sf != nil {
return sf.Nature, true
}
return Nature{}, false
}
func (n *Nature) IsFastMap() bool {
return n.Type != nil &&
n.Type.Kind() == reflect.Map &&
n.Type.Key().Kind() == reflect.String &&
n.Type.Elem().Kind() == reflect.Interface
}
func (n *Nature) Get(c *Cache, name string) (Nature, bool) {
if n.Kind == reflect.Map && n.TypeData != nil {
f, ok := n.Fields[name]
return f, ok
}
return n.getSlow(c, name)
}
func (n *Nature) getSlow(c *Cache, name string) (Nature, bool) {
if nt, ok := n.MethodByName(c, name); ok {
return nt, true
}
if n.Kind == reflect.Struct {
if sf := n.structField(c, nil, name); sf != nil {
return sf.Nature, true
}
}
return Nature{}, false
}
func (n *Nature) FieldIndex(c *Cache, name string) ([]int, bool) {
if n.Kind != reflect.Struct {
return nil, false
}
if sf := n.structField(c, nil, name); sf != nil {
return sf.Index, true
}
return nil, false
}
func (n *Nature) All(c *Cache) map[string]Nature {
table := make(map[string]Nature)
if n.Type == nil {
return table
}
for i := 0; i < n.Type.NumMethod(); i++ {
for i := 0; i < n.NumMethods(c); i++ {
method := n.Type.Method(i)
table[method.Name] = Nature{
Type: method.Type,
Method: true,
MethodIndex: method.Index,
nt := c.FromType(method.Type)
if nt.TypeData == nil {
nt.TypeData = new(TypeData)
}
nt.Method = true
nt.MethodIndex = method.Index
table[method.Name] = nt
}
t := deref.Type(n.Type)
switch t.Kind() {
case reflect.Struct:
for name, nt := range StructFields(t) {
for name, nt := range StructFields(c, t) {
if _, ok := table[name]; ok {
continue
}
@@ -249,13 +452,116 @@ func (n Nature) All() map[string]Nature {
}
case reflect.Map:
for key, nt := range n.Fields {
if _, ok := table[key]; ok {
continue
if n.TypeData != nil {
for key, nt := range n.Fields {
if _, ok := table[key]; ok {
continue
}
table[key] = nt
}
table[key] = nt
}
}
return table
}
func (n *Nature) IsNumber() bool {
return n.IsInteger || n.IsFloat
}
func (n *Nature) PromoteNumericNature(c *Cache, rhs Nature) Nature {
if n.IsUnknown(c) || rhs.IsUnknown(c) {
return Nature{}
}
if n.IsFloat || rhs.IsFloat {
return c.FromType(floatType)
}
return c.FromType(intType)
}
func (n *Nature) IsTime() bool {
return n.Type == timeType
}
func (n *Nature) IsDuration() bool {
return n.Type == durationType
}
func (n *Nature) IsBool() bool {
return n.Kind == reflect.Bool
}
func (n *Nature) IsString() bool {
return n.Kind == reflect.String
}
func (n *Nature) IsArray() bool {
return n.Kind == reflect.Slice || n.Kind == reflect.Array
}
func (n *Nature) IsMap() bool {
return n.Kind == reflect.Map
}
func (n *Nature) IsStruct() bool {
return n.Kind == reflect.Struct
}
func (n *Nature) IsFunc() bool {
return n.Kind == reflect.Func
}
func (n *Nature) IsPointer() bool {
return n.Kind == reflect.Ptr
}
func (n *Nature) IsAnyOf(cs ...NatureCheck) bool {
var result bool
for i := 0; i < len(cs) && !result; i++ {
switch cs[i] {
case BoolCheck:
result = n.IsBool()
case StringCheck:
result = n.IsString()
case IntegerCheck:
result = n.IsInteger
case NumberCheck:
result = n.IsNumber()
case MapCheck:
result = n.IsMap()
case ArrayCheck:
result = n.IsArray()
case TimeCheck:
result = n.IsTime()
case DurationCheck:
result = n.IsDuration()
default:
panic(fmt.Sprintf("unknown check value %d", cs[i]))
}
}
return result
}
func (n *Nature) ComparableTo(c *Cache, rhs Nature) bool {
return n.IsUnknown(c) || rhs.IsUnknown(c) ||
n.Nil || rhs.Nil ||
n.IsNumber() && rhs.IsNumber() ||
n.IsDuration() && rhs.IsDuration() ||
n.IsTime() && rhs.IsTime() ||
n.IsArray() && rhs.IsArray() ||
n.AssignableTo(rhs)
}
func (n *Nature) MaybeCompatible(c *Cache, rhs Nature, cs ...NatureCheck) bool {
nIsUnknown := n.IsUnknown(c)
rshIsUnknown := rhs.IsUnknown(c)
return nIsUnknown && rshIsUnknown ||
nIsUnknown && rhs.IsAnyOf(cs...) ||
rshIsUnknown && n.IsAnyOf(cs...)
}
func (n *Nature) MakeArrayOf(c *Cache) Nature {
nt := c.FromType(arrayType)
nt.Ref = n
return nt
}
+205 -62
View File
@@ -6,84 +6,227 @@ import (
"github.com/expr-lang/expr/internal/deref"
)
func fieldName(field reflect.StructField) (string, bool) {
switch taggedName := field.Tag.Get("expr"); taggedName {
func fieldName(fieldName string, tag reflect.StructTag) (string, bool) {
switch taggedName := tag.Get("expr"); taggedName {
case "-":
return "", false
case "":
return field.Name, true
return fieldName, true
default:
return taggedName, true
}
}
func fetchField(t reflect.Type, name string) (reflect.StructField, bool) {
// If t is not a struct, early return.
if t.Kind() != reflect.Struct {
return reflect.StructField{}, false
}
type structData struct {
rType reflect.Type
fields map[string]*structField
numField, ownIdx, anonIdx int
// First check all structs fields.
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Search all fields, even embedded structs.
if n, ok := fieldName(field); ok && n == name {
return field, true
}
}
// Second check fields of embedded structs.
for i := 0; i < t.NumField(); i++ {
anon := t.Field(i)
if anon.Anonymous {
anonType := anon.Type
if anonType.Kind() == reflect.Pointer {
anonType = anonType.Elem()
}
if field, ok := fetchField(anonType, name); ok {
field.Index = append(anon.Index, field.Index...)
return field, true
}
}
}
return reflect.StructField{}, false
curParent, curChild *structData
curChildIndex []int
}
func StructFields(t reflect.Type) map[string]Nature {
table := make(map[string]Nature)
type structField struct {
Nature
Index []int
}
t = deref.Type(t)
func (s *structData) finished() bool {
return s.ownIdx >= s.numField && // no own fields left to visit
s.anonIdx >= s.numField && // no embedded fields to visit
s.curChild == nil // no child in process of visiting
}
func (s *structData) structField(c *Cache, parentEmbed *structData, name string) *structField {
if s.fields == nil {
if s.numField > 0 {
s.fields = make(map[string]*structField, s.numField)
}
} else if f := s.fields[name]; f != nil {
return f
}
if s.finished() {
return nil
}
// Lookup own fields first.
for ; s.ownIdx < s.numField; s.ownIdx++ {
field := s.rType.Field(s.ownIdx)
// BUG: we should skip if !field.IsExported() here
if field.Anonymous && s.anonIdx < 0 {
// start iterating anon fields on the first instead of zero
s.anonIdx = s.ownIdx
}
fName, ok := fieldName(field.Name, field.Tag)
if !ok || fName == "" {
// name can still be empty for a type created at runtime with
// reflect
continue
}
nt := c.FromType(field.Type)
sf := &structField{
Nature: nt,
Index: field.Index,
}
s.fields[fName] = sf
if parentEmbed != nil {
parentEmbed.trySet(fName, sf)
}
if fName == name {
return sf
}
}
if s.curChild != nil {
sf := s.findInEmbedded(c, parentEmbed, s.curChild, s.curChildIndex, name)
if sf != nil {
return sf
}
}
// Lookup embedded fields through anon own fields
for ; s.anonIdx >= 0 && s.anonIdx < s.numField; s.anonIdx++ {
field := s.rType.Field(s.anonIdx)
// we do enter embedded non-exported types because they could contain
// exported fields
if !field.Anonymous {
continue
}
t, k, _ := deref.TypeKind(field.Type, field.Type.Kind())
if k != reflect.Struct {
continue
}
childEmbed := c.getStruct(t).structData
sf := s.findInEmbedded(c, parentEmbed, childEmbed, field.Index, name)
if sf != nil {
return sf
}
}
return nil
}
func (s *structData) findInEmbedded(
c *Cache,
parentEmbed, childEmbed *structData,
childIndex []int,
name string,
) *structField {
// Set current parent/child data. This allows trySet to handle child fields
// and add them to our struct and to the parent as well if needed
s.curParent = parentEmbed
s.curChild = childEmbed
s.curChildIndex = childIndex
defer func() {
// Ensure to cleanup references
s.curParent = nil
if childEmbed.finished() {
// If the child can still have more fields to explore then keep it
// referened to look it up again if we need to
s.curChild = nil
s.curChildIndex = nil
}
}()
// See if the child has already cached its fields. This is still important
// to check even if it's the s.unfinishedEmbedded because it may have
// explored new fields since the last time we visited it
for name, sf := range childEmbed.fields {
s.trySet(name, sf)
}
// Recheck if we have what we needed from the above sync
if sf := s.fields[name]; sf != nil {
return sf
}
// Try finding in the child again in case it hasn't finished
if !childEmbed.finished() {
if childEmbed.structField(c, s, name) != nil {
return s.fields[name]
}
}
return nil
}
func (s *structData) trySet(name string, sf *structField) {
if _, ok := s.fields[name]; ok {
return
}
sf = &structField{
Nature: sf.Nature,
Index: append(s.curChildIndex, sf.Index...),
}
s.fields[name] = sf
if s.curParent != nil {
s.curParent.trySet(name, sf)
}
}
func StructFields(c *Cache, t reflect.Type) map[string]Nature {
table := make(map[string]Nature)
if t == nil {
return table
}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Anonymous {
for name, typ := range StructFields(f.Type) {
if _, ok := table[name]; ok {
continue
}
typ.FieldIndex = append(f.Index, typ.FieldIndex...)
table[name] = typ
}
}
name, ok := fieldName(f)
if !ok {
continue
}
table[name] = Nature{
Type: f.Type,
FieldIndex: f.Index,
}
t, k, _ := deref.TypeKind(t, t.Kind())
if k == reflect.Struct {
// lookup for a field with an empty name, which will cause to never find a
// match, meaning everything will have been cached.
sd := c.getStruct(t).structData
sd.structField(c, nil, "")
for name, sf := range sd.fields {
table[name] = sf.Nature
}
}
return table
}
type methodset struct {
rType reflect.Type
kind reflect.Kind
methods map[string]*method
numMethod, idx int
}
type method struct {
reflect.Method
nature Nature
}
func (s *methodset) method(c *Cache, name string) *method {
if s.methods == nil {
s.methods = make(map[string]*method, s.numMethod)
} else if m := s.methods[name]; m != nil {
return m
}
for ; s.idx < s.numMethod; s.idx++ {
rm := s.rType.Method(s.idx)
if !rm.IsExported() {
continue
}
nt := c.FromType(rm.Type)
if s.rType.Kind() != reflect.Interface {
nt.Method = true
nt.MethodIndex = rm.Index
// In case of interface type method will not have a receiver,
// and to prevent checker decreasing numbers of in arguments
// return method type as not method (second argument is false).
// Also, we can not use m.Index here, because it will be
// different indexes for different types which implement
// the same interface.
}
m := &method{
Method: rm,
nature: nt,
}
s.methods[rm.Name] = m
if rm.Name == name {
return m
}
}
return nil
}
-190
View File
@@ -1,190 +0,0 @@
package checker
import (
"reflect"
"time"
. "github.com/expr-lang/expr/checker/nature"
)
var (
unknown = Nature{}
nilNature = Nature{Nil: true}
boolNature = Nature{Type: reflect.TypeOf(true)}
integerNature = Nature{Type: reflect.TypeOf(0)}
floatNature = Nature{Type: reflect.TypeOf(float64(0))}
stringNature = Nature{Type: reflect.TypeOf("")}
arrayNature = Nature{Type: reflect.TypeOf([]any{})}
mapNature = Nature{Type: reflect.TypeOf(map[string]any{})}
timeNature = Nature{Type: reflect.TypeOf(time.Time{})}
durationNature = Nature{Type: reflect.TypeOf(time.Duration(0))}
)
var (
anyType = reflect.TypeOf(new(any)).Elem()
timeType = reflect.TypeOf(time.Time{})
durationType = reflect.TypeOf(time.Duration(0))
arrayType = reflect.TypeOf([]any{})
)
func arrayOf(nt Nature) Nature {
return Nature{
Type: arrayType,
ArrayOf: &nt,
}
}
func isNil(nt Nature) bool {
return nt.Nil
}
func combined(l, r Nature) Nature {
if isUnknown(l) || isUnknown(r) {
return unknown
}
if isFloat(l) || isFloat(r) {
return floatNature
}
return integerNature
}
func anyOf(nt Nature, fns ...func(Nature) bool) bool {
for _, fn := range fns {
if fn(nt) {
return true
}
}
return false
}
func or(l, r Nature, fns ...func(Nature) bool) bool {
if isUnknown(l) && isUnknown(r) {
return true
}
if isUnknown(l) && anyOf(r, fns...) {
return true
}
if isUnknown(r) && anyOf(l, fns...) {
return true
}
return false
}
func isUnknown(nt Nature) bool {
return nt.IsUnknown()
}
func isInteger(nt Nature) bool {
switch nt.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fallthrough
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return nt.PkgPath() == ""
}
return false
}
func isFloat(nt Nature) bool {
switch nt.Kind() {
case reflect.Float32, reflect.Float64:
return nt.PkgPath() == ""
}
return false
}
func isNumber(nt Nature) bool {
return isInteger(nt) || isFloat(nt)
}
func isTime(nt Nature) bool {
switch nt.Type {
case timeType:
return true
}
return false
}
func isDuration(nt Nature) bool {
switch nt.Type {
case durationType:
return true
}
return false
}
func isBool(nt Nature) bool {
switch nt.Kind() {
case reflect.Bool:
return true
}
return false
}
func isString(nt Nature) bool {
switch nt.Kind() {
case reflect.String:
return true
}
return false
}
func isArray(nt Nature) bool {
switch nt.Kind() {
case reflect.Slice, reflect.Array:
return true
}
return false
}
func isMap(nt Nature) bool {
switch nt.Kind() {
case reflect.Map:
return true
}
return false
}
func isStruct(nt Nature) bool {
switch nt.Kind() {
case reflect.Struct:
return true
}
return false
}
func isFunc(nt Nature) bool {
switch nt.Kind() {
case reflect.Func:
return true
}
return false
}
func kind(t reflect.Type) reflect.Kind {
if t == nil {
return reflect.Invalid
}
return t.Kind()
}
func isComparable(l, r Nature) bool {
if isUnknown(l) || isUnknown(r) {
return true
}
if isNil(l) || isNil(r) {
return true
}
if isNumber(l) && isNumber(r) {
return true
}
if isDuration(l) && isDuration(r) {
return true
}
if isTime(l) && isTime(r) {
return true
}
if isArray(l) && isArray(r) {
return true
}
return l.AssignableTo(r)
}
+19 -10
View File
@@ -5,6 +5,7 @@ import (
"math"
"reflect"
"regexp"
"runtime/debug"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
@@ -24,7 +25,7 @@ const (
func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
err = fmt.Errorf("%v\n%s", r, debug.Stack())
}
}()
@@ -36,6 +37,12 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
debugInfo: make(map[string]string),
}
if config != nil {
c.ntCache = &c.config.NtCache
} else {
c.ntCache = new(Cache)
}
c.compile(tree.Node)
if c.config != nil {
@@ -74,6 +81,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
type compiler struct {
config *conf.Config
ntCache *Cache
locations []file.Location
bytecode []Opcode
variables int
@@ -302,12 +310,12 @@ func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
if env.IsFastMap() {
c.emit(OpLoadFast, c.addConstant(node.Value))
} else if ok, index, name := checker.FieldIndex(env, node); ok {
} else if ok, index, name := checker.FieldIndex(c.ntCache, env, node); ok {
c.emit(OpLoadField, c.addConstant(&runtime.Field{
Index: index,
Path: []string{name},
}))
} else if ok, index, name := checker.MethodIndex(env, node); ok {
} else if ok, index, name := checker.MethodIndex(c.ntCache, env, node); ok {
c.emit(OpLoadMethod, c.addConstant(&runtime.Method{
Name: name,
Index: index,
@@ -653,7 +661,7 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {
env = c.config.Env
}
if ok, index, name := checker.MethodIndex(env, node); ok {
if ok, index, name := checker.MethodIndex(c.ntCache, env, node); ok {
c.compile(node.Node)
c.emit(OpMethod, c.addConstant(&runtime.Method{
Name: name,
@@ -664,14 +672,14 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {
op := OpFetch
base := node.Node
ok, index, nodeName := checker.FieldIndex(env, node)
ok, index, nodeName := checker.FieldIndex(c.ntCache, env, node)
path := []string{nodeName}
if ok {
op = OpFetchField
for !node.Optional {
if ident, isIdent := base.(*ast.IdentifierNode); isIdent {
if ok, identIndex, name := checker.FieldIndex(env, ident); ok {
if ok, identIndex, name := checker.FieldIndex(c.ntCache, env, ident); ok {
index = append(identIndex, index...)
path = append([]string{name}, path...)
c.emitLocation(ident.Location(), OpLoadField, c.addConstant(
@@ -682,7 +690,7 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {
}
if member, isMember := base.(*ast.MemberNode); isMember {
if ok, memberIndex, name := checker.FieldIndex(env, member); ok {
if ok, memberIndex, name := checker.FieldIndex(c.ntCache, env, member); ok {
index = append(memberIndex, index...)
path = append([]string{name}, path...)
node = member
@@ -743,7 +751,7 @@ func (c *compiler) CallNode(node *ast.CallNode) {
}
}
case *ast.IdentifierNode:
if t, ok := c.config.Env.MethodByName(callee.Value); ok && t.Method {
if t, ok := c.config.Env.MethodByName(c.ntCache, callee.Value); ok && t.Method {
fnInOffset = 1
fnNumIn--
}
@@ -777,7 +785,7 @@ func (c *compiler) CallNode(node *ast.CallNode) {
c.compile(node.Callee)
if c.config != nil {
isMethod, _, _ := checker.MethodIndex(c.config.Env, node.Callee)
isMethod, _, _ := checker.MethodIndex(c.ntCache, c.config.Env, node.Callee)
if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok {
c.emit(OpCallTyped, index)
return
@@ -1080,7 +1088,8 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
for i, arg := range node.Arguments {
c.compile(arg)
argType := arg.Type()
if argType.Kind() == reflect.Ptr || arg.Nature().IsUnknown() {
argNature := arg.Nature()
if argType.Kind() == reflect.Ptr || argNature.IsUnknown(c.ntCache) {
if f.Deref == nil {
// By default, builtins expect arguments to be dereferenced.
c.emit(OpDeref)
+3 -2
View File
@@ -34,6 +34,7 @@ type Config struct {
Functions FunctionsTable
Builtins FunctionsTable
Disabled map[string]bool // disabled builtins
NtCache nature.Cache
}
// CreateNew creates new config with default values.
@@ -61,7 +62,7 @@ func New(env any) *Config {
func (c *Config) WithEnv(env any) {
c.EnvObject = env
c.Env = Env(env)
c.Env = EnvWithCache(&c.NtCache, env)
c.Strict = c.Env.Strict
}
@@ -92,7 +93,7 @@ func (c *Config) IsOverridden(name string) bool {
if _, ok := c.Functions[name]; ok {
return true
}
if _, ok := c.Env.Get(name); ok {
if _, ok := c.Env.Get(&c.NtCache, name); ok {
return true
}
return false
+26 -18
View File
@@ -9,35 +9,42 @@ import (
"github.com/expr-lang/expr/types"
)
// Env returns the Nature of the given environment.
//
// Deprecated: use EnvWithCache instead.
func Env(env any) Nature {
return EnvWithCache(new(Cache), env)
}
func EnvWithCache(c *Cache, env any) Nature {
if env == nil {
return Nature{
Type: reflect.TypeOf(map[string]any{}),
Strict: true,
}
n := c.NatureOf(map[string]any{})
n.Strict = true
return n
}
switch env := env.(type) {
case types.Map:
return env.Nature()
nt := env.Nature()
return nt
}
v := reflect.ValueOf(env)
d := deref.Value(v)
t := v.Type()
switch d.Kind() {
switch deref.Value(v).Kind() {
case reflect.Struct:
return Nature{
Type: v.Type(),
Strict: true,
}
n := c.FromType(t)
n.Strict = true
return n
case reflect.Map:
n := Nature{
Type: v.Type(),
Fields: make(map[string]Nature, v.Len()),
Strict: true,
n := c.FromType(v.Type())
if n.TypeData == nil {
n.TypeData = new(TypeData)
}
n.Strict = true
n.Fields = make(map[string]Nature, v.Len())
for _, key := range v.MapKeys() {
elem := v.MapIndex(key)
@@ -49,14 +56,15 @@ func Env(env any) Nature {
switch face := face.(type) {
case types.Map:
n.Fields[key.String()] = face.Nature()
nt := face.Nature()
n.Fields[key.String()] = nt
default:
if face == nil {
n.Fields[key.String()] = Nature{Nil: true}
n.Fields[key.String()] = c.NatureOf(nil)
continue
}
n.Fields[key.String()] = Nature{Type: reflect.TypeOf(face)}
n.Fields[key.String()] = c.NatureOf(face)
}
}
+5 -2
View File
@@ -85,7 +85,9 @@ func CreateDoc(i any) *Context {
PkgPath: deref.Type(reflect.TypeOf(i)).PkgPath(),
}
for name, t := range conf.Env(i).All() {
cache := new(nature.Cache)
env := conf.EnvWithCache(cache, i)
for name, t := range env.All(cache) {
if _, ok := c.Variables[Identifier(name)]; ok {
continue
}
@@ -221,7 +223,8 @@ appendix:
c.Types[name] = a
}
for name, field := range nature.StructFields(t) {
ntCache := new(nature.Cache)
for name, field := range nature.StructFields(ntCache, t) {
if isPrivate(name) || isProtobuf(name) {
continue
}
+1
View File
@@ -48,6 +48,7 @@ func Operator(operator string, fn ...string) Option {
Overloads: fn,
Env: &c.Env,
Functions: c.Functions,
NtCache: &c.NtCache,
}
c.Visitors = append(c.Visitors, p)
}
+9
View File
@@ -45,3 +45,12 @@ func Value(v reflect.Value) reflect.Value {
}
return v
}
func TypeKind(t reflect.Type, k reflect.Kind) (_ reflect.Type, _ reflect.Kind, changed bool) {
for k == reflect.Pointer {
changed = true
t = t.Elem()
k = t.Kind()
}
return t, k, changed
}
-7
View File
@@ -92,13 +92,6 @@ func (l *Lexer) peek() rune {
return eof
}
func (l *Lexer) peekByte() (byte, bool) {
if l.end.byte >= 0 && l.end.byte < len(l.source.String()) {
return l.source.String()[l.end.byte], true
}
return 0, false
}
func (l *Lexer) backup() {
if l.eof {
l.eof = false
+2 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/parser/lexer"
. "github.com/expr-lang/expr/parser/lexer"
"github.com/expr-lang/expr/parser/operator"
"github.com/expr-lang/expr/parser/utils"
@@ -49,7 +48,7 @@ var predicates = map[string]struct {
// Parser is a reusable parser. The zero value is ready for use.
type Parser struct {
lexer *lexer.Lexer
lexer *Lexer
current, stashed Token
hasStash bool
err *file.Error
@@ -60,7 +59,7 @@ type Parser struct {
func (p *Parser) Parse(input string, config *conf.Config) (*Tree, error) {
if p.lexer == nil {
p.lexer = lexer.New()
p.lexer = New()
}
p.config = config
source := file.NewSource(input)
+3 -2
View File
@@ -16,6 +16,7 @@ type OperatorOverloading struct {
Env *nature.Nature // Env type.
Functions conf.FunctionsTable // Env functions.
applied bool // Flag to indicate if any changes were made to the tree.
NtCache *nature.Cache
}
func (p *OperatorOverloading) Visit(node *ast.Node) {
@@ -62,7 +63,7 @@ func (p *OperatorOverloading) FindSuitableOperatorOverload(l, r reflect.Type) (r
func (p *OperatorOverloading) findSuitableOperatorOverloadInTypes(l, r reflect.Type) (reflect.Type, string, bool) {
for _, fn := range p.Overloads {
fnType, ok := p.Env.Get(fn)
fnType, ok := p.Env.Get(p.NtCache, fn)
if !ok {
continue
}
@@ -109,7 +110,7 @@ func checkTypeSuits(t reflect.Type, l reflect.Type, r reflect.Type, firstInIndex
func (p *OperatorOverloading) Check() {
for _, fn := range p.Overloads {
fnType, foundType := p.Env.Get(fn)
fnType, foundType := p.Env.Get(p.NtCache, fn)
fnFunc, foundFunc := p.Functions[fn]
if !foundFunc && (!foundType || fnType.Type.Kind() != reflect.Func) {
panic(fmt.Errorf("function %s for %s operator does not exist in the environment", fn, p.Operator))
+14 -11
View File
@@ -44,7 +44,7 @@ func TypeOf(v any) Type {
type anyType struct{}
func (anyType) Nature() Nature {
return Nature{Type: nil}
return FromType(nil)
}
func (anyType) Equal(t Type) bool {
@@ -58,7 +58,7 @@ func (anyType) String() string {
type nilType struct{}
func (nilType) Nature() Nature {
return Nature{Nil: true}
return NatureOf(nil)
}
func (nilType) Equal(t Type) bool {
@@ -77,7 +77,7 @@ type rtype struct {
}
func (r rtype) Nature() Nature {
return Nature{Type: r.t}
return FromType(r.t)
}
func (r rtype) Equal(t Type) bool {
@@ -100,11 +100,12 @@ type Map map[string]Type
const Extra = "[[__extra_keys__]]"
func (m Map) Nature() Nature {
nt := Nature{
Type: reflect.TypeOf(map[string]any{}),
Fields: make(map[string]Nature, len(m)),
Strict: true,
nt := NatureOf(map[string]any{})
if nt.TypeData == nil {
nt.TypeData = new(TypeData)
}
nt.Fields = make(map[string]Nature, len(m))
nt.Strict = true
for k, v := range m {
if k == Extra {
nt.Strict = false
@@ -155,11 +156,13 @@ type array struct {
func (a array) Nature() Nature {
of := a.of.Nature()
return Nature{
Type: reflect.TypeOf([]any{}),
Fields: make(map[string]Nature, 1),
ArrayOf: &of,
nt := NatureOf([]any{})
if nt.TypeData == nil {
nt.TypeData = new(TypeData)
}
nt.Fields = make(map[string]Nature, 1)
nt.Ref = &of
return nt
}
func (a array) Equal(t Type) bool {