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