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
10 changes: 10 additions & 0 deletions vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ import (
"github.com/expr-lang/expr/internal/deref"
)

// Proxy is an interface that allows intercepting object property access.
type Proxy interface {
// GetProperty returns the value of the property with the given key.
GetProperty(key any) any
}

func Fetch(from, i any) any {
if proxy, ok := from.(Proxy); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relying only on real time to check for proxy interface is not the best strategy. We need to do a pre-optimization and check at compile time whether a special opcode for fetching using this interface can be inserted ahead of time. With just a runtime check for proxy, I cannot accept this pull request.

Copy link
Author

@x1unix x1unix Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've a couple of questions:

  • By ahead of time optimization, do you mean to implement env check in optimizer.Optimize method?
    • Current API allows using different envs for parsing, optimization and evaluation.
      This means that during eval stage, user can supply an env that may have a proxy (e.g. foo.bar.baz), but this wasn't accounted during parse/optimize stage, so iface check still might be required inside OpFetch.
    • Proxy may be located deeply inside env (or populated at runtime), or returned from a func.
    • Initially, I wanted to use type check instead of an interface as it is 2x cheaper, but couldn't find a suitable approach (see benchmark). Happy to accept different proposals.
    • As 90% of users probably won't need proxies, this feature can be opt-in (feature flag). This will keep perf the same as before and will skip proxy checks by default.

return proxy.GetProperty(i)
}

v := reflect.ValueOf(from)
if v.Kind() == reflect.Invalid {
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
Expand Down
105 changes: 105 additions & 0 deletions vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/internal/testify/require"
"github.com/expr-lang/expr/vm/runtime"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/checker"
Expand Down Expand Up @@ -267,6 +268,110 @@ func TestRun_InnerMethodWithError_NilSafe(t *testing.T) {
require.Equal(t, nil, out)
}

var _ runtime.Proxy = (*proxyNode)(nil)

type proxyNode struct {
parent *proxyNode
values map[any]any
}

func (n *proxyNode) GetProperty(key any) any {
if value, ok := n.values[key]; ok {
return value
}
if n.parent != nil {
return n.parent.GetProperty(key)
}
return nil
}

func (n *proxyNode) SetProperty(key any, value any) {
n.values[key] = value
}

func TestRun_Proxy_Read(t *testing.T) {
cases := []struct {
label string
expr string
env any
expect any
}{
{
label: "proxy env map member",
expr: `foo.bar`,
env: map[string]any{
"foo": &proxyNode{
values: map[any]any{
"bar": "baz",
},
},
},
expect: "baz",
},
{
label: "read from root",
expr: `value`,
env: &proxyNode{
values: map[any]any{
"value": "hello world",
},
},
expect: "hello world",
},
{
label: "read from child",
expr: `child.value`,
env: &proxyNode{
values: map[any]any{
"child": &proxyNode{
values: map[any]any{
"value": "hello world",
},
},
},
},
expect: "hello world",
},
{
label: "read from grandchild",
expr: `grandchild.child.value`,
env: &proxyNode{
values: map[any]any{
"grandchild": &proxyNode{
values: map[any]any{
"child": &proxyNode{
values: map[any]any{
"value": "hello world",
},
},
},
},
},
},
expect: "hello world",
},
}

for _, c := range cases {
t.Run(c.label, func(t *testing.T) {
tree, err := parser.Parse(c.expr)
require.NoError(t, err)

funcConf := conf.CreateNew()
_, err = checker.Check(tree, funcConf)
require.NoError(t, err)

program, err := compiler.Compile(tree, funcConf)
require.NoError(t, err)

out, err := vm.Run(program, c.env)
require.NoError(t, err)

require.Equal(t, c.expect, out)
})
}
}

func TestRun_TaggedFieldName(t *testing.T) {
input := `value`

Expand Down