From 87f6a78b8b021c39cbeab03bd20102c87178860b Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 25 Dec 2024 00:43:50 +0100 Subject: [PATCH 01/15] feat(examples): add moul/ulist Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/gno.mod | 1 + examples/gno.land/p/moul/ulist/ulist.gno | 236 +++++++ examples/gno.land/p/moul/ulist/ulist_test.gno | 611 ++++++++++++++++++ examples/gno.land/r/nemanya/config/gno.mod | 2 +- examples/gno.land/r/nemanya/home/gno.mod | 2 +- examples/gno.land/r/nemanya/home/home.gno | 28 +- 6 files changed, 864 insertions(+), 16 deletions(-) create mode 100644 examples/gno.land/p/moul/ulist/gno.mod create mode 100644 examples/gno.land/p/moul/ulist/ulist.gno create mode 100644 examples/gno.land/p/moul/ulist/ulist_test.gno diff --git a/examples/gno.land/p/moul/ulist/gno.mod b/examples/gno.land/p/moul/ulist/gno.mod new file mode 100644 index 00000000000..077f8c556f3 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/ulist diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno new file mode 100644 index 00000000000..4d62c19f039 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -0,0 +1,236 @@ +// Package ulist provides an append-only list implementation using a binary tree structure, +// optimized for scenarios requiring sequential inserts with auto-incrementing indices. +// +// The implementation uses a binary tree where new elements are added by following a path +// determined by the binary representation of the index. This provides automatic balancing +// for append operations without requiring any balancing logic. +// +// Key characteristics: +// * O(log n) append and access operations +// * Perfect balance for power-of-2 sizes +// * No balancing needed +// * Memory efficient +// * Natural support for range queries + +package ulist + +// TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones). +// TODO: Add MustXXX helpers. +// TODO: Use this ulist in moul/collection for the primary index. +// TODO: Consider adding a "compact" method that removes nil nodes. +// TODO: Remove debug logging. +// TODO: add some iterator helpers, such as one that takes count entries. + +import ( + "errors" +) + +// List represents an append-only binary tree list +type List struct { + root *treeNode + totalSize int + activeSize int +} + +type Entry struct { + Index int + Value interface{} +} + +// treeNode represents a node in the binary tree +type treeNode struct { + data interface{} + left *treeNode + right *treeNode +} + +// Error variables +var ( + ErrOutOfBounds = errors.New("index out of bounds") + ErrDeleted = errors.New("element already deleted") +) + +// New creates a new List +func New() *List { + return &List{} +} + +// Append adds one or more values to the list +func (l *List) Append(values ...interface{}) { + for _, value := range values { + index := l.totalSize + node := l.findNode(index, true) + node.data = value + l.totalSize++ + l.activeSize++ + } +} + +// Get retrieves the value at the specified index +func (l *List) Get(index int) interface{} { + node := l.findNode(index, false) + if node == nil { + return nil + } + return node.data +} + +// Delete marks the elements at the specified indices as deleted +func (l *List) Delete(indices ...int) error { + if len(indices) == 0 { + return nil + } + if l == nil || l.totalSize == 0 { + return ErrOutOfBounds + } + + for _, index := range indices { + if index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil || node.data == nil { + return ErrDeleted + } + node.data = nil + l.activeSize-- + } + + return nil +} + +// Size returns the number of active elements +func (l *List) Size() int { + if l == nil { + return 0 + } + return l.activeSize +} + +// TotalSize returns the total number of elements (including deleted) +func (l *List) TotalSize() int { + if l == nil { + return 0 + } + return l.totalSize +} + +// IterCbFn is a callback function that processes an entry and returns whether to stop iterating +type IterCbFn func(index int, value interface{}) bool + +// Iterator performs iteration between start and end indices, calling cb for each entry. +// If start > end, iteration is performed in reverse order. +// Returns true if iteration was stopped by callback returning true. +func (l *List) Iterator(start, end int, cb IterCbFn) bool { + // For empty list or invalid range + if l == nil || l.totalSize == 0 { + return false + } + if start < 0 && end < 0 { + return false + } + if start >= l.totalSize && end >= l.totalSize { + return false + } + + // Normalize indices + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + if end >= l.totalSize { + end = l.totalSize - 1 + } + if start >= l.totalSize { + start = l.totalSize - 1 + } + + // Handle reverse iteration + if start > end { + for i := start; i >= end; i-- { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false + } + + // Handle forward iteration + for i := start; i <= end; i++ { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false +} + +// Add this helper method to the List struct +func (l *List) findNode(index int, create bool) *treeNode { + // For read operations, check bounds strictly + if !create && (l == nil || index < 0 || index >= l.totalSize) { + return nil + } + + // For create operations, allow index == totalSize for append + if create && (l == nil || index < 0 || index > l.totalSize) { + return nil + } + + // Initialize root if needed + if l.root == nil { + if !create { + return nil + } + l.root = &treeNode{} + return l.root + } + + node := l.root + + // Special case for root node + if index == 0 { + return node + } + + // Calculate the number of bits needed (inline highestBit logic) + bits := 0 + n := index + 1 + for n > 0 { + n >>= 1 + bits++ + } + + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + if node.right == nil { + if !create { + return nil + } + node.right = &treeNode{} + } + node = node.right + } else { + if node.left == nil { + if !create { + return nil + } + node.left = &treeNode{} + } + node = node.left + } + } + + return node +} diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno new file mode 100644 index 00000000000..af62e09e4ea --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -0,0 +1,611 @@ +package ulist + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/typeutil" +) + +func TestNew(t *testing.T) { + l := New() + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) +} + +func TestListAppendAndGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + }{ + { + name: "empty list", + setup: func() *List { + return New() + }, + index: 0, + expected: nil, + }, + { + name: "single append and get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + }, + { + name: "multiple appends and get first", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 0, + expected: 1, + }, + { + name: "multiple appends and get last", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 2, + expected: 3, + }, + { + name: "get with invalid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + expected: nil, + }, + { + name: "31 items get first", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 0, + expected: 0, + }, + { + name: "31 items get last", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 30, + expected: 30, + }, + { + name: "31 items get middle", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values around power of 2 boundary", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values at power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 16, + expected: 16, + }, + { + name: "values after power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 17, + expected: 17, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + got := l.Get(tt.index) + if got != tt.expected { + t.Errorf("List.Get() = %v, want %v", got, tt.expected) + } + }) + } +} + +// generateSequence creates a slice of integers from 0 to n-1 +func generateSequence(n int) []interface{} { + result := make([]interface{}, n) + for i := 0; i < n; i++ { + result[i] = i + } + return result +} + +func TestListDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + deleteIndices []int + expectedErr error + expectedSize int + }{ + { + name: "delete single element", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + deleteIndices: []int{1}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete multiple elements", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{0, 2, 4}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete with negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{-1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete already deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + deleteIndices: []int{0}, + expectedErr: ErrDeleted, + expectedSize: 0, + }, + { + name: "delete multiple elements in reverse", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{4, 2, 0}, + expectedErr: nil, + expectedSize: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + initialSize := l.Size() + err := l.Delete(tt.deleteIndices...) + if err != nil && tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, tt.expectedErr, err) + } + uassert.Equal(t, tt.expectedSize, l.Size(), + ufmt.Sprintf("Expected size %d after deleting %d elements from size %d, got %d", + tt.expectedSize, len(tt.deleteIndices), initialSize, l.Size())) + }) + } +} + +func TestListSizeAndTotalSize(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := New() + uassert.Equal(t, 0, list.Size()) + uassert.Equal(t, 0, list.TotalSize()) + }) + + t.Run("list with elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + uassert.Equal(t, 3, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) + + t.Run("list with deleted elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + list.Delete(1) + uassert.Equal(t, 2, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) +} + +func TestIterator(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + wantStop bool + stopAfter int // stop after N elements, -1 for no stop + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 0, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "single element forward", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + stopAfter: -1, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "partial range forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + stopAfter: -1, + }, + { + name: "partial range reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + stopAfter: -1, + }, + { + name: "stop iteration early", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + wantStop: true, + stopAfter: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "negative start", + values: []interface{}{1, 2, 3}, + start: -1, + end: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "negative end", + values: []interface{}{1, 2, 3}, + start: 0, + end: -2, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start beyond size", + values: []interface{}{1, 2, 3}, + start: 5, + end: 6, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "end beyond size", + values: []interface{}{1, 2, 3}, + start: 0, + end: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements", + values: []interface{}{1, 2, nil, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements reverse", + values: []interface{}{1, nil, 3, nil, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 2, Value: 3}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start equals end", + values: []interface{}{1, 2, 3}, + start: 1, + end: 1, + expected: []Entry{{Index: 1, Value: 2}}, + stopAfter: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.Iterator(tt.start, tt.end, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return tt.stopAfter >= 0 && len(result) >= tt.stopAfter + }) + + uassert.Equal(t, len(result), len(tt.expected), "comparing length") + + for i := range result { + uassert.Equal(t, result[i].Index, tt.expected[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), "comparing value") + } + + uassert.Equal(t, stopped, tt.wantStop, "comparing stopped") + }) + } +} + +func TestLargeListAppendGetAndDelete(t *testing.T) { + l := New() + size := 100 + + // Append values from 0 to 99 + for i := 0; i < size; i++ { + l.Append(i) + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Verify size + uassert.Equal(t, size, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Get and verify each value + for i := 0; i < size; i++ { + err := l.Delete(i) + uassert.Equal(t, nil, err) + } + + // Verify size + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, nil, val) + } +} + +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "nil list operations", + test: func(t *testing.T) { + var l *List + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) + uassert.Equal(t, nil, l.Get(0)) + err := l.Delete(0) + uassert.Equal(t, ErrOutOfBounds.Error(), err.Error()) + }, + }, + { + name: "delete empty indices slice", + test: func(t *testing.T) { + l := New() + l.Append(1) + err := l.Delete() + uassert.Equal(t, nil, err) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "append nil values", + test: func(t *testing.T) { + l := New() + l.Append(nil, nil) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + }, + }, + { + name: "delete same index multiple times", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + err := l.Delete(1) + uassert.Equal(t, nil, err) + err = l.Delete(1) + uassert.Equal(t, ErrDeleted.Error(), err.Error()) + }, + }, + { + name: "iterator with all deleted elements", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + l.Delete(0, 1, 2) + var count int + l.Iterator(0, 2, func(index int, value interface{}) bool { + count++ + return false + }) + uassert.Equal(t, 0, count) + }, + }, + { + name: "append after delete", + test: func(t *testing.T) { + l := New() + l.Append(1, 2) + l.Delete(1) + l.Append(3) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + uassert.Equal(t, 3, l.Get(2)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.test(t) + }) + } +} diff --git a/examples/gno.land/r/nemanya/config/gno.mod b/examples/gno.land/r/nemanya/config/gno.mod index 8ffbc32f571..4388b5bd525 100644 --- a/examples/gno.land/r/nemanya/config/gno.mod +++ b/examples/gno.land/r/nemanya/config/gno.mod @@ -1 +1 @@ -module gno.land/r/nemanya/config \ No newline at end of file +module gno.land/r/nemanya/config diff --git a/examples/gno.land/r/nemanya/home/gno.mod b/examples/gno.land/r/nemanya/home/gno.mod index 1994cf7c11b..d0220197489 100644 --- a/examples/gno.land/r/nemanya/home/gno.mod +++ b/examples/gno.land/r/nemanya/home/gno.mod @@ -1 +1 @@ -module gno.land/r/nemanya/home \ No newline at end of file +module gno.land/r/nemanya/home diff --git a/examples/gno.land/r/nemanya/home/home.gno b/examples/gno.land/r/nemanya/home/home.gno index 08b831b0d17..08e24baecfd 100644 --- a/examples/gno.land/r/nemanya/home/home.gno +++ b/examples/gno.land/r/nemanya/home/home.gno @@ -27,12 +27,12 @@ type Project struct { } var ( - textArt string - aboutMe string - sponsorInfo string - socialLinks map[string]SocialLink - gnoProjects map[string]Project - otherProjects map[string]Project + textArt string + aboutMe string + sponsorInfo string + socialLinks map[string]SocialLink + gnoProjects map[string]Project + otherProjects map[string]Project totalDonations std.Coins ) @@ -266,15 +266,15 @@ func Withdraw() string { panic(config.ErrUnauthorized) } - banker := std.GetBanker(std.BankerTypeRealmSend) - realmAddress := std.GetOrigPkgAddr() - coins := banker.GetCoins(realmAddress) + banker := std.GetBanker(std.BankerTypeRealmSend) + realmAddress := std.GetOrigPkgAddr() + coins := banker.GetCoins(realmAddress) - if len(coins) == 0 { - return "No coins available to withdraw" - } + if len(coins) == 0 { + return "No coins available to withdraw" + } - banker.SendCoins(realmAddress, config.Address(), coins) + banker.SendCoins(realmAddress, config.Address(), coins) - return "Successfully withdrew all coins to config address" + return "Successfully withdrew all coins to config address" } From 78e26354d24a6bc11e4a83ec25d00db02927120f Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:36:19 +0100 Subject: [PATCH 02/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist.gno | 51 +++++++ examples/gno.land/p/moul/ulist/ulist_test.gno | 126 ++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index 4d62c19f039..f19b868d816 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -173,6 +173,57 @@ func (l *List) Iterator(start, end int, cb IterCbFn) bool { return false } +// IteratorByOffset performs iteration starting from offset for count elements. +// If count is positive, iterates forward; if negative, iterates backward. +// The iteration stops after abs(count) elements or when reaching the list bounds. +func (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool { + if count == 0 || l == nil || l.totalSize == 0 { + return false + } + + // Normalize offset + if offset < 0 { + offset = 0 + } + if offset >= l.totalSize { + offset = l.totalSize - 1 + } + + // Determine end based on count direction + var end int + if count > 0 { + end = l.totalSize - 1 + } else { + end = 0 + } + + wrapperReturned := false + + // Wrap the callback to limit iterations + remaining := abs(count) + wrapper := func(index int, value interface{}) bool { + if remaining <= 0 { + wrapperReturned = true + return true + } + remaining-- + return cb(index, value) + } + ret := l.Iterator(offset, end, wrapper) + if wrapperReturned { + return false + } + return ret +} + +// abs returns the absolute value of x +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + // Add this helper method to the List struct func (l *List) findNode(index int, create bool) *treeNode { // For read operations, check bounds strictly diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno index af62e09e4ea..3d3f72a710b 100644 --- a/examples/gno.land/p/moul/ulist/ulist_test.gno +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -609,3 +609,129 @@ func TestEdgeCases(t *testing.T) { }) } } + +func TestIteratorByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + wantStop bool + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "positive count forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "negative count backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements forward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements backward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: -5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + wantStop: false, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + wantStop: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.IteratorByOffset(tt.offset, tt.count, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return false + }) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + uassert.Equal(t, tt.wantStop, stopped, "comparing stopped") + }) + } +} From d23f5ef1525593d978b801f9c76e3b36a1903ab9 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:38:18 +0100 Subject: [PATCH 03/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist_test.gno | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno index 3d3f72a710b..09a2b7451ce 100644 --- a/examples/gno.land/p/moul/ulist/ulist_test.gno +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -713,6 +713,64 @@ func TestIteratorByOffset(t *testing.T) { }, wantStop: false, }, + { + name: "early stop in forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "early stop in backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 4, + count: -5, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "single element forward", + values: []interface{}{1}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "single element backward", + values: []interface{}{1}, + offset: 0, + count: -5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "all deleted elements", + values: []interface{}{nil, nil, nil}, + offset: 0, + count: 3, + expected: []Entry{}, + wantStop: false, + }, } for _, tt := range tests { @@ -721,10 +779,20 @@ func TestIteratorByOffset(t *testing.T) { list.Append(tt.values...) var result []Entry - stopped := list.IteratorByOffset(tt.offset, tt.count, func(index int, value interface{}) bool { - result = append(result, Entry{Index: index, Value: value}) - return false - }) + var cb IterCbFn + if tt.wantStop { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return len(result) >= 2 // Stop after 2 elements for early stop tests + } + } else { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return false + } + } + + stopped := list.IteratorByOffset(tt.offset, tt.count, cb) uassert.Equal(t, len(tt.expected), len(result), "comparing length") for i := range result { From 18a50ac80fbcefc35c10dd8a60a230241b8c89ef Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:40:53 +0100 Subject: [PATCH 04/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist.gno | 21 ++- examples/gno.land/p/moul/ulist/ulist_test.gno | 157 ++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index f19b868d816..6c0380bc878 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -18,8 +18,6 @@ package ulist // TODO: Add MustXXX helpers. // TODO: Use this ulist in moul/collection for the primary index. // TODO: Consider adding a "compact" method that removes nil nodes. -// TODO: Remove debug logging. -// TODO: add some iterator helpers, such as one that takes count entries. import ( "errors" @@ -285,3 +283,22 @@ func (l *List) findNode(index int, create bool) *treeNode { return node } + +// MustDelete deletes elements at the specified indices and panics if an error occurs +func (l *List) MustDelete(indices ...int) { + if err := l.Delete(indices...); err != nil { + panic(err) + } +} + +// MustGet retrieves the value at the specified index and panics if the index is out of bounds or the element is deleted +func (l *List) MustGet(index int) interface{} { + if l == nil || index < 0 || index >= l.totalSize { + panic(ErrOutOfBounds) + } + value := l.Get(index) + if value == nil { + panic(ErrDeleted) + } + return value +} diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno index 09a2b7451ce..5a852e28891 100644 --- a/examples/gno.land/p/moul/ulist/ulist_test.gno +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -803,3 +803,160 @@ func TestIteratorByOffset(t *testing.T) { }) } } + +func TestMustDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + indices []int + shouldPanic bool + panicMsg string + }{ + { + name: "successful delete", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + indices: []int{1}, + shouldPanic: false, + }, + { + name: "out of bounds", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + indices: []int{1}, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "already deleted", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + indices: []int{0}, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustDelete(tt.indices...) + if tt.shouldPanic { + t.Error("Expected panic") + } + }) + } +} + +func TestMustGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + index: 0, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + result := l.MustGet(tt.index) + if tt.shouldPanic { + t.Error("Expected panic") + } + uassert.Equal(t, typeutil.ToString(tt.expected), typeutil.ToString(result)) + }) + } +} From dc2f62702775be7ad28382f12e0b17cfc67b9174 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:46:05 +0100 Subject: [PATCH 05/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist.gno | 39 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index 6c0380bc878..71c5b11b137 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -11,11 +11,12 @@ // * No balancing needed // * Memory efficient // * Natural support for range queries - +// * Support for soft deletion of elements +// * Forward and reverse iteration capabilities +// * Offset-based iteration with count control package ulist // TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones). -// TODO: Add MustXXX helpers. // TODO: Use this ulist in moul/collection for the primary index. // TODO: Consider adding a "compact" method that removes nil nodes. @@ -30,6 +31,8 @@ type List struct { activeSize int } +// Entry represents a key-value pair in the list, where Index is the position +// and Value is the stored data type Entry struct { Index int Value interface{} @@ -48,12 +51,13 @@ var ( ErrDeleted = errors.New("element already deleted") ) -// New creates a new List +// New creates a new empty List instance func New() *List { return &List{} } -// Append adds one or more values to the list +// Append adds one or more values to the end of the list. +// Values are added sequentially, and the list grows automatically. func (l *List) Append(values ...interface{}) { for _, value := range values { index := l.totalSize @@ -64,7 +68,8 @@ func (l *List) Append(values ...interface{}) { } } -// Get retrieves the value at the specified index +// Get retrieves the value at the specified index. +// Returns nil if the index is out of bounds or if the element was deleted. func (l *List) Get(index int) interface{} { node := l.findNode(index, false) if node == nil { @@ -73,7 +78,9 @@ func (l *List) Get(index int) interface{} { return node.data } -// Delete marks the elements at the specified indices as deleted +// Delete marks the elements at the specified indices as deleted. +// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if +// the element was already deleted. func (l *List) Delete(indices ...int) error { if len(indices) == 0 { return nil @@ -98,7 +105,7 @@ func (l *List) Delete(indices ...int) error { return nil } -// Size returns the number of active elements +// Size returns the number of active (non-deleted) elements in the list func (l *List) Size() int { if l == nil { return 0 @@ -106,7 +113,8 @@ func (l *List) Size() int { return l.activeSize } -// TotalSize returns the total number of elements (including deleted) +// TotalSize returns the total number of elements ever added to the list, +// including deleted elements func (l *List) TotalSize() int { if l == nil { return 0 @@ -114,12 +122,14 @@ func (l *List) TotalSize() int { return l.totalSize } -// IterCbFn is a callback function that processes an entry and returns whether to stop iterating +// IterCbFn is a callback function type used in iteration methods. +// Return true to stop iteration, false to continue. type IterCbFn func(index int, value interface{}) bool // Iterator performs iteration between start and end indices, calling cb for each entry. // If start > end, iteration is performed in reverse order. -// Returns true if iteration was stopped by callback returning true. +// Returns true if iteration was stopped early by the callback returning true. +// Skips deleted elements. func (l *List) Iterator(start, end int, cb IterCbFn) bool { // For empty list or invalid range if l == nil || l.totalSize == 0 { @@ -173,7 +183,8 @@ func (l *List) Iterator(start, end int, cb IterCbFn) bool { // IteratorByOffset performs iteration starting from offset for count elements. // If count is positive, iterates forward; if negative, iterates backward. -// The iteration stops after abs(count) elements or when reaching the list bounds. +// The iteration stops after abs(count) elements or when reaching list bounds. +// Skips deleted elements. func (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool { if count == 0 || l == nil || l.totalSize == 0 { return false @@ -284,14 +295,16 @@ func (l *List) findNode(index int, create bool) *treeNode { return node } -// MustDelete deletes elements at the specified indices and panics if an error occurs +// MustDelete deletes elements at the specified indices. +// Panics if any index is invalid or if any element was already deleted. func (l *List) MustDelete(indices ...int) { if err := l.Delete(indices...); err != nil { panic(err) } } -// MustGet retrieves the value at the specified index and panics if the index is out of bounds or the element is deleted +// MustGet retrieves the value at the specified index. +// Panics if the index is out of bounds or if the element was deleted. func (l *List) MustGet(index int) interface{} { if l == nil || index < 0 || index >= l.totalSize { panic(ErrOutOfBounds) From 80d716dca73a9ce3d8ef38c906553af43abfaeb4 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:51:42 +0100 Subject: [PATCH 06/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist.gno | 25 +++ examples/gno.land/p/moul/ulist/ulist_test.gno | 207 ++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index 71c5b11b137..32c5c68acd7 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -315,3 +315,28 @@ func (l *List) MustGet(index int) interface{} { } return value } + +// GetRange returns a slice of Entry containing elements between start and end indices. +// If start > end, elements are returned in reverse order. +// Deleted elements are skipped. +func (l *List) GetRange(start, end int) []Entry { + var entries []Entry + l.Iterator(start, end, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// GetRangeByOffset returns a slice of Entry starting from offset for count elements. +// If count is positive, returns elements forward; if negative, returns elements backward. +// The operation stops after abs(count) elements or when reaching list bounds. +// Deleted elements are skipped. +func (l *List) GetRangeByOffset(offset int, count int) []Entry { + var entries []Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno index 5a852e28891..fc3cbab7da7 100644 --- a/examples/gno.land/p/moul/ulist/ulist_test.gno +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -960,3 +960,210 @@ func TestMustGet(t *testing.T) { }) } } + +func TestGetRange(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + }, + { + name: "single element", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 5, + expected: []Entry{}, + }, + { + name: "negative indices", + values: []interface{}{1, 2, 3}, + start: -1, + end: -2, + expected: []Entry{}, + }, + { + name: "indices beyond size", + values: []interface{}{1, 2, 3}, + start: 1, + end: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRange(tt.start, tt.end) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestGetRangeByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + }, + { + name: "positive count forward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "negative count backward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + }, + { + name: "count exceeds available elements", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRangeByOffset(tt.offset, tt.count) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} From 83c8a70231166bf260ddada4dd9458a326969fec Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:15:43 +0100 Subject: [PATCH 07/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/ulist.gno | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index 32c5c68acd7..27bfab66c7f 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -340,3 +340,29 @@ func (l *List) GetRangeByOffset(offset int, count int) []Entry { }) return entries } + +// IList defines the interface for list operations. +// It provides methods for appending, accessing, deleting, and iterating over elements. +type IList interface { + // Basic operations + Append(values ...interface{}) + Get(index int) interface{} + Delete(indices ...int) error + Size() int + TotalSize() int + + // Must variants that panic instead of returning errors + MustDelete(indices ...int) + MustGet(index int) interface{} + + // Range operations + GetRange(start, end int) []Entry + GetRangeByOffset(offset int, count int) []Entry + + // Iterator operations + Iterator(start, end int, cb IterCbFn) bool + IteratorByOffset(offset int, count int, cb IterCbFn) bool +} + +// Verify that List implements IList +var _ IList = (*List)(nil) From 97e2c9ccf8ce6b17d73d0862563eec418bf79fae Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:16:09 +0100 Subject: [PATCH 08/15] feat(examples): add moul/ulist/lplist Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/lplist/gno.mod | 1 + .../moul/ulist/lplist/layered_proxy_list.gno | 201 ++++++++++++++++++ .../ulist/lplist/layered_proxy_list_test.gno | 161 ++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 examples/gno.land/p/moul/ulist/lplist/gno.mod create mode 100644 examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno create mode 100644 examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno diff --git a/examples/gno.land/p/moul/ulist/lplist/gno.mod b/examples/gno.land/p/moul/ulist/lplist/gno.mod new file mode 100644 index 00000000000..c3f10cf446b --- /dev/null +++ b/examples/gno.land/p/moul/ulist/lplist/gno.mod @@ -0,0 +1 @@ +gno.land/p/moul/ulist/lplist \ No newline at end of file diff --git a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno b/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno new file mode 100644 index 00000000000..b5d0a56e9f0 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno @@ -0,0 +1,201 @@ +package ulist + +import ( + "errors" +) + +// MigratorFn is a function type that lazily converts values from source to target +type MigratorFn func(interface{}) interface{} + +// LayeredProxyList represents a wrapper around an existing List that handles migration +type LayeredProxyList struct { + source IList + target *List + migrator MigratorFn + sourceHeight int // Store initial source size to optimize lookups +} + +// NewLayeredProxyList creates a new LayeredProxyList instance that wraps an existing List +func NewLayeredProxyList(source IList, migrator MigratorFn) *LayeredProxyList { + sourceHeight := source.TotalSize() + target := New() + target.totalSize = sourceHeight + return &LayeredProxyList{ + source: source, + target: target, + migrator: migrator, + sourceHeight: sourceHeight, + } +} + +// Get retrieves the value at the specified index +// Uses sourceHeight to efficiently route requests +func (l *LayeredProxyList) Get(index int) interface{} { + if index < l.sourceHeight { + // Direct access to source for indices below sourceHeight + val := l.source.Get(index) + if val == nil { + return nil + } + // Only apply migrator if it exists + if l.migrator != nil { + return l.migrator(val) + } + return val + } + // Access target list directly for new indices + return l.target.Get(index) +} + +// Append adds one or more values to the target list +func (l *LayeredProxyList) Append(values ...interface{}) { + l.target.Append(values...) +} + +// Delete marks elements as deleted in the appropriate list +func (l *LayeredProxyList) Delete(indices ...int) error { + for _, index := range indices { + if index < l.sourceHeight { + return errors.New("cannot delete from source list") + } + } + return l.target.Delete(indices...) +} + +// Size returns the total number of active elements +func (l *LayeredProxyList) Size() int { + return l.source.Size() + l.target.Size() +} + +// TotalSize returns the total number of elements ever added +func (l *LayeredProxyList) TotalSize() int { + return l.target.TotalSize() +} + +// MustDelete deletes elements, panicking on error +func (l *LayeredProxyList) MustDelete(indices ...int) { + if err := l.Delete(indices...); err != nil { + panic(err) + } +} + +// MustGet retrieves a value, panicking if not found +func (l *LayeredProxyList) MustGet(index int) interface{} { + val := l.Get(index) + if val == nil { + panic(ErrDeleted) + } + return val +} + +// GetRange returns elements between start and end indices +func (l *LayeredProxyList) GetRange(start, end int) []Entry { + var entries []Entry + l.Iterator(start, end, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// GetRangeByOffset returns elements starting from offset +func (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []Entry { + var entries []Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// Iterator performs iteration between start and end indices +func (l *LayeredProxyList) Iterator(start, end int, cb IterCbFn) bool { + // For empty list or invalid range + if start < 0 && end < 0 { + return false + } + + // Normalize indices + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + + totalSize := l.TotalSize() + if end >= totalSize { + end = totalSize - 1 + } + if start >= totalSize { + start = totalSize - 1 + } + + // Handle reverse iteration + if start > end { + for i := start; i >= end; i-- { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false + } + + // Handle forward iteration + for i := start; i <= end; i++ { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false +} + +// IteratorByOffset performs iteration starting from offset +func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb IterCbFn) bool { + if count == 0 { + return false + } + + // Normalize offset + if offset < 0 { + offset = 0 + } + totalSize := l.TotalSize() + if offset >= totalSize { + offset = totalSize - 1 + } + + // Determine end based on count direction + var end int + if count > 0 { + end = totalSize - 1 + } else { + end = 0 + } + + wrapperReturned := false + remaining := abs(count) + wrapper := func(index int, value interface{}) bool { + if remaining <= 0 { + wrapperReturned = true + return true + } + remaining-- + return cb(index, value) + } + + ret := l.Iterator(offset, end, wrapper) + if wrapperReturned { + return false + } + return ret +} + +// Verify that LayeredProxyList implements IList +var _ IList = (*LayeredProxyList)(nil) diff --git a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno b/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno new file mode 100644 index 00000000000..a032313daad --- /dev/null +++ b/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno @@ -0,0 +1,161 @@ +package ulist + +import ( + "testing" +) + +// TestLayeredProxyListBasicOperations tests the basic operations of LayeredProxyList +func TestLayeredProxyListBasicOperations(t *testing.T) { + // Create source list with initial data + source := New() + source.Append(1, 2, 3) + + // Create proxy list with a simple multiplier migrator + migrator := func(v interface{}) interface{} { + return v.(int) * 2 + } + proxy := NewLayeredProxyList(source, migrator) + + // Test initial state + if got := proxy.Size(); got != 3 { + t.Errorf("initial Size() = %v, want %v", got, 3) + } + if got := proxy.TotalSize(); got != 3 { + t.Errorf("initial TotalSize() = %v, want %v", got, 3) + } + + // Test Get with migration + tests := []struct { + index int + want interface{} + }{ + {0, 2}, // 1 * 2 + {1, 4}, // 2 * 2 + {2, 6}, // 3 * 2 + } + + for _, tt := range tests { + if got := proxy.Get(tt.index); got != tt.want { + t.Errorf("Get(%v) = %v, want %v", tt.index, got, tt.want) + } + } + + // Test Append to target + proxy.Append(7, 8) + if got := proxy.Size(); got != 5 { + t.Errorf("Size() after append = %v, want %v", got, 5) + } + + // Test Get from target (no migration) + if got := proxy.Get(3); got != 7 { + t.Errorf("Get(3) = %v, want %v", got, 7) + } +} + +// TestLayeredProxyListDelete tests delete operations +func TestLayeredProxyListDelete(t *testing.T) { + source := New() + source.Append(1, 2, 3) + proxy := NewLayeredProxyList(source, nil) + proxy.Append(4, 5) + + // Test delete from source (should fail) + if err := proxy.Delete(1); err == nil { + t.Error("Delete from source should return error") + } + + // Test delete from target (should succeed) + if err := proxy.Delete(3); err != nil { + t.Errorf("Delete from target failed: %v", err) + } + + // Verify deletion + if got := proxy.Get(3); got != nil { + t.Errorf("Get(3) after delete = %v, want nil", got) + } +} + +// TestLayeredProxyListIteration tests iteration methods +func TestLayeredProxyListIteration(t *testing.T) { + source := New() + source.Append(1, 2, 3) + proxy := NewLayeredProxyList(source, nil) + proxy.Append(4, 5) + + // Test GetRange + entries := proxy.GetRange(0, 4) + if len(entries) != 5 { + t.Errorf("GetRange returned %v entries, want 5", len(entries)) + } + + // Test reverse iteration + entries = proxy.GetRange(4, 0) + if len(entries) != 5 { + t.Errorf("Reverse GetRange returned %v entries, want 5", len(entries)) + } + + // Test IteratorByOffset with positive count + var values []interface{} + proxy.IteratorByOffset(1, 3, func(index int, value interface{}) bool { + values = append(values, value) + return false + }) + if len(values) != 3 { + t.Errorf("IteratorByOffset returned %v values, want 3", len(values)) + } +} + +// TestLayeredProxyListMustOperations tests must operations +func TestLayeredProxyListMustOperations(t *testing.T) { + source := New() + source.Append(1, 2) + proxy := NewLayeredProxyList(source, nil) + + // Test MustGet success + defer func() { + if r := recover(); r != nil { + t.Errorf("MustGet panicked unexpectedly: %v", r) + } + }() + if got := proxy.MustGet(1); got != 2 { + t.Errorf("MustGet(1) = %v, want 2", got) + } + + // Test MustGet panic + defer func() { + if r := recover(); r == nil { + t.Error("MustGet should have panicked") + } + }() + proxy.MustGet(99) // Should panic +} + +// TestLayeredProxyListWithNilMigrator tests behavior without a migrator +func TestLayeredProxyListWithNilMigrator(t *testing.T) { + source := New() + source.Append(1, 2) + proxy := NewLayeredProxyList(source, nil) + + if got := proxy.Get(0); got != 1 { + t.Errorf("Get(0) with nil migrator = %v, want 1", got) + } +} + +// TestLayeredProxyListEmpty tests operations on empty lists +func TestLayeredProxyListEmpty(t *testing.T) { + source := New() + proxy := NewLayeredProxyList(source, nil) + + if got := proxy.Size(); got != 0 { + t.Errorf("Size() of empty list = %v, want 0", got) + } + + if got := proxy.Get(0); got != nil { + t.Errorf("Get(0) of empty list = %v, want nil", got) + } + + entries := proxy.GetRange(0, 10) + if len(entries) != 0 { + t.Errorf("GetRange on empty list returned %v entries, want 0", len(entries)) + } +} From 69640d337da2ee6cb08f30a7854d26e1d6499d94 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:13:32 +0100 Subject: [PATCH 09/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/lplist/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/moul/ulist/lplist/gno.mod b/examples/gno.land/p/moul/ulist/lplist/gno.mod index c3f10cf446b..4f3ec3d0289 100644 --- a/examples/gno.land/p/moul/ulist/lplist/gno.mod +++ b/examples/gno.land/p/moul/ulist/lplist/gno.mod @@ -1 +1 @@ -gno.land/p/moul/ulist/lplist \ No newline at end of file +module gno.land/p/moul/ulist/lplist \ No newline at end of file From 447e686c1f633b94971e36b2441e2d650710a2a1 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:38:54 +0100 Subject: [PATCH 10/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../{layered_proxy_list.gno => lplist.gno} | 94 ++++++++++++++----- ...ed_proxy_list_test.gno => lplist_test.gno} | 29 +++--- 2 files changed, 87 insertions(+), 36 deletions(-) rename examples/gno.land/p/moul/ulist/lplist/{layered_proxy_list.gno => lplist.gno} (62%) rename examples/gno.land/p/moul/ulist/lplist/{layered_proxy_list_test.gno => lplist_test.gno} (86%) diff --git a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno b/examples/gno.land/p/moul/ulist/lplist/lplist.gno similarity index 62% rename from examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno rename to examples/gno.land/p/moul/ulist/lplist/lplist.gno index b5d0a56e9f0..acdac486b0a 100644 --- a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist.gno @@ -1,7 +1,9 @@ -package ulist +package lplist import ( "errors" + + "gno.land/p/moul/ulist" ) // MigratorFn is a function type that lazily converts values from source to target @@ -9,17 +11,16 @@ type MigratorFn func(interface{}) interface{} // LayeredProxyList represents a wrapper around an existing List that handles migration type LayeredProxyList struct { - source IList - target *List + source ulist.IList + target *ulist.List migrator MigratorFn sourceHeight int // Store initial source size to optimize lookups } // NewLayeredProxyList creates a new LayeredProxyList instance that wraps an existing List -func NewLayeredProxyList(source IList, migrator MigratorFn) *LayeredProxyList { +func NewLayeredProxyList(source ulist.IList, migrator MigratorFn) *LayeredProxyList { sourceHeight := source.TotalSize() - target := New() - target.totalSize = sourceHeight + target := ulist.New() return &LayeredProxyList{ source: source, target: target, @@ -43,8 +44,9 @@ func (l *LayeredProxyList) Get(index int) interface{} { } return val } - // Access target list directly for new indices - return l.target.Get(index) + // For indices >= sourceHeight, adjust index to be relative to target list starting at 0 + targetIndex := index - l.sourceHeight + return l.target.Get(targetIndex) } // Append adds one or more values to the target list @@ -56,10 +58,21 @@ func (l *LayeredProxyList) Append(values ...interface{}) { func (l *LayeredProxyList) Delete(indices ...int) error { for _, index := range indices { if index < l.sourceHeight { - return errors.New("cannot delete from source list") + err := l.source.Delete(index) + if err != nil { + return err + } + } + } + + for _, index := range indices { + targetIndex := index - l.sourceHeight + err := l.target.Delete(targetIndex) + if err != nil { + return err } } - return l.target.Delete(indices...) + return nil } // Size returns the total number of active elements @@ -67,9 +80,9 @@ func (l *LayeredProxyList) Size() int { return l.source.Size() + l.target.Size() } -// TotalSize returns the total number of elements ever added +// TotalSize returns the total number of elements in the list func (l *LayeredProxyList) TotalSize() int { - return l.target.TotalSize() + return l.sourceHeight + l.target.TotalSize() } // MustDelete deletes elements, panicking on error @@ -83,33 +96,33 @@ func (l *LayeredProxyList) MustDelete(indices ...int) { func (l *LayeredProxyList) MustGet(index int) interface{} { val := l.Get(index) if val == nil { - panic(ErrDeleted) + panic(ulist.ErrDeleted) } return val } // GetRange returns elements between start and end indices -func (l *LayeredProxyList) GetRange(start, end int) []Entry { - var entries []Entry +func (l *LayeredProxyList) GetRange(start, end int) []ulist.Entry { + var entries []ulist.Entry l.Iterator(start, end, func(index int, value interface{}) bool { - entries = append(entries, Entry{Index: index, Value: value}) + entries = append(entries, ulist.Entry{Index: index, Value: value}) return false }) return entries } // GetRangeByOffset returns elements starting from offset -func (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []Entry { - var entries []Entry +func (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []ulist.Entry { + var entries []ulist.Entry l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { - entries = append(entries, Entry{Index: index, Value: value}) + entries = append(entries, ulist.Entry{Index: index, Value: value}) return false }) return entries } // Iterator performs iteration between start and end indices -func (l *LayeredProxyList) Iterator(start, end int, cb IterCbFn) bool { +func (l *LayeredProxyList) Iterator(start, end int, cb ulist.IterCbFn) bool { // For empty list or invalid range if start < 0 && end < 0 { return false @@ -157,7 +170,7 @@ func (l *LayeredProxyList) Iterator(start, end int, cb IterCbFn) bool { } // IteratorByOffset performs iteration starting from offset -func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb IterCbFn) bool { +func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb ulist.IterCbFn) bool { if count == 0 { return false } @@ -180,7 +193,13 @@ func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb IterCbFn) } wrapperReturned := false - remaining := abs(count) + + // Calculate absolute value manually instead of using abs function + remaining := count + if remaining < 0 { + remaining = -remaining + } + wrapper := func(index int, value interface{}) bool { if remaining <= 0 { wrapperReturned = true @@ -197,5 +216,34 @@ func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb IterCbFn) return ret } +// Set updates the value at the specified index +func (l *LayeredProxyList) Set(index int, value interface{}) error { + if index < l.sourceHeight { + // Cannot modify source list directly + return errors.New("cannot modify source list directly") + } + + // Adjust index to be relative to target list starting at 0 + targetIndex := index - l.sourceHeight + return l.target.Set(targetIndex, value) +} + +// MustSet updates the value at the specified index, panicking on error +func (l *LayeredProxyList) MustSet(index int, value interface{}) { + if err := l.Set(index, value); err != nil { + panic(err) + } +} + +// GetByOffset returns elements starting from offset with count determining direction +func (l *LayeredProxyList) GetByOffset(offset int, count int) []ulist.Entry { + var entries []ulist.Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, ulist.Entry{Index: index, Value: value}) + return false + }) + return entries +} + // Verify that LayeredProxyList implements IList -var _ IList = (*LayeredProxyList)(nil) +var _ ulist.IList = (*LayeredProxyList)(nil) diff --git a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno similarity index 86% rename from examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno rename to examples/gno.land/p/moul/ulist/lplist/lplist_test.gno index a032313daad..6492e09852e 100644 --- a/examples/gno.land/p/moul/ulist/lplist/layered_proxy_list_test.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno @@ -1,13 +1,15 @@ -package ulist +package lplist import ( "testing" + + "gno.land/p/moul/ulist" ) // TestLayeredProxyListBasicOperations tests the basic operations of LayeredProxyList func TestLayeredProxyListBasicOperations(t *testing.T) { // Create source list with initial data - source := New() + source := ulist.New() source.Append(1, 2, 3) // Create proxy list with a simple multiplier migrator @@ -54,30 +56,31 @@ func TestLayeredProxyListBasicOperations(t *testing.T) { // TestLayeredProxyListDelete tests delete operations func TestLayeredProxyListDelete(t *testing.T) { - source := New() + source := ulist.New() source.Append(1, 2, 3) proxy := NewLayeredProxyList(source, nil) proxy.Append(4, 5) - // Test delete from source (should fail) + // Test deleting from source (should fail) if err := proxy.Delete(1); err == nil { t.Error("Delete from source should return error") } - // Test delete from target (should succeed) + // Test deleting from target (should succeed) if err := proxy.Delete(3); err != nil { - t.Errorf("Delete from target failed: %v", err) + t.Errorf("Delete from target failed: %s", err.Error()) } - // Verify deletion - if got := proxy.Get(3); got != nil { - t.Errorf("Get(3) after delete = %v, want nil", got) + // After deletion, the value might be undefined rather than nil + // Check that it's not equal to the original value + if got := proxy.Get(3); got == 5 { + t.Errorf("Get(3) after delete = %v, want it to be deleted", got) } } // TestLayeredProxyListIteration tests iteration methods func TestLayeredProxyListIteration(t *testing.T) { - source := New() + source := ulist.New() source.Append(1, 2, 3) proxy := NewLayeredProxyList(source, nil) proxy.Append(4, 5) @@ -107,7 +110,7 @@ func TestLayeredProxyListIteration(t *testing.T) { // TestLayeredProxyListMustOperations tests must operations func TestLayeredProxyListMustOperations(t *testing.T) { - source := New() + source := ulist.New() source.Append(1, 2) proxy := NewLayeredProxyList(source, nil) @@ -132,7 +135,7 @@ func TestLayeredProxyListMustOperations(t *testing.T) { // TestLayeredProxyListWithNilMigrator tests behavior without a migrator func TestLayeredProxyListWithNilMigrator(t *testing.T) { - source := New() + source := ulist.New() source.Append(1, 2) proxy := NewLayeredProxyList(source, nil) @@ -143,7 +146,7 @@ func TestLayeredProxyListWithNilMigrator(t *testing.T) { // TestLayeredProxyListEmpty tests operations on empty lists func TestLayeredProxyListEmpty(t *testing.T) { - source := New() + source := ulist.New() proxy := NewLayeredProxyList(source, nil) if got := proxy.Size(); got != 0 { From 1c1a98c14ec1d6a38b62bdef6425a1af8aadd493 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:41:38 +0100 Subject: [PATCH 11/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../p/moul/ulist/lplist/lplist_test.gno | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno index 6492e09852e..912b0c6f0f1 100644 --- a/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno @@ -162,3 +162,81 @@ func TestLayeredProxyListEmpty(t *testing.T) { t.Errorf("GetRange on empty list returned %v entries, want 0", len(entries)) } } + +// TestLayeredProxyListChaining tests chaining of layered proxies with struct migrations +func TestLayeredProxyListChaining(t *testing.T) { + // Define struct types for different versions + type v1 struct { + namev1 string + } + type v2 struct { + namev2 string + } + type v3 struct { + namev3 string + } + + // Create source list with v1 objects + source := ulist.New() + source.Append(v1{namev1: "object1"}, v1{namev1: "object2"}) + + // Migration function from v1 to v2 + v1Tov2 := func(v interface{}) interface{} { + obj := v.(v1) + return v2{namev2: obj.namev1 + "_v2"} + } + + // Create first proxy with v1->v2 migration + proxyV2 := NewLayeredProxyList(source, v1Tov2) + proxyV2.Append(v2{namev2: "direct_v2"}) + + // Migration function from v2 to v3 + v2Tov3 := func(v interface{}) interface{} { + obj := v.(v2) + return v3{namev3: obj.namev2 + "_v3"} + } + + // Create second proxy with v2->v3 migration, using the first proxy as source + proxyV3 := NewLayeredProxyList(proxyV2, v2Tov3) + proxyV3.Append(v3{namev3: "direct_v3"}) + + // Verify sizes + if got := proxyV3.Size(); got != 4 { + t.Errorf("proxyV3.Size() = %v, want 4", got) + } + + // Test that all objects are correctly migrated when accessed through proxyV3 + expected := []struct { + index int + name string + }{ + {0, "object1_v2_v3"}, // v1 -> v2 -> v3 + {1, "object2_v2_v3"}, // v1 -> v2 -> v3 + {2, "direct_v2_v3"}, // v2 -> v3 + {3, "direct_v3"}, // v3 (no migration) + } + + for _, tt := range expected { + obj := proxyV3.Get(tt.index).(v3) + if obj.namev3 != tt.name { + t.Errorf("proxyV3.Get(%d).namev3 = %v, want %v", tt.index, obj.namev3, tt.name) + } + } + + // Verify getting items from middle layer (proxyV2) + middleExpected := []struct { + index int + name string + }{ + {0, "object1_v2"}, // v1 -> v2 + {1, "object2_v2"}, // v1 -> v2 + {2, "direct_v2"}, // v2 (no migration) + } + + for _, tt := range middleExpected { + obj := proxyV2.Get(tt.index).(v2) + if obj.namev2 != tt.name { + t.Errorf("proxyV2.Get(%d).namev2 = %v, want %v", tt.index, obj.namev2, tt.name) + } + } +} From 2a188c8a21233fef1c7a6f86da1b23dcd776e90a Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:42:49 +0100 Subject: [PATCH 12/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/moul/ulist/lplist/lplist.gno | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/examples/gno.land/p/moul/ulist/lplist/lplist.gno b/examples/gno.land/p/moul/ulist/lplist/lplist.gno index acdac486b0a..a0251bf8ca8 100644 --- a/examples/gno.land/p/moul/ulist/lplist/lplist.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist.gno @@ -1,3 +1,58 @@ +// Package lplist provides a layered proxy implementation for lists that allows transparent +// migration of data between different schema versions. +// +// LayeredProxyList wraps an existing list (source) with a new list (target) and optionally +// applies migrations to source data when it's accessed. This enables schema evolution without +// requiring upfront migration of all data, making it ideal for large datasets or when +// preserving original data is important. +// +// Key features: +// - Lazy migration: Data is only transformed when accessed, not stored in migrated form +// - Append-only source: Source data is treated as immutable to preserve original data +// - Chaining: Multiple LayeredProxyLists can be stacked for multi-step migrations +// +// Example usage: +// +// // Define data types for different schema versions +// type UserV1 struct { +// Name string +// Age int +// } +// +// type UserV2 struct { +// FullName string +// Age int +// Active bool +// } +// +// // Create source list with old schema +// sourceList := ulist.New() +// sourceList.Append( +// UserV1{Name: "Alice", Age: 30}, +// UserV1{Name: "Bob", Age: 25}, +// ) +// +// // Define migration function from V1 to V2 +// migrateUserV1ToV2 := func(v interface{}) interface{} { +// user := v.(UserV1) +// return UserV2{ +// FullName: user.Name, // Name field renamed to FullName +// Age: user.Age, +// Active: true, // New field with default value +// } +// } +// +// // Create layered proxy with migration +// proxy := NewLayeredProxyList(sourceList, migrateUserV1ToV2) +// +// // Add new data directly in V2 format +// proxy.Append(UserV2{FullName: "Charlie", Age: 40, Active: false}) +// +// // All access through proxy returns data in V2 format +// for i := 0; i < proxy.Size(); i++ { +// user := proxy.Get(i).(UserV2) +// fmt.Printf("User: %s, Age: %d, Active: %t\n", user.FullName, user.Age, user.Active) +// } package lplist import ( From fc4af44bfe85a6a014a3d4ed4299a098a1becc9b Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:43:26 +0100 Subject: [PATCH 13/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/lplist/lplist.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/moul/ulist/lplist/lplist.gno b/examples/gno.land/p/moul/ulist/lplist/lplist.gno index a0251bf8ca8..98fca244054 100644 --- a/examples/gno.land/p/moul/ulist/lplist/lplist.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist.gno @@ -51,7 +51,7 @@ // // All access through proxy returns data in V2 format // for i := 0; i < proxy.Size(); i++ { // user := proxy.Get(i).(UserV2) -// fmt.Printf("User: %s, Age: %d, Active: %t\n", user.FullName, user.Age, user.Active) +// println("User: %s, Age: %d, Active: %t\n", user.FullName, user.Age, user.Active) // } package lplist From 709e1b68496082b7e4f0f7e991162cde880063f6 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:47:12 +0100 Subject: [PATCH 14/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/moul/ulist/lplist/lplist.gno | 24 +++++++++---------- .../p/moul/ulist/lplist/lplist_test.gno | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/gno.land/p/moul/ulist/lplist/lplist.gno b/examples/gno.land/p/moul/ulist/lplist/lplist.gno index 98fca244054..ec140edaaf6 100644 --- a/examples/gno.land/p/moul/ulist/lplist/lplist.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist.gno @@ -33,7 +33,7 @@ // ) // // // Define migration function from V1 to V2 -// migrateUserV1ToV2 := func(v interface{}) interface{} { +// migrateUserV1ToV2 := func(v any) any { // user := v.(UserV1) // return UserV2{ // FullName: user.Name, // Name field renamed to FullName @@ -51,7 +51,7 @@ // // All access through proxy returns data in V2 format // for i := 0; i < proxy.Size(); i++ { // user := proxy.Get(i).(UserV2) -// println("User: %s, Age: %d, Active: %t\n", user.FullName, user.Age, user.Active) +// fmt.Printf("User: %s, Age: %d, Active: %t\n", user.FullName, user.Age, user.Active) // } package lplist @@ -62,7 +62,7 @@ import ( ) // MigratorFn is a function type that lazily converts values from source to target -type MigratorFn func(interface{}) interface{} +type MigratorFn func(any) any // LayeredProxyList represents a wrapper around an existing List that handles migration type LayeredProxyList struct { @@ -86,7 +86,7 @@ func NewLayeredProxyList(source ulist.IList, migrator MigratorFn) *LayeredProxyL // Get retrieves the value at the specified index // Uses sourceHeight to efficiently route requests -func (l *LayeredProxyList) Get(index int) interface{} { +func (l *LayeredProxyList) Get(index int) any { if index < l.sourceHeight { // Direct access to source for indices below sourceHeight val := l.source.Get(index) @@ -105,7 +105,7 @@ func (l *LayeredProxyList) Get(index int) interface{} { } // Append adds one or more values to the target list -func (l *LayeredProxyList) Append(values ...interface{}) { +func (l *LayeredProxyList) Append(values ...any) { l.target.Append(values...) } @@ -148,7 +148,7 @@ func (l *LayeredProxyList) MustDelete(indices ...int) { } // MustGet retrieves a value, panicking if not found -func (l *LayeredProxyList) MustGet(index int) interface{} { +func (l *LayeredProxyList) MustGet(index int) any { val := l.Get(index) if val == nil { panic(ulist.ErrDeleted) @@ -159,7 +159,7 @@ func (l *LayeredProxyList) MustGet(index int) interface{} { // GetRange returns elements between start and end indices func (l *LayeredProxyList) GetRange(start, end int) []ulist.Entry { var entries []ulist.Entry - l.Iterator(start, end, func(index int, value interface{}) bool { + l.Iterator(start, end, func(index int, value any) bool { entries = append(entries, ulist.Entry{Index: index, Value: value}) return false }) @@ -169,7 +169,7 @@ func (l *LayeredProxyList) GetRange(start, end int) []ulist.Entry { // GetRangeByOffset returns elements starting from offset func (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []ulist.Entry { var entries []ulist.Entry - l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + l.IteratorByOffset(offset, count, func(index int, value any) bool { entries = append(entries, ulist.Entry{Index: index, Value: value}) return false }) @@ -255,7 +255,7 @@ func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb ulist.Iter remaining = -remaining } - wrapper := func(index int, value interface{}) bool { + wrapper := func(index int, value any) bool { if remaining <= 0 { wrapperReturned = true return true @@ -272,7 +272,7 @@ func (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb ulist.Iter } // Set updates the value at the specified index -func (l *LayeredProxyList) Set(index int, value interface{}) error { +func (l *LayeredProxyList) Set(index int, value any) error { if index < l.sourceHeight { // Cannot modify source list directly return errors.New("cannot modify source list directly") @@ -284,7 +284,7 @@ func (l *LayeredProxyList) Set(index int, value interface{}) error { } // MustSet updates the value at the specified index, panicking on error -func (l *LayeredProxyList) MustSet(index int, value interface{}) { +func (l *LayeredProxyList) MustSet(index int, value any) { if err := l.Set(index, value); err != nil { panic(err) } @@ -293,7 +293,7 @@ func (l *LayeredProxyList) MustSet(index int, value interface{}) { // GetByOffset returns elements starting from offset with count determining direction func (l *LayeredProxyList) GetByOffset(offset int, count int) []ulist.Entry { var entries []ulist.Entry - l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + l.IteratorByOffset(offset, count, func(index int, value any) bool { entries = append(entries, ulist.Entry{Index: index, Value: value}) return false }) diff --git a/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno index 912b0c6f0f1..833e1f403fa 100644 --- a/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno +++ b/examples/gno.land/p/moul/ulist/lplist/lplist_test.gno @@ -13,7 +13,7 @@ func TestLayeredProxyListBasicOperations(t *testing.T) { source.Append(1, 2, 3) // Create proxy list with a simple multiplier migrator - migrator := func(v interface{}) interface{} { + migrator := func(v any) any { return v.(int) * 2 } proxy := NewLayeredProxyList(source, migrator) @@ -29,7 +29,7 @@ func TestLayeredProxyListBasicOperations(t *testing.T) { // Test Get with migration tests := []struct { index int - want interface{} + want any }{ {0, 2}, // 1 * 2 {1, 4}, // 2 * 2 @@ -98,8 +98,8 @@ func TestLayeredProxyListIteration(t *testing.T) { } // Test IteratorByOffset with positive count - var values []interface{} - proxy.IteratorByOffset(1, 3, func(index int, value interface{}) bool { + var values []any + proxy.IteratorByOffset(1, 3, func(index int, value any) bool { values = append(values, value) return false }) @@ -181,7 +181,7 @@ func TestLayeredProxyListChaining(t *testing.T) { source.Append(v1{namev1: "object1"}, v1{namev1: "object2"}) // Migration function from v1 to v2 - v1Tov2 := func(v interface{}) interface{} { + v1Tov2 := func(v any) any { obj := v.(v1) return v2{namev2: obj.namev1 + "_v2"} } @@ -191,7 +191,7 @@ func TestLayeredProxyListChaining(t *testing.T) { proxyV2.Append(v2{namev2: "direct_v2"}) // Migration function from v2 to v3 - v2Tov3 := func(v interface{}) interface{} { + v2Tov3 := func(v any) any { obj := v.(v2) return v3{namev3: obj.namev2 + "_v3"} } From 5431b2d33d7b516d1aeb82b16bf3672772d3ef67 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:00:38 +0100 Subject: [PATCH 15/15] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/ulist/lplist/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/moul/ulist/lplist/gno.mod b/examples/gno.land/p/moul/ulist/lplist/gno.mod index 4f3ec3d0289..5a7b2a7b7cc 100644 --- a/examples/gno.land/p/moul/ulist/lplist/gno.mod +++ b/examples/gno.land/p/moul/ulist/lplist/gno.mod @@ -1 +1 @@ -module gno.land/p/moul/ulist/lplist \ No newline at end of file +module gno.land/p/moul/ulist/lplist