Skip to content

Commit

Permalink
feat(examples): add moul/ulist
Browse files Browse the repository at this point in the history
Signed-off-by: moul <[email protected]>
  • Loading branch information
moul committed Dec 27, 2024
1 parent 3fd5571 commit ed2b486
Show file tree
Hide file tree
Showing 3 changed files with 835 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/gno.land/p/moul/ulist/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/moul/ulist
345 changes: 345 additions & 0 deletions examples/gno.land/p/moul/ulist/ulist.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
// 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.

import (
"errors"
)

// Debug controls whether debug logging is enabled
var Debug bool = false

// log prints debug messages when Debug is true
func log(args ...interface{}) {
if Debug {
println(args...)
}
}

// Error variables
var (
ErrOutOfBounds = errors.New("index out of bounds")
ErrDeleted = errors.New("element already deleted")
)

// Entry represents a key-value pair in the list
type Entry struct {
Index int
Value interface{}
}

// List represents an append-only binary tree list
type List struct {
root *treeNode
totalSize int
activeSize int
}

// treeNode represents a node in the binary tree
type treeNode struct {
data interface{}
left *treeNode
right *treeNode
}

// 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 {
if l.root == nil {
l.root = &treeNode{data: value}
l.totalSize++
l.activeSize++
continue
}

index := l.totalSize
bits := highestBit(index + 1)
node := l.root

// 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 {
node.right = &treeNode{}
}
node = node.right
} else {
if node.left == nil {
node.left = &treeNode{}
}
node = node.left
}
}

// After traversing, set the value in the current node
node.data = value
l.totalSize++
l.activeSize++
}
}

// Get retrieves the value at the specified index
func (l *List) Get(index int) interface{} {
if index >= l.totalSize || index < 0 {
return nil
}

node := l.root
if node == nil {
return nil
}

// Special case for root node
if index == 0 {
return node.data
}

// Calculate the number of bits needed
bits := highestBit(index + 1)

// Start from the second highest bit
for level := bits - 2; level >= 0; level-- {
bit := (index & (1 << uint(level))) != 0

if bit {
node = node.right
} else {
node = node.left
}

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
}

for _, index := range indices {
node := l.getNodeByIndex(index)
if node == nil {
return ErrOutOfBounds
}
if node.data == nil {
return ErrDeleted
}
node.data = nil
l.activeSize--
}

return nil
}

// deleteOne marks a single element as deleted by index, now also unified
// with getNodeByIndex for consistent traversal semantics.
func (l *List) deleteOne(index int) error {
if index < 0 || index >= l.totalSize {
return ErrOutOfBounds
}
node := l.getNodeByIndex(index)
if node == nil {
return ErrOutOfBounds
}
if node.data == nil {
return ErrDeleted
}
node.data = nil
l.activeSize--
return nil
}

// getNodeByIndex retrieves the *treeNode for zero-based index using the
// exact same path bits that appending used.
// This ensures your "Get path calculation" matches "Append path calculation".
func (l *List) getNodeByIndex(index int) *treeNode {
if index < 0 || index >= l.totalSize {
log("Get: index out of bounds", index, l.totalSize)
return nil
}
if index == 0 {
// root is index 0
return l.root
}
path := buildPathForIndex(index) // unify logic with Append
current := l.root
for _, step := range path {
if step {
current = current.right
} else {
current = current.left
}
if current == nil {
return nil
}
}
return current
}

// Size returns the number of active elements
func (l *List) Size() int {
return l.activeSize
}

// TotalSize returns the total number of elements (including deleted)
func (l *List) TotalSize() int {
return l.totalSize
}

// formatBinary returns a string representation of n in binary with specified width
func formatBinary(n, width int) string {
result := make([]byte, width)
for i := width - 1; i >= 0; i-- {
if (n & (1 << uint(i))) != 0 {
result[width-1-i] = '1'
} else {
result[width-1-i] = '0'
}
}
return string(result)
}

// highestBit returns the position of the highest bit set in n
func highestBit(n int) int {
pos := 0
for n > 0 {
log(" highestBit:", n, "->", pos)
n >>= 1
pos++
}
return pos
}

// buildPathForIndex replicates the same path logic that appendOne() uses.
// The resulting bool slice is from the first branch step to the last, with
// "false" = go left, "true" = go right. Index zero is special-cased at the caller.
func buildPathForIndex(index int) []bool {
// In your logs, for index=2, you show "bits: 2 binary: 10" and proceed
// level 0 bit: false -> left, final insert: left
// This indicates you read from LSB upward.
// So we gather bits from least-significant to most, then reverse them
// except we skip the highest bit (the "root" bit).
// Example: index=2 -> binary: 10 -> skip highest bit => path = [false]

bits := []bool{}
for index > 0 {
// pick off least significant bit
bits = append(bits, (index&1) == 1)
index >>= 1
}

// Now bits[0] is the LSB. For index=2 (“10” in binary), bits=[false,true].
// The final “true” is the highest bit, so skip it:
if len(bits) > 0 {
bits = bits[:len(bits)-1]
}

// Reverse the remaining bits so the leftmost in bits is the first decision.
for i, j := 0, len(bits)-1; i < j; i, j = i+1, j-1 {
bits[i], bits[j] = bits[j], bits[i]
}
return bits
}

// 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 {
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
}

log("Iterator: start=", start, "end=", end, "totalSize=", l.totalSize)

// For empty list or invalid range
if l == nil || l.totalSize == 0 || start < 0 || end < 0 {
log("Iterator: empty list or invalid range")
return false
}

// Handle reverse iteration
if start > end {
log("Iterator: reverse mode")
count := 0
for i := start; i >= end; i-- {
val := l.Get(i)
log("Iterator reverse: i=", i, "val=", val)
if val != nil {
count++
if cb(i, val) {
log("Iterator reverse: callback returned false, stopping after", count, "items")
return true
}
}
}
log("Iterator reverse: completed with", count, "items")
return false
}

// Handle forward iteration
log("Iterator: forward mode")
count := 0
for i := start; i <= end; i++ {
val := l.Get(i)
log("Iterator forward: i=", i, "val=", val)
if val != nil {
count++
if cb(i, val) {
log("Iterator forward: callback returned false, stopping after", count, "items")
return true
}
}
}
log("Iterator forward: completed with", count, "items")
return false
}
Loading

0 comments on commit ed2b486

Please sign in to comment.