Skip to content
Draft
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
4 changes: 2 additions & 2 deletions builtin/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,10 +608,10 @@ func get(params ...any) (out any, err error) {
return name == fieldName
}
})
if ok && field.IsExported() {
if ok && (field.IsExported() || t.PkgPath() == "") {
value := v.FieldByIndex(field.Index)
if value.IsValid() {
return value.Interface(), nil
return runtime.ValueInterface(value), nil
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion checker/nature/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (s *structData) structField(c *Cache, parentEmbed *structData, name string)
// start iterating anon fields on the first instead of zero
s.anonIdx = s.ownIdx
}
if !field.IsExported() {
if !field.IsExported() && s.rType.PkgPath() != "" {
continue
}
fName, ok := fieldName(field.Name, field.Tag)
Expand Down
71 changes: 71 additions & 0 deletions test/issues/936/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package issue936

import (
"reflect"
"testing"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/internal/testify/require"
)

// TestIssue936 tests that dynamic struct types created with reflect.StructOf
// compile and evaluate correctly even when fields have lowercase names (which
// require PkgPath to be set, making them appear "unexported" to reflect).
func TestIssue936(t *testing.T) {
dynType := reflect.StructOf([]reflect.StructField{
{
Name: "value",
Type: reflect.TypeFor[bool](),
PkgPath: "github.com/some/package",
},
})
env := reflect.New(dynType).Elem().Interface()

// Compilation should succeed.
program, err := expr.Compile("value", expr.Env(env))
require.NoError(t, err)

// Evaluation should also succeed and return the zero value (false).
result, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, false, result)
}

// TestIssue936MultipleFields tests a dynamic struct with multiple field types.
func TestIssue936MultipleFields(t *testing.T) {
dynType := reflect.StructOf([]reflect.StructField{
{
Name: "name",
Type: reflect.TypeFor[string](),
PkgPath: "github.com/some/package",
},
{
Name: "count",
Type: reflect.TypeFor[int](),
PkgPath: "github.com/some/package",
},
{
Name: "active",
Type: reflect.TypeFor[bool](),
PkgPath: "github.com/some/package",
},
})
env := reflect.New(dynType).Elem().Interface()

for _, tc := range []struct {
expr string
}{
{`name == ""`},
{`count == 0`},
{`active == false`},
} {
t.Run(tc.expr, func(t *testing.T) {
program, err := expr.Compile(tc.expr, expr.Env(env))
require.NoError(t, err)

result, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, result)
})
}
}
58 changes: 54 additions & 4 deletions vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func Fetch(from, i any) any {
f: fieldName,
}
if cv, ok := fieldCache.Load(key); ok {
return v.FieldByIndex(cv.([]int)).Interface()
return ValueInterface(v.FieldByIndex(cv.([]int)))
}
field, ok := t.FieldByNameFunc(func(name string) bool {
field, _ := t.FieldByName(name)
Expand All @@ -90,11 +90,11 @@ func Fetch(from, i any) any {
return name == fieldName
}
})
if ok && field.IsExported() {
if ok && (field.IsExported() || t.PkgPath() == "") {
value := v.FieldByIndex(field.Index)
if value.IsValid() {
fieldCache.Store(key, field.Index)
return value.Interface()
return ValueInterface(value)
}
}
}
Expand All @@ -119,7 +119,7 @@ func FetchField(from any, field *Field) any {
// is a struct as we already did it on compilation step.
value := fieldByIndex(v, field)
if value.IsValid() {
return value.Interface()
return ValueInterface(value)
}
}
panic(fmt.Sprintf("cannot get %v from %T", field.Path[0], from))
Expand All @@ -143,6 +143,56 @@ func fieldByIndex(v reflect.Value, field *Field) reflect.Value {
return v
}

// ValueInterface returns the interface value of v. For unexported fields in
// anonymous struct types (e.g. created with reflect.StructOf), reflect requires
// PkgPath to be set for lowercase-named fields, which marks them as unexported.
// In this case, Interface() panics, so we use type-specific getters instead.
// This function supports primitive kinds (bool, ints, uints, floats, complex,
// string) and panics for other kinds (struct, slice, map, pointer, etc.) that
// cannot be retrieved without the Interface() call.
func ValueInterface(v reflect.Value) any {
if v.CanInterface() {
return v.Interface()
}
switch v.Kind() {
case reflect.Bool:
return v.Bool()
case reflect.Int:
return int(v.Int())
case reflect.Int8:
return int8(v.Int())
case reflect.Int16:
return int16(v.Int())
case reflect.Int32:
return int32(v.Int())
case reflect.Int64:
return v.Int()
case reflect.Uint:
return uint(v.Uint())
case reflect.Uint8:
return uint8(v.Uint())
case reflect.Uint16:
return uint16(v.Uint())
case reflect.Uint32:
return uint32(v.Uint())
case reflect.Uint64:
return v.Uint()
case reflect.Uintptr:
return uintptr(v.Uint())
case reflect.Float32:
return float32(v.Float())
case reflect.Float64:
return v.Float()
case reflect.Complex64:
return complex64(v.Complex())
case reflect.Complex128:
return v.Complex()
case reflect.String:
return v.String()
}
panic(fmt.Sprintf("reflect: cannot interface value of kind %s obtained from unexported field", v.Kind()))
}

type Method struct {
Index int
Name string
Expand Down