Skip to content
Merged
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
3 changes: 3 additions & 0 deletions common/types/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package(
go_library(
name = "go_default_library",
srcs = [
"aggregate_sizer.go",
"any_value.go",
"bool.go",
"bytes.go",
Expand All @@ -28,6 +29,7 @@ go_library(
"overflow.go",
"provider.go",
"regex.go",
"size_calc.go",
"string.go",
"struct.go",
"timestamp.go",
Expand Down Expand Up @@ -76,6 +78,7 @@ go_test(
"optional_test.go",
"provider_test.go",
"regex_test.go",
"size_calc_test.go",
"string_test.go",
"timestamp_test.go",
"types_test.go",
Expand Down
43 changes: 43 additions & 0 deletions common/types/aggregate_sizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

// AggregateSizer calculates the recursive element size of values.
type AggregateSizer interface {
// AggregateSize returns the size of the input value, if known.
// Otherwise, a unit size of 1 is returned.
AggregateSize(val any) uint32
}

// AggregateSizeVisitor interface for ref.Val implementations capable of returning
// their total recursive element count.
type AggregateSizeVisitor interface {
// AggregateSize returns the total count of nested atomic elements (capped at math.MaxUint32).
AggregateSize(sizer AggregateSizer) uint32
}

// Helper for computing aggregate sizes of traits.Foldable types.
type foldableAggregateSizer struct {
sizer AggregateSizer
total uint32
}

// FoldEntry implements the traits.FoldEntry interface method and counts the aggregate size
// keys and values.
func (f *foldableAggregateSizer) FoldEntry(k, v any) bool {
f.total = safeAddUint32(f.total, f.sizer.AggregateSize(k))
f.total = safeAddUint32(f.total, f.sizer.AggregateSize(v))
return true
}
33 changes: 24 additions & 9 deletions common/types/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,10 @@ func NewMutableList(adapter Adapter) traits.MutableLister {
// The `Adapter` enables native type to CEL type conversions.
type baseList struct {
Adapter
value any

// size indicates the number of elements within the list.
// Since objects are immutable the size of a list is static.
size int

// get returns a value at the specified integer index.
// The index is guaranteed to be checked against the list index range.
get func(int) any
value any
size int
aggSize uint32
get func(int) any
}

// Add implements the traits.Adder interface method.
Expand Down Expand Up @@ -269,6 +264,19 @@ func (l *baseList) Size() ref.Val {
return Int(l.size)
}

// AggregateSize implements the AggregateSizeVisitor interface method.
func (l *baseList) AggregateSize(sizer AggregateSizer) uint32 {
if l.aggSize != 0 {
return l.aggSize
}
total := uint32(1)
for i := range l.size {
total = safeAddUint32(total, sizer.AggregateSize(l.get(i)))
}
l.aggSize = total
return total
}

// Type implements the ref.Val interface method.
func (l *baseList) Type() ref.Type {
return ListType
Expand Down Expand Up @@ -322,11 +330,13 @@ func (l *mutableList) Add(other ref.Val) ref.Val {
case *mutableList:
l.mutableValues = append(l.mutableValues, otherList.mutableValues...)
l.size += len(otherList.mutableValues)
l.aggSize = 0
case traits.Lister:
for i := IntZero; i < otherList.Size().(Int); i++ {
l.size++
l.mutableValues = append(l.mutableValues, otherList.Get(i))
}
l.aggSize = 0
default:
return MaybeNoSuchOverloadErr(otherList)
}
Expand Down Expand Up @@ -480,6 +490,11 @@ func (l *concatList) Size() ref.Val {
return l.cachedSize
}

// AggregateSize implements the AggregateSizeVisitor interface method.
func (l *concatList) AggregateSize(sizer AggregateSizer) uint32 {
return safeAddUint32(sizer.AggregateSize(l.prevList), sizer.AggregateSize(l.nextList))
Comment thread
l46kok marked this conversation as resolved.
}

// String converts the concatenated list to a human-readable string.
func (l *concatList) String() string {
var sb strings.Builder
Expand Down
64 changes: 64 additions & 0 deletions common/types/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,3 +930,67 @@ func TestConcatListSizeCached(t *testing.T) {
}
}
}

func TestListCalculateSize(t *testing.T) {
adapter := DefaultTypeAdapter

// List literal: [1, [3, 4], [[7, 8], [9, 10]]]
l1 := NewRefValList(adapter, []ref.Val{Int(3), Int(4)})
l2_1 := NewRefValList(adapter, []ref.Val{Int(7), Int(8)})
l2_2 := NewRefValList(adapter, []ref.Val{Int(9), Int(10)})
l2 := NewRefValList(adapter, []ref.Val{l2_1, l2_2})
nested := NewRefValList(adapter, []ref.Val{Int(1), l1, l2})

tests := []struct {
name string
val ref.Val
want uint32
}{
{
name: "empty_list",
val: NewRefValList(adapter, []ref.Val{}),
want: 1,
},
{
name: "flat_list",
val: l1,
want: 3,
},
{
name: "nested_list",
val: nested,
want: 12,
},
{
name: "concat_list",
val: l1.Add(l2_1),
want: 6,
},
{
name: "string_list",
val: NewStringList(adapter, []string{"hello", "world"}),
want: 11,
},
{
name: "dynamic_list",
val: NewDynamicList(adapter, []any{int64(1), []int64{3, 4}}),
want: 5,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
sizer, ok := tc.val.(AggregateSizeVisitor)
if !ok {
t.Fatalf("expected AggregateSizeVisitor implementation for %T", tc.val)
}
if got := sizer.AggregateSize(NewSizeCalculator()); got != tc.want {
t.Errorf("got aggregate size %d, want %d", got, tc.want)
}
// Caching check (memoized aggSize)
if got := sizer.AggregateSize(NewSizeCalculator()); got != tc.want {
t.Errorf("memoized AggregateSize() got %d, want %d", got, tc.want)
}
})
}
}
31 changes: 29 additions & 2 deletions common/types/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ type baseMap struct {
// value is the native Go value upon which the map type operators.
value any

// size is the number of entries in the map.
size int
size int
aggSize uint32
}

// Contains implements the traits.Container interface method.
Expand Down Expand Up @@ -303,6 +303,17 @@ func (m *baseMap) Size() ref.Val {
return Int(m.size)
}

// AggregateSize implements the AggregateSizeVisitor interface method.
func (m *baseMap) AggregateSize(sizer AggregateSizer) uint32 {
if m.aggSize != 0 {
return m.aggSize
}
f := foldableAggregateSizer{sizer: sizer, total: 1}
m.Fold(&f)
m.aggSize = f.total
return f.total
}

// String converts the map into a human-readable string.
func (m *baseMap) String() string {
var sb strings.Builder
Expand Down Expand Up @@ -380,6 +391,8 @@ func (m *mutableMap) Insert(k, v ref.Val) ref.Val {
return NewErr("insert failed: key %v already exists", k)
}
m.mutableValues[k] = v
m.size++
m.aggSize = 0
return m
}

Expand Down Expand Up @@ -909,6 +922,20 @@ func (m *protoMap) Size() ref.Val {
return Int(m.value.Len())
}

// AggregateSize implements the AggregateSizeVisitor interface method.
func (m *protoMap) AggregateSize(sizer AggregateSizer) uint32 {
if m.value == nil {
return 0
}
total := uint32(1)
m.value.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
total = safeAddUint32(total, sizer.AggregateSize(k))
total = safeAddUint32(total, sizer.AggregateSize(v))
return true
})
return total
}

// Type implements the ref.Val interface method.
func (m *protoMap) Type() ref.Type {
return MapType
Expand Down
95 changes: 95 additions & 0 deletions common/types/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1252,3 +1252,98 @@ func (m proxyLegacyMap) Iterator() traits.Iterator {
func (m proxyLegacyMap) Size() ref.Val {
return m.proxy.Size()
}

func TestMapCalculateSize(t *testing.T) {
adapter := DefaultTypeAdapter

// Setup helper data
l := NewRefValList(adapter, []ref.Val{Int(2), Int(3)})
refValMap := NewRefValMap(adapter, map[ref.Val]ref.Val{
String("a"): Int(1),
String("b"): l,
})

ifaceMap := NewStringInterfaceMap(adapter, map[string]any{
"a": int64(1),
"b": []any{int64(2), int64(3)},
})

mutMap := NewMutableMap(adapter, map[ref.Val]ref.Val{
String("a"): Int(1),
String("b"): l,
})
// Initial evaluation before insert to test aggSize reset
_ = mutMap.(AggregateSizeVisitor).AggregateSize(NewSizeCalculator())
mutMap.Insert(String("c"), Int(4))

reg, err := NewRegistry(&proto3pb.TestAllTypes{})
if err != nil {
t.Fatalf("NewRegistry() failed: %v", err)
}
msg := &proto3pb.TestAllTypes{
MapStringString: map[string]string{
"a": "b",
"c": "d",
},
}
pbMsg := reg.NativeToValue(msg).(traits.Indexer)
pm := pbMsg.Get(String("map_string_string")).(traits.Mapper)

tests := []struct {
name string
val ref.Val
want uint32
}{
{
name: "empty_ref_val_map",
val: NewRefValMap(adapter, map[ref.Val]ref.Val{}),
want: 1,
},
{
name: "ref_val_map_nested",
val: refValMap,
want: 7,
},
{
name: "string_interface_map",
val: ifaceMap,
want: 7,
},
{
name: "string_string_map",
val: NewStringStringMap(adapter, map[string]string{"k1": "v1", "k2": "v2"}),
want: 9,
},
{
name: "mutable_map_after_insert",
val: mutMap,
want: 9,
},
{
name: "proto_map",
val: pm,
want: 5,
},
{
name: "nil_proto_map",
val: &protoMap{},
want: 0,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
sizer, ok := tc.val.(AggregateSizeVisitor)
if !ok {
t.Fatalf("expected AggregateSizeVisitor implementation for %T", tc.val)
}
if got := sizer.AggregateSize(NewSizeCalculator()); got != tc.want {
t.Errorf("got aggregate size %d, want %d", got, tc.want)
}
// Caching check (memoized aggSize)
if got := sizer.AggregateSize(NewSizeCalculator()); got != tc.want {
t.Errorf("memoized AggregateSize() got %d, want %d", got, tc.want)
}
})
}
}
17 changes: 17 additions & 0 deletions common/types/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,23 @@ func (o *nativeObj) Value() any {
return o.val
}

// AggregateSize implements the AggregateSizeVisitor interface method.
func (o *nativeObj) AggregateSize(sizer AggregateSizer) uint32 {
refVal := reflect.Indirect(o.refValue)
if !refVal.IsValid() {
return 0
}
total := uint32(1)
for _, fieldType := range o.valType.fieldsByName {
fieldValue := refVal.FieldByIndex(fieldType.Index)
if !fieldValue.IsValid() || fieldValue.IsZero() {
continue
}
total = safeAddUint32(total, sizer.AggregateSize(fieldValue))
}
return total
}

func newNativeTypes(rawType reflect.Type, fieldNameHandler NativeTypesFieldNameHandler) ([]*NativeType, error) {
nt, err := newNativeType(rawType, fieldNameHandler)
if err != nil {
Expand Down
Loading
Loading