From 2dd4bea29e876077e660a54f64d63dcfdede4324 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 23 Mar 2026 09:16:00 +0100 Subject: [PATCH 1/3] fix: cache concatList.Size() to prevent O(N^2) evaluation time concatList.Size() recomputes the total size by recursively calling Size() on prevList and nextList. After N concatenations, Size() on each iteration produces O(N^2) total time, bypassing CostLimit. Cache the result in a sync.Once field so repeated calls return in O(1). --- common/types/list.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/common/types/list.go b/common/types/list.go index 324c0f969..670fac0c2 100644 --- a/common/types/list.go +++ b/common/types/list.go @@ -18,6 +18,7 @@ import ( "fmt" "reflect" "strings" + "sync" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" @@ -353,9 +354,11 @@ func (l *mutableList) ToImmutableList() traits.Lister { // The `Adapter` enables native type to CEL type conversions. type concatList struct { Adapter - value any - prevList traits.Lister - nextList traits.Lister + value any + prevList traits.Lister + nextList traits.Lister + sizeOnce sync.Once + cachedSize ref.Val } // Add implements the traits.Adder interface method. @@ -477,7 +480,10 @@ func (l *concatList) Iterator() traits.Iterator { // Size implements the traits.Sizer interface method. func (l *concatList) Size() ref.Val { - return l.prevList.Size().(Int).Add(l.nextList.Size()) + l.sizeOnce.Do(func() { + l.cachedSize = l.prevList.Size().(Int).Add(l.nextList.Size()) + }) + return l.cachedSize } // String converts the concatenated list to a human-readable string. From 266b76cab663050e8d5b5b2b206c4a0ed3787f21 Mon Sep 17 00:00:00 2001 From: Florian Pradines Date: Mon, 23 Mar 2026 20:56:19 +0100 Subject: [PATCH 2/3] refactor: compute concatList size at construction time --- common/types/list.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/common/types/list.go b/common/types/list.go index 670fac0c2..c1f18a0c2 100644 --- a/common/types/list.go +++ b/common/types/list.go @@ -18,7 +18,6 @@ import ( "fmt" "reflect" "strings" - "sync" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" @@ -133,10 +132,7 @@ func (l *baseList) Add(other ref.Val) ref.Val { if otherList.Size() == IntZero { return l } - return &concatList{ - Adapter: l.Adapter, - prevList: l, - nextList: otherList} + return newConcatList(l.Adapter, l, otherList) } // Contains implements the traits.Container interface method. @@ -357,10 +353,19 @@ type concatList struct { value any prevList traits.Lister nextList traits.Lister - sizeOnce sync.Once cachedSize ref.Val } +func newConcatList(adapter Adapter, prevList, nextList traits.Lister) *concatList { + return &concatList{ + Adapter: adapter, + prevList: prevList, + nextList: nextList, + cachedSize: prevList.Size().(Int).Add(nextList.Size()), + } +} + + // Add implements the traits.Adder interface method. func (l *concatList) Add(other ref.Val) ref.Val { otherList, ok := other.(traits.Lister) @@ -373,10 +378,7 @@ func (l *concatList) Add(other ref.Val) ref.Val { if otherList.Size() == IntZero { return l } - return &concatList{ - Adapter: l.Adapter, - prevList: l, - nextList: otherList} + return newConcatList(l.Adapter, l, otherList) } // Contains implements the traits.Container interface method. @@ -480,9 +482,6 @@ func (l *concatList) Iterator() traits.Iterator { // Size implements the traits.Sizer interface method. func (l *concatList) Size() ref.Val { - l.sizeOnce.Do(func() { - l.cachedSize = l.prevList.Size().(Int).Add(l.nextList.Size()) - }) return l.cachedSize } From 73830d02c78dbb4983c6f5d58db3f7ea0f501010 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 24 Mar 2026 11:43:46 +0100 Subject: [PATCH 3/3] refactor: move size checks into newConcatList, add regression test --- common/types/list.go | 25 ++++++++++--------------- common/types/list_test.go | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/common/types/list.go b/common/types/list.go index c1f18a0c2..028770ed6 100644 --- a/common/types/list.go +++ b/common/types/list.go @@ -126,12 +126,6 @@ func (l *baseList) Add(other ref.Val) ref.Val { if !ok { return MaybeNoSuchOverloadErr(other) } - if l.Size() == IntZero { - return other - } - if otherList.Size() == IntZero { - return l - } return newConcatList(l.Adapter, l, otherList) } @@ -356,28 +350,29 @@ type concatList struct { cachedSize ref.Val } -func newConcatList(adapter Adapter, prevList, nextList traits.Lister) *concatList { +func newConcatList(adapter Adapter, prevList, nextList traits.Lister) ref.Val { + prevSize := prevList.Size().(Int) + nextSize := nextList.Size().(Int) + if prevSize == IntZero { + return nextList.(ref.Val) + } + if nextSize == IntZero { + return prevList.(ref.Val) + } return &concatList{ Adapter: adapter, prevList: prevList, nextList: nextList, - cachedSize: prevList.Size().(Int).Add(nextList.Size()), + cachedSize: prevSize.Add(nextSize), } } - // Add implements the traits.Adder interface method. func (l *concatList) Add(other ref.Val) ref.Val { otherList, ok := other.(traits.Lister) if !ok { return MaybeNoSuchOverloadErr(other) } - if l.Size() == IntZero { - return other - } - if otherList.Size() == IntZero { - return l - } return newConcatList(l.Adapter, l, otherList) } diff --git a/common/types/list_test.go b/common/types/list_test.go index d99cdd004..ca134b716 100644 --- a/common/types/list_test.go +++ b/common/types/list_test.go @@ -910,3 +910,23 @@ func validateIterator123(t *testing.T, list traits.Lister) { t.Errorf("Iterator did not iterate until last value") } } + +func TestConcatListSizeCached(t *testing.T) { + reg := newTestRegistry(t) + // Build a deep chain of concat lists + var list traits.Lister = NewDynamicList(reg, []int64{0}) + for i := 0; i < 200; i++ { + list = list.Add(NewDynamicList(reg, []int64{0})).(traits.Lister) + } + // Size() should return instantly since it's precomputed + size := list.Size() + if size != Int(201) { + t.Errorf("Expected size 201, got %v", size) + } + // Call Size() many times to confirm no quadratic behavior + for i := 0; i < 1000; i++ { + if list.Size() != Int(201) { + t.Errorf("Size() returned inconsistent value on call %d", i) + } + } +}