From e6e0fd572edc0832332f114eaa370b50e8803acc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:09:53 +0000 Subject: [PATCH 1/2] Initial plan From b53046b044596179b49a8105ed9eb525f0455b2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:30:25 +0000 Subject: [PATCH 2/2] fix: support dynamic struct types from reflect.StructOf in expr.Compile (#936) Co-authored-by: antonmedv <141232+antonmedv@users.noreply.github.com> --- builtin/lib.go | 4 +- checker/nature/utils.go | 2 +- test/issues/936/issue_test.go | 71 +++++++++++++++++++++++++++++++++++ vm/runtime/runtime.go | 58 ++++++++++++++++++++++++++-- 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 test/issues/936/issue_test.go diff --git a/builtin/lib.go b/builtin/lib.go index 61748da0..d6a3b0a4 100644 --- a/builtin/lib.go +++ b/builtin/lib.go @@ -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 } } } diff --git a/checker/nature/utils.go b/checker/nature/utils.go index 2af94600..28c32dcf 100644 --- a/checker/nature/utils.go +++ b/checker/nature/utils.go @@ -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) diff --git a/test/issues/936/issue_test.go b/test/issues/936/issue_test.go new file mode 100644 index 00000000..2c374df0 --- /dev/null +++ b/test/issues/936/issue_test.go @@ -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) + }) + } +} diff --git a/vm/runtime/runtime.go b/vm/runtime/runtime.go index bc6f2b4d..e2286cec 100644 --- a/vm/runtime/runtime.go +++ b/vm/runtime/runtime.go @@ -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) @@ -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) } } } @@ -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)) @@ -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