#pragma warning disable MA0006 #pragma warning disable MA0008 #nullable disable // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; namespace Jint.Collections; /// /// DictionarySlim<string, TValue> is similar to Dictionary<TKey, TValue> but optimized in three ways: /// 1) It allows access to the value by ref replacing the common TryGetValue and Add pattern. /// 2) It does not store the hash code (assumes it is cheap to equate values). /// 3) It does not accept an equality comparer (assumes Object.GetHashCode() and Object.Equals() or overridden implementation are cheap and sufficient). /// [DebuggerTypeProxy(typeof(DictionarySlimDebugView<>))] [DebuggerDisplay("Count = {Count}")] internal sealed class StringDictionarySlim : IReadOnlyCollection> { // We want to initialize without allocating arrays. We also want to avoid null checks. // Array.Empty would give divide by zero in modulo operation. So we use static one element arrays. // The first add will cause a resize replacing these with real arrays of three elements. // Arrays are wrapped in a class to avoid being duplicated for each private static readonly Entry[] InitialEntries = new Entry[1]; private int _count; // 0-based index into _entries of head of free chain: -1 means empty private int _freeList = -1; // 1-based index into _entries; 0 means empty private int[] _buckets; private Entry[] _entries; [DebuggerDisplay("({key}, {value})->{next}")] private struct Entry { public Key key; public TValue value; // 0-based index of next entry in chain: -1 means end of chain // also encodes whether this entry _itself_ is part of the free list by changing sign and subtracting 3, // so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc. public int next; } public StringDictionarySlim() { _buckets = HashHelpers.SizeOneIntArray; _entries = InitialEntries; } public StringDictionarySlim(int capacity) { if (capacity < 2) capacity = 2; // 1 would indicate the dummy array capacity = HashHelpers.PowerOf2(capacity); _buckets = new int[capacity]; _entries = new Entry[capacity]; } public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } /// /// Clears the dictionary. Note that this invalidates any active enumerators. /// public void Clear() { _count = 0; _freeList = -1; _buckets = HashHelpers.SizeOneIntArray; _entries = InitialEntries; } public bool ContainsKey(Key key) { Entry[] entries = _entries; for (int i = _buckets[key.HashCode & (_buckets.Length-1)] - 1; (uint)i < (uint)entries.Length; i = entries[i].next) { if (key.Name == entries[i].key.Name) return true; } return false; } public bool TryGetValue(Key key, out TValue value) { Entry[] entries = _entries; for (int i = _buckets[key.HashCode & (_buckets.Length - 1)] - 1; (uint)i < (uint)entries.Length; i = entries[i].next) { if (key.Name == entries[i].key.Name) { value = entries[i].value; return true; } } value = default; return false; } public bool Remove(Key key) { Entry[] entries = _entries; int bucketIndex = key.HashCode & (_buckets.Length - 1); int entryIndex = _buckets[bucketIndex] - 1; int lastIndex = -1; while (entryIndex != -1) { Entry candidate = entries[entryIndex]; if (candidate.key == key) { if (lastIndex != -1) { // Fixup preceding element in chain to point to next (if any) entries[lastIndex].next = candidate.next; } else { // Fixup bucket to new head (if any) _buckets[bucketIndex] = candidate.next + 1; } entries[entryIndex] = default; entries[entryIndex].next = -3 - _freeList; // New head of free list _freeList = entryIndex; _count--; return true; } lastIndex = entryIndex; entryIndex = candidate.next; } return false; } public void SetOrUpdateValue(Key key, Func updater, TState state) { ref var currentValue = ref GetOrAddValueRef(key); currentValue = updater(currentValue, state); } // Not safe for concurrent _reads_ (at least, if either of them add) // For concurrent reads, prefer TryGetValue(key, out value) /// /// Gets the value for the specified key, or, if the key is not present, /// adds an entry and returns the value by ref. This makes it possible to /// add or update a value in a single look up operation. /// /// Key to look for /// Reference to the new or existing value public ref TValue GetOrAddValueRef(Key key) { Entry[] entries = _entries; int bucketIndex = key.HashCode & (_buckets.Length - 1); for (int i = _buckets[bucketIndex] - 1; (uint)i < (uint)entries.Length; i = entries[i].next) { if (key.Name == entries[i].key.Name) return ref entries[i].value; } return ref AddKey(key, bucketIndex); } public bool TryAdd(Key key, TValue value) { Entry[] entries = _entries; int bucketIndex = key.HashCode & (_buckets.Length - 1); for (int i = _buckets[bucketIndex] - 1; (uint)i < (uint)entries.Length; i = entries[i].next) { if (key.Name == entries[i].key.Name) { return false; } } AddKey(key, bucketIndex) = value; return true; } /// /// Adds a new item and expects key to not to exist. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddDangerous(in Key key, TValue value) { AddKey(key, key.HashCode & (_buckets.Length - 1)) = value; } public ref TValue this[Key key] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref GetOrAddValueRef(key); } [MethodImpl(MethodImplOptions.NoInlining)] private ref TValue AddKey(Key key, int bucketIndex) { Entry[] entries = _entries; int entryIndex; if (_freeList != -1) { entryIndex = _freeList; _freeList = -3 - entries[_freeList].next; } else { if (_count == entries.Length || entries.Length == 1) { entries = Resize(); bucketIndex = key.HashCode & (_buckets.Length - 1); // entry indexes were not changed by Resize } entryIndex = _count; } entries[entryIndex].key = key; entries[entryIndex].next = _buckets[bucketIndex] - 1; _buckets[bucketIndex] = entryIndex + 1; _count++; return ref entries[entryIndex].value; } private Entry[] Resize() { Debug.Assert(_entries.Length == _count || _entries.Length == 1); // We only copy _count, so if it's longer we will miss some int count = _count; int newSize = _entries.Length * 2; if ((uint)newSize > (uint)int.MaxValue) // uint cast handles overflow throw new InvalidOperationException("Capacity Overflow"); var entries = new Entry[newSize]; Array.Copy(_entries, 0, entries, 0, count); var newBuckets = new int[entries.Length]; while (count-- > 0) { int bucketIndex = entries[count].key.HashCode & (newBuckets.Length - 1); entries[count].next = newBuckets[bucketIndex] - 1; newBuckets[bucketIndex] = count + 1; } _buckets = newBuckets; _entries = entries; return entries; } /// /// Gets an enumerator over the dictionary /// public Enumerator GetEnumerator() => new Enumerator(this); // avoid boxing /// /// Gets an enumerator over the dictionary /// IEnumerator> IEnumerable>.GetEnumerator() => new Enumerator(this); /// /// Gets an enumerator over the dictionary /// IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); public struct Enumerator : IEnumerator> { private readonly StringDictionarySlim _dictionary; private int _index; private int _count; private KeyValuePair _current; internal Enumerator(StringDictionarySlim dictionary) { _dictionary = dictionary; _index = 0; _count = _dictionary._count; _current = default; } public bool MoveNext() { if (_count == 0) { _current = default; return false; } _count--; while (_dictionary._entries[_index].next < -1) _index++; _current = new KeyValuePair( _dictionary._entries[_index].key, _dictionary._entries[_index++].value); return true; } public KeyValuePair Current => _current; object IEnumerator.Current => _current; void IEnumerator.Reset() { _index = 0; _count = _dictionary._count; _current = default; } public void Dispose() { } } internal static class HashHelpers { internal static readonly int[] SizeOneIntArray = new int[1]; internal static int PowerOf2(int v) { if ((v & (v - 1)) == 0) return v; int i = 2; while (i < v) i <<= 1; return i; } } internal sealed class DictionarySlimDebugView { private readonly StringDictionarySlim _dictionary; public DictionarySlimDebugView(StringDictionarySlim dictionary) { _dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary)); } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public KeyValuePair[] Items => _dictionary.ToArray(); } }