using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; namespace Jint.Runtime { internal enum InsertionBehavior : byte { /// /// Specifies that an existing entry with the same key should be overwritten if encountered. /// OverwriteExisting = 1, /// /// Specifies that if an existing entry with the same key is encountered, an exception should be thrown. /// ThrowOnExisting = 2, /// /// Specifies that if existing entry with the same key is encountered, the update will be skipped. /// SkipIfExists = 3 } /// /// Taken from .NET source to create performant specialized dictionary containing structs for Jint. /// internal sealed class StructDictionary where TValue : struct { private static readonly EqualityComparer _comparer; static StructDictionary() { // we want to use same comparer as default dictionary impl that is hidden from us // .NET Core uses non-randomized hash code generation that is faster than default try { Dictionary dictionary = new Dictionary(); var field = dictionary.GetType().GetField("_comparer", BindingFlags.Instance | BindingFlags.NonPublic); field = field ?? dictionary.GetType().GetField("comparer", BindingFlags.Instance | BindingFlags.NonPublic); _comparer = field?.GetValue(dictionary) as EqualityComparer ?? EqualityComparer.Default; } catch { _comparer = EqualityComparer.Default; } } private struct Entry { public int hashCode; // Lower 31 bits of hash code, -1 if unused public int next; // Index of next entry, -1 if last public string key; // Key of entry public TValue value; // Value of entry } private int[] _buckets; private Entry[] _entries; private int _count; private int _freeList; private int _freeCount; private KeyCollection _keys; private ValueCollection _values; public StructDictionary() : this(0) { } public StructDictionary(int capacity) { if (capacity > 0) Initialize(capacity); } public int Count => _count - _freeCount; public KeyCollection Keys { get { if (_keys == null) _keys = new KeyCollection(this); return _keys; } } public ValueCollection Values { get { if (_values == null) _values = new ValueCollection(this); return _values; } } public ref TValue GetItem(string key) { int i = FindEntry(key); if (i >= 0) return ref _entries[i].value; ExceptionHelper.ThrowArgumentException("key " + key + " not part of dictionary"); return ref _entries[0].value; } public void Clear() { int count = _count; if (count > 0) { Array.Clear(_buckets, 0, _buckets.Length); _count = 0; _freeList = -1; _freeCount = 0; Array.Clear(_entries, 0, count); } } public bool ContainsKey(string key) => FindEntry(key) >= 0; public Enumerator GetEnumerator() => new Enumerator(this, Enumerator.KeyValuePair); private int FindEntry(string key) { int i = -1; int[] buckets = _buckets; Entry[] entries = _entries; int collisionCount = 0; if (buckets != null) { int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; // Value in _buckets is 1-based i = buckets[hashCode % buckets.Length] - 1; do { // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 // Test in if to drop range check for following array access if ((uint) i >= (uint) entries.Length || (entries[i].hashCode == hashCode && entries[i].key == key)) { break; } i = entries[i].next; if (collisionCount >= entries.Length) { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. ExceptionHelper.ThrowInvalidOperationException(); } collisionCount++; } while (true); } return i; } private int Initialize(int capacity) { int size = HashHelpers.GetPrime(capacity); _freeList = -1; _buckets = new int[size]; _entries = new Entry[size]; return size; } public bool TryInsert(string key, in TValue value, InsertionBehavior behavior) { if (_buckets == null) { Initialize(0); } Entry[] entries = _entries; int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; int collisionCount = 0; ref int bucket = ref _buckets[hashCode % _buckets.Length]; // Value in _buckets is 1-based int i = bucket - 1; do { // Should be a while loop https://github.com/dotnet/coreclr/issues/15476 // Test uint in if rather than loop condition to drop range check for following array access if ((uint) i >= (uint) entries.Length) { break; } if (entries[i].hashCode == hashCode && entries[i].key == key) { if (behavior == InsertionBehavior.OverwriteExisting) { entries[i].value = value; return true; } if (behavior == InsertionBehavior.SkipIfExists) { return true; } if (behavior == InsertionBehavior.ThrowOnExisting) { ExceptionHelper.ThrowArgumentException("key already exists"); } return false; } i = entries[i].next; if (collisionCount >= entries.Length) { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. ExceptionHelper.ThrowInvalidOperationException(); } collisionCount++; } while (true); bool updateFreeList = false; int index; if (_freeCount > 0) { index = _freeList; updateFreeList = true; _freeCount--; } else { int count = _count; if (count == entries.Length) { Resize(); bucket = ref _buckets[hashCode % _buckets.Length]; } index = count; _count = count + 1; entries = _entries; } ref Entry entry = ref entries[index]; if (updateFreeList) { _freeList = entry.next; } entry.hashCode = hashCode; // Value in _buckets is 1-based entry.next = bucket - 1; entry.key = key; entry.value = value; // Value in _buckets is 1-based bucket = index + 1; return true; } private void Resize() => Resize(HashHelpers.ExpandPrime(_count), false); private void Resize(int newSize, bool forceNewHashCodes) { // Value types never rehash Debug.Assert(!forceNewHashCodes || default(string) == null); Debug.Assert(newSize >= _entries.Length); int[] buckets = new int[newSize]; Entry[] entries = new Entry[newSize]; int count = _count; Array.Copy(_entries, 0, entries, 0, count); if (forceNewHashCodes) { for (int i = 0; i < count; i++) { if (entries[i].hashCode >= 0) { entries[i].hashCode = (_comparer.GetHashCode(entries[i].key) & 0x7FFFFFFF); } } } for (int i = 0; i < count; i++) { if (entries[i].hashCode >= 0) { int bucket = entries[i].hashCode % newSize; // Value in _buckets is 1-based entries[i].next = buckets[bucket] - 1; // Value in _buckets is 1-based buckets[bucket] = i + 1; } } _buckets = buckets; _entries = entries; } // The overload Remove(string key, out TValue value) is a copy of this method with one additional // statement to copy the value for entry being removed into the output parameter. // Code has been intentionally duplicated for performance reasons. public bool Remove(string key) { int[] buckets = _buckets; Entry[] entries = _entries; int collisionCount = 0; if (buckets != null) { int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; int bucket = hashCode % buckets.Length; int last = -1; // Value in buckets is 1-based int i = buckets[bucket] - 1; while (i >= 0) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && entry.key == key) { if (last < 0) { // Value in buckets is 1-based buckets[bucket] = entry.next + 1; } else { entries[last].next = entry.next; } entry.hashCode = -1; entry.next = _freeList; entry.key = null; entry.value = default; _freeList = i; _freeCount++; return true; } last = i; i = entry.next; if (collisionCount >= entries.Length) { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. ExceptionHelper.ThrowInvalidOperationException(); } collisionCount++; } } return false; } // This overload is a copy of the overload Remove(string key) with one additional // statement to copy the value for entry being removed into the output parameter. // Code has been intentionally duplicated for performance reasons. public bool Remove(string key, out TValue value) { int[] buckets = _buckets; Entry[] entries = _entries; int collisionCount = 0; if (buckets != null) { int hashCode = _comparer.GetHashCode(key) & 0x7FFFFFFF; int bucket = hashCode % buckets.Length; int last = -1; // Value in buckets is 1-based int i = buckets[bucket] - 1; while (i >= 0) { ref Entry entry = ref entries[i]; if (entry.hashCode == hashCode && entry.key == key) { if (last < 0) { // Value in buckets is 1-based buckets[bucket] = entry.next + 1; } else { entries[last].next = entry.next; } value = entry.value; entry.hashCode = -1; entry.next = _freeList; entry.key = null; entry.value = default; _freeList = i; _freeCount++; return true; } last = i; i = entry.next; if (collisionCount >= entries.Length) { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. ExceptionHelper.ThrowInvalidOperationException(); } collisionCount++; } } value = default; return false; } public bool TryGetValue(string key, out TValue value) { int i = FindEntry(key); if (i >= 0) { value = _entries[i].value; return true; } value = default; return false; } /// /// Ensures that the dictionary can hold up to 'capacity' entries without any further expansion of its backing storage /// public int EnsureCapacity(int capacity) { int currentCapacity = _entries == null ? 0 : _entries.Length; if (currentCapacity >= capacity) return currentCapacity; if (_buckets == null) return Initialize(capacity); int newSize = HashHelpers.GetPrime(capacity); Resize(newSize, forceNewHashCodes: false); return newSize; } public struct Enumerator : IEnumerator>, IDictionaryEnumerator { private readonly StructDictionary _dictionary; private int _index; private KeyValuePair _current; private readonly int _getEnumeratorRetType; // What should Enumerator.Current return? internal const int DictEntry = 1; internal const int KeyValuePair = 2; internal Enumerator(StructDictionary dictionary, int getEnumeratorRetType) { _dictionary = dictionary; _index = 0; _getEnumeratorRetType = getEnumeratorRetType; _current = new KeyValuePair(); } public bool MoveNext() { // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends. // dictionary.count+1 could be negative if dictionary.count is int.MaxValue while ((uint) _index < (uint) _dictionary._count) { ref Entry entry = ref _dictionary._entries[_index++]; if (entry.hashCode >= 0) { _current = new KeyValuePair(entry.key, entry.value); return true; } } _index = _dictionary._count + 1; _current = new KeyValuePair(); return false; } public KeyValuePair Current => _current; public void Dispose() { } object IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } if (_getEnumeratorRetType == DictEntry) { return new DictionaryEntry(_current.Key, _current.Value); } else { return new KeyValuePair(_current.Key, _current.Value); } } } void IEnumerator.Reset() { _index = 0; _current = new KeyValuePair(); } DictionaryEntry IDictionaryEnumerator.Entry { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } return new DictionaryEntry(_current.Key, _current.Value); } } object IDictionaryEnumerator.Key { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } return _current.Key; } } object IDictionaryEnumerator.Value { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } return _current.Value; } } } public sealed class KeyCollection { private readonly StructDictionary _dictionary; public KeyCollection(StructDictionary dictionary) { if (dictionary == null) { ExceptionHelper.ThrowArgumentNullException(nameof(dictionary)); } _dictionary = dictionary; } public Enumerator GetEnumerator() => new Enumerator(_dictionary); public int Count => _dictionary.Count; public struct Enumerator : IEnumerator, IEnumerator { private readonly StructDictionary _dictionary; private int _index; private string _currenstring; internal Enumerator(StructDictionary dictionary) { _dictionary = dictionary; _index = 0; _currenstring = default; } public void Dispose() { } public bool MoveNext() { while ((uint) _index < (uint) _dictionary._count) { ref Entry entry = ref _dictionary._entries[_index++]; if (entry.hashCode >= 0) { _currenstring = entry.key; return true; } } _index = _dictionary._count + 1; _currenstring = default; return false; } public string Current => _currenstring; object IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } return _currenstring; } } void IEnumerator.Reset() { _index = 0; _currenstring = default; } } } public sealed class ValueCollection { private readonly StructDictionary _dictionary; public ValueCollection(StructDictionary dictionary) { _dictionary = dictionary; } public Enumerator GetEnumerator() => new Enumerator(_dictionary); public int Count => _dictionary.Count; public struct Enumerator : IEnumerator { private readonly StructDictionary _dictionary; private int _index; private TValue _currentValue; internal Enumerator(StructDictionary dictionary) { _dictionary = dictionary; _index = 0; _currentValue = default; } public void Dispose() { } public bool MoveNext() { while ((uint) _index < (uint) _dictionary._count) { ref Entry entry = ref _dictionary._entries[_index++]; if (entry.hashCode >= 0) { _currentValue = entry.value; return true; } } _index = _dictionary._count + 1; _currentValue = default; return false; } public TValue Current => _currentValue; object IEnumerator.Current { get { if (_index == 0 || (_index == _dictionary._count + 1)) { ExceptionHelper.ThrowInvalidOperationException(); } return _currentValue; } } void IEnumerator.Reset() { _index = 0; _currentValue = default; } } } private static class HashHelpers { public const int HashCollisionThreshold = 100; // This is the maximum prime smaller than Array.MaxArrayLength public const int MaxPrimeArrayLength = 0x7FEFFFFD; public const int HashPrime = 101; // Table of prime numbers to use as hash table sizes. // A typical resize algorithm would pick the smallest prime number in this array // that is larger than twice the previous capacity. // Suppose our Hashtable currently has capacity x and enough elements are added // such that a resize needs to occur. Resizing first computes 2x then finds the // first prime in the table greater than 2x, i.e. if primes are ordered // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n. // Doubling is important for preserving the asymptotic complexity of the // hashtable operations such as add. Having a prime guarantees that double // hashing does not lead to infinite loops. IE, your hash function will be // h1(key) + i*h2(key), 0 <= i < size. h2 and the size must be relatively prime. // We prefer the low computation costs of higher prime numbers over the increased // memory allocation of a fixed prime number i.e. when right sizing a HashSet. public static readonly int[] primes = { 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 }; public static bool IsPrime(int candidate) { if ((candidate & 1) != 0) { int limit = (int) Math.Sqrt(candidate); for (int divisor = 3; divisor <= limit; divisor += 2) { if ((candidate % divisor) == 0) return false; } return true; } return (candidate == 2); } public static int GetPrime(int min) { if (min < 0) throw new ArgumentException(); for (int i = 0; i < primes.Length; i++) { int prime = primes[i]; if (prime >= min) return prime; } //outside of our predefined table. //compute the hard way. for (int i = (min | 1); i < int.MaxValue; i += 2) { if (IsPrime(i) && ((i - 1) % HashPrime != 0)) return i; } return min; } // Returns size of hashtable to grow to. public static int ExpandPrime(int oldSize) { int newSize = 2 * oldSize; // Allow the hashtables to grow to maximum possible size (~2G elements) before encountering capacity overflow. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast if ((uint) newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize) { Debug.Assert(MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength"); return MaxPrimeArrayLength; } return GetPrime(newSize); } } } }