First
This commit is contained in:
commit
79b5ab98aa
13 changed files with 2016 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
36
NiceBtree.sln
Normal file
36
NiceBtree.sln
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentMap", "PersistentMap\PersistentMap.csproj", "{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{9E499000-5E37-42F8-89D2-E18A53F0EF0C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "benchmarks\AgainstImmutableDict\AgainstImmutableDict.csproj", "{13304F19-7ED3-4C40-9A08-46D539667D50}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}
|
||||
{13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E499000-5E37-42F8-89D2-E18A53F0EF0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E499000-5E37-42F8-89D2-E18A53F0EF0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E499000-5E37-42F8-89D2-E18A53F0EF0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E499000-5E37-42F8-89D2-E18A53F0EF0C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13304F19-7ED3-4C40-9A08-46D539667D50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13304F19-7ED3-4C40-9A08-46D539667D50}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
629
PersistentMap/BTreeFunctions.cs
Normal file
629
PersistentMap/BTreeFunctions.cs
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PersistentMap
|
||||
{
|
||||
public static class BTreeFunctions
|
||||
{
|
||||
// ---------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
Node<K> current = root;
|
||||
while (true)
|
||||
{
|
||||
if (current.IsLeaf)
|
||||
{
|
||||
var leaf = current.AsLeaf<V>();
|
||||
// Leaf uses standard FindIndex (Lower Bound) to find exact match
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
value = leaf.Values[index];
|
||||
return true;
|
||||
}
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// FIX: Internal uses FindRoutingIndex (Upper Bound)
|
||||
var internalNode = current.AsInternal();
|
||||
int index = FindRoutingIndex(internalNode, key, strategy);
|
||||
current = internalNode.Children[index]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Node<K> Set<K, V, TStrategy>(Node<K> root, K key, V value, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Root CoW check
|
||||
root = root.EnsureEditable(owner);
|
||||
|
||||
var splitResult = InsertRecursive(root, key, value, strategy, owner);
|
||||
|
||||
if (splitResult != null)
|
||||
{
|
||||
// The root split. Create a new root.
|
||||
var newRoot = new InternalNode<K>(owner);
|
||||
newRoot.Children[0] = root;
|
||||
newRoot.Keys[0] = splitResult.Separator;
|
||||
newRoot.Children[1] = splitResult.NewNode;
|
||||
newRoot.SetCount(1);
|
||||
|
||||
// Prefixes for internal nodes are derived from the separator keys
|
||||
newRoot._prefixes![0] = strategy.GetPrefix(splitResult.Separator);
|
||||
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
root = root.EnsureEditable(owner);
|
||||
|
||||
bool rebalanceNeeded = RemoveRecursive<K,V,TStrategy>(root, key, strategy, owner);
|
||||
|
||||
// If root is internal and became empty (count 0), replace with its only child
|
||||
if (rebalanceNeeded)
|
||||
{
|
||||
if (!root.IsLeaf)
|
||||
{
|
||||
// CHANGE: Use AsInternal()
|
||||
var internalRoot = root.AsInternal();
|
||||
if (internalRoot.Header.Count == 0)
|
||||
{
|
||||
return internalRoot.Children[0]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 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<K, TStrategy>(Node<K> node, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
long keyPrefix = strategy.GetPrefix(key);
|
||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||
|
||||
// Pass strategy to Refine
|
||||
return RefineSearch(index, node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int RefineSearch<K, TStrategy>(int startIndex, Span<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = startIndex;
|
||||
// JIT can now inline 'strategy.Compare' here!
|
||||
while (i < keys.Length && strategy.Compare(keys[i], key) < 0)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// 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<K, TStrategy>(InternalNode<K> node, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
long keyPrefix = strategy.GetPrefix(key);
|
||||
|
||||
// SIMD still finds >=.
|
||||
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 RefineRouting<K, TStrategy>(int startIndex, K[] keys, int count, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = startIndex;
|
||||
// DIFFERENCE: We continue past valid matches.
|
||||
// We want the first key STRICTLY GREATER than target.
|
||||
// If keys[i] == key, we increment (go to right child).
|
||||
while (i < count && strategy.Compare(keys[i], key) <= 0)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Insertion Logic
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private class SplitResult<K>
|
||||
{
|
||||
public Node<K> NewNode;
|
||||
public K Separator;
|
||||
public SplitResult(Node<K> newNode, K separator) { NewNode = newNode; Separator = separator; }
|
||||
}
|
||||
|
||||
private static SplitResult<K>? InsertRecursive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// DELETE the single FindIndex call at the top.
|
||||
// int index = FindIndex(node, key, strategy); <-- REMOVE THIS
|
||||
|
||||
// --- LEAF CASE ---
|
||||
if (node.IsLeaf)
|
||||
{
|
||||
var leaf = node.AsLeaf<V>();
|
||||
// Leaf uses FindIndex
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
leaf.Values[index] = value;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (leaf.Header.Count < LeafNode<K, V>.Capacity)
|
||||
{
|
||||
InsertIntoLeaf(leaf, index, key, value, strategy);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return SplitLeaf(leaf, index, key, value, strategy, owner);
|
||||
}
|
||||
}
|
||||
|
||||
// --- INTERNAL CASE ---
|
||||
var internalNode = node.AsInternal();
|
||||
|
||||
// FIX: Internal uses FindRoutingIndex
|
||||
int childIndex = FindRoutingIndex(internalNode, key, strategy);
|
||||
|
||||
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
|
||||
internalNode.Children[childIndex] = child;
|
||||
|
||||
var split = InsertRecursive(child, key, value, strategy, owner);
|
||||
|
||||
if (split != null)
|
||||
{
|
||||
// ... checks ...
|
||||
// Use childIndex here
|
||||
if (internalNode.Header.Count < InternalNode<K>.Capacity - 1)
|
||||
{
|
||||
InsertIntoInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return SplitInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy, owner);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static void InsertIntoLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, K key, V value, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
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<V> of length 'count'
|
||||
Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index);
|
||||
|
||||
Array.Copy(leaf._prefixes!, index, leaf._prefixes!, index + 1, count - index);
|
||||
}
|
||||
|
||||
leaf.Keys[index] = key;
|
||||
|
||||
// This fails if leaf.Values is a Span<V> of length 'count'
|
||||
leaf.Values[index] = value;
|
||||
|
||||
leaf._prefixes![index] = strategy.GetPrefix(key);
|
||||
|
||||
leaf.SetCount(count + 1);
|
||||
}
|
||||
private static SplitResult<K> SplitLeaf<K, V, TStrategy>(LeafNode<K, V> left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
var right = new LeafNode<K, V>(owner);
|
||||
int totalCount = left.Header.Count;
|
||||
|
||||
// Heuristics
|
||||
int splitPoint;
|
||||
if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively)
|
||||
else if (insertIndex == 0) splitPoint = 0; // Prepend: Right gets all
|
||||
else splitPoint = totalCount / 2;
|
||||
|
||||
// Move items to Right
|
||||
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);
|
||||
// Manually copy prefixes if needed or re-calculate
|
||||
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint+i];
|
||||
}
|
||||
|
||||
// Update Counts
|
||||
left.SetCount(splitPoint);
|
||||
right.SetCount(moveCount);
|
||||
|
||||
// Insert the New Item into the correct node
|
||||
if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0))
|
||||
{
|
||||
InsertIntoLeaf(left, insertIndex, key, value, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
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<K>(right, right.Keys[0]);
|
||||
}
|
||||
|
||||
private static void InsertIntoInternal<K, TStrategy>(InternalNode<K> node, int index, K separator, Node<K> newChild, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int count = node.Header.Count;
|
||||
|
||||
// Shift Keys and Prefixes
|
||||
if (index < count)
|
||||
{
|
||||
Array.Copy(node.Keys, index, node.Keys, index + 1, count - index);
|
||||
|
||||
// FIX: Shift raw prefix array
|
||||
Array.Copy(node._prefixes!, index, node._prefixes!, index + 1, count - index);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Since it's a small struct buffer (size 33), a loop is fine/fast.
|
||||
for (int i = count + 1; i > index + 1; i--)
|
||||
{
|
||||
node.Children[i] = node.Children[i - 1];
|
||||
}
|
||||
|
||||
node.Keys[index] = separator;
|
||||
|
||||
// FIX: Write to raw array
|
||||
node._prefixes![index] = strategy.GetPrefix(separator);
|
||||
|
||||
node.Children[index + 1] = newChild;
|
||||
node.SetCount(count + 1);
|
||||
}
|
||||
|
||||
private static SplitResult<K> SplitInternal<K, TStrategy>(InternalNode<K> left, int insertIndex, K separator, Node<K> newChild, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
var right = new InternalNode<K>(owner);
|
||||
int count = left.Header.Count;
|
||||
int splitPoint = count / 2; // Internal nodes usually split 50/50 to keep tree fat
|
||||
|
||||
// The key at splitPoint moves UP to become the separator.
|
||||
// Keys > splitPoint move to Right.
|
||||
|
||||
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);
|
||||
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint + 1 + i];
|
||||
|
||||
// Move Children to Right
|
||||
// Left has children 0..splitPoint. Right has children splitPoint+1..End
|
||||
for (int i = 0; i <= moveCount; i++)
|
||||
{
|
||||
right.Children[i] = left.Children[splitPoint + 1 + i];
|
||||
}
|
||||
|
||||
left.SetCount(splitPoint);
|
||||
right.SetCount(moveCount);
|
||||
|
||||
// Determine where to insert the new Separator/Child
|
||||
// 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?
|
||||
// Usually easier to insert into temp buffer and split,
|
||||
// but here we can branch:
|
||||
// If insertIndex <= splitPoint, insert left. Else right.
|
||||
}
|
||||
|
||||
// Simplified insertion into split nodes:
|
||||
if (insertIndex <= splitPoint)
|
||||
{
|
||||
InsertIntoInternal(left, insertIndex, separator, newChild, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
InsertIntoInternal(right, insertIndex - (splitPoint + 1), separator, newChild, strategy);
|
||||
}
|
||||
|
||||
return new SplitResult<K>(right, upKey);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Removal Logic
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Removal Logic (Fixed Type Inference & Casting)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
|
||||
if (node.IsLeaf)
|
||||
{
|
||||
var leaf = node.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, strategy);
|
||||
// Exact match check
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
|
||||
RemoveFromLeaf(leaf, index);
|
||||
return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
return false; // Key not found
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// Internal Node
|
||||
var internalNode = node.AsInternal();
|
||||
|
||||
int index = FindRoutingIndex<K, TStrategy>(internalNode, key, strategy);
|
||||
|
||||
// Descend
|
||||
var child = internalNode.Children[index]!.EnsureEditable(owner);
|
||||
internalNode.Children[index] = child;
|
||||
|
||||
// FIX 1: Explicitly specify <K, V> here.
|
||||
// The compiler cannot infer 'V' because it is not in the arguments.
|
||||
bool childUnderflow = RemoveRecursive<K, V, TStrategy>(child, key, strategy, owner);
|
||||
|
||||
if (childUnderflow)
|
||||
{
|
||||
// FIX 2: HandleUnderflow also needs to know <V> to perform Leaf casts correctly
|
||||
return HandleUnderflow<K, V, TStrategy>(internalNode, index, strategy, owner);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveFromLeaf<K, V>(LeafNode<K, V> leaf, int index)
|
||||
{
|
||||
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);
|
||||
|
||||
var p = leaf._prefixes;
|
||||
for(int i=index; i<count-1; i++) p[i] = p[i+1];
|
||||
|
||||
leaf.SetCount(count - 1);
|
||||
}
|
||||
|
||||
// FIX 3: Added <V> to HandleUnderflow
|
||||
private static bool HandleUnderflow<K, V, TStrategy>(InternalNode<K> parent, int childIndex, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Try to borrow from Right Sibling
|
||||
if (childIndex < parent.Header.Count)
|
||||
{
|
||||
var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner);
|
||||
parent.Children[childIndex + 1] = rightSibling;
|
||||
var leftChild = parent.Children[childIndex]!;
|
||||
|
||||
if (CanBorrow(rightSibling))
|
||||
{
|
||||
RotateLeft<K, V, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Merge<K, V, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
|
||||
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
}
|
||||
// Try to borrow from Left Sibling
|
||||
else if (childIndex > 0)
|
||||
{
|
||||
var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner);
|
||||
parent.Children[childIndex - 1] = leftSibling;
|
||||
var rightChild = parent.Children[childIndex]!;
|
||||
|
||||
if (CanBorrow(leftSibling))
|
||||
{
|
||||
RotateRight<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Merge Left and Current. Note separator index is 'childIndex - 1'
|
||||
Merge<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
|
||||
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanBorrow<K>(Node<K> node)
|
||||
{
|
||||
// Note: LeafNode<K, V>.MergeThreshold is constant 8, so we can access it statically or via 8
|
||||
return node.Header.Count > 8 + 1;
|
||||
}
|
||||
|
||||
// FIX 4: Added <V> to Merge/Rotate so we can cast to LeafNode<K, V> successfully.
|
||||
private static void Merge<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Case A: Merging Leaves
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
|
||||
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);
|
||||
Array.Copy(rightLeaf._prefixes.ToArray(), 0, leftLeaf._prefixes.ToArray(), lCount, rCount);
|
||||
|
||||
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;
|
||||
leftInternal._prefixes[lCount] = strategy.GetPrefix(separator);
|
||||
|
||||
int rCount = rightInternal.Header.Count;
|
||||
Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount);
|
||||
Array.Copy(rightInternal._prefixes.ToArray(), 0, leftInternal._prefixes.ToArray(), lCount + 1, rCount);
|
||||
|
||||
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);
|
||||
|
||||
var pp = parent._prefixes;
|
||||
for(int i=separatorIndex; i<pCount-1; i++) pp[i] = pp[i+1];
|
||||
|
||||
for(int i = separatorIndex + 2; i <= pCount; i++)
|
||||
{
|
||||
parent.Children[i - 1] = parent.Children[i];
|
||||
}
|
||||
|
||||
parent.SetCount(pCount - 1);
|
||||
}
|
||||
|
||||
private static void RotateLeft<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Move one item from Right to Left
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
|
||||
// Move first of right to end of left
|
||||
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
|
||||
RemoveFromLeaf(rightLeaf, 0);
|
||||
|
||||
// Update Parent Separator
|
||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
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];
|
||||
parent._prefixes[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<rCount; i++) rightInternal.Children[i] = rightInternal.Children[i+1];
|
||||
|
||||
// Shift keys
|
||||
Array.Copy(rightInternal.Keys, 1, rightInternal.Keys, 0, rCount - 1);
|
||||
var rp = rightInternal._prefixes;
|
||||
for(int i=0; i<rCount-1; i++) rp[i] = rp[i+1];
|
||||
|
||||
rightInternal.SetCount(rCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RotateRight<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// Move one item from Left to Right
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
int last = leftLeaf.Header.Count - 1;
|
||||
|
||||
InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy);
|
||||
RemoveFromLeaf(leftLeaf, last);
|
||||
|
||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftInternal = (InternalNode<K>)left;
|
||||
var rightInternal = (InternalNode<K>)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];
|
||||
parent._prefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
|
||||
|
||||
// 3. Truncate Left
|
||||
leftInternal.SetCount(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
PersistentMap/BaseOrderedMap.cs
Normal file
83
PersistentMap/BaseOrderedMap.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
internal Node<K> _root;
|
||||
internal readonly TStrategy _strategy;
|
||||
|
||||
public int Count { get; protected set; }
|
||||
|
||||
protected BaseOrderedMap(Node<K> root, TStrategy strategy, int count)
|
||||
{
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
|
||||
Count = count;
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Read Operations (Shared)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public bool TryGetValue(K key, out V value)
|
||||
{
|
||||
return BTreeFunctions.TryGetValue(_root, key, _strategy, out value);
|
||||
}
|
||||
|
||||
public bool ContainsKey(K key)
|
||||
{
|
||||
return BTreeFunctions.TryGetValue<K,V, TStrategy>(_root, key, _strategy, out _);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Bootstrap / Factory Helpers
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public static PersistentMap<K, V, TStrategy> Create(TStrategy strategy)
|
||||
{
|
||||
// Start with an empty leaf owned by None so the first write triggers CoW.
|
||||
var emptyRoot = new LeafNode<K, V>(OwnerId.None);
|
||||
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
|
||||
}
|
||||
|
||||
public static TransientMap<K, V, TStrategy> CreateTransient(TStrategy strategy)
|
||||
{
|
||||
var emptyRoot = new LeafNode<K, V>(OwnerId.None);
|
||||
return new TransientMap<K, V, TStrategy>(emptyRoot, strategy,0);
|
||||
}
|
||||
|
||||
|
||||
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
|
||||
{
|
||||
return AsEnumerable().GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
// 1. Full Scan
|
||||
public BTreeEnumerable<K, V, TStrategy> AsEnumerable()
|
||||
=> new(_root, _strategy, false, default, false, default);
|
||||
|
||||
// 2. Exact Range
|
||||
public BTreeEnumerable<K, V, TStrategy> Range(K min, K max)
|
||||
=> new(_root, _strategy, true, min, true, max);
|
||||
|
||||
// 3. Start From (Open Ended)
|
||||
public BTreeEnumerable<K, V, TStrategy> From(K min) => new(_root, _strategy, true, min, false, default);
|
||||
|
||||
// 4. Until (Start at beginning)
|
||||
public BTreeEnumerable<K, V, TStrategy> Until(K max)
|
||||
=> new(_root, _strategy, false, default, true, max);
|
||||
}
|
||||
245
PersistentMap/Iterator.cs
Normal file
245
PersistentMap/Iterator.cs
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
|
||||
|
||||
public struct BTreeEnumerable<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>>
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
private readonly Node<K> _root;
|
||||
private readonly TStrategy _strategy;
|
||||
private readonly K _min, _max;
|
||||
private readonly bool _hasMin, _hasMax;
|
||||
|
||||
public BTreeEnumerable(Node<K> root, TStrategy strategy, bool hasMin, K min, bool hasMax, K max)
|
||||
{
|
||||
_root = root; _strategy = strategy;
|
||||
_hasMin = hasMin; _min = min;
|
||||
_hasMax = hasMax; _max = max;
|
||||
}
|
||||
|
||||
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
|
||||
{
|
||||
return new BTreeEnumerator<K, V, TStrategy>(_root, _strategy, _hasMin, _min, _hasMax, _max);
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
// Fixed-size buffer for the path.
|
||||
// Depth 16 * 32 (branching factor) = Exabytes of capacity.
|
||||
[InlineArray(16)]
|
||||
internal struct IterNodeBuffer<K>
|
||||
{
|
||||
private Node<K> _element0;
|
||||
}
|
||||
|
||||
[InlineArray(16)]
|
||||
internal struct IterIndexBuffer<K>
|
||||
{
|
||||
private int _element0;
|
||||
}
|
||||
|
||||
public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
private readonly TStrategy _strategy;
|
||||
private readonly Node<K> _root;
|
||||
|
||||
// --- BOUNDS ---
|
||||
private readonly bool _hasMax;
|
||||
private readonly K _maxKey;
|
||||
private readonly bool _hasMin;
|
||||
private readonly K _minKey;
|
||||
|
||||
// --- INLINE STACK ---
|
||||
private IterNodeBuffer<K> _nodeStack;
|
||||
private IterIndexBuffer<K> _indexStack;
|
||||
private int _depth;
|
||||
|
||||
// --- STATE ---
|
||||
private LeafNode<K, V>? _currentLeaf;
|
||||
private int _currentLeafIndex;
|
||||
private KeyValuePair<K, V> _current;
|
||||
|
||||
// Unified Constructor
|
||||
// We use boolean flags because 'K' might be a struct where 'null' is impossible.
|
||||
public BTreeEnumerator(Node<K> root, TStrategy strategy, bool hasMin, K minKey, bool hasMax, K maxKey)
|
||||
{
|
||||
_root = root;
|
||||
_strategy = strategy;
|
||||
_hasMax = hasMax;
|
||||
_maxKey = maxKey;
|
||||
_hasMin = hasMin;
|
||||
_minKey = minKey;
|
||||
|
||||
_nodeStack = new IterNodeBuffer<K>();
|
||||
_indexStack = new IterIndexBuffer<K>(); // Explicit struct init
|
||||
_depth = 0;
|
||||
_currentLeaf = null;
|
||||
_currentLeafIndex = -1;
|
||||
_current = default;
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
if (hasMin)
|
||||
{
|
||||
Seek(minKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
DiveLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logic 1: Unbounded Start (Go to very first item)
|
||||
private void DiveLeft()
|
||||
{
|
||||
Node<K> node = _root;
|
||||
_depth = 0;
|
||||
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
_nodeStack[_depth] = internalNode;
|
||||
_indexStack[_depth] = 0; // Always take left-most child
|
||||
_depth++;
|
||||
node = internalNode.Children[0]!;
|
||||
}
|
||||
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
_currentLeafIndex = -1; // Position before the first element (0)
|
||||
}
|
||||
|
||||
// Logic 2: Bounded Start (Go to specific key)
|
||||
private void Seek(K key)
|
||||
{
|
||||
Node<K> node = _root;
|
||||
_depth = 0;
|
||||
|
||||
// Dive using Routing
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, _strategy);
|
||||
|
||||
_nodeStack[_depth] = internalNode;
|
||||
_indexStack[_depth] = idx;
|
||||
_depth++;
|
||||
|
||||
node = internalNode.Children[idx]!;
|
||||
}
|
||||
|
||||
// Find index in Leaf
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
int index = BTreeFunctions.FindIndex<K, TStrategy>(_currentLeaf, key, _strategy);
|
||||
|
||||
// Set position to (index - 1) so that the first MoveNext() lands on 'index'
|
||||
_currentLeafIndex = index - 1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_currentLeaf == null) return false;
|
||||
|
||||
// 1. Try to advance in current leaf
|
||||
if (++_currentLeafIndex < _currentLeaf.Header.Count)
|
||||
{
|
||||
// OPTIMIZATION: Check Max Bound (if active)
|
||||
if (_hasMax)
|
||||
{
|
||||
// If Current Key > Max Key, we are done.
|
||||
if (_strategy.Compare(_currentLeaf.Keys[_currentLeafIndex], _maxKey) > 0)
|
||||
{
|
||||
_currentLeaf = null; // Close iterator
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Leaf exhausted. Find next leaf.
|
||||
if (FindNextLeaf())
|
||||
{
|
||||
// Found new leaf, index reset to 0.
|
||||
// Check Max Bound immediately for the first item
|
||||
if (_hasMax)
|
||||
{
|
||||
if (_strategy.Compare(_currentLeaf!.Keys[0], _maxKey) > 0)
|
||||
{
|
||||
_currentLeaf = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[0], _currentLeaf.Values[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool FindNextLeaf()
|
||||
{
|
||||
while (_depth > 0)
|
||||
{
|
||||
_depth--;
|
||||
var internalNode = _nodeStack[_depth].AsInternal();
|
||||
int currentIndex = _indexStack[_depth];
|
||||
|
||||
if (currentIndex < internalNode.Header.Count)
|
||||
{
|
||||
int nextIndex = currentIndex + 1;
|
||||
_indexStack[_depth] = nextIndex;
|
||||
_depth++;
|
||||
|
||||
Node<K> node = internalNode.Children[nextIndex]!;
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
_nodeStack[_depth] = node;
|
||||
_indexStack[_depth] = 0;
|
||||
_depth++;
|
||||
node = node.AsInternal().Children[0]!;
|
||||
}
|
||||
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
_currentLeafIndex = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public KeyValuePair<K, V> Current => _current;
|
||||
object IEnumerator.Current => _current;
|
||||
public void Reset()
|
||||
{
|
||||
// 1. Clear current state
|
||||
_depth = 0;
|
||||
_currentLeaf = null;
|
||||
_currentLeafIndex = -1;
|
||||
_current = default;
|
||||
|
||||
// 2. Re-initialize based on how the iterator was created
|
||||
if (_root != null)
|
||||
{
|
||||
if (_hasMin)
|
||||
{
|
||||
// If we had a start range, find it again
|
||||
Seek(_minKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, go back to the very first leaf
|
||||
DiveLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
162
PersistentMap/KeyStrategies.cs
Normal file
162
PersistentMap/KeyStrategies.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
public interface IKeyStrategy<K>
|
||||
{
|
||||
int Compare(K x, K y);
|
||||
long GetPrefix(K key);
|
||||
}
|
||||
|
||||
|
||||
public struct UnicodeStrategy : IKeyStrategy<string>
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(string? x, string? y) => string.CompareOrdinal(x, y);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return long.MinValue;
|
||||
|
||||
// 1. Prepare Buffer (8 bytes)
|
||||
// stackalloc is virtually free (pointer bump)
|
||||
Span<byte> utf8Bytes = stackalloc byte[8];
|
||||
|
||||
// 2. Transcode (The "Safe" Magic)
|
||||
// This intrinsic handles ASCII efficiently and converts Surrogates/Chinese
|
||||
// into bytes that maintain the correct "Magnitude" (Sort Order).
|
||||
// Invalid surrogates become 0xEF (Replacement Char), which sorts > ASCII.
|
||||
System.Text.Unicode.Utf8.FromUtf16(
|
||||
key.AsSpan(0, Math.Min(key.Length, 8)),
|
||||
utf8Bytes,
|
||||
out _,
|
||||
out _,
|
||||
replaceInvalidSequences: true); // True ensures we get 0xEF for broken chars
|
||||
|
||||
// 3. Load as Big Endian Long
|
||||
long packed = BinaryPrimitives.ReadInt64BigEndian(utf8Bytes);
|
||||
|
||||
// 4. Sign Toggle
|
||||
// Maps the byte range 0x00..0xFF to the signed long range Min..Max
|
||||
// Essential for the < and > operators to work correctly.
|
||||
return packed ^ unchecked((long)0x8080808080808080);
|
||||
}
|
||||
}
|
||||
|
||||
public struct IntStrategy : IKeyStrategy<int>
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(int x, int y) => x.CompareTo(y);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(int key)
|
||||
{
|
||||
// Pack the 32-bit int into the high 32-bits of the long.
|
||||
// This preserves sorting order when scanning the long array.
|
||||
// Cast to uint first to prevent sign extension confusion during the shift,
|
||||
// though standard int shifting usually works fine for direct mapping.
|
||||
return (long)key << 32;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Helper for SIMD accelerated prefix scanning.
|
||||
/// </summary>
|
||||
public static class PrefixScanner
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int FindFirstGreaterOrEqual(ReadOnlySpan<long> prefixes, long targetPrefix)
|
||||
{
|
||||
// Fallback for short arrays or unsupported hardware
|
||||
if (!Avx2.IsSupported || prefixes.Length < 4)
|
||||
return LinearScan(prefixes, targetPrefix);
|
||||
|
||||
return Avx512F.IsSupported
|
||||
? ScanAvx512(prefixes, targetPrefix)
|
||||
: ScanAvx2(prefixes, targetPrefix);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearScan(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
for (var i = 0; i < prefixes.Length; i++)
|
||||
if (prefixes[i] >= target)
|
||||
return i;
|
||||
return prefixes.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx2(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
// Create a vector where every element is the target prefix
|
||||
var vTarget = Vector256.Create(target);
|
||||
var i = 0;
|
||||
var len = prefixes.Length;
|
||||
|
||||
// Process 4 longs at a time (256 bits)
|
||||
for (; i <= len - 4; i += 4)
|
||||
fixed (long* ptr = prefixes)
|
||||
{
|
||||
var vData = Avx2.LoadVector256(ptr + i);
|
||||
|
||||
// Compare: result is -1 (all 1s) if true, 0 if false
|
||||
// We want Data >= Target.
|
||||
// AVX2 CompareGreaterThan is for signed. Longs should be treated carefully,
|
||||
// but for text prefixes (positive), signed compare is usually sufficient.
|
||||
// Effectively: !(Data < Target) could be safer if signs vary,
|
||||
// but here we assume prefixes are derived from unsigned chars.
|
||||
// Standard AVX2 hack for CompareGreaterOrEqual (Signed):
|
||||
// No native _mm256_cmpge_epi64 in AVX2.
|
||||
// Use CompareGreaterThan(Data, Target - 1)
|
||||
var vResult = Avx2.CompareGreaterThan(vData, Vector256.Create(target - 1));
|
||||
|
||||
var mask = Avx2.MoveMask(vResult.AsByte());
|
||||
|
||||
if (mask != 0)
|
||||
{
|
||||
// Identify the first set bit corresponding to a 64-bit element
|
||||
// MoveMask returns 32 bits (1 per byte). Each long is 8 bytes.
|
||||
// We check bits 0, 8, 16, 24.
|
||||
if ((mask & 0xFF) != 0) return i + 0;
|
||||
if ((mask & 0xFF00) != 0) return i + 1;
|
||||
if ((mask & 0xFF0000) != 0) return i + 2;
|
||||
return i + 3;
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(prefixes.Slice(i), target) + i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx512(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
var vTarget = Vector512.Create(target);
|
||||
var i = 0;
|
||||
var len = prefixes.Length;
|
||||
|
||||
for (; i <= len - 8; i += 8)
|
||||
fixed (long* ptr = prefixes)
|
||||
{
|
||||
var vData = Avx512F.LoadVector512(ptr + i);
|
||||
// AVX512 has dedicated Compare Greater Than or Equal Long
|
||||
var mask = Avx512F.CompareGreaterThanOrEqual(vData, vTarget);
|
||||
|
||||
if (mask != Vector512<long>.Zero)
|
||||
{
|
||||
// Extract most significant bit mask
|
||||
var m = mask.ExtractMostSignificantBits();
|
||||
// Count trailing zeros to find the index
|
||||
return i + BitOperations.TrailingZeroCount(m);
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(prefixes.Slice(i), target) + i;
|
||||
}
|
||||
}
|
||||
310
PersistentMap/Nodes.cs
Normal file
310
PersistentMap/Nodes.cs
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
[Flags]
|
||||
public enum NodeFlags : byte
|
||||
{
|
||||
None = 0,
|
||||
IsLeaf = 1 << 0,
|
||||
IsRoot = 1 << 1,
|
||||
HasPrefixes = 1 << 2
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct NodeHeader
|
||||
{
|
||||
// 6 Bytes: OwnerId for Copy-on-Write (CoW)
|
||||
public OwnerId Owner;
|
||||
|
||||
// 1 Byte: Number of items currently used
|
||||
public byte Count;
|
||||
|
||||
// 1 Byte: Type flags (Leaf, Root, etc.)
|
||||
public NodeFlags Flags;
|
||||
|
||||
public NodeHeader(OwnerId owner, byte count, NodeFlags flags)
|
||||
{
|
||||
Owner = owner;
|
||||
Count = count;
|
||||
Flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
// Constraint: Internal Nodes fixed at 32 children.
|
||||
// This removes the need for a separate array allocation for children references.
|
||||
[InlineArray(32)]
|
||||
public struct NodeBuffer<V>
|
||||
{
|
||||
private Node<V>? _element0;
|
||||
}
|
||||
|
||||
public abstract class Node<K>
|
||||
{
|
||||
public NodeHeader Header;
|
||||
|
||||
// FIX: Change to 'internal' so BTreeFunctions can shift the array directly.
|
||||
internal long[]? _prefixes;
|
||||
|
||||
protected Node(OwnerId owner, NodeFlags flags)
|
||||
{
|
||||
Header = new NodeHeader(owner, 0, flags);
|
||||
}
|
||||
|
||||
public abstract Span<K> GetKeys();
|
||||
|
||||
// Keep this for Search (Read-Only): it limits the view to valid items only.
|
||||
public Span<long> Prefixes => _prefixes.AsSpan(0, Header.Count);
|
||||
|
||||
public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0;
|
||||
|
||||
public abstract Node<K> EnsureEditable(OwnerId transactionId);
|
||||
|
||||
public void SetCount(int newCount) => Header.Count = (byte)newCount;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public LeafNode<K, V> AsLeaf<V>()
|
||||
{
|
||||
// Zero-overhead cast. Assumes you checked IsLeaf or know logic flow.
|
||||
return Unsafe.As<LeafNode<K, V>>(this);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public InternalNode<K> AsInternal()
|
||||
{
|
||||
// Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow.
|
||||
return Unsafe.As<InternalNode<K>>(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LeafNode<K, V> : Node<K>
|
||||
{
|
||||
public const int Capacity = 64;
|
||||
public const int MergeThreshold = 8;
|
||||
|
||||
// Leaf stores Keys and Values
|
||||
public K[] Keys;
|
||||
public LeafNode<K, V>? Next; // For range scans
|
||||
public V[] Values;
|
||||
|
||||
public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes)
|
||||
{
|
||||
Keys = new K[Capacity];
|
||||
Values = new V[Capacity];
|
||||
_prefixes = new long[Capacity];
|
||||
}
|
||||
|
||||
// Copy Constructor for CoW
|
||||
private LeafNode(LeafNode<K, V> original, OwnerId newOwner)
|
||||
: base(newOwner, original.Header.Flags)
|
||||
{
|
||||
Header.Count = original.Header.Count;
|
||||
Next = original.Next;
|
||||
|
||||
// Allocate new arrays
|
||||
Keys = new K[Capacity];
|
||||
Values = new V[Capacity];
|
||||
_prefixes = new long[Capacity];
|
||||
|
||||
// Copy data
|
||||
Array.Copy(original.Keys, Keys, original.Header.Count);
|
||||
Array.Copy(original.Values, Values, original.Header.Count);
|
||||
if (original._prefixes != null)
|
||||
Array.Copy(original._prefixes, _prefixes, original.Header.Count);
|
||||
}
|
||||
|
||||
public override Node<K> EnsureEditable(OwnerId transactionId)
|
||||
{
|
||||
// CASE 1: Persistent Mode (transactionId is None).
|
||||
// We MUST create a copy, because we cannot distinguish "Shared Immutable Node (0)"
|
||||
// from "New Mutable Node (0)" based on ID alone.
|
||||
// However, since BTreeFunctions only calls this once before descending,
|
||||
// we won't copy the same fresh node twice.
|
||||
if (transactionId == OwnerId.None)
|
||||
{
|
||||
return new LeafNode<K, V>(this, OwnerId.None);
|
||||
}
|
||||
|
||||
// CASE 2: Transient Mode.
|
||||
// If we own the node, return it.
|
||||
if (Header.Owner == transactionId)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
// CASE 3: CoW needed (Ownership mismatch).
|
||||
return new LeafNode<K, V>(this, transactionId);
|
||||
}
|
||||
|
||||
public override Span<K> GetKeys()
|
||||
{
|
||||
return Keys.AsSpan(0, Header.Count);
|
||||
}
|
||||
|
||||
public Span<V> GetValues()
|
||||
{
|
||||
return Values.AsSpan(0, Header.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InternalNode<K> : Node<K>
|
||||
{
|
||||
public const int Capacity = 32;
|
||||
|
||||
// Inline buffer for children (no array object overhead)
|
||||
public NodeBuffer<K> Children;
|
||||
|
||||
// Internal stores Keys (separators) and Children
|
||||
public K[] Keys;
|
||||
|
||||
public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes)
|
||||
{
|
||||
Keys = new K[Capacity];
|
||||
_prefixes = new long[Capacity];
|
||||
// Children buffer is a struct, zero-initialized by default
|
||||
}
|
||||
|
||||
// Copy Constructor for CoW
|
||||
private InternalNode(InternalNode<K> original, OwnerId newOwner)
|
||||
: base(newOwner, original.Header.Flags)
|
||||
{
|
||||
Header.Count = original.Header.Count;
|
||||
|
||||
Keys = new K[Capacity];
|
||||
_prefixes = new long[Capacity];
|
||||
|
||||
// Copy Keys and Prefixes
|
||||
Array.Copy(original.Keys, Keys, original.Header.Count);
|
||||
if (original._prefixes != null)
|
||||
Array.Copy(original._prefixes, _prefixes, original.Header.Count);
|
||||
|
||||
// Copy Children (Manual loop required for InlineArray in generic context usually,
|
||||
// but here we can iterate the span)
|
||||
var srcChildren = original.GetChildren();
|
||||
for (var i = 0; i < srcChildren.Length; i++) Children[i] = srcChildren[i];
|
||||
}
|
||||
|
||||
public override Node<K> EnsureEditable(OwnerId transactionId)
|
||||
{
|
||||
if (transactionId == OwnerId.None)
|
||||
{
|
||||
return new InternalNode<K>(this, OwnerId.None);
|
||||
}
|
||||
|
||||
if (Header.Owner == transactionId)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return new InternalNode<K>(this, transactionId);
|
||||
}
|
||||
public override Span<K> GetKeys()
|
||||
{
|
||||
return Keys.AsSpan(0, Header.Count);
|
||||
}
|
||||
|
||||
// Exposes the InlineArray as a Span
|
||||
public Span<Node<K>?> GetChildren()
|
||||
{
|
||||
return MemoryMarshal.CreateSpan<Node<K>?>(ref Children[0]!, Header.Count + 1);
|
||||
}
|
||||
|
||||
public void SetChild(int index, Node<K> node)
|
||||
{
|
||||
Children[index] = node;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto, Pack = 1)]
|
||||
public readonly struct OwnerId(uint id, ushort gen) : IEquatable<OwnerId>
|
||||
{
|
||||
private const int BatchSize = 100;
|
||||
|
||||
// The max of allocated IDs globally.
|
||||
// Starts at 0, so the first batch reserves IDs 1 to 100.
|
||||
private static long _globalHighWaterMark;
|
||||
|
||||
|
||||
// These fields are unique to each thread. They initialize to 0/default.
|
||||
// The current ID value this thread is handing out.
|
||||
[ThreadStatic] private static long _localCurrentId;
|
||||
|
||||
// How many IDs are left in this thread's current batch.
|
||||
[ThreadStatic] private static int _localRemaining;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Instance Data (6 Bytes)
|
||||
// ---------------------------------------------------------
|
||||
private readonly uint Id = id; // 4 bytes
|
||||
private readonly ushort Gen = gen; // 2 bytes
|
||||
|
||||
/// <summary>
|
||||
/// Generates the next unique OwnerId.
|
||||
/// mostly non-blocking (thread-local), hits Interlocked only once per 100 IDs.
|
||||
/// </summary>
|
||||
public static OwnerId Next()
|
||||
{
|
||||
// We have IDs remaining in our local batch.
|
||||
// This executes with zero locking overhead.
|
||||
if (_localRemaining > 0)
|
||||
{
|
||||
_localRemaining--;
|
||||
var val = ++_localCurrentId;
|
||||
return new OwnerId((uint)val, (ushort)(val >> 32));
|
||||
}
|
||||
|
||||
// SLOW PATH: We ran out (or this is the thread's first call).
|
||||
return NextBatch();
|
||||
}
|
||||
|
||||
private static OwnerId NextBatch()
|
||||
{
|
||||
// Atomically reserve a new block of IDs from the global counter.
|
||||
// Only one thread contends for this cache line at a time.
|
||||
var reservedEnd = Interlocked.Add(ref _globalHighWaterMark, BatchSize);
|
||||
|
||||
// Calculate the start of our new range.
|
||||
var reservedStart = reservedEnd - BatchSize + 1;
|
||||
|
||||
// Reset the local cache.
|
||||
// We set _localCurrentId to (start - 1) so that the first increment
|
||||
// inside the logic below lands exactly on 'reservedStart'.
|
||||
_localCurrentId = reservedStart - 1;
|
||||
_localRemaining = BatchSize;
|
||||
|
||||
// Perform the generation logic (same as Fast Path)
|
||||
_localRemaining--;
|
||||
var val = ++_localCurrentId;
|
||||
return new OwnerId((uint)val, (ushort)(val >> 32));
|
||||
}
|
||||
|
||||
public static readonly OwnerId None = new(0, 0);
|
||||
|
||||
public bool IsNone => Id == 0 && Gen == 0;
|
||||
|
||||
public bool Equals(OwnerId other)
|
||||
{
|
||||
return Id == other.Id && Gen == other.Gen;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is OwnerId other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id, Gen);
|
||||
}
|
||||
|
||||
public static bool operator ==(OwnerId left, OwnerId right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(OwnerId left, OwnerId right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
43
PersistentMap/PersistentMap.cs
Normal file
43
PersistentMap/PersistentMap.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
public sealed class PersistentMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
internal PersistentMap(Node<K> root, TStrategy strategy, int count)
|
||||
: base(root, strategy, count) { }
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Immutable Write API (Returns new Map)
|
||||
// ---------------------------------------------------------
|
||||
public PersistentMap<K, V, TStrategy> Set(K key, V value)
|
||||
{
|
||||
// OPTIMIZATION: Use OwnerId.None (0).
|
||||
// This signals EnsureEditable to always copy the root path,
|
||||
// producing a new tree of nodes that also have OwnerId.None.
|
||||
var newRoot = BTreeFunctions.Set(_root, key, value, _strategy, OwnerId.None);
|
||||
return new PersistentMap<K, V, TStrategy>(newRoot, _strategy, Count + 1);
|
||||
}
|
||||
|
||||
public static PersistentMap<K, V, TStrategy> Empty(TStrategy strategy)
|
||||
{
|
||||
// Create an empty Leaf Node.
|
||||
// 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent.
|
||||
// This ensures that any subsequent Set/Remove will clone this node
|
||||
// instead of modifying it in place.
|
||||
var emptyRoot = new LeafNode<K, V>(default(OwnerId));
|
||||
|
||||
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
|
||||
}
|
||||
|
||||
public PersistentMap<K, V, TStrategy> Remove(K key)
|
||||
{
|
||||
var newRoot = BTreeFunctions.Remove<K,V, TStrategy>(_root, key, _strategy, OwnerId.None);
|
||||
return new PersistentMap<K, V, TStrategy>(newRoot, _strategy, Count - 1);
|
||||
}
|
||||
|
||||
public TransientMap<K, V, TStrategy> ToTransient()
|
||||
{
|
||||
return new TransientMap<K, V, TStrategy>(_root, _strategy, Count);
|
||||
}
|
||||
}
|
||||
43
PersistentMap/TransientMap.cs
Normal file
43
PersistentMap/TransientMap.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentMap;
|
||||
|
||||
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// This is mutable, but we treat it as readonly for the ID generation logic usually.
|
||||
private OwnerId _transactionId;
|
||||
|
||||
internal TransientMap(Node<K> root, TStrategy strategy, int count)
|
||||
: base(root, strategy, count)
|
||||
{
|
||||
_transactionId = OwnerId.Next();
|
||||
}
|
||||
|
||||
public void Set(K key, V value)
|
||||
{
|
||||
_root = BTreeFunctions.Set(_root, key, value, _strategy, _transactionId);
|
||||
}
|
||||
|
||||
public void Remove(K key)
|
||||
{
|
||||
_root = BTreeFunctions.Remove<K,V, TStrategy>(_root, key, _strategy, _transactionId);
|
||||
}
|
||||
|
||||
public PersistentMap<K, V, TStrategy> 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)
|
||||
|
||||
var snapshot = new PersistentMap<K, V, TStrategy>(_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.
|
||||
_transactionId = OwnerId.Next();
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
160
TestProject1/FuzzTest.cs
Normal file
160
TestProject1/FuzzTest.cs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using PersistentMap;
|
||||
|
||||
public class BTreeFuzzTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly IntStrategy _strategy = new();
|
||||
|
||||
public BTreeFuzzTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fuzz_Insert_And_Remove_consistency()
|
||||
{
|
||||
// CONFIGURATION
|
||||
const int Iterations = 100_000; // High enough to trigger all splits/merges
|
||||
const int KeyRange = 5000; // Small enough to cause frequent collisions
|
||||
int Seed = Environment.TickCount;
|
||||
|
||||
// ORACLES
|
||||
var reference = new SortedDictionary<int, int>();
|
||||
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
|
||||
|
||||
var random = new Random(Seed);
|
||||
_output.WriteLine($"Starting Fuzz Test with Seed: {Seed}");
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
// 1. Pick an Action: 70% Insert/Update, 30% Remove
|
||||
bool isInsert = random.NextDouble() < 0.7;
|
||||
int key = random.Next(KeyRange);
|
||||
int val = key * 100;
|
||||
|
||||
if (isInsert)
|
||||
{
|
||||
// ACTION: INSERT
|
||||
reference[key] = val;
|
||||
subject.Set(key, val);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ACTION: REMOVE
|
||||
if (reference.ContainsKey(key))
|
||||
{
|
||||
reference.Remove(key);
|
||||
subject.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try removing non-existent key (should be safe)
|
||||
subject.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. VERIFY CONSISTENCY (Expensive but necessary)
|
||||
// We check consistency every 1000 ops or if the tree is small,
|
||||
// to keep the test fast enough.
|
||||
if (i % 1000 == 0 || reference.Count < 100)
|
||||
{
|
||||
AssertConsistency(reference, subject);
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
AssertConsistency(reference, subject);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_output.WriteLine($"FAILED at iteration with SEED: {Seed}");
|
||||
throw; // Re-throw to fail the test
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fuzz_Range_Queries()
|
||||
{
|
||||
// Validates that your Range Enumerator matches LINQ on the reference
|
||||
const int Iterations = 1000;
|
||||
const int KeyRange = 2000;
|
||||
int Seed = Environment.TickCount;
|
||||
|
||||
var reference = new SortedDictionary<int, int>();
|
||||
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
|
||||
var random = new Random(Seed);
|
||||
|
||||
// Fill Data
|
||||
for(int i=0; i<KeyRange; i++)
|
||||
{
|
||||
int k = random.Next(KeyRange);
|
||||
reference[k] = k;
|
||||
subject.Set(k, k);
|
||||
}
|
||||
|
||||
var persistent = subject.ToPersistent();
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
int min = random.Next(KeyRange);
|
||||
int max = min + random.Next(KeyRange - min); // Ensure max >= min
|
||||
|
||||
// 1. Reference Result (LINQ)
|
||||
// Note: SortedDictionary doesn't have a direct Range query, so we filter memory.
|
||||
var expected = reference
|
||||
.Where(kv => kv.Key >= min && kv.Key <= max)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
// 2. Subject Result
|
||||
var actual = persistent.Range(min, max)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
// 3. Compare
|
||||
if (!expected.SequenceEqual(actual))
|
||||
{
|
||||
_output.WriteLine($"Range Mismatch! Range: [{min}, {max}]");
|
||||
_output.WriteLine($"Expected: {string.Join(",", expected)}");
|
||||
_output.WriteLine($"Actual: {string.Join(",", actual)}");
|
||||
Assert.Fail("Range query results differ.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertConsistency(SortedDictionary<int, int> expected, TransientMap<int, int, IntStrategy> actual)
|
||||
{
|
||||
// 1. Count
|
||||
// if (expected.Count != actual.Count)
|
||||
// {
|
||||
// throw new Exception($"Count Mismatch! Expected {expected.Count}, Got {actual.Count}");
|
||||
// }
|
||||
|
||||
// 2. Full Scan Verification
|
||||
using var enumerator = actual.GetEnumerator();
|
||||
foreach (var kvp in expected)
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
throw new Exception("Enumerator ended too early!");
|
||||
}
|
||||
|
||||
if (enumerator.Current.Key != kvp.Key || enumerator.Current.Value != kvp.Value)
|
||||
{
|
||||
throw new Exception($"Content Mismatch! Expected [{kvp.Key}:{kvp.Value}], Got [{enumerator.Current.Key}:{enumerator.Current.Value}]");
|
||||
}
|
||||
}
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
throw new Exception("Enumerator has extra items!");
|
||||
}
|
||||
}
|
||||
}
|
||||
152
TestProject1/IteratorTests.cs
Normal file
152
TestProject1/IteratorTests.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using Xunit;
|
||||
using PersistentMap;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class EnumeratorTests
|
||||
{
|
||||
// Use IntStrategy for simple numeric testing
|
||||
private readonly IntStrategy _strategy = new();
|
||||
|
||||
// Helper to create a populated PersistentMap quickly
|
||||
private PersistentMap<int, int, IntStrategy> CreateMap(params int[] keys)
|
||||
{
|
||||
var map = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
|
||||
foreach (var k in keys)
|
||||
{
|
||||
map.Set(k, k * 10); // Value is key * 10 (e.g., 5 -> 50)
|
||||
}
|
||||
return map.ToPersistent();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyMap_EnumeratesNothing()
|
||||
{
|
||||
var map = PersistentMap<int, int, IntStrategy>.Empty(_strategy);
|
||||
var list = new List<int>();
|
||||
|
||||
foreach(var kv in map) list.Add(kv.Key);
|
||||
|
||||
Assert.Empty(list);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullScan_ReturnsSortedItems()
|
||||
{
|
||||
// Insert in random order to prove sorting works
|
||||
var map = CreateMap(5, 1, 3, 2, 4);
|
||||
|
||||
var keys = map.AsEnumerable().Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_Inclusive_ExactMatches()
|
||||
{
|
||||
// Tree: 10, 20, 30, 40, 50
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
// Range 20..40 should give 20, 30, 40
|
||||
var result = map.Range(20, 40).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(new[] { 20, 30, 40 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_Inclusive_WithGaps()
|
||||
{
|
||||
// Tree: 10, 20, 30, 40, 50
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
// Range 15..45:
|
||||
// Start: 15 -> Seeks to first key >= 15 (which is 20)
|
||||
// End: 45 -> Stops after 40 (next key 50 is > 45)
|
||||
var result = map.Range(15, 45).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(new[] { 20, 30, 40 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_SingleItem_Exact()
|
||||
{
|
||||
var map = CreateMap(1, 2, 3);
|
||||
|
||||
// Range 2..2 should just return 2
|
||||
var result = map.Range(2, 2).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void From_StartOpen_ToEnd()
|
||||
{
|
||||
// Tree: 10, 20, 30, 40, 50
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
// From 35 -> Should be 40, 50 (everything >= 35)
|
||||
var result = map.From(35).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(new[] { 40, 50 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Until_StartTo_EndOpen()
|
||||
{
|
||||
// Tree: 10, 20, 30, 40, 50
|
||||
var map = CreateMap(10, 20, 30, 40, 50);
|
||||
|
||||
// Until 25 -> Should be 10, 20 (everything <= 25)
|
||||
var result = map.Until(25).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(new[] { 10, 20 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_OutOfBounds_ReturnsEmpty()
|
||||
{
|
||||
var map = CreateMap(10, 20, 30);
|
||||
|
||||
// Range 40..50 (Past end)
|
||||
Assert.Empty(map.Range(40, 50));
|
||||
|
||||
// Range 0..5 (Before start)
|
||||
Assert.Empty(map.Range(0, 5));
|
||||
|
||||
// Range 15..16 (Gap between keys)
|
||||
Assert.Empty(map.Range(15, 16));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Range_LargeDataset_CrossesLeafBoundaries()
|
||||
{
|
||||
// Force splits. Leaf Capacity is 32, so 100 items ensures ~3-4 leaves.
|
||||
var tMap = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
|
||||
for(int i=0; i<100; i++) tMap.Set(i, i);
|
||||
var map = tMap.ToPersistent();
|
||||
|
||||
// Range spanning multiple leaves (e.g. 20 to 80)
|
||||
var list = map.Range(20, 80).Select(x => x.Key).ToList();
|
||||
|
||||
Assert.Equal(61, list.Count); // 20 through 80 inclusive is 61 items
|
||||
Assert.Equal(20, list.First());
|
||||
Assert.Equal(80, list.Last());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_Reset_Works()
|
||||
{
|
||||
// Verify that Reset() logic (re-seek/re-dive) works
|
||||
var map = CreateMap(1, 2, 3);
|
||||
using var enumerator = map.GetEnumerator();
|
||||
|
||||
enumerator.MoveNext(); // 1
|
||||
enumerator.MoveNext(); // 2
|
||||
|
||||
enumerator.Reset(); // Should go back to start
|
||||
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(1, enumerator.Current.Key);
|
||||
}
|
||||
}
|
||||
61
TestProject1/PersistenceTests.cs
Normal file
61
TestProject1/PersistenceTests.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
namespace TestProject1;
|
||||
using PersistentMap;
|
||||
public class PersistenceTests
|
||||
{
|
||||
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
|
||||
|
||||
[Fact]
|
||||
public void PersistentMap_IsTrulyImmutable()
|
||||
{
|
||||
var v0 = BaseOrderedMap<string, int, UnicodeStrategy>.Create(_strategy);
|
||||
var v1 = v0.Set("A", 1);
|
||||
var v2 = v1.Set("B", 2);
|
||||
|
||||
// v0 should be empty
|
||||
Assert.False(v0.ContainsKey("A"));
|
||||
|
||||
// v1 should have A but not B
|
||||
Assert.True(v1.ContainsKey("A"));
|
||||
Assert.False(v1.ContainsKey("B"));
|
||||
|
||||
// v2 should have both
|
||||
Assert.True(v2.ContainsKey("A"));
|
||||
Assert.True(v2.ContainsKey("B"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransientToPersistent_CreatesSnapshot()
|
||||
{
|
||||
var tMap = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
tMap.Set("A", 1);
|
||||
|
||||
// Create Snapshot
|
||||
var pMap = tMap.ToPersistent();
|
||||
|
||||
// Mutate Transient Map further
|
||||
tMap.Set("B", 2);
|
||||
tMap.Remove("A");
|
||||
|
||||
// Assert Transient State
|
||||
Assert.False(tMap.ContainsKey("A"));
|
||||
Assert.True(tMap.ContainsKey("B"));
|
||||
|
||||
// Assert Persistent Snapshot (Should be isolated)
|
||||
Assert.True(pMap.ContainsKey("A")); // A should still be here
|
||||
Assert.False(pMap.ContainsKey("B")); // B should not exist
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistentToTransient_DoesNotCorruptSource()
|
||||
{
|
||||
var pMap = BaseOrderedMap<string, int, UnicodeStrategy>.Create(_strategy);
|
||||
pMap = pMap.Set("Fixed", 1);
|
||||
|
||||
var tMap = pMap.ToTransient();
|
||||
tMap.Set("Fixed", 999); // Modify shared key
|
||||
|
||||
// pMap should remain 1
|
||||
Assert.True(pMap.TryGetValue("Fixed", out int val));
|
||||
Assert.Equal(1, val);
|
||||
}
|
||||
}
|
||||
87
TestProject1/StressTest.cs
Normal file
87
TestProject1/StressTest.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
namespace TestProject1;
|
||||
|
||||
using PersistentMap;
|
||||
|
||||
public class StressTests
|
||||
{
|
||||
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
|
||||
|
||||
[Fact]
|
||||
public void LargeInsert_SplitsCorrectly()
|
||||
{
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
int count = 10_000;
|
||||
|
||||
// 1. Insert 10k items
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
// Pad with 0s to ensure consistent length sorting for simple debugging
|
||||
map.Set($"Key_{i:D6}", i);
|
||||
}
|
||||
|
||||
|
||||
// 2. Read back all items
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bool found = map.TryGetValue($"Key_{i:D6}", out int val);
|
||||
Assert.True(found, $"Failed to find Key_{i:D6}");
|
||||
Assert.Equal(i, val);
|
||||
}
|
||||
|
||||
// 3. Verify Non-existent
|
||||
Assert.False(map.ContainsKey("Key_999999"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReverseInsert_HandlesPrependSplits()
|
||||
{
|
||||
// Inserting in reverse order triggers the "Left/Right 90/10" split heuristic specific to prepends
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
int count = 5000;
|
||||
|
||||
for (int i = count; i > 0; i--)
|
||||
{
|
||||
map.Set(i.ToString("D6"), i);
|
||||
}
|
||||
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
Assert.True(map.ContainsKey(i.ToString("D6")));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Random_InsertDelete_Churn()
|
||||
{
|
||||
// Fuzzing test to catch edge cases in Rebalance/Merge
|
||||
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
|
||||
var rng = new Random(12345);
|
||||
var reference = new Dictionary<string, int>();
|
||||
|
||||
for (int i = 0; i < 5000; i++)
|
||||
{
|
||||
string key = rng.Next(0, 1000).ToString(); // High collision chance
|
||||
int op = rng.Next(0, 3); // 0=Set, 1=Remove, 2=Check
|
||||
|
||||
if (op == 0)
|
||||
{
|
||||
map.Set(key, i);
|
||||
reference[key] = i;
|
||||
}
|
||||
else if (op == 1)
|
||||
{
|
||||
map.Remove(key);
|
||||
reference.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool mapHas = map.TryGetValue(key, out int v1);
|
||||
bool refHas = reference.TryGetValue(key, out int v2);
|
||||
|
||||
Assert.Equal(refHas, mapHas);
|
||||
if (mapHas) Assert.Equal(v2, v1);
|
||||
}
|
||||
}
|
||||
Console.WriteLine("bp");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue