mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
* 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]>
233 lines
5.4 KiB
Go
233 lines
5.4 KiB
Go
package nature
|
|
|
|
import (
|
|
"reflect"
|
|
|
|
"github.com/expr-lang/expr/internal/deref"
|
|
)
|
|
|
|
func fieldName(fieldName string, tag reflect.StructTag) (string, bool) {
|
|
switch taggedName := tag.Get("expr"); taggedName {
|
|
case "-":
|
|
return "", false
|
|
case "":
|
|
return fieldName, true
|
|
default:
|
|
return taggedName, true
|
|
}
|
|
}
|
|
|
|
type structData struct {
|
|
rType reflect.Type
|
|
fields map[string]*structField
|
|
numField, ownIdx, anonIdx int
|
|
|
|
curParent, curChild *structData
|
|
curChildIndex []int
|
|
}
|
|
|
|
type structField struct {
|
|
Nature
|
|
Index []int
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|