From 7bea233edc53bf064c2754aa16163bc217967eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Thu, 16 Apr 2026 11:51:38 +0200 Subject: [PATCH] Did some code cleanup, added some extra thingies. switched to spans. Let google gemini do whatever it wanted.. --- PersistentMap/BTreeFunctions.cs | 641 +++++++++++++++++++---------- PersistentMap/BaseOrderedMap.cs | 116 +++++- PersistentMap/Iterator.cs | 8 +- PersistentMap/KeyStrategies.cs | 10 +- PersistentMap/PersistentMap.csproj | 11 + PersistentMap/Readme.org | 113 +++++ PersistentMap/TransientMap.cs | 13 +- TestProject1/FuzzTest.cs | 4 +- TestProject1/OrderedQueriesTest.cs | 181 ++++++++ TestProject1/TestProject1.csproj | 25 ++ TestProject1/UnitTest1.cs | 70 ++++ 11 files changed, 944 insertions(+), 248 deletions(-) create mode 100644 PersistentMap/PersistentMap.csproj create mode 100644 PersistentMap/Readme.org create mode 100644 TestProject1/OrderedQueriesTest.cs create mode 100644 TestProject1/TestProject1.csproj create mode 100644 TestProject1/UnitTest1.cs diff --git a/PersistentMap/BTreeFunctions.cs b/PersistentMap/BTreeFunctions.cs index ed4da06..9c44c18 100644 --- a/PersistentMap/BTreeFunctions.cs +++ b/PersistentMap/BTreeFunctions.cs @@ -13,6 +13,9 @@ namespace PersistentMap public static bool TryGetValue(Node root, K key, TStrategy strategy, out V value) where TStrategy : IKeyStrategy { + // 1. Calculate ONCE + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + Node current = root; while (true) { @@ -20,7 +23,7 @@ namespace PersistentMap { var leaf = current.AsLeaf(); // Leaf uses standard FindIndex (Lower Bound) to find exact match - int index = FindIndex(leaf, key, strategy); + int index = FindIndex(leaf, key, keyPrefix, strategy); if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { value = leaf.Values[index]; @@ -33,172 +36,172 @@ namespace PersistentMap { // FIX: Internal uses FindRoutingIndex (Upper Bound) var internalNode = current.AsInternal(); - int index = FindRoutingIndex(internalNode, key, strategy); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); current = internalNode.Children[index]!; } } } // Public API -public static Node Set(Node root, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool countChanged) -{ - root = root.EnsureEditable(owner); - - var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged); - - if (splitResult != null) - { - var newRoot = new InternalNode(owner); - newRoot.Children[0] = root; - newRoot.Keys[0] = splitResult.Separator; - newRoot.Children[1] = splitResult.NewNode; - newRoot.SetCount(1); - if (strategy.UsesPrefixes) - newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); - - return newRoot; - } - - return root; -} - -// Recursive Helper -private static SplitResult? InsertRecursive(Node node, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool added) -{ - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - int index = FindIndex(leaf, key, strategy); - - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) + public static Node Set(Node root, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool countChanged) { - leaf.Values[index] = value; - added = false; // Key existed, value updated. Count does not change. - return null; - } + root = root.EnsureEditable(owner); - added = true; // New key. Count +1. - if (leaf.Header.Count < LeafNode.Capacity) - { - InsertIntoLeaf(leaf, index, key, value, strategy); - return null; - } - else - { - return SplitLeaf(leaf, index, key, value, strategy, owner); - } - } - else - { - var internalNode = node.AsInternal(); - int index = FindRoutingIndex(internalNode, key, strategy); - - var child = internalNode.Children[index]!.EnsureEditable(owner); - internalNode.Children[index] = child; + var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged); - var split = InsertRecursive(child, key, value, strategy, owner, out added); - - if (split != null) - { - if (internalNode.Header.Count < InternalNode.Capacity - 1) + if (splitResult != null) { - InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy); - return null; + var newRoot = new InternalNode(owner); + newRoot.Children[0] = root; + newRoot.Keys[0] = splitResult.Separator; + newRoot.Children[1] = splitResult.NewNode; + newRoot.SetCount(1); + if (strategy.UsesPrefixes) + newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); + + return newRoot; + } + + return root; + } + + // Recursive Helper + private static SplitResult? InsertRecursive(Node node, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool added) + { + // 1. Calculate ONCE + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + + if (node.IsLeaf) + { + var leaf = node.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) + { + leaf.Values[index] = value; + added = false; // Key existed, value updated. Count does not change. + return null; + } + + added = true; // New key. Count +1. + if (leaf.Header.Count < LeafNode.Capacity) + { + InsertIntoLeaf(leaf, index, key, value, strategy); + return null; + } + else + { + return SplitLeaf(leaf, index, key, value, strategy, owner); + } } else { - return SplitInternal(internalNode, index, split.Separator, split.NewNode, strategy, owner); + var internalNode = node.AsInternal(); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + + var child = internalNode.Children[index]!.EnsureEditable(owner); + internalNode.Children[index] = child; + + var split = InsertRecursive(child, key, value, strategy, owner, out added); + + if (split != null) + { + if (internalNode.Header.Count < InternalNode.Capacity - 1) + { + InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy); + return null; + } + else + { + return SplitInternal(internalNode, index, split.Separator, split.NewNode, strategy, owner); + } + } + return null; } } - return null; - } -} -// Public API -public static Node Remove(Node root, K key, TStrategy strategy, OwnerId owner, out bool countChanged) -where TStrategy : IKeyStrategy -{ - root = root.EnsureEditable(owner); - - bool rebalanceNeeded = RemoveRecursive(root, key, strategy, owner, out countChanged); - - if (rebalanceNeeded) - { - if (!root.IsLeaf) + // Public API + public static Node Remove(Node root, K key, TStrategy strategy, OwnerId owner, out bool countChanged) + where TStrategy : IKeyStrategy { - var internalRoot = root.AsInternal(); - if (internalRoot.Header.Count == 0) + root = root.EnsureEditable(owner); + + bool rebalanceNeeded = RemoveRecursive(root, key, strategy, owner, out countChanged); + + if (rebalanceNeeded) { - return internalRoot.Children[0]!; + if (!root.IsLeaf) + { + var internalRoot = root.AsInternal(); + if (internalRoot.Header.Count == 0) + { + return internalRoot.Children[0]!; + } + } + } + + return root; + } + + // Recursive Helper + private static bool RemoveRecursive(Node node, K key, TStrategy strategy, OwnerId owner, out bool removed) + where TStrategy : IKeyStrategy + { + // 1. Calculate ONCE + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + + if (node.IsLeaf) + { + var leaf = node.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) + { + RemoveFromLeaf(leaf, index, strategy); + removed = true; // Item removed. Count -1. + return leaf.Header.Count .MergeThreshold; + } + + removed = false; // Item not found. + return false; + } + else + { + var internalNode = node.AsInternal(); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + + var child = internalNode.Children[index]!.EnsureEditable(owner); + internalNode.Children[index] = child; + + bool childUnderflow = RemoveRecursive(child, key, strategy, owner, out removed); + + if (removed && childUnderflow) + { + return HandleUnderflow(internalNode, index, strategy, owner); + } + return false; } } - } - - return root; -} - -// Recursive Helper -private static bool RemoveRecursive(Node node, K key, TStrategy strategy, OwnerId owner, out bool removed) -where TStrategy : IKeyStrategy -{ - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - int index = FindIndex(leaf, key, strategy); - - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) - { - RemoveFromLeaf(leaf, index, strategy); - removed = true; // Item removed. Count -1. - return leaf.Header.Count < LeafNode.MergeThreshold; - } - - removed = false; // Item not found. - return false; - } - else - { - var internalNode = node.AsInternal(); - int index = FindRoutingIndex(internalNode, key, strategy); - - var child = internalNode.Children[index]!.EnsureEditable(owner); - internalNode.Children[index] = child; - - bool childUnderflow = RemoveRecursive(child, key, strategy, owner, out removed); - - if (removed && childUnderflow) - { - return HandleUnderflow(internalNode, index, strategy, owner); - } - return false; - } -} // --------------------------------------------------------- // Internal Helpers: Search // --------------------------------------------------------- // Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound) - // 2. Propagate to Helpers [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int FindIndex(Node node, K key, TStrategy strategy) + internal static int FindIndex(Node node, K key, long keyPrefix, TStrategy strategy) where TStrategy : IKeyStrategy { if (strategy.UsesPrefixes) { - long keyPrefix = strategy.GetPrefix(key); + // Use the pre-calculated prefix here! int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); - - // Pass strategy to Refine return RefineSearch(index, node.GetKeys(), key, strategy); } - - Span keys = node.GetKeys(); - - + return LinearSearchKeys(node.GetKeys(), key, strategy); } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LinearSearchKeys(Span keys, K key, TStrategy strategy) where TStrategy : IKeyStrategy @@ -227,26 +230,21 @@ where TStrategy : IKeyStrategy // Used by Internal Nodes: Finds the child index to descend into. // If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound). + [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int FindRoutingIndex(InternalNode node, K key, TStrategy strategy) + internal static int FindRoutingIndex(InternalNode node, K key, long keyPrefix, TStrategy strategy) where TStrategy : IKeyStrategy { if (!strategy.UsesPrefixes) { - // C. Fallback return LinearSearchRouting(node.GetKeys(), key, strategy); } - - - long keyPrefix = strategy.GetPrefix(key); - - // SIMD still finds >=. + + // Use the pre-calculated prefix here! int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); - - // Refine using Upper Bound logic (<= 0 instead of < 0) return RefineRouting(index, node.Keys, node.Header.Count, key, strategy); } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LinearSearchRouting(Span keys, K key, TStrategy strategy) where TStrategy : IKeyStrategy @@ -261,7 +259,7 @@ where TStrategy : IKeyStrategy return i; } -// Overload for primitive types (avoids IComparer call overhead in fallback) + // Overload for primitive types (avoids IComparer call overhead in fallback) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LinearSearchRouting(Span keys, T key) where T : struct, IComparable { @@ -298,16 +296,18 @@ where TStrategy : IKeyStrategy private static SplitResult? InsertRecur2sive(Node node, K key, V value, TStrategy strategy, OwnerId owner) where TStrategy : IKeyStrategy { - // DELETE the single FindIndex call at the top. - // int index = FindIndex(node, key, strategy); <-- REMOVE THIS + + // 1. Calculate ONCE + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + // --- LEAF CASE --- if (node.IsLeaf) { var leaf = node.AsLeaf(); // Leaf uses FindIndex - int index = FindIndex(leaf, key, strategy); - + int index = FindIndex(leaf, key, keyPrefix, strategy); + if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { leaf.Values[index] = value; @@ -327,12 +327,12 @@ where TStrategy : IKeyStrategy // --- INTERNAL CASE --- var internalNode = node.AsInternal(); - + // FIX: Internal uses FindRoutingIndex - int childIndex = FindRoutingIndex(internalNode, key, strategy); - + int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + var child = internalNode.Children[childIndex]!.EnsureEditable(owner); - internalNode.Children[childIndex] = child; + internalNode.Children[childIndex] = child; var split = InsertRecursive(child, key, value, strategy, owner, out _); @@ -358,26 +358,25 @@ where TStrategy : IKeyStrategy int count = leaf.Header.Count; if (index < count) { - // Arrays allow access up to .Length (64), ignoring 'Count' - Array.Copy(leaf.Keys, index, leaf.Keys, index + 1, count - index); - - // This fails if leaf.Values is a Span of length 'count' - Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index); + int moveCount = count - index; + // Fast Span memory moves + leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1)); + leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1)); if (strategy.UsesPrefixes) { - leaf.AllPrefixes.Slice(index, count-index) - .CopyTo(leaf.AllPrefixes.Slice(index+1)); + leaf.AllPrefixes.Slice(index, count - index) + .CopyTo(leaf.AllPrefixes.Slice(index + 1)); } } leaf.Keys[index] = key; - + // This fails if leaf.Values is a Span of length 'count' - leaf.Values[index] = value; + leaf.Values[index] = value; if (strategy.UsesPrefixes) leaf.AllPrefixes![index] = strategy.GetPrefix(key); - + leaf.SetCount(count + 1); } private static SplitResult SplitLeaf(LeafNode left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner) @@ -385,7 +384,7 @@ where TStrategy : IKeyStrategy { var right = new LeafNode(owner); int totalCount = left.Header.Count; - + // Heuristics int splitPoint; if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively) @@ -396,11 +395,12 @@ where TStrategy : IKeyStrategy int moveCount = totalCount - splitPoint; if (moveCount > 0) { - Array.Copy(left.Keys, splitPoint, right.Keys, 0, moveCount); - Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount); + // Fast Span memory moves + left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0)); + left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0)); // Manually copy prefixes if needed or re-calculate if (strategy.UsesPrefixes) - for(int i=0; i InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy); } - // Linked List Maintenance - right.Next = left.Next; - left.Next = right; // In B+ Tree, the separator is the first key of the right node return new SplitResult(right, right.Keys[0]); @@ -429,20 +426,24 @@ where TStrategy : IKeyStrategy where TStrategy : IKeyStrategy { int count = node.Header.Count; - + // Shift Keys and Prefixes if (index < count) { - Array.Copy(node.Keys, index, node.Keys, index + 1, count - index); - + + int moveCount = count - index; + + // Fast Span memory moves + node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1)); + // FIX: Shift raw prefix array if (strategy.UsesPrefixes) { - node.AllPrefixes.Slice(index, count-index) - .CopyTo(node.AllPrefixes.Slice(index +1)); + node.AllPrefixes.Slice(index, count - index) + .CopyTo(node.AllPrefixes.Slice(index + 1)); } } - + // Shift Children // Children buffer is indexable like an array but requires manual loop or Unsafe copy // if we don't want to use unsafe pointers. @@ -453,11 +454,11 @@ where TStrategy : IKeyStrategy } node.Keys[index] = separator; - + // FIX: Write to raw array if (strategy.UsesPrefixes) node.AllPrefixes![index] = strategy.GetPrefix(separator); - + node.Children[index + 1] = newChild; node.SetCount(count + 1); } @@ -471,16 +472,21 @@ where TStrategy : IKeyStrategy // The key at splitPoint moves UP to become the separator. // Keys > splitPoint move to Right. - - K upKey = left.Keys[splitPoint]; - + + K upKey = left.Keys[splitPoint]; + // Move Keys/Prefixes to Right int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up - Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount); - if (strategy.UsesPrefixes) - for(int i=0; i 0) + { + left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0)); - // Move Children to Right + if (strategy.UsesPrefixes) + { + left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0)); + } + } // Left has children 0..splitPoint. Right has children splitPoint+1..End for (int i = 0; i <= moveCount; i++) { @@ -494,7 +500,7 @@ where TStrategy : IKeyStrategy // Note: We extracted 'upKey' from the original array. // We now have to compare the *incoming* separator with 'upKey' // to see if it goes Left or Right. - + if (insertIndex == splitPoint) { // Special case: The new key is exactly the one pushing up? @@ -526,16 +532,22 @@ where TStrategy : IKeyStrategy private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { int count = leaf.Header.Count; - Array.Copy(leaf.Keys, index + 1, leaf.Keys, index, count - index - 1); - Array.Copy(leaf.Values, index + 1, leaf.Values, index, count - index - 1); + int moveCount = count - index - 1; - if (strategy.UsesPrefixes) + if (moveCount > 0) { - var p = leaf.AllPrefixes; - for (int i = index; i < count - 1; i++) p[i] = p[i + 1]; + // Fast Span memory moves + leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index)); + leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index)); + + if (strategy.UsesPrefixes) + { + // Replaced manual 'for' loop with native slice copy + leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index)); + } } leaf.SetCount(count - 1); @@ -582,8 +594,8 @@ where TStrategy : IKeyStrategy return parent.Header.Count < LeafNode.MergeThreshold; } } - - return true; + + return true; } private static bool CanBorrow(Node node) @@ -601,65 +613,69 @@ where TStrategy : IKeyStrategy { var leftLeaf = left.AsLeaf(); var rightLeaf = right.AsLeaf(); - + int lCount = leftLeaf.Header.Count; int rCount = rightLeaf.Header.Count; - - Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount); - Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount); + rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount)); + rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount)); if (strategy.UsesPrefixes) { rightLeaf.AllPrefixes.Slice(0, rCount) .CopyTo(leftLeaf.AllPrefixes.Slice(lCount)); } - + leftLeaf.SetCount(lCount + rCount); - leftLeaf.Next = rightLeaf.Next; } // Case B: Merging Internal Nodes else { var leftInternal = left.AsInternal(); var rightInternal = right.AsInternal(); - + // Pull separator from parent K separator = parent.Keys[separatorIndex]; - + int lCount = leftInternal.Header.Count; leftInternal.Keys[lCount] = separator; if (strategy.UsesPrefixes) leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator); - + int rCount = rightInternal.Header.Count; - Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount); + rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1)); if (strategy.UsesPrefixes) { rightInternal.AllPrefixes.Slice(0, rCount) .CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1)); } - + for (int i = 0; i <= rCount; i++) { leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i]; } - + leftInternal.SetCount(lCount + 1 + rCount); } // Remove Separator and Right Child from Parent int pCount = parent.Header.Count; - Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1); - if (strategy.UsesPrefixes) + int moveCount = pCount - separatorIndex - 1; + + if (moveCount > 0) { - var pp = parent.AllPrefixes; - for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1]; + parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex)); + + if (strategy.UsesPrefixes) + { + // Replaced manual 'for' loop with native slice copy + parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex)); + } } - for(int i = separatorIndex + 2; i <= pCount; i++) + for (int i = separatorIndex + 2; i <= pCount; i++) { parent.Children[i - 1] = parent.Children[i]; } - + parent.SetCount(pCount - 1); } @@ -671,11 +687,11 @@ where TStrategy : IKeyStrategy { var leftLeaf = left.AsLeaf(); var rightLeaf = right.AsLeaf(); - + // Move first of right to end of left InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy); RemoveFromLeaf(rightLeaf, 0, strategy); - + // Update Parent Separator parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) @@ -685,30 +701,35 @@ where TStrategy : IKeyStrategy { var leftInternal = left.AsInternal(); var rightInternal = right.AsInternal(); - + // 1. Move Parent Separator to Left End K sep = parent.Keys[separatorIndex]; InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy); - + // 2. Move Right[0] Key to Parent parent.Keys[separatorIndex] = rightInternal.Keys[0]; if (strategy.UsesPrefixes) parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]); - + // 3. Fix Right (Remove key 0 and shift child 0 out) // We basically remove key at 0. Child 0 was moved to left. Child 1 becomes Child 0. // Re-using Remove logic implies shifts. // Manual shift for performance: int rCount = rightInternal.Header.Count; - + // Shift children - for(int i=0; i 1) + { + // Fast Span memory moves (Replaces Array.Copy & manual loop) + rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0)); + + if (strategy.UsesPrefixes) + { + rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0)); + } + } + rightInternal.SetCount(rCount - 1); } } @@ -722,10 +743,10 @@ where TStrategy : IKeyStrategy var leftLeaf = left.AsLeaf(); var rightLeaf = right.AsLeaf(); int last = leftLeaf.Header.Count - 1; - + InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy); RemoveFromLeaf(leftLeaf, last, strategy); - + parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); @@ -735,20 +756,184 @@ where TStrategy : IKeyStrategy var leftInternal = (InternalNode)left; var rightInternal = (InternalNode)right; int last = leftInternal.Header.Count - 1; - + // 1. Move Parent Separator to Right Start K sep = parent.Keys[separatorIndex]; // The child moving to right is the *last* child of left (index count) InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy); - + // 2. Move Left[last] Key to Parent parent.Keys[separatorIndex] = leftInternal.Keys[last]; if (strategy.UsesPrefixes) parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); - + // 3. Truncate Left leftInternal.SetCount(last); } } + + + + public static bool TryGetMin(Node root, out K key, out V value) + { + var current = root; + while (!current.IsLeaf) + { + current = current.AsInternal().Children[0]!; + } + + var leaf = current.AsLeaf(); + if (leaf.Header.Count == 0) + { + key = default!; + value = default!; + return false; + } + + key = leaf.Keys![0]; + value = leaf.Values[0]; + return true; + } + + public static bool TryGetMax(Node root, out K key, out V value) + { + var current = root; + while (!current.IsLeaf) + { + var internalNode = current.AsInternal(); + current = internalNode.Children[internalNode.Header.Count]!; + } + + var leaf = current.AsLeaf(); + if (leaf.Header.Count == 0) + { + key = default!; + value = default!; + return false; + } + + int last = leaf.Header.Count - 1; + key = leaf.Keys![last]; + value = leaf.Values[last]; + return true; + } + + public static bool TryGetSuccessor(Node root, K key, TStrategy strategy, out K nextKey, out V nextValue) + where TStrategy : IKeyStrategy + { + InternalNode[] path = new InternalNode[32]; + int[] indices = new int[32]; + int depth = 0; + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + + + var current = root; + while (!current.IsLeaf) + { + var internalNode = current.AsInternal(); + int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + path[depth] = internalNode; + indices[depth] = idx; + depth++; + current = internalNode.Children[idx]!; + } + + var leaf = current.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) index++; + + // 1. Successor is in the same leaf + if (index < leaf.Header.Count) + { + nextKey = leaf.Keys![index]; + nextValue = leaf.Values[index]; + return true; + } + + // 2. Successor is in the next leaf (We must backtrack up the tree!) + for (int i = depth - 1; i >= 0; i--) + { + // If we haven't reached the right-most child of this parent + if (indices[i] < path[i].Header.Count) + { + // Take one step right, then go absolute left all the way down + current = path[i].Children[indices[i] + 1]!; + while (!current.IsLeaf) + { + current = current.AsInternal().Children[0]!; + } + + var targetLeaf = current.AsLeaf(); + nextKey = targetLeaf.Keys![0]; + nextValue = targetLeaf.Values[0]; + return true; + } + } + + nextKey = default!; + nextValue = default!; + return false; + } + + public static bool TryGetPredecessor(Node root, K key, TStrategy strategy, out K prevKey, out V prevValue) + where TStrategy : IKeyStrategy + { + // Max depth of a B-Tree is small, preallocate a small array to track the descent path. + InternalNode[] path = new InternalNode[32]; + int[] indices = new int[32]; + int depth = 0; + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + + + var current = root; + while (!current.IsLeaf) + { + var internalNode = current.AsInternal(); + int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + path[depth] = internalNode; + indices[depth] = idx; + depth++; + current = internalNode.Children[idx]!; + } + + var leaf = current.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + // Easy case: Predecessor is in the same leaf + if (index > 0) + { + prevKey = leaf.Keys![index - 1]; + prevValue = leaf.Values[index - 1]; + return true; + } + + // Hard case: We need to backtrack to find the first left branch we ignored + for (int i = depth - 1; i >= 0; i--) + { + if (indices[i] > 0) + { + // Jump to the left sibling branch, then take the absolute right-most path down + current = path[i].Children[indices[i] - 1]!; + while (!current.IsLeaf) + { + var internalNode = current.AsInternal(); + current = internalNode.Children[internalNode.Header.Count]!; + } + + var targetLeaf = current.AsLeaf(); + int last = targetLeaf.Header.Count - 1; + prevKey = targetLeaf.Keys![last]; + prevValue = targetLeaf.Values[last]; + return true; + } + } + + prevKey = default!; + prevValue = default!; + return false; + } } -} \ No newline at end of file + + +} diff --git a/PersistentMap/BaseOrderedMap.cs b/PersistentMap/BaseOrderedMap.cs index 4d6b6ad..ee99960 100644 --- a/PersistentMap/BaseOrderedMap.cs +++ b/PersistentMap/BaseOrderedMap.cs @@ -80,4 +80,118 @@ public abstract class BaseOrderedMap : IEnumerable Until(K max) => new(_root, _strategy, false, default, true, max); -} \ No newline at end of file + + + + // --------------------------------------------------------- +// Navigation Operations +// --------------------------------------------------------- + +public bool TryGetMin(out K key, out V value) => BTreeFunctions.TryGetMin(_root, out key, out value); + +public bool TryGetMax(out K key, out V value) => BTreeFunctions.TryGetMax(_root, out key, out value); + +public bool TryGetSuccessor(K key, out K nextKey, out V nextValue) => BTreeFunctions.TryGetSuccessor(_root, key, _strategy, out nextKey, out nextValue); + +public bool TryGetPredecessor(K key, out K prevKey, out V prevValue) => BTreeFunctions.TryGetPredecessor(_root, key, _strategy, out prevKey, out prevValue); + +// --------------------------------------------------------- +// Set Operations (Linear Merge O(N+M)) +// --------------------------------------------------------- + +public IEnumerable> Intersect(BaseOrderedMap other) +{ + using var enum1 = this.GetEnumerator(); + using var enum2 = other.GetEnumerator(); + + bool has1 = enum1.MoveNext(); + bool has2 = enum2.MoveNext(); + + while (has1 && has2) + { + int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key); + if (cmp == 0) + { + yield return enum1.Current; + has1 = enum1.MoveNext(); + has2 = enum2.MoveNext(); + } + else if (cmp < 0) has1 = enum1.MoveNext(); + else has2 = enum2.MoveNext(); + } +} + +public IEnumerable> Except(BaseOrderedMap other) +{ + using var enum1 = this.GetEnumerator(); + using var enum2 = other.GetEnumerator(); + + bool has1 = enum1.MoveNext(); + bool has2 = enum2.MoveNext(); + + while (has1 && has2) + { + int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key); + if (cmp == 0) + { + has1 = enum1.MoveNext(); + has2 = enum2.MoveNext(); + } + else if (cmp < 0) + { + yield return enum1.Current; + has1 = enum1.MoveNext(); + } + else + { + has2 = enum2.MoveNext(); + } + } + + while (has1) + { + yield return enum1.Current; + has1 = enum1.MoveNext(); + } +} + +public IEnumerable> SymmetricExcept(BaseOrderedMap other) +{ + using var enum1 = this.GetEnumerator(); + using var enum2 = other.GetEnumerator(); + + bool has1 = enum1.MoveNext(); + bool has2 = enum2.MoveNext(); + + while (has1 && has2) + { + int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key); + if (cmp == 0) + { + has1 = enum1.MoveNext(); + has2 = enum2.MoveNext(); + } + else if (cmp < 0) + { + yield return enum1.Current; + has1 = enum1.MoveNext(); + } + else + { + yield return enum2.Current; + has2 = enum2.MoveNext(); + } + } + + while (has1) + { + yield return enum1.Current; + has1 = enum1.MoveNext(); + } + while (has2) + { + yield return enum2.Current; + has2 = enum2.MoveNext(); + } +} +} diff --git a/PersistentMap/Iterator.cs b/PersistentMap/Iterator.cs index 7706f71..f6ace28 100644 --- a/PersistentMap/Iterator.cs +++ b/PersistentMap/Iterator.cs @@ -120,12 +120,14 @@ public struct BTreeEnumerator : IEnumerator> { Node node = _root; _depth = 0; + long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0; + // Dive using Routing while (!node.IsLeaf) { var internalNode = node.AsInternal(); - int idx = BTreeFunctions.FindRoutingIndex(internalNode, key, _strategy); + int idx = BTreeFunctions.FindRoutingIndex(internalNode, key, keyPrefix, _strategy); _nodeStack[_depth] = internalNode; _indexStack[_depth] = idx; @@ -136,7 +138,7 @@ public struct BTreeEnumerator : IEnumerator> // Find index in Leaf _currentLeaf = node.AsLeaf(); - int index = BTreeFunctions.FindIndex(_currentLeaf, key, _strategy); + int index = BTreeFunctions.FindIndex(_currentLeaf, key, keyPrefix, _strategy); // Set position to (index - 1) so that the first MoveNext() lands on 'index' _currentLeafIndex = index - 1; @@ -242,4 +244,4 @@ public struct BTreeEnumerator : IEnumerator> } } public void Dispose() { } - } \ No newline at end of file + } diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs index e0af3a2..6d5a30e 100644 --- a/PersistentMap/KeyStrategies.cs +++ b/PersistentMap/KeyStrategies.cs @@ -117,10 +117,10 @@ public static class PrefixScanner // If target is MinValue, any value in prefixes is >= target. // So the first element (index 0) is the match. // TODO: evaluate if this is needed. - if (targetPrefix == long.MinValue) - { - return 0; - } + //if (targetPrefix == long.MinValue) + //{ + // return 0; + //} // Fallback for short arrays or unsupported hardware if (!Avx2.IsSupported || prefixes.Length < 4) @@ -207,4 +207,4 @@ public static class PrefixScanner return LinearScan(prefixes.Slice(i), target) + i; } -} \ No newline at end of file +} diff --git a/PersistentMap/PersistentMap.csproj b/PersistentMap/PersistentMap.csproj new file mode 100644 index 0000000..27d8e13 --- /dev/null +++ b/PersistentMap/PersistentMap.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + 14 + enable + enable + true + + + diff --git a/PersistentMap/Readme.org b/PersistentMap/Readme.org new file mode 100644 index 0000000..a67ee4a --- /dev/null +++ b/PersistentMap/Readme.org @@ -0,0 +1,113 @@ +* NiceBtree (PersistentMap) + +A high-performance, persistent (Copy-on-Write) B+ Tree implemented in C#. + +It is designed for zero-overhead reads, SIMD-accelerated key routing, and allocation-free range queries. It supports both fully immutable usage and "Transient" mode for high-throughput bulk mutations. + +** Features +- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tree yields a new version while sharing unmodified nodes. +- *Transient Mode*: Perform bulk mutations in-place with standard mutable performance, then freeze it into a =PersistentMap= in $O(1)$ time. +- *SIMD Prefix Scanning*: Uses AVX2/AVX512 to vectorize B+ tree routing and binary searches via =long= key-prefixes. +- *Linear Time Set Operations*: Sort-merge based =Intersect=, =Except=, and =SymmetricExcept= execute in $O(N+M)$ time using lazy evaluation. + + +** When should I use this? +Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot. + +** Quick Start + +*** 1. Basic Immutable Usage +By default, the map is immutable. Every write operation returns a new, updated version of the map. + +#+begin_src csharp +// Create a map with a specific key strategy (e.g., Int, Unicode, Double) +var map1 = BaseOrderedMap.Create(new IntStrategy()); + +// Set returns a new tree instance. map1 remains empty. +var map2 = map1.Set(1, "Apple") + .Set(2, "Banana") + .Set(3, "Cherry"); + +if (map2.TryGetValue(2, out var value)) +{ + Console.WriteLine(value); // "Banana" +} +#+end_src + +*** 2. Transient Mode (Bulk Mutations) +If you need to insert thousands of elements, creating a new persistent tree on every insert is too slow. Use a =TransientMap= to mutate the tree in-place, then lock it into a persistent snapshot. + +#+begin_src csharp +var transientMap = BaseOrderedMap.CreateTransient(new IntStrategy()); + +// Mutates in-place. No allocations for unchanged tree paths. +for (int i = 0; i < 10_000; i++) +{ + transientMap.Set(i, $"Value_{i}"); +} + +// O(1) freeze. Returns a thread-safe immutable PersistentMap. +var persistentSnapshot = transientMap.ToPersistent(); +#+end_src + +*** 3. Range Queries and Iteration +Because it is a B+ tree, leaf nodes are linked. Range queries require zero allocations and simply walk the leaves. + +#+begin_src csharp +var map = GetPopulatedMap(); + +// Iterate exact bounds +foreach (var kvp in map.Range(min: 10, max: 50)) +{ + Console.WriteLine($"{kvp.Key}: {kvp.Value}"); +} + +// Open-ended queries +var greaterThan100 = map.From(100); +var lessThan50 = map.Until(50); +var allElements = map.AsEnumerable(); +#+end_src + +*** 4. Tree Navigation +Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound. + +#+begin_src csharp +// Get extremes +map.TryGetMin(out int minKey, out string minVal); +map.TryGetMax(out int maxKey, out string maxVal); + +// Get the immediate next/previous element (works even if '42' doesn't exist) +if (map.TryGetSuccessor(42, out int nextKey, out string nextVal)) +{ + Console.WriteLine($"The key immediately after 42 is {nextKey}"); +} + +if (map.TryGetPredecessor(42, out int prevKey, out string prevVal)) +{ + Console.WriteLine($"The key immediately before 42 is {prevKey}"); +} +#+end_src + +*** 5. Set Operations +Set operations take advantage of the tree's underlying sorted linked-list structure to merge trees in linear $O(N+M)$ time. + +#+begin_src csharp +var mapA = CreateMap(1, 2, 3, 4); +var mapB = CreateMap(3, 4, 5, 6); + +// Returns { 3, 4 } +var common = mapA.Intersect(mapB); + +// Returns { 1, 2 } +var onlyInA = mapA.Except(mapB); + +// Returns { 1, 2, 5, 6 } +var symmetricDiff = mapA.SymmetricExcept(mapB); +#+end_src + +** Architecture Notes: Key Strategies +NiceBtree uses =IKeyStrategy= to map generic keys (like =string= or =double=) into sortable =long= prefixes. This achieves two things: +1. Enables AVX512/AVX2 vector instructions to search internal nodes simultaneously. +2. Avoids expensive =IComparable= interface calls or =string.Compare= during the initial descent of the tree, only falling back to exact comparisons when refining the search within a leaf. + +This means that it will be fast for integers and anything you can pack in 8 bytes. If you use stings with high prefix entropy, this will be /very/ performant. If you don't, it is just another b+tree. diff --git a/PersistentMap/TransientMap.cs b/PersistentMap/TransientMap.cs index f5f282d..568b20d 100644 --- a/PersistentMap/TransientMap.cs +++ b/PersistentMap/TransientMap.cs @@ -2,7 +2,7 @@ using System.Collections; namespace PersistentMap; -public sealed class TransientMap : BaseOrderedMap, IEnumerable> where TStrategy : IKeyStrategy +public sealed class TransientMap : BaseOrderedMap where TStrategy : IKeyStrategy { // This is mutable, but we treat it as readonly for the ID generation logic usually. private OwnerId _transactionId; @@ -27,17 +27,12 @@ public sealed class TransientMap : BaseOrderedMap ToPersistent() { - // 1. Create the snapshot. - // The nodes currently have _transactionId. - // The PersistentMap will read them fine (it reads anything). - // BUT: If we write to PersistentMap, it uses OwnerId.None, so it COPIES. (Safe) + // 1. Create the snapshot by copying all relevant information var snapshot = new PersistentMap(_root, _strategy, Count); - // 2. Protect the snapshot from THIS TransientMap. - // If we Set() again on this map, we have the same _transactionId. - // We would mutate the nodes we just gave to the snapshot. - // FIX: "Seal" the current transaction by rolling to a new ID. + // 2. Protect the snapshot from THIS TransientMap by getting a new ownerId + // so that future edits will be done by CoW _transactionId = OwnerId.Next(); return snapshot; diff --git a/TestProject1/FuzzTest.cs b/TestProject1/FuzzTest.cs index cde5ee8..cded93b 100644 --- a/TestProject1/FuzzTest.cs +++ b/TestProject1/FuzzTest.cs @@ -21,7 +21,7 @@ public class BTreeFuzzTests // CONFIGURATION const int Iterations = 100_000; // High enough to trigger all splits/merges const int KeyRange = 5000; // Small enough to cause frequent collisions - const bool showOps = true; + const bool showOps = false; int Seed = 2135974; // Environment.TickCount; // ORACLES @@ -167,4 +167,4 @@ public class BTreeFuzzTests throw new Exception("Enumerator has extra items!"); } } -} \ No newline at end of file +} diff --git a/TestProject1/OrderedQueriesTest.cs b/TestProject1/OrderedQueriesTest.cs new file mode 100644 index 0000000..b9b996a --- /dev/null +++ b/TestProject1/OrderedQueriesTest.cs @@ -0,0 +1,181 @@ +using System.Linq; +using Xunit; +using PersistentMap; + +namespace PersistentMap.Tests +{ + public class BTreeExtendedOperationsTests + { + // Helper to quickly spin up a populated map + private TransientMap CreateMap(params int[] keys) + { + var map = BaseOrderedMap.CreateTransient(new IntStrategy()); + foreach (var key in keys) + { + map.Set(key, $"val_{key}"); + } + return map; + } + + [Fact] + public void MinMax_OnEmptyTree_ReturnsFalse() + { + var map = CreateMap(); + + bool hasMin = map.TryGetMin(out int minKey, out string minVal); + bool hasMax = map.TryGetMax(out int maxKey, out string maxVal); + + Assert.False(hasMin); + Assert.False(hasMax); + Assert.Equal(default, minKey); + Assert.Equal(default, maxKey); + } + + [Fact] + public void MinMax_OnPopulatedTree_ReturnsCorrectExtremes() + { + var map = CreateMap(50, 10, 40, 20, 30); // Insert out of order + + bool hasMin = map.TryGetMin(out int minKey, out string minVal); + bool hasMax = map.TryGetMax(out int maxKey, out string maxVal); + + Assert.True(hasMin); + Assert.Equal(10, minKey); + Assert.Equal("val_10", minVal); + + Assert.True(hasMax); + Assert.Equal(50, maxKey); + Assert.Equal("val_50", maxVal); + } + + [Theory] + [InlineData(20, true, 30)] // Exact match, get next + [InlineData(25, true, 30)] // Missing key, gets first greater + [InlineData(50, false, 0)] // Max element has no successor + [InlineData(60, false, 0)] // Out of bounds high + public void Successor_ReturnsCorrectNextKey(int searchKey, bool expectedSuccess, int expectedNextKey) + { + var map = CreateMap(10, 20, 30, 40, 50); + + bool success = map.TryGetSuccessor(searchKey, out int nextKey, out string nextVal); + + Assert.Equal(expectedSuccess, success); + if (expectedSuccess) + { + Assert.Equal(expectedNextKey, nextKey); + Assert.Equal($"val_{expectedNextKey}", nextVal); + } + } + + [Theory] + [InlineData(40, true, 30)] // Exact match, get previous + [InlineData(35, true, 30)] // Missing key, gets largest smaller + [InlineData(10, false, 0)] // Min element has no predecessor + [InlineData(5, false, 0)] // Out of bounds low + public void Predecessor_ReturnsCorrectPreviousKey(int searchKey, bool expectedSuccess, int expectedPrevKey) + { + var map = CreateMap(10, 20, 30, 40, 50); + + bool success = map.TryGetPredecessor(searchKey, out int prevKey, out string prevVal); + + Assert.Equal(expectedSuccess, success); + if (expectedSuccess) + { + Assert.Equal(expectedPrevKey, prevKey); + Assert.Equal($"val_{expectedPrevKey}", prevVal); + } + } + + [Fact] + public void SuccessorPredecessor_CrossNodeBoundaries_WorksCorrectly() + { + // Insert 200 elements to guarantee leaf node splits (Capacity is 64) + // and internal node creation. + var keys = Enumerable.Range(1, 200).ToArray(); + var map = CreateMap(keys); + + // Test boundaries between multiple leaves + for (int i = 1; i < 200; i++) + { + // Successor + bool sFound = map.TryGetSuccessor(i, out int next, out _); + Assert.True(sFound); + Assert.Equal(i + 1, next); + + // Predecessor + bool pFound = map.TryGetPredecessor(i + 1, out int prev, out _); + Assert.True(pFound); + Assert.Equal(i, prev); + } + } + + [Fact] + public void SetOperations_Intersect_ReturnsCommonElements() + { + var mapA = CreateMap(1, 2, 3, 4, 5); + var mapB = CreateMap(4, 5, 6, 7, 8); + + var intersect = mapA.Intersect(mapB).Select(kvp => kvp.Key).ToArray(); + + Assert.Equal(new[] { 4, 5 }, intersect); + } + + [Fact] + public void SetOperations_Except_ReturnsElementsOnlyInFirstMap() + { + var mapA = CreateMap(1, 2, 3, 4, 5); + var mapB = CreateMap(4, 5, 6, 7, 8); + + var aExceptB = mapA.Except(mapB).Select(kvp => kvp.Key).ToArray(); + var bExceptA = mapB.Except(mapA).Select(kvp => kvp.Key).ToArray(); + + Assert.Equal(new[] { 1, 2, 3 }, aExceptB); + Assert.Equal(new[] { 6, 7, 8 }, bExceptA); + } + + [Fact] + public void SetOperations_SymmetricExcept_ReturnsNonOverlappingElements() + { + var mapA = CreateMap(1, 2, 3, 4, 5); + var mapB = CreateMap(4, 5, 6, 7, 8); + + var symmetric = mapA.SymmetricExcept(mapB).Select(kvp => kvp.Key).ToArray(); + + // Should return elements exclusively in A or B, but not both. + // Expected sorted naturally: 1, 2, 3, 6, 7, 8 + Assert.Equal(new[] { 1, 2, 3, 6, 7, 8 }, symmetric); + } + + [Fact] + public void SetOperations_WithEmptyMaps_HandleGracefully() + { + var populatedMap = CreateMap(1, 2, 3); + var emptyMap = CreateMap(); + + // Intersect with empty is empty + Assert.Empty(populatedMap.Intersect(emptyMap)); + Assert.Empty(emptyMap.Intersect(populatedMap)); + + // Populated Except empty is Populated + Assert.Equal(new[] { 1, 2, 3 }, populatedMap.Except(emptyMap).Select(k => k.Key)); + + // Empty Except Populated is empty + Assert.Empty(emptyMap.Except(populatedMap)); + + // Symmetric Except with empty is just the populated map + Assert.Equal(new[] { 1, 2, 3 }, populatedMap.SymmetricExcept(emptyMap).Select(k => k.Key)); + Assert.Equal(new[] { 1, 2, 3 }, emptyMap.SymmetricExcept(populatedMap).Select(k => k.Key)); + } + + [Fact] + public void SetOperations_CompleteOverlap_HandlesCorrectly() + { + var mapA = CreateMap(1, 2, 3); + var mapB = CreateMap(1, 2, 3); + + Assert.Equal(new[] { 1, 2, 3 }, mapA.Intersect(mapB).Select(k => k.Key)); + Assert.Empty(mapA.Except(mapB)); + Assert.Empty(mapA.SymmetricExcept(mapB)); + } + } +} diff --git a/TestProject1/TestProject1.csproj b/TestProject1/TestProject1.csproj new file mode 100644 index 0000000..e36bb42 --- /dev/null +++ b/TestProject1/TestProject1.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestProject1/UnitTest1.cs b/TestProject1/UnitTest1.cs new file mode 100644 index 0000000..a9a9d77 --- /dev/null +++ b/TestProject1/UnitTest1.cs @@ -0,0 +1,70 @@ +namespace TestProject1; + +using Xunit; +using PersistentMap; + +public class BasicTests +{ + private readonly UnicodeStrategy _strategy = new UnicodeStrategy(); + + [Fact] + public void Transient_InsertAndGet_Works() + { + var map = BaseOrderedMap.CreateTransient(_strategy); + + map.Set("Apple", 1); + map.Set("Banana", 2); + map.Set("Cherry", 3); + + Assert.True(map.TryGetValue("Apple", out int v1)); + Assert.Equal(1, v1); + + Assert.True(map.TryGetValue("Banana", out int v2)); + Assert.Equal(2, v2); + + Assert.False(map.TryGetValue("Date", out _)); + } + + [Fact] + public void Transient_Update_Works() + { + var map = BaseOrderedMap.CreateTransient(_strategy); + map.Set("Key", 100); + map.Set("Key", 200); // Overwrite + + map.TryGetValue("Key", out int val); + Assert.Equal(200, val); + } + + [Fact] + public void Transient_Remove_Works() + { + var map = BaseOrderedMap.CreateTransient(_strategy); + map.Set("A", 1); + map.Set("B", 2); + map.Set("C", 3); + + map.Remove("B"); + + Assert.True(map.ContainsKey("A")); + Assert.False(map.ContainsKey("B")); + Assert.True(map.ContainsKey("C")); + } + + [Fact] + public void Transient_PrefixCollision_HandlesCollision() + { + // UnicodeStrategy only packs the first 4 chars. + // "Test1" and "Test2" have the same prefix "Test". + var map = BaseOrderedMap.CreateTransient(_strategy); + + map.Set("Test1", 1); + map.Set("Test2", 2); + + Assert.True(map.TryGetValue("Test1", out var v1)); + Assert.Equal(1, v1); + + Assert.True(map.TryGetValue("Test2", out var v2)); + Assert.Equal(2, v2); + } +} \ No newline at end of file