From b4ebf6c81dae104f2da8adaf3515f833bcd27484 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:55:30 +0100 Subject: [PATCH] feat(examples): add p/moul/cow (#3325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package `cow` provides a Copy-on-Write AVL tree implementation. I rebased my commits to make it easier to review the differences with the original `avl.Tree`. Here’s the commit with the changes: https://github.com/gnolang/gno/pull/3325/commits/dd4e91c2cade677f1cb3d9db109f7ebdf88c3659 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- examples/gno.land/p/moul/cow/gno.mod | 1 + examples/gno.land/p/moul/cow/node.gno | 518 ++++++++++++++ examples/gno.land/p/moul/cow/node_test.gno | 795 +++++++++++++++++++++ examples/gno.land/p/moul/cow/tree.gno | 164 +++++ examples/gno.land/p/moul/cow/tree_test.gno | 392 ++++++++++ 5 files changed, 1870 insertions(+) create mode 100644 examples/gno.land/p/moul/cow/gno.mod create mode 100644 examples/gno.land/p/moul/cow/node.gno create mode 100644 examples/gno.land/p/moul/cow/node_test.gno create mode 100644 examples/gno.land/p/moul/cow/tree.gno create mode 100644 examples/gno.land/p/moul/cow/tree_test.gno diff --git a/examples/gno.land/p/moul/cow/gno.mod b/examples/gno.land/p/moul/cow/gno.mod new file mode 100644 index 00000000000..e5dec0bc5b4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/cow diff --git a/examples/gno.land/p/moul/cow/node.gno b/examples/gno.land/p/moul/cow/node.gno new file mode 100644 index 00000000000..0c30871d7c4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node.gno @@ -0,0 +1,518 @@ +package cow + +//---------------------------------------- +// Node + +// Node represents a node in an AVL tree. +type Node struct { + key string // key is the unique identifier for the node. + value interface{} // value is the data stored in the node. + height int8 // height is the height of the node in the tree. + size int // size is the number of nodes in the subtree rooted at this node. + leftNode *Node // leftNode is the left child of the node. + rightNode *Node // rightNode is the right child of the node. +} + +// NewNode creates a new node with the given key and value. +func NewNode(key string, value interface{}) *Node { + return &Node{ + key: key, + value: value, + height: 0, + size: 1, + } +} + +// Size returns the size of the subtree rooted at the node. +func (node *Node) Size() int { + if node == nil { + return 0 + } + return node.size +} + +// IsLeaf checks if the node is a leaf node (has no children). +func (node *Node) IsLeaf() bool { + return node.height == 0 +} + +// Key returns the key of the node. +func (node *Node) Key() string { + return node.key +} + +// Value returns the value of the node. +func (node *Node) Value() interface{} { + return node.value +} + +// _copy creates a copy of the node (excluding value). +func (node *Node) _copy() *Node { + if node.height == 0 { + panic("Why are you copying a value node?") + } + return &Node{ + key: node.key, + height: node.height, + size: node.size, + leftNode: node.leftNode, + rightNode: node.rightNode, + } +} + +// Has checks if a node with the given key exists in the subtree rooted at the node. +func (node *Node) Has(key string) (has bool) { + if node == nil { + return false + } + if node.key == key { + return true + } + if node.height == 0 { + return false + } + if key < node.key { + return node.getLeftNode().Has(key) + } + return node.getRightNode().Has(key) +} + +// Get searches for a node with the given key in the subtree rooted at the node +// and returns its index, value, and whether it exists. +func (node *Node) Get(key string) (index int, value interface{}, exists bool) { + if node == nil { + return 0, nil, false + } + + if node.height == 0 { + if node.key == key { + return 0, node.value, true + } + if node.key < key { + return 1, nil, false + } + return 0, nil, false + } + + if key < node.key { + return node.getLeftNode().Get(key) + } + + rightNode := node.getRightNode() + index, value, exists = rightNode.Get(key) + index += node.size - rightNode.size + return index, value, exists +} + +// GetByIndex retrieves the key-value pair of the node at the given index +// in the subtree rooted at the node. +func (node *Node) GetByIndex(index int) (key string, value interface{}) { + if node.height == 0 { + if index == 0 { + return node.key, node.value + } + panic("GetByIndex asked for invalid index") + } + // TODO: could improve this by storing the sizes + leftNode := node.getLeftNode() + if index < leftNode.size { + return leftNode.GetByIndex(index) + } + return node.getRightNode().GetByIndex(index - leftNode.size) +} + +// Set inserts a new node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +// +// XXX consider a better way to do this... perhaps split Node from Node. +func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) { + if node == nil { + return NewNode(key, value), false + } + + // Always create a new node for leaf nodes + if node.height == 0 { + return node.setLeaf(key, value) + } + + // Copy the node before modifying + newNode := node._copy() + if key < node.key { + newNode.leftNode, updated = node.getLeftNode().Set(key, value) + } else { + newNode.rightNode, updated = node.getRightNode().Set(key, value) + } + + if !updated { + newNode.calcHeightAndSize() + return newNode.balance(), updated + } + + return newNode, updated +} + +// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +func (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) { + if key == node.key { + return NewNode(key, value), true + } + + if key < node.key { + return &Node{ + key: node.key, + height: 1, + size: 2, + leftNode: NewNode(key, value), + rightNode: node, + }, false + } + + return &Node{ + key: key, + height: 1, + size: 2, + leftNode: node, + rightNode: NewNode(key, value), + }, false +} + +// Remove deletes the node with the given key from the subtree rooted at the node. +// returns the new root of the subtree, the new leftmost leaf key (if changed), +// the removed value and the removal was successful. +func (node *Node) Remove(key string) ( + newNode *Node, newKey string, value interface{}, removed bool, +) { + if node == nil { + return nil, "", nil, false + } + if node.height == 0 { + if key == node.key { + return nil, "", node.value, true + } + return node, "", nil, false + } + if key < node.key { + var newLeftNode *Node + newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) + if !removed { + return node, "", value, false + } + if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, true + } + node = node._copy() + node.leftNode = newLeftNode + node.calcHeightAndSize() + node = node.balance() + return node, newKey, value, true + } + + var newRightNode *Node + newRightNode, newKey, value, removed = node.getRightNode().Remove(key) + if !removed { + return node, "", value, false + } + if newRightNode == nil { // right node held value, was removed + return node.leftNode, "", value, true + } + node = node._copy() + node.rightNode = newRightNode + if newKey != "" { + node.key = newKey + } + node.calcHeightAndSize() + node = node.balance() + return node, "", value, true +} + +// getLeftNode returns the left child of the node. +func (node *Node) getLeftNode() *Node { + return node.leftNode +} + +// getRightNode returns the right child of the node. +func (node *Node) getRightNode() *Node { + return node.rightNode +} + +// rotateRight performs a right rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateRight() *Node { + node = node._copy() + l := node.getLeftNode() + _l := l._copy() + + _lrCached := _l.rightNode + _l.rightNode = node + node.leftNode = _lrCached + + node.calcHeightAndSize() + _l.calcHeightAndSize() + + return _l +} + +// rotateLeft performs a left rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateLeft() *Node { + node = node._copy() + r := node.getRightNode() + _r := r._copy() + + _rlCached := _r.leftNode + _r.leftNode = node + node.rightNode = _rlCached + + node.calcHeightAndSize() + _r.calcHeightAndSize() + + return _r +} + +// calcHeightAndSize updates the height and size of the node based on its children. +// NOTE: mutates height and size +func (node *Node) calcHeightAndSize() { + node.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1 + node.size = node.getLeftNode().size + node.getRightNode().size +} + +// calcBalance calculates the balance factor of the node. +func (node *Node) calcBalance() int { + return int(node.getLeftNode().height) - int(node.getRightNode().height) +} + +// balance balances the subtree rooted at the node and returns the new root. +// NOTE: assumes that node can be modified +// TODO: optimize balance & rotate +func (node *Node) balance() (newSelf *Node) { + balance := node.calcBalance() + if balance >= -1 { + return node + } + if balance > 1 { + if node.getLeftNode().calcBalance() >= 0 { + // Left Left Case + return node.rotateRight() + } + // Left Right Case + left := node.getLeftNode() + node.leftNode = left.rotateLeft() + return node.rotateRight() + } + + if node.getRightNode().calcBalance() <= 0 { + // Right Right Case + return node.rotateLeft() + } + + // Right Left Case + right := node.getRightNode() + node.rightNode = right.rotateRight() + return node.rotateLeft() +} + +// Shortcut for TraverseInRange. +func (node *Node) Iterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, true, true, cb) +} + +// Shortcut for TraverseInRange. +func (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, false, true, cb) +} + +// TraverseInRange traverses all nodes, including inner nodes. +// Start is inclusive and end is exclusive when ascending, +// Start and end are inclusive when descending. +// Empty start and empty end denote no start and no end. +// If leavesOnly is true, only visit leaf nodes. +// NOTE: To simulate an exclusive reverse traversal, +// just append 0x00 to start. +func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + afterStart := (start == "" || start < node.key) + startOrAfter := (start == "" || start <= node.key) + beforeEnd := false + if ascending { + beforeEnd = (end == "" || node.key < end) + } else { + beforeEnd = (end == "" || node.key <= end) + } + + // Run callback per inner/leaf node. + stop := false + if (!node.IsLeaf() && !leavesOnly) || + (node.IsLeaf() && startOrAfter && beforeEnd) { + stop = cb(node) + if stop { + return stop + } + } + if node.IsLeaf() { + return stop + } + + if ascending { + // check lower nodes, then higher + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } else { + // check the higher nodes first + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } + + return stop +} + +// TraverseByOffset traverses all nodes, including inner nodes. +// A limit of math.MaxInt means no limit. +func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + + // fast paths. these happen only if TraverseByOffset is called directly on a leaf. + if limit <= 0 || offset >= node.size { + return false + } + if node.IsLeaf() { + if offset > 0 { + return false + } + return cb(node) + } + + // go to the actual recursive function. + return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// TraverseByOffset traverses the subtree rooted at the node by offset and limit, +// in either ascending or descending order, and applies the callback function to each traversed node. +// If leavesOnly is true, only leaf nodes are visited. +func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + // caller guarantees: offset < node.size; limit > 0. + if !leavesOnly { + if cb(node) { + return true + } + } + first, second := node.getLeftNode(), node.getRightNode() + if descending { + first, second = second, first + } + if first.IsLeaf() { + // either run or skip, based on offset + if offset > 0 { + offset-- + } else { + cb(first) + limit-- + if limit <= 0 { + return false + } + } + } else { + // possible cases: + // 1 the offset given skips the first node entirely + // 2 the offset skips none or part of the first node, but the limit requires some of the second node. + // 3 the offset skips none or part of the first node, and the limit stops our search on the first node. + if offset >= first.size { + offset -= first.size // 1 + } else { + if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + return true + } + // number of leaves which could actually be called from inside + delta := first.size - offset + offset = 0 + if delta >= limit { + return true // 3 + } + limit -= delta // 2 + } + } + + // because of the caller guarantees and the way we handle the first node, + // at this point we know that limit > 0 and there must be some values in + // this second node that we include. + + // => if the second node is a leaf, it has to be included. + if second.IsLeaf() { + return cb(second) + } + // => if it is not a leaf, it will still be enough to recursively call this + // function with the updated offset and limit + return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// Only used in testing... +func (node *Node) lmd() *Node { + if node.height == 0 { + return node + } + return node.getLeftNode().lmd() +} + +// Only used in testing... +func (node *Node) rmd() *Node { + if node.height == 0 { + return node + } + return node.getRightNode().rmd() +} + +func maxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +// Equal compares two nodes for structural equality. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (node *Node) Equal(other *Node) bool { + // Handle nil cases + if node == nil || other == nil { + return node == other + } + + // Compare node properties + if node.key != other.key || + node.value != other.value || + node.height != other.height || + node.size != other.size { + return false + } + + // Compare children + leftEqual := (node.leftNode == nil && other.leftNode == nil) || + (node.leftNode != nil && other.leftNode != nil && node.leftNode.Equal(other.leftNode)) + if !leftEqual { + return false + } + + rightEqual := (node.rightNode == nil && other.rightNode == nil) || + (node.rightNode != nil && other.rightNode != nil && node.rightNode.Equal(other.rightNode)) + return rightEqual +} diff --git a/examples/gno.land/p/moul/cow/node_test.gno b/examples/gno.land/p/moul/cow/node_test.gno new file mode 100644 index 00000000000..c7225fe1ab0 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node_test.gno @@ -0,0 +1,795 @@ +package cow + +import ( + "fmt" + "sort" + "strings" + "testing" +) + +func TestTraverseByOffset(t *testing.T) { + const testStrings = `Alfa +Alfred +Alpha +Alphabet +Beta +Beth +Book +Browser` + tt := []struct { + name string + desc bool + }{ + {"ascending", false}, + {"descending", true}, + } + + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + sl := strings.Split(testStrings, "\n") + + // sort a first time in the order opposite to how we'll be traversing + // the tree, to ensure that we are not just iterating through with + // insertion order. + sort.Strings(sl) + if !tt.desc { + reverseSlice(sl) + } + + r := NewNode(sl[0], nil) + for _, v := range sl[1:] { + r, _ = r.Set(v, nil) + } + + // then sort sl in the order we'll be traversing it, so that we can + // compare the result with sl. + reverseSlice(sl) + + var result []string + for i := 0; i < len(sl); i++ { + r.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + } + + if !slicesEqual(sl, result) { + t.Errorf("want %v got %v", sl, result) + } + + for l := 2; l <= len(sl); l++ { + // "slices" + for i := 0; i <= len(sl); i++ { + max := i + l + if max > len(sl) { + max = len(sl) + } + exp := sl[i:max] + actual := []string{} + + r.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool { + actual = append(actual, tr.Key()) + return false + }) + if !slicesEqual(exp, actual) { + t.Errorf("want %v got %v", exp, actual) + } + } + } + }) + } +} + +func TestHas(t *testing.T) { + tests := []struct { + name string + input []string + hasKey string + expected bool + }{ + { + "has key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "B", + true, + }, + { + "does not have key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "F", + false, + }, + { + "has key in single-node tree", + []string{"A"}, + "A", + true, + }, + { + "does not have key in single-node tree", + []string{"A"}, + "B", + false, + }, + { + "does not have key in empty tree", + []string{}, + "A", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + result := tree.Has(tt.hasKey) + + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestGet(t *testing.T) { + tests := []struct { + name string + input []string + getKey string + expectIdx int + expectVal interface{} + expectExists bool + }{ + { + "get existing key", + []string{"C", "A", "B", "E", "D"}, + "B", + 1, + nil, + true, + }, + { + "get non-existent key (smaller)", + []string{"C", "A", "B", "E", "D"}, + "@", + 0, + nil, + false, + }, + { + "get non-existent key (larger)", + []string{"C", "A", "B", "E", "D"}, + "F", + 5, + nil, + false, + }, + { + "get from empty tree", + []string{}, + "A", + 0, + nil, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + idx, val, exists := tree.Get(tt.getKey) + + if idx != tt.expectIdx { + t.Errorf("Expected index %d, got %d", tt.expectIdx, idx) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + + if exists != tt.expectExists { + t.Errorf("Expected exists %t, got %t", tt.expectExists, exists) + } + }) + } +} + +func TestGetByIndex(t *testing.T) { + tests := []struct { + name string + input []string + idx int + expectKey string + expectVal interface{} + expectPanic bool + }{ + { + "get by valid index", + []string{"C", "A", "B", "E", "D"}, + 2, + "C", + nil, + false, + }, + { + "get by valid index (smallest)", + []string{"C", "A", "B", "E", "D"}, + 0, + "A", + nil, + false, + }, + { + "get by valid index (largest)", + []string{"C", "A", "B", "E", "D"}, + 4, + "E", + nil, + false, + }, + { + "get by invalid index (negative)", + []string{"C", "A", "B", "E", "D"}, + -1, + "", + nil, + true, + }, + { + "get by invalid index (out of range)", + []string{"C", "A", "B", "E", "D"}, + 5, + "", + nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + if tt.expectPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected a panic but didn't get one") + } + }() + } + + key, val := tree.GetByIndex(tt.idx) + + if !tt.expectPanic { + if key != tt.expectKey { + t.Errorf("Expected key %s, got %s", tt.expectKey, key) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + } + }) + } +} + +func TestRemove(t *testing.T) { + tests := []struct { + name string + input []string + removeKey string + expected []string + }{ + { + "remove leaf node", + []string{"C", "A", "B", "D"}, + "B", + []string{"A", "C", "D"}, + }, + { + "remove node with one child", + []string{"C", "A", "B", "D"}, + "A", + []string{"B", "C", "D"}, + }, + { + "remove node with two children", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove root node", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove non-existent key", + []string{"C", "A", "B", "E", "D"}, + "F", + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree, _, _, _ = tree.Remove(tt.removeKey) + + result := make([]string, 0) + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestTraverse(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "empty tree", + []string{}, + []string{}, + }, + { + "single node tree", + []string{"A"}, + []string{"A"}, + }, + { + "small tree", + []string{"C", "A", "B", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "large tree", + []string{"H", "D", "L", "B", "F", "J", "N", "A", "C", "E", "G", "I", "K", "M", "O"}, + []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + t.Run("iterate", func(t *testing.T) { + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + + t.Run("ReverseIterate", func(t *testing.T) { + var result []string + tree.ReverseIterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, len(tt.expected)) + copy(expected, tt.expected) + for i, j := 0, len(expected)-1; i < j; i, j = i+1, j-1 { + expected[i], expected[j] = expected[j], expected[i] + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + + t.Run("TraverseInRange", func(t *testing.T) { + var result []string + start, end := "C", "M" + tree.TraverseInRange(start, end, true, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, 0) + for _, key := range tt.expected { + if key >= start && key < end { + expected = append(expected, key) + } + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + }) + } +} + +func TestRotateWhenHeightDiffers(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation when left subtree is higher", + []string{"E", "C", "A", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "left rotation when right subtree is higher", + []string{"A", "C", "E", "D", "F"}, + []string{"A", "C", "D", "E", "F"}, + }, + { + "left-right rotation", + []string{"E", "A", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "right-left rotation", + []string{"A", "E", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + // perform rotation or balance + tree = tree.balance() + + // check tree structure + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestRotateAndBalance(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation", + []string{"A", "B", "C", "D", "E"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left rotation", + []string{"E", "D", "C", "B", "A"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left-right rotation", + []string{"C", "A", "E", "B", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "right-left rotation", + []string{"C", "E", "A", "D", "B"}, + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree = tree.balance() + + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func slicesEqual(w1, w2 []string) bool { + if len(w1) != len(w2) { + return false + } + for i := 0; i < len(w1); i++ { + if w1[0] != w2[0] { + return false + } + } + return true +} + +func maxint8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func reverseSlice(ss []string) { + for i := 0; i < len(ss)/2; i++ { + j := len(ss) - 1 - i + ss[i], ss[j] = ss[j], ss[i] + } +} + +func TestNodeStructuralSharing(t *testing.T) { + t.Run("unmodified paths remain shared", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + originalRight := root.rightNode + newRoot, _ := root.Set("A", 10) + + if newRoot.rightNode != originalRight { + t.Error("Unmodified right subtree should remain shared") + } + }) + + t.Run("multiple modifications reuse shared structure", func(t *testing.T) { + // Create initial tree + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Store original nodes + originalRight := root.rightNode + + // First modification + mod1, _ := root.Set("A", 10) + + // Second modification + mod2, _ := mod1.Set("C", 30) + + // Check sharing in first modification + if mod1.rightNode != originalRight { + t.Error("First modification should share unmodified right subtree") + } + + // Check that second modification creates new right node + if mod2.rightNode == originalRight { + t.Error("Second modification should create new right node") + } + }) +} + +func TestNodeCopyOnWrite(t *testing.T) { + t.Run("copy preserves structure", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Only copy non-leaf nodes + if !root.IsLeaf() { + copied := root._copy() + if copied == root { + t.Error("Copy should create new instance") + } + + // Create temporary trees to use Equal method + original := &Tree{node: root} + copiedTree := &Tree{node: copied} + if !original.Equal(copiedTree) { + t.Error("Copied node should preserve structure") + } + } + }) + + t.Run("removal copy pattern", func(t *testing.T) { + // Create a more complex tree to test removal + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + root, _ = root.Set("D", 4) // Add this to ensure proper tree structure + + // Store references to original nodes + originalRight := root.rightNode + originalRightRight := originalRight.rightNode + + // Remove "A" which should only affect the left subtree + newRoot, _, _, _ := root.Remove("A") + + // Verify right subtree remains unchanged and shared + if newRoot.rightNode != originalRight { + t.Error("Right subtree should remain shared during removal of left node") + } + + // Also verify deeper nodes remain shared + if newRoot.rightNode.rightNode != originalRightRight { + t.Error("Deep right subtree should remain shared during removal") + } + + // Verify original tree is unchanged + if _, _, exists := root.Get("A"); !exists { + t.Error("Original tree should remain unchanged") + } + }) + + t.Run("copy leaf node panic", func(t *testing.T) { + leaf := NewNode("A", 1) + + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when copying leaf node") + } + }() + + // This should panic with our specific message + leaf._copy() + }) +} + +func TestNodeEqual(t *testing.T) { + tests := []struct { + name string + node1 func() *Node + node2 func() *Node + expected bool + }{ + { + name: "nil nodes", + node1: func() *Node { return nil }, + node2: func() *Node { return nil }, + expected: true, + }, + { + name: "one nil node", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return nil }, + expected: false, + }, + { + name: "single leaf nodes equal", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 1) }, + expected: true, + }, + { + name: "single leaf nodes different key", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("B", 1) }, + expected: false, + }, + { + name: "single leaf nodes different value", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 2) }, + expected: false, + }, + { + name: "complex trees equal", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "complex trees different structure", + node1: func() *Node { + // Create a tree with structure: + // B + // / \ + // A D + n := NewNode("B", 2) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + node2: func() *Node { + // Create a tree with structure: + // C + // / \ + // A D + n := NewNode("C", 3) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + expected: false, // These trees should be different + }, + { + name: "complex trees same structure despite different insertion order", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("A", 1).Set("B", 2) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "truly different structures", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + return n // Tree with just two nodes + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("C", 3) + return n // Different two-node tree + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node1 := tt.node1() + node2 := tt.node2() + result := node1.Equal(node2) + if result != tt.expected { + t.Errorf("Expected Equal to return %v for %s", tt.expected, tt.name) + println("\nComparison failed:") + println("Tree 1:") + printTree(node1, 0) + println("Tree 2:") + printTree(node2, 0) + } + }) + } +} + +// Helper function to print tree structure +func printTree(node *Node, level int) { + if node == nil { + return + } + indent := strings.Repeat(" ", level) + println(fmt.Sprintf("%sKey: %s, Value: %v, Height: %d, Size: %d", + indent, node.key, node.value, node.height, node.size)) + printTree(node.leftNode, level+1) + printTree(node.rightNode, level+1) +} diff --git a/examples/gno.land/p/moul/cow/tree.gno b/examples/gno.land/p/moul/cow/tree.gno new file mode 100644 index 00000000000..befd0a414f6 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree.gno @@ -0,0 +1,164 @@ +// Package cow provides a Copy-on-Write (CoW) AVL tree implementation. +// This is a fork of gno.land/p/demo/avl that adds CoW functionality +// while maintaining the original AVL tree interface and properties. +// +// Copy-on-Write creates a copy of a data structure only when it is modified, +// while still presenting the appearance of a full copy. When a tree is cloned, +// it initially shares all its nodes with the original tree. Only when a +// modification is made to either the original or the clone are new nodes created, +// and only along the path from the root to the modified node. +// +// Key features: +// - O(1) cloning operation +// - Minimal memory usage through structural sharing +// - Full AVL tree functionality (self-balancing, ordered operations) +// - Thread-safe for concurrent reads of shared structures +// +// While the CoW mechanism handles structural copying automatically, users need +// to consider how to handle the values stored in the tree: +// +// 1. Simple Values (int, string, etc.): +// - These are copied by value automatically +// - No additional handling needed +// +// 2. Complex Values (structs, pointers): +// - Only the reference is copied by default +// - Users must implement their own deep copy mechanism if needed +// +// Example: +// +// // Create original tree +// original := cow.NewTree() +// original.Set("key1", "value1") +// +// // Create a clone - O(1) operation +// clone := original.Clone() +// +// // Modify clone - only affected nodes are copied +// clone.Set("key1", "modified") +// +// // Original remains unchanged +// val, _ := original.Get("key1") // Returns "value1" +package cow + +type IterCbFn func(key string, value interface{}) bool + +//---------------------------------------- +// Tree + +// The zero struct can be used as an empty tree. +type Tree struct { + node *Node +} + +// NewTree creates a new empty AVL tree. +func NewTree() *Tree { + return &Tree{ + node: nil, + } +} + +// Size returns the number of key-value pair in the tree. +func (tree *Tree) Size() int { + return tree.node.Size() +} + +// Has checks whether a key exists in the tree. +// It returns true if the key exists, otherwise false. +func (tree *Tree) Has(key string) (has bool) { + return tree.node.Has(key) +} + +// Get retrieves the value associated with the given key. +// It returns the value and a boolean indicating whether the key exists. +func (tree *Tree) Get(key string) (value interface{}, exists bool) { + _, value, exists = tree.node.Get(key) + return +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree. +// It returns the key and value at the given index. +func (tree *Tree) GetByIndex(index int) (key string, value interface{}) { + return tree.node.GetByIndex(index) +} + +// Set inserts a key-value pair into the tree. +// If the key already exists, the value will be updated. +// It returns a boolean indicating whether the key was newly inserted or updated. +func (tree *Tree) Set(key string, value interface{}) (updated bool) { + newnode, updated := tree.node.Set(key, value) + tree.node = newnode + return updated +} + +// Remove removes a key-value pair from the tree. +// It returns the removed value and a boolean indicating whether the key was found and removed. +func (tree *Tree) Remove(key string) (value interface{}, removed bool) { + newnode, _, value, removed := tree.node.Remove(key) + tree.node = newnode + return value, removed +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) Iterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Equal returns true if the two trees contain the same key-value pairs. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (tree *Tree) Equal(other *Tree) bool { + if tree == nil || other == nil { + return tree == other + } + return tree.node.Equal(other.node) +} + +// Clone creates a shallow copy of the tree +func (tree *Tree) Clone() *Tree { + if tree == nil { + return nil + } + return &Tree{ + node: tree.node, + } +} diff --git a/examples/gno.land/p/moul/cow/tree_test.gno b/examples/gno.land/p/moul/cow/tree_test.gno new file mode 100644 index 00000000000..6ee816455b8 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree_test.gno @@ -0,0 +1,392 @@ +package cow + +import ( + "testing" +) + +func TestNewTree(t *testing.T) { + tree := NewTree() + if tree.node != nil { + t.Error("Expected tree.node to be nil") + } +} + +func TestTreeSize(t *testing.T) { + tree := NewTree() + if tree.Size() != 0 { + t.Error("Expected empty tree size to be 0") + } + + tree.Set("key1", "value1") + tree.Set("key2", "value2") + if tree.Size() != 2 { + t.Error("Expected tree size to be 2") + } +} + +func TestTreeHas(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + if !tree.Has("key1") { + t.Error("Expected tree to have key1") + } + + if tree.Has("key2") { + t.Error("Expected tree to not have key2") + } +} + +func TestTreeGet(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, exists := tree.Get("key1") + if !exists || value != "value1" { + t.Error("Expected Get to return value1 and true") + } + + _, exists = tree.Get("key2") + if exists { + t.Error("Expected Get to return false for non-existent key") + } +} + +func TestTreeGetByIndex(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + + key, value := tree.GetByIndex(0) + if key != "key1" || value != "value1" { + t.Error("Expected GetByIndex(0) to return key1 and value1") + } + + key, value = tree.GetByIndex(1) + if key != "key2" || value != "value2" { + t.Error("Expected GetByIndex(1) to return key2 and value2") + } + + defer func() { + if r := recover(); r == nil { + t.Error("Expected GetByIndex to panic for out-of-range index") + } + }() + tree.GetByIndex(2) +} + +func TestTreeRemove(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, removed := tree.Remove("key1") + if !removed || value != "value1" || tree.Size() != 0 { + t.Error("Expected Remove to remove key-value pair") + } + + _, removed = tree.Remove("key2") + if removed { + t.Error("Expected Remove to return false for non-existent key") + } +} + +func TestTreeIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.Iterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key1", "key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key3", "key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.IterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +// Verify that Tree implements avl.ITree +// var _ avl.ITree = (*Tree)(nil) // TODO: fix gnovm bug: ./examples/gno.land/p/moul/cow: test pkg: panic: gno.land/p/moul/cow/tree_test.gno:166:5: name avl not defined in fileset with files [node.gno tree.gno node_test.gno tree_test.gno]: + +func TestCopyOnWrite(t *testing.T) { + // Create original tree + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Create a clone + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + clone.Set("D", 4) + + // Verify original is unchanged + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original tree was modified: expected B=2, got B=%v", val) + } + if original.Has("D") { + t.Error("Original tree was modified: found key D") + } + + // Verify clone has new values + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone not updated: expected B=20, got B=%v", val) + } + if val, _ := clone.Get("D"); val != 4 { + t.Errorf("Clone not updated: expected D=4, got D=%v", val) + } +} + +func TestCopyOnWriteEdgeCases(t *testing.T) { + t.Run("nil tree clone", func(t *testing.T) { + var original *Tree + clone := original.Clone() + if clone != nil { + t.Error("Expected nil clone from nil tree") + } + }) + + t.Run("empty tree clone", func(t *testing.T) { + original := NewTree() + clone := original.Clone() + + // Modify clone + clone.Set("A", 1) + + if original.Size() != 0 { + t.Error("Original empty tree was modified") + } + if clone.Size() != 1 { + t.Error("Clone was not modified") + } + }) + + t.Run("multiple clones", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + // Create multiple clones + clone1 := original.Clone() + clone2 := original.Clone() + clone3 := clone1.Clone() + + // Modify each clone differently + clone1.Set("A", 10) + clone2.Set("B", 20) + clone3.Set("C", 30) + + // Check original remains unchanged + if val, _ := original.Get("A"); val != 1 { + t.Errorf("Original modified: expected A=1, got A=%v", val) + } + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original modified: expected B=2, got B=%v", val) + } + + // Verify each clone has correct values + if val, _ := clone1.Get("A"); val != 10 { + t.Errorf("Clone1 incorrect: expected A=10, got A=%v", val) + } + if val, _ := clone2.Get("B"); val != 20 { + t.Errorf("Clone2 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone3.Get("C"); val != 30 { + t.Errorf("Clone3 incorrect: expected C=30, got C=%v", val) + } + }) + + t.Run("clone after removal", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Remove a node and then clone + original.Remove("B") + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + + // Verify original state + if original.Has("B") { + t.Error("Original tree should not have key B") + } + + // Verify clone state + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone incorrect: expected B=20, got B=%v", val) + } + }) + + t.Run("concurrent modifications", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + clone1 := original.Clone() + clone2 := original.Clone() + + // Modify same key in different clones + clone1.Set("B", 20) + clone2.Set("B", 30) + + // Each clone should have its own value + if val, _ := clone1.Get("B"); val != 20 { + t.Errorf("Clone1 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone2.Get("B"); val != 30 { + t.Errorf("Clone2 incorrect: expected B=30, got B=%v", val) + } + }) + + t.Run("deep tree modifications", func(t *testing.T) { + original := NewTree() + // Create a deeper tree + keys := []string{"M", "F", "T", "B", "H", "P", "Z"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Modify a deep node + clone.Set("H", "modified") + + // Check original remains unchanged + if val, _ := original.Get("H"); val != "H" { + t.Errorf("Original modified: expected H='H', got H=%v", val) + } + + // Verify clone modification + if val, _ := clone.Get("H"); val != "modified" { + t.Errorf("Clone incorrect: expected H='modified', got H=%v", val) + } + }) + + t.Run("rebalancing test", func(t *testing.T) { + original := NewTree() + // Insert nodes that will cause rotations + keys := []string{"A", "B", "C", "D", "E"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Add more nodes to clone to trigger rebalancing + clone.Set("F", "F") + clone.Set("G", "G") + + // Verify original structure remains unchanged + originalKeys := collectKeys(original) + expectedOriginal := []string{"A", "B", "C", "D", "E"} + if !slicesEqual(originalKeys, expectedOriginal) { + t.Errorf("Original tree structure changed: got %v, want %v", originalKeys, expectedOriginal) + } + + // Verify clone has all keys + cloneKeys := collectKeys(clone) + expectedClone := []string{"A", "B", "C", "D", "E", "F", "G"} + if !slicesEqual(cloneKeys, expectedClone) { + t.Errorf("Clone tree structure incorrect: got %v, want %v", cloneKeys, expectedClone) + } + }) + + t.Run("value mutation test", func(t *testing.T) { + type MutableValue struct { + Data string + } + + original := NewTree() + mutable := &MutableValue{Data: "original"} + original.Set("key", mutable) + + clone := original.Clone() + + // Modify the mutable value + mutable.Data = "modified" + + // Both original and clone should see the modification + // because we're not deep copying values + origVal, _ := original.Get("key") + cloneVal, _ := clone.Get("key") + + if origVal.(*MutableValue).Data != "modified" { + t.Error("Original value not modified as expected") + } + if cloneVal.(*MutableValue).Data != "modified" { + t.Error("Clone value not modified as expected") + } + }) +} + +// Helper function to collect all keys in order +func collectKeys(tree *Tree) []string { + var keys []string + tree.Iterate("", "", func(key string, _ interface{}) bool { + keys = append(keys, key) + return false + }) + return keys +}