This commit is contained in:
Linus Björnstam 2026-02-01 20:52:23 +01:00
commit 79b5ab98aa
13 changed files with 2016 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

36
NiceBtree.sln Normal file
View 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

View 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);
}
}
}
}

View 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
View 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() { }
}

View 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
View 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);
}
}

View 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);
}
}

View 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
View 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!");
}
}
}

View 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);
}
}

View 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);
}
}

View 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");
}
}