Skip to content

Commit

Permalink
Merge scope hierarchy for evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
wildum committed Nov 25, 2024
1 parent f0211d0 commit e64d648
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 0 deletions.
57 changes: 57 additions & 0 deletions internal/runtime/internal/controller/value_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ func (vc *valueCache) BuildContext() *vm.Scope {
}
}

// The syntax pkg only checks the first key when choosing the scope to evaluate an expression.
// if you have:
// - parent: "test": "component1": v
// - child: "test": "component2": v
// the expression "test.component1.v" would result in an error because it would use the child scope.
// To avoid such conflict, we merge the scopes into one:
// - parent: nil
// - child: "test": {"component1": v, "component2": v}
scope.Variables = mergeScopes(scope)
scope.Parent = nil

return scope
}

Expand Down Expand Up @@ -234,3 +245,49 @@ func (vc *valueCache) buildValue(from []ComponentID, offset int) interface{} {
}
return attrs
}

// mergeScopes merges the Variables maps of the scope hierarchy into a new map.
// The child always overrides the parent's value.
func mergeScopes(scope *vm.Scope) map[string]any {
merged := make(map[string]any)

var mergeFrom func(*vm.Scope)
mergeFrom = func(s *vm.Scope) {
if s == nil {
return
}
mergeFrom(s.Parent)
for k, v := range s.Variables {
if pm, ok1 := merged[k].(map[string]any); ok1 {
if cm, ok2 := v.(map[string]any); ok2 {
merged[k] = mergeNestedMaps(pm, cm)
continue
}
}
merged[k] = v
}
}

mergeFrom(scope)
return merged
}

func mergeNestedMaps(parent, child map[string]any) map[string]any {
merged := make(map[string]any)

for k, v := range parent {
merged[k] = v
}

for k, v := range child {
if pm, ok1 := merged[k].(map[string]any); ok1 {
if cm, ok2 := v.(map[string]any); ok2 {
merged[k] = mergeNestedMaps(pm, cm)
continue
}
}
merged[k] = v
}

return merged
}
117 changes: 117 additions & 0 deletions internal/runtime/internal/controller/value_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controller
import (
"testing"

"github.com/grafana/alloy/syntax/vm"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -174,3 +175,119 @@ func TestModuleArgumentCache(t *testing.T) {
})
}
}

func TestScopeMerge(t *testing.T) {
vc := newValueCache()
scope := vm.NewScope(
map[string]any{
"test": map[string]any{
"scope": barArgs{Number: 13},
},
},
)
vc.SetScope(scope)
vc.CacheExports(ComponentID{"foo", "bar"}, barArgs{Number: 12})
res := vc.BuildContext()

expected := map[string]any{
"test": map[string]any{
"scope": barArgs{
Number: 13,
},
},
"foo": map[string]any{
"bar": barArgs{
Number: 12,
},
},
}
require.Equal(t, expected, res.Variables)
}

func TestScopeMergeConflict(t *testing.T) {
vc := newValueCache()
scope := vm.NewScope(
map[string]any{
"test": map[string]any{
"scope": barArgs{Number: 13},
},
},
)
vc.SetScope(scope)
vc.CacheExports(ComponentID{"test", "bar"}, barArgs{Number: 12})
res := vc.BuildContext()

expected := map[string]any{
"test": map[string]any{
"scope": barArgs{
Number: 13,
},
"bar": barArgs{
Number: 12,
},
},
}
require.Equal(t, expected, res.Variables)
}

func TestScopeMergeOverride(t *testing.T) {
vc := newValueCache()
scope := vm.NewScope(
map[string]any{
"test": map[string]any{
"scope": barArgs{Number: 13},
},
},
)
vc.SetScope(scope)
vc.CacheExports(ComponentID{"test", "scope"}, barArgs{Number: 12})
res := vc.BuildContext()

expected := map[string]any{
"test": map[string]any{
"scope": barArgs{
Number: 12,
},
},
}
require.Equal(t, expected, res.Variables)
}

func TestScopeMergeMoreNesting(t *testing.T) {
vc := newValueCache()
parentScope := vm.NewScope(
map[string]any{
"test": map[string]any{
"cp1": map[string]any{
"scope": barArgs{Number: 13},
},
},
},
)
scope := vm.NewScopeWithParent(
parentScope,
map[string]any{
"test": map[string]any{
"cp2": barArgs{Number: 12},
},
},
)
vc.SetScope(scope)
vc.CacheExports(ComponentID{"test", "cp1", "foo"}, barArgs{Number: 12})
vc.CacheExports(ComponentID{"test", "cp1", "bar", "fizz"}, barArgs{Number: 2})
res := vc.BuildContext()

expected := map[string]any{
"test": map[string]any{
"cp1": map[string]any{
"scope": barArgs{Number: 13},
"foo": barArgs{Number: 12},
"bar": map[string]any{
"fizz": barArgs{Number: 2},
},
},
"cp2": barArgs{Number: 12},
},
}
require.Equal(t, expected, res.Variables)
}

0 comments on commit e64d648

Please sign in to comment.