diff --git a/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs index bc528e67c0a7e..d01bb5ca0aa9b 100644 --- a/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs @@ -489,7 +489,7 @@ public void ICollection_Generic_CopyTo_IndexEqualToArrayCount_ThrowsArgumentExce ICollection collection = GenericICollectionFactory(count); T[] array = new T[count]; if (count > 0) - Assert.Throws(() => collection.CopyTo(array, count)); + Assert.ThrowsAny(() => collection.CopyTo(array, count)); else collection.CopyTo(array, count); // does nothing since the array is empty } @@ -511,7 +511,7 @@ public void ICollection_Generic_CopyTo_NotEnoughSpaceInOffsettedArray_ThrowsArgu { ICollection collection = GenericICollectionFactory(count); T[] array = new T[count]; - Assert.Throws(() => collection.CopyTo(array, 1)); + Assert.ThrowsAny(() => collection.CopyTo(array, 1)); } } diff --git a/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs index 815e4c6b08380..6f73030eb02ac 100644 --- a/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ICollection.NonGeneric.Tests.cs @@ -154,7 +154,10 @@ public void ICollection_NonGeneric_SyncRootUnique(int count) { ICollection collection1 = NonGenericICollectionFactory(count); ICollection collection2 = NonGenericICollectionFactory(count); - Assert.NotSame(collection1.SyncRoot, collection2.SyncRoot); + if (!ReferenceEquals(collection1, collection2)) + { + Assert.NotSame(collection1.SyncRoot, collection2.SyncRoot); + } } } diff --git a/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs index ce7682dd36f6c..e57af446e8dfc 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs @@ -260,16 +260,19 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemGet_DefaultKey(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - if (!DefaultValueAllowed) - { - Assert.Throws(() => dictionary[default(TKey)]); - } - else + if (!IsReadOnly) { - TValue value = CreateTValue(3452); - dictionary[default(TKey)] = value; - Assert.Equal(value, dictionary[default(TKey)]); + IDictionary dictionary = GenericIDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default(TKey)]); + } + else + { + TValue value = CreateTValue(3452); + dictionary[default(TKey)] = value; + Assert.Equal(value, dictionary[default(TKey)]); + } } } @@ -315,16 +318,19 @@ public void IDictionary_Generic_ItemGet_PresentKeyReturnsCorrectValue(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_DefaultKey(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - if (!DefaultValueAllowed) - { - Assert.Throws(() => dictionary[default(TKey)] = CreateTValue(3)); - } - else + if (!IsReadOnly) { - TValue value = CreateTValue(3452); - dictionary[default(TKey)] = value; - Assert.Equal(value, dictionary[default(TKey)]); + IDictionary dictionary = GenericIDictionaryFactory(count); + if (!DefaultValueAllowed) + { + Assert.Throws(() => dictionary[default(TKey)] = CreateTValue(3)); + } + else + { + TValue value = CreateTValue(3452); + dictionary[default(TKey)] = value; + Assert.Equal(value, dictionary[default(TKey)]); + } } } @@ -344,23 +350,29 @@ public void IDictionary_Generic_ItemSet_OnReadOnlyDictionary_ThrowsNotSupportedE [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_AddsNewValueWhenNotPresent(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - TKey missingKey = GetNewKey(dictionary); - dictionary[missingKey] = CreateTValue(543); - Assert.Equal(count + 1, dictionary.Count); + if (!IsReadOnly) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + TKey missingKey = GetNewKey(dictionary); + dictionary[missingKey] = CreateTValue(543); + Assert.Equal(count + 1, dictionary.Count); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_ItemSet_ReplacesExistingValueWhenPresent(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - TKey existingKey = GetNewKey(dictionary); - dictionary.Add(existingKey, CreateTValue(5342)); - TValue newValue = CreateTValue(1234); - dictionary[existingKey] = newValue; - Assert.Equal(count + 1, dictionary.Count); - Assert.Equal(newValue, dictionary[existingKey]); + if (!IsReadOnly) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + TKey existingKey = GetNewKey(dictionary); + dictionary.Add(existingKey, CreateTValue(5342)); + TValue newValue = CreateTValue(1234); + dictionary[existingKey] = newValue; + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(newValue, dictionary[existingKey]); + } } #endregion @@ -380,19 +392,22 @@ public void IDictionary_Generic_Keys_ContainsAllCorrectKeys(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - int previousCount = keys.Count; - if (count > 0) - Assert.NotEmpty(keys); - dictionary.Clear(); - if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(keys); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, keys.Count); + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + int previousCount = keys.Count; + if (count > 0) + Assert.NotEmpty(keys); + dictionary.Clear(); + if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(keys); + } + else + { + Assert.Equal(previousCount, keys.Count); + } } } @@ -400,22 +415,25 @@ public void IDictionary_Generic_Keys_ModifyingTheDictionaryUpdatesTheCollection( [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Keys_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - IEnumerator keysEnum = keys.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) - { - Assert.Throws(() => keysEnum.MoveNext()); - Assert.Throws(() => keysEnum.Reset()); - } - else + if (!IsReadOnly) { - if (keysEnum.MoveNext()) + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + IEnumerator keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) { - _ = keysEnum.Current; + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + else + { + if (keysEnum.MoveNext()) + { + _ = keysEnum.Current; + } + keysEnum.Reset(); } - keysEnum.Reset(); } } @@ -461,16 +479,19 @@ public void IDictionary_Generic_Values_ContainsAllCorrectValues(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Values_IncludeDuplicatesMultipleTimes(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - int seed = 431; - foreach (KeyValuePair pair in dictionary.ToList()) + if (!IsReadOnly) { - TKey missingKey = CreateTKey(seed++); - while (dictionary.ContainsKey(missingKey)) - missingKey = CreateTKey(seed++); - dictionary.Add(missingKey, pair.Value); + IDictionary dictionary = GenericIDictionaryFactory(count); + int seed = 431; + foreach (KeyValuePair pair in dictionary.ToList()) + { + TKey missingKey = CreateTKey(seed++); + while (dictionary.ContainsKey(missingKey)) + missingKey = CreateTKey(seed++); + dictionary.Add(missingKey, pair.Value); + } + Assert.Equal(count * 2, dictionary.Values.Count); } - Assert.Equal(count * 2, dictionary.Values.Count); } [Theory] @@ -482,14 +503,18 @@ public void IDictionary_Generic_Values_ModifyingTheDictionaryUpdatesTheCollectio int previousCount = values.Count; if (count > 0) Assert.NotEmpty(values); - dictionary.Clear(); - if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(values); - } - else + + if (!IsReadOnly) { - Assert.Equal(previousCount, values.Count); + dictionary.Clear(); + if (IDictionary_Generic_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(values); + } + else + { + Assert.Equal(previousCount, values.Count); + } } } @@ -497,22 +522,25 @@ public void IDictionary_Generic_Values_ModifyingTheDictionaryUpdatesTheCollectio [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_Generic_Values_Enumeration_ParentDictionaryModifiedInvalidates(int count) { - IDictionary dictionary = GenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - IEnumerator valuesEnum = values.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) - { - Assert.Throws(() => valuesEnum.MoveNext()); - Assert.Throws(() => valuesEnum.Reset()); - } - else + if (!IsReadOnly) { - if (valuesEnum.MoveNext()) + IDictionary dictionary = GenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + IEnumerator valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_Generic_Keys_Values_Enumeration_ThrowsInvalidOperation_WhenParentModified) { - _ = valuesEnum.Current; + Assert.Throws(() => valuesEnum.MoveNext()); + Assert.Throws(() => valuesEnum.Reset()); + } + else + { + if (valuesEnum.MoveNext()) + { + _ = valuesEnum.Current; + } + valuesEnum.Reset(); } - valuesEnum.Reset(); } } diff --git a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs index f70229b28db38..578456c0481bb 100644 --- a/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/IDictionary.NonGeneric.Tests.cs @@ -94,6 +94,8 @@ protected object GetNewKey(IDictionary dictionary) /// protected virtual bool IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates => true; + protected virtual bool ExpectedIsFixedSize => false; + #endregion #region ICollection Helper Methods @@ -208,7 +210,7 @@ protected override IEnumerable GetModifyEnumerables(ModifyOper public void IDictionary_NonGeneric_IsFixedSize_Validity(int count) { IDictionary collection = NonGenericIDictionaryFactory(count); - Assert.False(collection.IsFixedSize); + Assert.Equal(ExpectedIsFixedSize, collection.IsFixedSize); } #endregion @@ -286,16 +288,19 @@ public void IDictionary_NonGeneric_ItemGet_PresenobjectReturnsCorrecobject(int c [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_NullKey(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - if (!NullAllowed) - { - Assert.Throws(() => dictionary[null] = CreateTValue(3)); - } - else + if (!IsReadOnly) { - object value = CreateTValue(3452); - dictionary[null] = value; - Assert.Equal(value, dictionary[null]); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + if (!NullAllowed) + { + Assert.Throws(() => dictionary[null] = CreateTValue(3)); + } + else + { + object value = CreateTValue(3452); + dictionary[null] = value; + Assert.Equal(value, dictionary[null]); + } } } @@ -315,23 +320,29 @@ public void IDictionary_NonGeneric_ItemSet_OnReadOnlyDictionary_ThrowsNotSupport [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_AddsNewValueWhenNotPresent(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - object missingKey = GetNewKey(dictionary); - dictionary[missingKey] = CreateTValue(543); - Assert.Equal(count + 1, dictionary.Count); + if (!IsReadOnly) + { + IDictionary dictionary = NonGenericIDictionaryFactory(count); + object missingKey = GetNewKey(dictionary); + dictionary[missingKey] = CreateTValue(543); + Assert.Equal(count + 1, dictionary.Count); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_ItemSet_ReplacesExistingValueWhenPresent(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - object existingKey = GetNewKey(dictionary); - dictionary.Add(existingKey, CreateTValue(5342)); - object newValue = CreateTValue(1234); - dictionary[existingKey] = newValue; - Assert.Equal(count + 1, dictionary.Count); - Assert.Equal(newValue, dictionary[existingKey]); + if (!IsReadOnly) + { + IDictionary dictionary = NonGenericIDictionaryFactory(count); + object existingKey = GetNewKey(dictionary); + dictionary.Add(existingKey, CreateTValue(5342)); + object newValue = CreateTValue(1234); + dictionary[existingKey] = newValue; + Assert.Equal(count + 1, dictionary.Count); + Assert.Equal(newValue, dictionary[existingKey]); + } } #endregion @@ -354,17 +365,20 @@ public void IDictionary_NonGeneric_Keys_ContainsAllCorrectobjects(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Keys_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - int previousCount = keys.Count; - dictionary.Clear(); - if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(keys); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, keys.Count); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + int previousCount = keys.Count; + dictionary.Clear(); + if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(keys); + } + else + { + Assert.Equal(previousCount, keys.Count); + } } } @@ -372,23 +386,26 @@ public void IDictionary_NonGeneric_Keys_ModifyingTheDictionaryUpdatesTheCollecti [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Keys_Enumeration_ParentDictionaryModifiedInvalidatesEnumerator(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection keys = dictionary.Keys; - IEnumerator keysEnum = keys.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) - { - Assert.Throws(() => keysEnum.MoveNext()); - Assert.Throws(() => keysEnum.Reset()); - } - else + if (!IsReadOnly) { - keysEnum.MoveNext(); - if (count > 0) + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection keys = dictionary.Keys; + IEnumerator keysEnum = keys.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) { - var cur = keysEnum.Current; + Assert.Throws(() => keysEnum.MoveNext()); + Assert.Throws(() => keysEnum.Reset()); + } + else + { + keysEnum.MoveNext(); + if (count > 0) + { + var cur = keysEnum.Current; + } + keysEnum.Reset(); } - keysEnum.Reset(); } } @@ -423,34 +440,40 @@ public void IDictionary_NonGeneric_Values_ContainsAllCorrecobjects(int count) [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Values_IncludeDuplicatesMultipleTimes(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - List entries = new List(); - - foreach (DictionaryEntry pair in dictionary) - entries.Add(pair); - foreach (DictionaryEntry pair in entries) + if (!IsReadOnly) { - object missingKey = GetNewKey(dictionary); - dictionary.Add(missingKey, (pair.Value)); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + List entries = new List(); + + foreach (DictionaryEntry pair in dictionary) + entries.Add(pair); + foreach (DictionaryEntry pair in entries) + { + object missingKey = GetNewKey(dictionary); + dictionary.Add(missingKey, (pair.Value)); + } + Assert.Equal(count * 2, dictionary.Values.Count); } - Assert.Equal(count * 2, dictionary.Values.Count); } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void IDictionary_NonGeneric_Values_ModifyingTheDictionaryUpdatesTheCollection(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - int previousCount = values.Count; - dictionary.Clear(); - if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) - { - Assert.Empty(values); - } - else + if (!IsReadOnly) { - Assert.Equal(previousCount, values.Count); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + int previousCount = values.Count; + dictionary.Clear(); + if (IDictionary_NonGeneric_Keys_Values_ModifyingTheDictionaryUpdatesTheCollection) + { + Assert.Empty(values); + } + else + { + Assert.Equal(previousCount, values.Count); + } } } @@ -458,24 +481,27 @@ public void IDictionary_NonGeneric_Values_ModifyingTheDictionaryUpdatesTheCollec [MemberData(nameof(ValidCollectionSizes))] public virtual void IDictionary_NonGeneric_Values_Enumeration_ParentDictionaryModifiedInvalidatesEnumerator(int count) { - IDictionary dictionary = NonGenericIDictionaryFactory(count); - ICollection values = dictionary.Values; - IEnumerator valuesEnum = values.GetEnumerator(); - dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); - if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) - { - Assert.Throws(() => valuesEnum.MoveNext()); - Assert.Throws(() => valuesEnum.Reset()); - Assert.Throws(() => valuesEnum.Current); - } - else + if (!IsReadOnly) { - valuesEnum.MoveNext(); - if (count > 0) + IDictionary dictionary = NonGenericIDictionaryFactory(count); + ICollection values = dictionary.Values; + IEnumerator valuesEnum = values.GetEnumerator(); + dictionary.Add(GetNewKey(dictionary), CreateTValue(3432)); + if (IDictionary_NonGeneric_Keys_Values_ParentDictionaryModifiedInvalidates) + { + Assert.Throws(() => valuesEnum.MoveNext()); + Assert.Throws(() => valuesEnum.Reset()); + Assert.Throws(() => valuesEnum.Current); + } + else { - var cur = valuesEnum.Current; + valuesEnum.MoveNext(); + if (count > 0) + { + var cur = valuesEnum.Current; + } + valuesEnum.Reset(); } - valuesEnum.Reset(); } } @@ -595,16 +621,34 @@ public void IDictionary_NonGeneric_Add_DuplicateKey(int count) public void IDictionary_NonGeneric_Remove_NullKey(int count) { IDictionary dictionary = NonGenericIDictionaryFactory(count); - if (!NullAllowed) + if (!IsReadOnly) { - Assert.Throws(() => dictionary.Remove(null)); + if (!NullAllowed) + { + Assert.Throws(() => dictionary.Remove(null)); + } + else + { + object value = CreateTValue(3452); + dictionary.Add(null, value); + dictionary.Remove(null); + Assert.Null(dictionary[null]); + } } - else + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IDictionary_NonGeneric_Remove_ThrowsWhenNotSupported(int count) + { + if (IsReadOnly) { - object value = CreateTValue(3452); - dictionary.Add(null, value); - dictionary.Remove(null); - Assert.Null(dictionary[null]); + IDictionary dictionary = NonGenericIDictionaryFactory(count); + foreach (DictionaryEntry entry in dictionary) + { + Assert.Throws(() => dictionary.Remove(entry.Key)); + break; + } } } diff --git a/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs b/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs index 1cc12e98dd405..9c0b36df38eeb 100644 --- a/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs +++ b/src/libraries/Common/tests/System/Collections/ISet.Generic.Tests.cs @@ -302,34 +302,51 @@ private void Validate_UnionWith(ISet set, IEnumerable enumerable) public void ISet_Generic_NullEnumerableArgument(int count) { ISet set = GenericISetFactory(count); - Assert.Throws(() => set.ExceptWith(null)); - Assert.Throws(() => set.IntersectWith(null)); Assert.Throws(() => set.IsProperSubsetOf(null)); Assert.Throws(() => set.IsProperSupersetOf(null)); Assert.Throws(() => set.IsSubsetOf(null)); Assert.Throws(() => set.IsSupersetOf(null)); Assert.Throws(() => set.Overlaps(null)); Assert.Throws(() => set.SetEquals(null)); - Assert.Throws(() => set.SymmetricExceptWith(null)); - Assert.Throws(() => set.UnionWith(null)); + if (!IsReadOnly) + { + Assert.Throws(() => set.ExceptWith(null)); + Assert.Throws(() => set.IntersectWith(null)); + Assert.Throws(() => set.SymmetricExceptWith(null)); + Assert.Throws(() => set.UnionWith(null)); + } + else + { + Assert.Throws(() => set.Add(CreateT(0))); + Assert.Throws(() => set.ExceptWith(null)); + Assert.Throws(() => set.IntersectWith(null)); + Assert.Throws(() => set.SymmetricExceptWith(null)); + Assert.Throws(() => set.UnionWith(null)); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_ExceptWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_ExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_ExceptWith(set, enumerable); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_IntersectWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_IntersectWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_IntersectWith(set, enumerable); + } } [Theory] @@ -390,18 +407,24 @@ public void ISet_Generic_SetEquals(EnumerableType enumerableType, int setLength, [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_SymmetricExceptWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_SymmetricExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_SymmetricExceptWith(set, enumerable); + } } [Theory] [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_UnionWith(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Validate_UnionWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Validate_UnionWith(set, enumerable); + } } #endregion @@ -412,8 +435,11 @@ public void ISet_Generic_UnionWith(EnumerableType enumerableType, int setLength, [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_ExceptWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_ExceptWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_ExceptWith(set, set); + } } [Theory] @@ -421,8 +447,11 @@ public void ISet_Generic_ExceptWith_Itself(int setLength) [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework throws InvalidOperationException")] public void ISet_Generic_IntersectWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_IntersectWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_IntersectWith(set, set); + } } [Theory] @@ -477,16 +506,22 @@ public void ISet_Generic_SetEquals_Itself(int setLength) [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_SymmetricExceptWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_SymmetricExceptWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_SymmetricExceptWith(set, set); + } } [Theory] [MemberData(nameof(ValidCollectionSizes))] public void ISet_Generic_UnionWith_Itself(int setLength) { - ISet set = GenericISetFactory(setLength); - Validate_UnionWith(set, set); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + Validate_UnionWith(set, set); + } } #endregion @@ -497,18 +532,24 @@ public void ISet_Generic_UnionWith_Itself(int setLength) [OuterLoop] public void ISet_Generic_ExceptWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_ExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_ExceptWith(set, enumerable); + } } [Fact] [OuterLoop] public void ISet_Generic_IntersectWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_IntersectWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_IntersectWith(set, enumerable); + } } [Fact] @@ -569,18 +610,24 @@ public void ISet_Generic_SetEquals_LargeSet() [OuterLoop] public void ISet_Generic_SymmetricExceptWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_SymmetricExceptWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_SymmetricExceptWith(set, enumerable); + } } [Fact] [OuterLoop] public void ISet_Generic_UnionWith_LargeSet() { - ISet set = GenericISetFactory(ISet_Large_Capacity); - IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); - Validate_UnionWith(set, enumerable); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(ISet_Large_Capacity); + IEnumerable enumerable = CreateEnumerable(EnumerableType.List, set, 150, 0, 0); + Validate_UnionWith(set, enumerable); + } } #endregion @@ -591,25 +638,28 @@ public void ISet_Generic_UnionWith_LargeSet() [MemberData(nameof(EnumerableTestData))] public void ISet_Generic_SymmetricExceptWith_AfterRemovingElements(EnumerableType enumerableType, int setLength, int enumerableLength, int numberOfMatchingElements, int numberOfDuplicateElements) { - ISet set = GenericISetFactory(setLength); - T value = CreateT(532); - if (!set.Contains(value)) - set.Add(value); - set.Remove(value); - IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); - Debug.Assert(enumerable != null); + if (!IsReadOnly) + { + ISet set = GenericISetFactory(setLength); + T value = CreateT(532); + if (!set.Contains(value)) + set.Add(value); + set.Remove(value); + IEnumerable enumerable = CreateEnumerable(enumerableType, set, enumerableLength, numberOfMatchingElements, numberOfDuplicateElements); + Debug.Assert(enumerable != null); - IEqualityComparer comparer = GetIEqualityComparer(); - HashSet expected = new HashSet(comparer); - foreach (T element in enumerable) - if (!set.Contains(element, comparer)) - expected.Add(element); - foreach (T element in set) - if (!enumerable.Contains(element, comparer)) - expected.Add(element); - set.SymmetricExceptWith(enumerable); - Assert.Equal(expected.Count, set.Count); - Assert.True(expected.SetEquals(set)); + IEqualityComparer comparer = GetIEqualityComparer(); + HashSet expected = new HashSet(comparer); + foreach (T element in enumerable) + if (!set.Contains(element, comparer)) + expected.Add(element); + foreach (T element in set) + if (!enumerable.Contains(element, comparer)) + expected.Add(element); + set.SymmetricExceptWith(enumerable); + Assert.Equal(expected.Count, set.Count); + Assert.True(expected.SetEquals(set)); + } } #endregion diff --git a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs index 78a1680518a52..add568dd56272 100644 --- a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs +++ b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.cs @@ -4,6 +4,119 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +namespace System.Collections.Frozen +{ + public static partial class FrozenDictionary + { + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable> source, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } + public static System.Collections.Frozen.FrozenDictionary ToFrozenDictionary(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Collections.Generic.IEqualityComparer? comparer = null) where TKey : notnull { throw null; } + } + public abstract partial class FrozenDictionary : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.ICollection, System.Collections.IDictionary, System.Collections.IEnumerable where TKey : notnull + { + internal FrozenDictionary() { } + public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } + public int Count { get { throw null; } } + public static System.Collections.Frozen.FrozenDictionary Empty { get { throw null; } } + public ref readonly TValue this[TKey key] { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Keys { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + TValue System.Collections.Generic.IDictionary.this[TKey key] { get { throw null; } set { } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + TValue System.Collections.Generic.IReadOnlyDictionary.this[TKey key] { get { throw null; } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + bool System.Collections.IDictionary.IsFixedSize { get { throw null; } } + bool System.Collections.IDictionary.IsReadOnly { get { throw null; } } + object? System.Collections.IDictionary.this[object key] { get { throw null; } set { } } + System.Collections.ICollection System.Collections.IDictionary.Keys { get { throw null; } } + System.Collections.ICollection System.Collections.IDictionary.Values { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Values { get { throw null; } } + public bool ContainsKey(TKey key) { throw null; } + public void CopyTo(System.Collections.Generic.KeyValuePair[] destination, int destinationIndex) { } + public void CopyTo(System.Span> destination) { } + public System.Collections.Frozen.FrozenDictionary.Enumerator GetEnumerator() { throw null; } + public ref readonly TValue GetValueRefOrNullRef(TKey key) { throw null; } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.ICollection>.Clear() { } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.IDictionary.Add(TKey key, TValue value) { } + bool System.Collections.Generic.IDictionary.Remove(TKey key) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + void System.Collections.IDictionary.Add(object key, object? value) { } + void System.Collections.IDictionary.Clear() { } + bool System.Collections.IDictionary.Contains(object key) { throw null; } + System.Collections.IDictionaryEnumerator System.Collections.IDictionary.GetEnumerator() { throw null; } + void System.Collections.IDictionary.Remove(object key) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TValue value) { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator, System.IDisposable + { + private readonly TKey[] _keys; + private readonly TValue[] _values; + private object _dummy; + private int _dummyPrimitive; + public readonly System.Collections.Generic.KeyValuePair Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } + } + public static partial class FrozenSet + { + public static System.Collections.Frozen.FrozenSet ToFrozenSet(this System.Collections.Generic.IEnumerable source, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + } + public abstract partial class FrozenSet : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.ISet, System.Collections.ICollection, System.Collections.IEnumerable + { + internal FrozenSet() { } + public System.Collections.Generic.IEqualityComparer Comparer { get { throw null; } } + public int Count { get { throw null; } } + public static System.Collections.Frozen.FrozenSet Empty { get { throw null; } } + public System.Collections.Immutable.ImmutableArray Items { get { throw null; } } + bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + public bool Contains(T item) { throw null; } + public void CopyTo(System.Span destination) { } + public void CopyTo(T[] destination, int destinationIndex) { } + public System.Collections.Frozen.FrozenSet.Enumerator GetEnumerator() { throw null; } + public bool IsProperSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } + public bool IsProperSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } + public bool IsSubsetOf(System.Collections.Generic.IEnumerable other) { throw null; } + public bool IsSupersetOf(System.Collections.Generic.IEnumerable other) { throw null; } + public bool Overlaps(System.Collections.Generic.IEnumerable other) { throw null; } + public bool SetEquals(System.Collections.Generic.IEnumerable other) { throw null; } + void System.Collections.Generic.ICollection.Add(T item) { } + void System.Collections.Generic.ICollection.Clear() { } + bool System.Collections.Generic.ICollection.Remove(T item) { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + bool System.Collections.Generic.ISet.Add(T item) { throw null; } + void System.Collections.Generic.ISet.ExceptWith(System.Collections.Generic.IEnumerable other) { } + void System.Collections.Generic.ISet.IntersectWith(System.Collections.Generic.IEnumerable other) { } + void System.Collections.Generic.ISet.SymmetricExceptWith(System.Collections.Generic.IEnumerable other) { } + void System.Collections.Generic.ISet.UnionWith(System.Collections.Generic.IEnumerable other) { } + void System.Collections.ICollection.CopyTo(System.Array array, int index) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(T equalValue, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out T actualValue) { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private readonly T[] _entries; + private object _dummy; + private int _dummyPrimitive; + public readonly T Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + void System.IDisposable.Dispose() { } + } + } +} namespace System.Collections.Immutable { public partial interface IImmutableDictionary : System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable diff --git a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs index d6c786d33e236..c419ecd91f218 100644 --- a/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs +++ b/src/libraries/System.Collections.Immutable/ref/System.Collections.Immutable.netcoreapp.cs @@ -4,6 +4,12 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +namespace System.Collections.Frozen +{ + public abstract partial class FrozenSet : System.Collections.Generic.IReadOnlySet + { + } +} namespace System.Collections.Immutable { public readonly partial struct ImmutableArray : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList, System.Collections.Immutable.IImmutableList, System.Collections.IStructuralComparable, System.Collections.IStructuralEquatable, System.IEquatable> diff --git a/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx b/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx index c92a6d097a100..8f13478c75da2 100644 --- a/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx +++ b/src/libraries/System.Collections.Immutable/src/Resources/Strings.resx @@ -87,4 +87,22 @@ This operation cannot be performed on a default instance of ImmutableArray<T>. Consider initializing the array, or checking the ImmutableArray<T>.IsDefault property. - \ No newline at end of file + + Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table. + + + Only single dimensional arrays are supported for the requested action. + + + The lower bound of target array must be zero. + + + Destination array is not long enough to copy all the items in the collection. Check array index and length. + + + Target array type is not compatible with the type of items in the collection. + + + Non-negative number required. + + diff --git a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj index 0017ce6e624c2..c21fe6b637635 100644 --- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) true @@ -6,12 +6,52 @@ The System.Collections.Immutable library is built-in as part of the shared framework in .NET Runtime. The package can be installed when you need to use it in other target frameworks. README.md + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -85,11 +125,9 @@ The System.Collections.Immutable library is built-in as part of the shared frame - - + - + diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs new file mode 100644 index 0000000000000..cc4fd2dad7323 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenDictionary.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides the default implementation to use when no other special-cases apply. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + internal sealed class DefaultFrozenDictionary : KeysAndValuesFrozenDictionary, IDictionary + where TKey : notnull + { + internal DefaultFrozenDictionary(Dictionary source, IEqualityComparer comparer) : + base(source, comparer) + { + } + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + IEqualityComparer comparer = Comparer; + + int hashCode = comparer.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (comparer.Equals(key, _keys[index])) + { + return ref _values[index]; + } + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs new file mode 100644 index 0000000000000..062573424eccd --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/DefaultFrozenSet.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Collections.Frozen +{ + /// Provides the default implementation to use when no other special-cases apply. + /// The type of the values in the set. + internal sealed class DefaultFrozenSet : ItemsFrozenSet.GSW> + { + internal DefaultFrozenSet(HashSet source, IEqualityComparer comparer) : + base(source, comparer) + { + } + + /// + private protected override int FindItemIndex(T item) + { + IEqualityComparer comparer = Comparer; + + int hashCode = item is null ? 0 : comparer.GetHashCode(item); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (comparer.Equals(item, _items[index])) + { + return index; + } + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private DefaultFrozenSet _set; + public void Store(FrozenSet set) => _set = (DefaultFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(T item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs new file mode 100644 index 0000000000000..aeca02be4c10e --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenDictionary.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides an empty to use when there are zero key/value pairs to be stored. + internal sealed class EmptyFrozenDictionary : FrozenDictionary + where TKey : notnull + { + internal EmptyFrozenDictionary() : base(EqualityComparer.Default) { } + + /// + private protected override ImmutableArray KeysCore => ImmutableArray.Empty; + + /// + private protected override ImmutableArray ValuesCore => ImmutableArray.Empty; + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(Array.Empty(), Array.Empty()); + + /// + private protected override int CountCore => 0; + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) => ref Unsafe.NullRef(); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs new file mode 100644 index 0000000000000..900e2d391eb7f --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/EmptyFrozenSet.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace System.Collections.Frozen +{ + /// Provides an empty to use when there are zero values to be stored. + internal sealed class EmptyFrozenSet : FrozenSet + { + internal EmptyFrozenSet() : base(EqualityComparer.Default) { } + + /// + private protected override ImmutableArray ItemsCore => ImmutableArray.Empty; + + /// + private protected override int CountCore => 0; + + /// + private protected override int FindItemIndex(T item) => -1; + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(Array.Empty()); + + /// + private protected override bool IsProperSubsetOfCore(IEnumerable other) => !OtherIsEmpty(other); + + /// + private protected override bool IsProperSupersetOfCore(IEnumerable other) => false; + + /// + private protected override bool IsSubsetOfCore(IEnumerable other) => true; + + /// + private protected override bool IsSupersetOfCore(IEnumerable other) => OtherIsEmpty(other); + + /// + private protected override bool OverlapsCore(IEnumerable other) => false; + + /// + private protected override bool SetEqualsCore(IEnumerable other) => OtherIsEmpty(other); + + private static bool OtherIsEmpty(IEnumerable other) => + other is IReadOnlyCollection s ? s.Count == 0 : // TODO https://github.com/dotnet/runtime/issues/42254: Remove if/when Any includes this check + !other.Any(); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs new file mode 100644 index 0000000000000..2ea2d8c8b4caf --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs @@ -0,0 +1,515 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// + /// Provides a set of initialization methods for instances of the class. + /// + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + public static class FrozenDictionary + { + /// Creates a with the specified key/value pairs. + /// The key/value pairs to use to populate the dictionary. + /// The comparer implementation to use to compare keys for equality. If null, is used. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// + /// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. This differs from , + /// with which multiple duplicate keys will result in an exception. + /// + /// A that contains the specified keys and values. + public static FrozenDictionary ToFrozenDictionary(this IEnumerable> source, IEqualityComparer? comparer = null) + where TKey : notnull + { + ThrowHelper.ThrowIfNull(source); + comparer ??= EqualityComparer.Default; + + // If the source is already frozen with the same comparer, it can simply be returned. + if (source is FrozenDictionary existing && + existing.Comparer.Equals(comparer)) + { + return existing; + } + + // Ensure we have a Dictionary<,> using the specified comparer such that all keys + // are non-null and unique according to that comparer. + if (source is not Dictionary uniqueValues || + (uniqueValues.Count != 0 && !uniqueValues.Comparer.Equals(comparer))) + { + uniqueValues = new Dictionary(comparer); + foreach (KeyValuePair pair in source) + { + uniqueValues[pair.Key] = pair.Value; + } + } + + return Freeze(uniqueValues); + } + + /// Creates a from an according to specified key selector function. + /// The type of the elements of . + /// The type of the key returned by . + /// An from which to create a . + /// A function to extract a key from each element. + /// An to compare keys. + /// A that contains the keys and values selected from the input sequence. + public static FrozenDictionary ToFrozenDictionary( + this IEnumerable source, Func keySelector, IEqualityComparer? comparer = null) + where TKey : notnull => + Freeze(source.ToDictionary(keySelector, comparer)); + + /// Creates a from an according to specified key selector and element selector functions. + /// The type of the elements of . + /// The type of the key returned by . + /// The type of the value returned by . + /// An from which to create a . + /// A function to extract a key from each element. + /// A transform function to produce a result element value from each element. + /// An to compare keys. + /// A that contains the keys and values selected from the input sequence. + public static FrozenDictionary ToFrozenDictionary( + this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer? comparer = null) + where TKey : notnull => + Freeze(source.ToDictionary(keySelector, elementSelector, comparer)); + + private static FrozenDictionary Freeze(Dictionary source) + where TKey : notnull + { + // If the input was empty, simply return the empty frozen dictionary singleton. The comparer is ignored. + if (source.Count == 0) + { + return FrozenDictionary.Empty; + } + + IEqualityComparer comparer = source.Comparer; + + if (typeof(TKey).IsValueType) + { + // Optimize for value types when the default comparer is being used. In such a case, the implementation + // may use EqualityComparer.Default.Equals/GetHashCode directly, with generic specialization enabling + // the Equals/GetHashCode methods to be devirtualized and possibly inlined. + if (ReferenceEquals(comparer, EqualityComparer.Default)) + { + // In the specific case of Int32 keys, we can optimize further to reduce memory consumption by using + // the underlying FrozenHashtable's Int32 index as the keys themselves, avoiding the need to store the + // same keys yet again. + return typeof(TKey) == typeof(int) ? + (FrozenDictionary)(object)new Int32FrozenDictionary((Dictionary)(object)source) : + new ValueTypeDefaultComparerFrozenDictionary(source); + } + } + else if (typeof(TKey) == typeof(string)) + { + // If the key is a string and the comparer is known to provide ordinal (case-sensitive or case-insensitive) semantics, + // we can use an implementation that's able to examine and optimize based on lengths and/or subsequences within those strings. + if (ReferenceEquals(comparer, EqualityComparer.Default) || + ReferenceEquals(comparer, StringComparer.Ordinal) || + ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + { + Dictionary stringEntries = (Dictionary)(object)source; + IEqualityComparer stringComparer = (IEqualityComparer)(object)comparer; + + FrozenDictionary frozenDictionary = + LengthBucketsFrozenDictionary.TryCreateLengthBucketsFrozenSet(stringEntries, stringComparer) ?? + (FrozenDictionary)new OrdinalStringFrozenDictionary(stringEntries, stringComparer); + + return (FrozenDictionary)(object)frozenDictionary; + } + } + + // No special-cases apply. Use the default frozen dictionary. + return new DefaultFrozenDictionary(source, comparer); + } + } + + /// Provides an immutable, read-only dictionary optimized for fast lookup and enumeration. + /// The type of the keys in the dictionary. + /// The type of the values in this dictionary. + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + [DebuggerTypeProxy(typeof(ImmutableDictionaryDebuggerProxy<,>))] + [DebuggerDisplay("Count = {Count}")] + public abstract class FrozenDictionary : IDictionary, IReadOnlyDictionary, IDictionary + where TKey : notnull + { + /// Initialize the dictionary. + /// The comparer to use and to expose from . + private protected FrozenDictionary(IEqualityComparer comparer) => Comparer = comparer; + + /// Gets an empty . + public static FrozenDictionary Empty { get; } = new EmptyFrozenDictionary(); + + /// Gets the comparer used by this dictionary. + public IEqualityComparer Comparer { get; } + + /// + /// Gets a collection containing the keys in the dictionary. + /// + /// + /// The order of the keys in the dictionary is unspecified, but it is the same order as the associated values returned by the property. + /// + public ImmutableArray Keys => KeysCore; + + /// + private protected abstract ImmutableArray KeysCore { get; } + + /// + ICollection IDictionary.Keys => + Keys is { Length: > 0 } keys ? keys : Array.Empty(); + + /// + IEnumerable IReadOnlyDictionary.Keys => + ((IDictionary)this).Keys; + + /// + ICollection IDictionary.Keys => Keys; + + /// + /// Gets a collection containing the values in the dictionary. + /// + /// + /// The order of the values in the dictionary is unspecified, but it is the same order as the associated keys returned by the property. + /// + public ImmutableArray Values => ValuesCore; + + /// + private protected abstract ImmutableArray ValuesCore { get; } + + ICollection IDictionary.Values => + Values is { Length: > 0 } values ? values : Array.Empty(); + + /// + ICollection IDictionary.Values => Values; + + /// + IEnumerable IReadOnlyDictionary.Values => + ((IDictionary)this).Values; + + /// Gets the number of key/value pairs contained in the dictionary. + public int Count => CountCore; + + /// + private protected abstract int CountCore { get; } + + /// Copies the elements of the dictionary to an array of type , starting at the specified . + /// The array that is the destination of the elements copied from the dictionary. + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] destination, int destinationIndex) + { + ThrowHelper.ThrowIfNull(destination); + CopyTo(destination.AsSpan(destinationIndex)); + } + + /// Copies the elements of the dictionary to a span of type . + /// The span that is the destination of the elements copied from the dictionary. + public void CopyTo(Span> destination) + { + if (destination.Length < Count) + { + ThrowHelper.ThrowIfDestinationTooSmall(); + } + + TKey[] keys = Keys.array!; + TValue[] values = Values.array!; + Debug.Assert(keys.Length == values.Length); + + for (int i = 0; i < keys.Length; i++) + { + destination[i] = new KeyValuePair(keys[i], values[i]); + } + } + + /// + void ICollection.CopyTo(Array array, int index) + { + ThrowHelper.ThrowIfNull(array); + + if (array.Rank != 1) + { + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); + } + + if ((uint)index > (uint)array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_NeedNonNegNum); + } + + if (array.Length - index < Count) + { + throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall, nameof(array)); + } + + if (array is KeyValuePair[] pairs) + { + foreach (KeyValuePair item in this) + { + pairs[index++] = new KeyValuePair(item.Key, item.Value); + } + } + else if (array is DictionaryEntry[] dictEntryArray) + { + foreach (KeyValuePair item in this) + { + dictEntryArray[index++] = new DictionaryEntry(item.Key, item.Value); + } + } + else + { + if (array is not object[] objects) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + + try + { + foreach (KeyValuePair item in this) + { + objects[index++] = new KeyValuePair(item.Key, item.Value); + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + } + } + + /// + bool ICollection>.IsReadOnly => true; + + /// + bool IDictionary.IsReadOnly => true; + + /// + bool IDictionary.IsFixedSize => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => this; + + /// + object? IDictionary.this[object key] + { + get + { + ThrowHelper.ThrowIfNull(key); + return key is TKey tkey && TryGetValue(tkey, out TValue? value) ? + value : + (object?)null; + } + set => throw new NotSupportedException(); + } + + /// Gets either a reference to a in the dictionary or a null reference if the key does not exist in the dictionary. + /// The key used for lookup. + /// A reference to a in the dictionary or a null reference if the key does not exist in the dictionary. + /// The null reference can be detected by calling . + public ref readonly TValue GetValueRefOrNullRef(TKey key) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); + } + + return ref GetValueRefOrNullRefCore(key); + } + + /// + private protected abstract ref readonly TValue GetValueRefOrNullRefCore(TKey key); + + /// Gets a reference to the value associated with the specified key. + /// The key of the value to get. + /// A reference to the value associated with the specified key. + /// does not exist in the collection. + public ref readonly TValue this[TKey key] + { + get + { + ref readonly TValue valueRef = ref GetValueRefOrNullRef(key); + + if (Unsafe.IsNullRef(ref Unsafe.AsRef(in valueRef))) + { + ThrowHelper.ThrowKeyNotFoundException(); + } + + return ref valueRef; + } + } + + /// + TValue IDictionary.this[TKey key] + { + get => this[key]; + set => throw new NotSupportedException(); + } + + /// + TValue IReadOnlyDictionary.this[TKey key] => + this[key]; + + /// Determines whether the dictionary contains the specified key. + /// The key to locate in the dictionary. + /// if the dictionary contains an element with the specified key; otherwise, . + public bool ContainsKey(TKey key) => + !Unsafe.IsNullRef(ref Unsafe.AsRef(in GetValueRefOrNullRef(key))); + + /// + bool IDictionary.Contains(object key) + { + ThrowHelper.ThrowIfNull(key); + return key is TKey tkey && ContainsKey(tkey); + } + + /// + bool ICollection>.Contains(KeyValuePair item) => + TryGetValue(item.Key, out TValue? value) && + EqualityComparer.Default.Equals(value, item.Value); + + /// Gets the value associated with the specified key. + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the value parameter. + /// + /// if the dictionary contains an element with the specified key; otherwise, . + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + ref readonly TValue valueRef = ref GetValueRefOrNullRef(key); + + if (!Unsafe.IsNullRef(ref Unsafe.AsRef(in valueRef))) + { + value = valueRef; + return true; + } + + value = default; + return false; + } + + /// Returns an enumerator that iterates through the dictionary. + /// An enumerator that iterates through the dictionary. + public Enumerator GetEnumerator() => GetEnumeratorCore(); + + /// + private protected abstract Enumerator GetEnumeratorCore(); + + /// + IEnumerator> IEnumerable>.GetEnumerator() => + Count == 0 ? ((IList>)Array.Empty>()).GetEnumerator() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? Array.Empty>().GetEnumerator() : + GetEnumerator(); + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() => + new DictionaryEnumerator(GetEnumerator()); + + /// + void IDictionary.Add(TKey key, TValue value) => throw new NotSupportedException(); + + /// + void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); + + /// + void IDictionary.Add(object key, object? value) => throw new NotSupportedException(); + + /// + bool IDictionary.Remove(TKey key) => throw new NotSupportedException(); + + /// + bool ICollection>.Remove(KeyValuePair item) => throw new NotSupportedException(); + + /// + void IDictionary.Remove(object key) => throw new NotSupportedException(); + + /// + void ICollection>.Clear() => throw new NotSupportedException(); + + /// + void IDictionary.Clear() => throw new NotSupportedException(); + + /// Enumerates the elements of a . + public struct Enumerator : IEnumerator> + { + private readonly TKey[] _keys; + private readonly TValue[] _values; + private int _index; + + /// Initialize the enumerator with the specified keys and values. + internal Enumerator(TKey[] keys, TValue[] values) + { + Debug.Assert(keys.Length == values.Length); + _keys = keys; + _values = values; + _index = -1; + } + + /// + public bool MoveNext() + { + _index++; + if ((uint)_index < (uint)_keys.Length) + { + return true; + } + + _index = _keys.Length; + return false; + } + + /// + public readonly KeyValuePair Current + { + get + { + if ((uint)_index >= (uint)_keys.Length) + { + ThrowHelper.ThrowInvalidOperationException(); + } + + return new KeyValuePair(_keys[_index], _values[_index]); + } + } + + /// + object IEnumerator.Current => Current; + + /// + void IEnumerator.Reset() => _index = -1; + + /// + void IDisposable.Dispose() { } + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs new file mode 100644 index 0000000000000..8ae7d526e1860 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides the core hash table for use in frozen collections. + /// + /// This hash table doesn't track any of the collection state. It merely keeps track + /// of hash codes and of mapping these hash codes to spans of entries within the collection. + /// + internal readonly struct FrozenHashTable + { + private readonly Bucket[] _buckets; + private readonly ulong _fastModMultiplier; + + private FrozenHashTable(int[] hashCodes, Bucket[] buckets, ulong fastModMultiplier) + { + Debug.Assert(hashCodes.Length != 0); + Debug.Assert(buckets.Length != 0); + + HashCodes = hashCodes; + _buckets = buckets; + _fastModMultiplier = fastModMultiplier; + } + + /// Initializes a frozen hash table. + /// The set of entries to track from the hash table. + /// A delegate that produces a hash code for a given entry. + /// A delegate that assigns the index to a specific entry. + /// The type of elements in the hash table. + /// + /// This method will iterate through the incoming entries and will invoke the hasher on each once. + /// It will then determine the optimal number of hash buckets to allocate and will populate the + /// bucket table. In the process of doing so, it calls out to the to indicate + /// the resulting index for that entry. + /// then uses this index to reference individual entries by indexing into . + /// + /// A frozen hash table. + public static FrozenHashTable Create(T[] entries, Func hasher, Action setter) + { + Debug.Assert(entries.Length != 0); + + int[] hashCodes = new int[entries.Length]; + for (int i = 0; i < entries.Length; i++) + { + hashCodes[i] = hasher(entries[i]); + } + + int numBuckets = CalcNumBuckets(hashCodes); + ulong fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)numBuckets); + + var chainBuddies = new Dictionary>(); + for (int index = 0; index < hashCodes.Length; index++) + { + int hashCode = hashCodes[index]; + uint bucket = HashHelpers.FastMod((uint)hashCode, (uint)numBuckets, fastModMultiplier); + + if (!chainBuddies.TryGetValue(bucket, out List? list)) + { + chainBuddies[bucket] = list = new List(); + } + + list.Add(new ChainBuddy(hashCode, index)); + } + + var buckets = new Bucket[numBuckets]; + + int count = 0; + foreach (List list in chainBuddies.Values) + { + uint bucket = HashHelpers.FastMod((uint)list[0].HashCode, (uint)buckets.Length, fastModMultiplier); + + buckets[bucket] = new Bucket(count, list.Count); + for (int i = 0; i < list.Count; i++) + { + hashCodes[count] = list[i].HashCode; + setter(count, entries[list[i].Index]); + count++; + } + } + + return new FrozenHashTable(hashCodes, buckets, fastModMultiplier); + } + + /// + /// Given a hash code, return the first index and last index for the associated matching entries. + /// + /// The hash code to probe for. + /// A variable that receives the index of the first matching entry. + /// A variable that receives the index of the last matching entry plus 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void FindMatchingEntries(int hashCode, out int startIndex, out int endIndex) + { + ref Bucket b = ref _buckets[HashHelpers.FastMod((uint)hashCode, (uint)_buckets.Length, _fastModMultiplier)]; + startIndex = b.StartIndex; + endIndex = b.EndIndex; + } + + public int Count => HashCodes.Length; + + internal int[] HashCodes { get; } + + /// + /// Given an array of hash codes, figure out the best number of hash buckets to use. + /// + /// + /// This tries to select a prime number of buckets. Rather than iterating through all possible bucket + /// sizes, starting at the exact number of hash codes and incrementing the bucket count by 1 per trial, + /// this is a trade-off between speed of determining a good number of buckets and maximumal density. + /// + private static int CalcNumBuckets(int[] hashCodes) + { + const double AcceptableCollisionRate = 0.05; // What is a satifactory rate of hash collisions? + const int LargeInputSizeThreshold = 1000; // What is the limit for an input to be considered "small"? + const int MaxSmallBucketTableMultiplier = 16; // How large a bucket table should be allowed for small inputs? + const int MaxLargeBucketTableMultiplier = 3; // How large a bucket table should be allowed for large inputs? + + // Filter out duplicate codes, since no increase in buckets will avoid collisions from duplicate input hash codes. + var codes = new HashSet(hashCodes); + Debug.Assert(codes.Count != 0); + + // In our precomputed primes table, find the index of the smallest prime that's at least as large as our number of + // hash codes. If there are more codes than in our precomputed primes table, which accomodates millions of values, + // give up and just use the next prime. + int minPrimeIndexInclusive = 0; + while (minPrimeIndexInclusive < HashHelpers.s_primes.Length && codes.Count > HashHelpers.s_primes[minPrimeIndexInclusive]) + { + minPrimeIndexInclusive++; + } + if (minPrimeIndexInclusive >= HashHelpers.s_primes.Length) + { + return HashHelpers.GetPrime(codes.Count); + } + + // Determine the largest number of buckets we're willing to use, based on a multiple of the number of inputs. + // For smaller inputs, we allow for a larger multiplier. + int maxNumBuckets = + codes.Count * + (codes.Count >= LargeInputSizeThreshold ? MaxLargeBucketTableMultiplier : MaxSmallBucketTableMultiplier); + + // Find the index of the smallest prime that accomodates our max buckets. + int maxPrimeIndexExclusive = minPrimeIndexInclusive; + while (maxPrimeIndexExclusive < HashHelpers.s_primes.Length && maxNumBuckets > HashHelpers.s_primes[maxPrimeIndexExclusive]) + { + maxPrimeIndexExclusive++; + } + if (maxPrimeIndexExclusive < HashHelpers.s_primes.Length) + { + Debug.Assert(maxPrimeIndexExclusive != 0); + maxNumBuckets = HashHelpers.s_primes[maxPrimeIndexExclusive - 1]; + } + + const int BitsPerInt32 = 32; + int[] seenBuckets = ArrayPool.Shared.Rent((maxNumBuckets / BitsPerInt32) + 1); + + int bestNumBuckets = maxNumBuckets; + int bestNumCollisions = codes.Count; + + // Iterate through each available prime between the min and max discovered. For each, compute + // the collision ratio. + for (int primeIndex = minPrimeIndexInclusive; primeIndex < maxPrimeIndexExclusive; primeIndex++) + { + // Get the number of buckets to try, and clear our seen bucket bitmap. + int numBuckets = HashHelpers.s_primes[primeIndex]; + Array.Clear(seenBuckets, 0, Math.Min(numBuckets, seenBuckets.Length)); + + // Determine the bucket for each hash code and mark it as seen. If it was already seen, + // track it as a collision. + int numCollisions = 0; + foreach (int code in codes) + { + uint bucketNum = (uint)code % (uint)numBuckets; + if ((seenBuckets[bucketNum / BitsPerInt32] & (1 << (int)bucketNum)) != 0) + { + numCollisions++; + if (numCollisions >= bestNumCollisions) + { + // If we've already hit the previously known best number of collisions, + // there's no point in continuing as worst case we'd just use that. + break; + } + } + else + { + seenBuckets[bucketNum / BitsPerInt32] |= 1 << (int)bucketNum; + } + } + + // If this evaluation resulted in fewer collisions, use it as the best instead. + // And if it's below our collision threshold, we're done. + if (numCollisions < bestNumCollisions) + { + bestNumBuckets = numBuckets; + + if (numCollisions / (double)codes.Count <= AcceptableCollisionRate) + { + break; + } + + bestNumCollisions = numCollisions; + } + } + + ArrayPool.Shared.Return(seenBuckets); + + return bestNumBuckets; + } + + private readonly struct ChainBuddy + { + public readonly int HashCode; + public readonly int Index; + + public ChainBuddy(int hashCode, int index) + { + HashCode = hashCode; + Index = index; + } + } + + private readonly struct Bucket + { + public readonly int StartIndex; + public readonly int EndIndex; + + public Bucket(int index, int count) + { + Debug.Assert(count > 0); + + StartIndex = index; + EndIndex = index + count - 1; + } + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs new file mode 100644 index 0000000000000..f8a838852785c --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.Frozen +{ + /// + /// Provides a set of initialization methods for instances of the class. + /// + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + public static class FrozenSet + { + /// Creates a with the specified values. + /// The values to use to populate the set. + /// The comparer implementation to use to compare values for equality. If null, is used. + /// The type of the values in the set. + /// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. + /// A frozen set. + public static FrozenSet ToFrozenSet(this IEnumerable source, IEqualityComparer? comparer = null) + { + ThrowHelper.ThrowIfNull(source); + comparer ??= EqualityComparer.Default; + + // If the source is already frozen with the same comparer, it can simply be returned. + if (source is FrozenSet existing && + existing.Comparer.Equals(comparer)) + { + return existing; + } + + // Ensure we have a HashSet<,> using the specified comparer such that all values + // are non-null and unique according to that comparer. + if (source is not HashSet uniqueValues || + (uniqueValues.Count != 0 && !uniqueValues.Comparer.Equals(comparer))) + { + uniqueValues = new HashSet(source, comparer); + } + + // If the input was empty, simply return the empty frozen set singleton. The comparer is ignored. + if (uniqueValues.Count == 0) + { + return FrozenSet.Empty; + } + + if (typeof(T).IsValueType) + { + // Optimize for value types when the default comparer is being used. In such a case, the implementation + // may use EqualityComparer.Default.Equals/GetHashCode directly, with generic specialization enabling + // the Equals/GetHashCode methods to be devirtualized and possibly inlined. + if (ReferenceEquals(comparer, EqualityComparer.Default)) + { + // In the specific case of Int32 keys, we can optimize further to reduce memory consumption by using + // the underlying FrozenHashtable's Int32 index as the values themselves, avoiding the need to store the + // same values yet again. + return typeof(T) == typeof(int) ? + (FrozenSet)(object)new Int32FrozenSet((HashSet)(object)uniqueValues) : + new ValueTypeDefaultComparerFrozenSet(uniqueValues); + } + } + else if (typeof(T) == typeof(string)) + { + // Null is rare as a value in the set and we don't optimize for it. This enables the ordinal string + // implementation to fast-path out on null inputs rather than having to accomodate null inputs. + if (!uniqueValues.Contains(default!)) + { + // If the value is a string and the comparer is known to provide ordinal (case-sensitive or case-insensitive) semantics, + // we can use an implementation that's able to examine and optimize based on lengths and/or subsequences within those strings. + if (ReferenceEquals(comparer, EqualityComparer.Default) || + ReferenceEquals(comparer, StringComparer.Ordinal) || + ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase)) + { + HashSet stringValues = (HashSet)(object)uniqueValues; + IEqualityComparer stringComparer = (IEqualityComparer)(object)comparer; + + FrozenSet frozenSet = + LengthBucketsFrozenSet.TryCreateLengthBucketsFrozenSet(stringValues, stringComparer) ?? + (FrozenSet)new OrdinalStringFrozenSet(stringValues, stringComparer); + + return (FrozenSet)(object)frozenSet; + } + } + } + + // No special-cases apply. Use the default frozen set. + return new DefaultFrozenSet(uniqueValues, comparer); + } + } + + /// Provides an immutable, read-only set optimized for fast lookup and enumeration. + /// The type of the values in this set. + /// + /// Frozen collections are immutable and are optimized for situations where a collection + /// is created very infrequently but is used very frequently at runtime. They have a relatively high + /// cost to create but provide excellent lookup performance. Thus, these are ideal for cases + /// where a collection is created once, potentially at the startup of an application, and used throughout + /// the remainder of the life of the application. Frozen collections should only be initialized with + /// trusted input. + /// + [DebuggerTypeProxy(typeof(ImmutableEnumerableDebuggerProxy<>))] + [DebuggerDisplay("Count = {Count}")] + public abstract class FrozenSet : ISet, +#if NET5_0_OR_GREATER + IReadOnlySet, +#endif + IReadOnlyCollection, ICollection + { + /// Initialize the set. + /// The comparer to use and to expose from . + private protected FrozenSet(IEqualityComparer comparer) => Comparer = comparer; + + /// Gets an empty . + public static FrozenSet Empty { get; } = new EmptyFrozenSet(); + + /// Gets the comparer used by this set. + public IEqualityComparer Comparer { get; } + + /// Gets a collection containing the values in the set. + /// The order of the values in the set is unspecified. + public ImmutableArray Items => ItemsCore; + + /// + private protected abstract ImmutableArray ItemsCore { get; } + + /// Gets the number of values contained in the set. + public int Count => CountCore; + + /// + private protected abstract int CountCore { get; } + + /// Copies the values in the set to an array, starting at the specified . + /// The array that is the destination of the values copied from the set. + /// The zero-based index in at which copying begins. + public void CopyTo(T[] destination, int destinationIndex) + { + ThrowHelper.ThrowIfNull(destination); + CopyTo(destination.AsSpan(destinationIndex)); + } + + /// Copies the values in the set to a span. + /// The span that is the destination of the values copied from the set. + public void CopyTo(Span destination) => + Items.AsSpan().CopyTo(destination); + + /// + void ICollection.CopyTo(Array array, int index) + { + if (array != null && array.Rank != 1) + { + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } + + Array.Copy(Items.array!, 0, array!, index, Items.Length); + } + + /// + bool ICollection.IsReadOnly => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => this; + + /// Determines whether the set contains the specified element. + /// The element to locate. + /// if the set contains the specified element; otherwise, . + public bool Contains(T item) => + FindItemIndex(item) >= 0; + + /// Searches the set for a given value and returns the equal value it finds, if any. + /// The value to search for. + /// The value from the set that the search found, or the default value of T when the search yielded no match. + /// A value indicating whether the search was successful. + public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue) + { + int index = FindItemIndex(equalValue); + if (index >= 0) + { + actualValue = Items[index]; + return true; + } + + actualValue = default; + return false; + } + + /// Finds the index of a specific value in a set. + /// The value to lookup. + /// The index of the value, or -1 if not found. + private protected abstract int FindItemIndex(T item); + + /// Returns an enumerator that iterates through the set. + /// An enumerator that iterates through the set. + public Enumerator GetEnumerator() => GetEnumeratorCore(); + + /// + private protected abstract Enumerator GetEnumeratorCore(); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? ((IList)Array.Empty()).GetEnumerator() : + GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => + Count == 0 ? Array.Empty().GetEnumerator() : + GetEnumerator(); + + /// + bool ISet.Add(T item) => throw new NotSupportedException(); + + /// + void ISet.ExceptWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.IntersectWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.SymmetricExceptWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.UnionWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ICollection.Add(T item) => throw new NotSupportedException(); + + /// + void ICollection.Clear() => throw new NotSupportedException(); + + /// + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + /// + public bool IsProperSubsetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsProperSubsetOfCore(other); + } + + /// + private protected abstract bool IsProperSubsetOfCore(IEnumerable other); + + /// + public bool IsProperSupersetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsProperSupersetOfCore(other); + } + + /// + private protected abstract bool IsProperSupersetOfCore(IEnumerable other); + + /// + public bool IsSubsetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsSubsetOfCore(other); + } + + /// + private protected abstract bool IsSubsetOfCore(IEnumerable other); + + /// + public bool IsSupersetOf(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return IsSupersetOfCore(other); + } + + /// + private protected abstract bool IsSupersetOfCore(IEnumerable other); + + /// + public bool Overlaps(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return OverlapsCore(other); + } + + /// + private protected abstract bool OverlapsCore(IEnumerable other); + + /// + public bool SetEquals(IEnumerable other) + { + ThrowHelper.ThrowIfNull(other); + return SetEqualsCore(other); + } + + /// + private protected abstract bool SetEqualsCore(IEnumerable other); + + /// Enumerates the values of a . + public struct Enumerator : IEnumerator + { + private readonly T[] _entries; + private int _index; + + internal Enumerator(T[] entries) + { + _entries = entries; + _index = -1; + } + + /// + public bool MoveNext() + { + _index++; + if ((uint)_index < (uint)_entries.Length) + { + return true; + } + + _index = _entries.Length; + return false; + } + + /// + public readonly T Current + { + get + { + if ((uint)_index >= (uint)_entries.Length) + { + ThrowHelper.ThrowInvalidOperationException(); + } + + return _entries[_index]; + } + } + + /// + object IEnumerator.Current => Current!; + + /// + void IEnumerator.Reset() => _index = -1; + + /// + void IDisposable.Dispose() { } + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs new file mode 100644 index 0000000000000..eae06ec68f854 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides an internal base class from which frozen set implementations may derive. + /// The primary purpose of this base type is to provide implementations of the various Is methods. + /// The type of values in the set. + /// + /// The type of a struct that implements the internal IGenericSpecializedWrapper and wraps this struct. + /// This is an optimization, to minimize the virtual calls necessary to implement these bulk operations. + /// + internal abstract class FrozenSetInternalBase : FrozenSet + where TThisWrapper : struct, FrozenSetInternalBase.IGenericSpecializedWrapper + { + /// A wrapper around this that enables access to important members without making virtual calls. + private readonly TThisWrapper _thisSet; + + protected FrozenSetInternalBase(IEqualityComparer comparer) : base(comparer) + { + _thisSet = default; + _thisSet.Store(this); + } + + /// + private protected override bool IsProperSubsetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + if (otherCount == 0) + { + // No set is a proper subset of an empty set. + return false; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count < otherCount && IsSubsetOfSetWithCompatibleComparer(otherAsSet); + } + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false); + return uniqueCount == _thisSet.Count && unfoundCount > 0; + } + + /// + private protected override bool IsProperSupersetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + if (otherCount == 0) + { + // If other is the empty set, then this is a superset (since we know this one isn't empty). + return true; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count > otherCount && ContainsAllElements(otherAsSet); + } + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true); + return uniqueCount < _thisSet.Count && unfoundCount == 0; + } + + /// + private protected override bool IsSubsetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count <= otherAsSet.Count && IsSubsetOfSetWithCompatibleComparer(otherAsSet); + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: false); + return uniqueCount == _thisSet.Count && unfoundCount >= 0; + } + + /// + private protected override bool IsSupersetOfCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // Try to compute the answer based purely on counts. + if (other is ICollection otherAsCollection) + { + int otherCount = otherAsCollection.Count; + + // If other is the empty set then this is a superset. + if (otherCount == 0) + { + return true; + } + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && + otherCount > _thisSet.Count && + ComparersAreCompatible(otherAsSet)) + { + return false; + } + } + + return ContainsAllElements(other); + } + + /// + private protected override bool OverlapsCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + foreach (T element in other) + { + if (_thisSet.FindItemIndex(element) >= 0) + { + return true; + } + } + + return false; + } + + /// + private protected override bool SetEqualsCore(IEnumerable other) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + // If the other is a set and is using the same equality comparer, the operation can be optimized. + if (other is IReadOnlySet otherAsSet && ComparersAreCompatible(otherAsSet)) + { + return _thisSet.Count == otherAsSet.Count && ContainsAllElements(otherAsSet); + } + + // We couldn't take a fast path; do the full comparison. + (int uniqueCount, int unfoundCount) = CheckUniqueAndUnfoundElements(other, returnIfUnfound: true); + return uniqueCount == _thisSet.Count && unfoundCount == 0; + } + + private bool ComparersAreCompatible(IReadOnlySet other) => + other switch + { + HashSet hs => _thisSet.Comparer.Equals(hs.Comparer), + SortedSet ss => _thisSet.Comparer.Equals(ss.Comparer), + ImmutableHashSet ihs => _thisSet.Comparer.Equals(ihs.KeyComparer), + ImmutableSortedSet iss => _thisSet.Comparer.Equals(iss.KeyComparer), + FrozenSet fs => _thisSet.Comparer.Equals(fs.Comparer), + _ => false + }; + + /// + /// Determines counts that can be used to determine equality, subset, and superset. + /// + /// + /// This is only used when other is an IEnumerable and not a known set. If other is a set + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = _count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = _count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = _count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than _count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + private unsafe KeyValuePair CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) + { + Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used."); + + const int BitsPerInt32 = 32; + int intArrayLength = (_thisSet.Count / BitsPerInt32) + 1; + + int[]? rentedArray = null; + Span seenItems = intArrayLength <= 256 ? + stackalloc int[256] : + (rentedArray = ArrayPool.Shared.Rent(intArrayLength)); + seenItems.Clear(); + + // Iterate through every item in the other collection. For each, if it's + // found in this set and hasn't yet been found in this set, track it. Otherwise, + // track that items in the other set weren't found in this one. + int unfoundCount = 0; // count of items in other not found in this + int uniqueFoundCount = 0; // count of unique items in other found in this + foreach (T item in other) + { + int index = _thisSet.FindItemIndex(item); + if (index >= 0) + { + if ((seenItems[index / BitsPerInt32] & (1 << index)) == 0) + { + // Item hasn't been seen yet. + seenItems[index / BitsPerInt32] |= 1 << index; + uniqueFoundCount++; + } + } + else + { + unfoundCount++; + if (returnIfUnfound) + { + break; + } + } + } + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + + return new KeyValuePair(uniqueFoundCount, unfoundCount); + } + + private bool ContainsAllElements(IEnumerable other) + { + foreach (T element in other) + { + if (_thisSet.FindItemIndex(element) < 0) + { + return false; + } + } + + return true; + } + + private bool IsSubsetOfSetWithCompatibleComparer(IReadOnlySet other) + { + foreach (T item in _thisSet) + { + if (!other.Contains(item)) + { + return false; + } + } + + return true; + } + + /// Used to enable generic specialization with reference types. + /// + /// The bulk Is operations may end up performing multiple operations on "this" set. + /// To avoid each of those incurring virtual dispatch to the derived type, the derived + /// type hands down a struct wrapper through which all calls are performed. This base + /// class uses that generic struct wrapper to specialize and devirtualize. + /// + internal interface IGenericSpecializedWrapper + { + void Store(FrozenSet @this); + int Count { get; } + int FindItemIndex(T item); + IEqualityComparer Comparer { get; } + Enumerator GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs new file mode 100644 index 0000000000000..8ff6dac1da1cb --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenDictionary.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary to use when the key is an and the default comparer is used. + /// The type of the values in the dictionary. + /// + /// This key type is specialized as a memory optimization, as the frozen hash table already contains the array of all + /// int values, and we can thus use its array as the keys rather than maintaining a duplicate copy. + /// + internal sealed class Int32FrozenDictionary : FrozenDictionary + { + private readonly FrozenHashTable _hashTable; + private readonly TValue[] _values; + + internal Int32FrozenDictionary(Dictionary source) : base(EqualityComparer.Default) + { + Debug.Assert(source.Count != 0); + + KeyValuePair[] entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _values = new TValue[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + pair => pair.Key, + (index, pair) => _values[index] = pair.Value); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_hashTable.HashCodes); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_hashTable.HashCodes, _values); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private protected override ref readonly TValue GetValueRefOrNullRefCore(int key) + { + _hashTable.FindMatchingEntries(key, out int index, out int endIndex); + + while (index <= endIndex) + { + if (key == _hashTable.HashCodes[index]) + { + return ref _values[index]; + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs new file mode 100644 index 0000000000000..a0c9ab657d418 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32FrozenSet.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set to use when the value is an and the default comparer is used. + internal sealed class Int32FrozenSet : FrozenSetInternalBase + { + private readonly FrozenHashTable _hashTable; + + internal Int32FrozenSet(HashSet source) : base(EqualityComparer.Default) + { + Debug.Assert(source.Count != 0); + + int[] entries = new int[source.Count]; + source.CopyTo(entries); + + _hashTable = FrozenHashTable.Create( + entries, + item => item, + (_, _) => { }); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_hashTable.HashCodes); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_hashTable.HashCodes); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override int FindItemIndex(int item) + { + _hashTable.FindMatchingEntries(item, out int index, out int endIndex); + + while (index <= endIndex) + { + if (item == _hashTable.HashCodes[index]) + { + return index; + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private Int32FrozenSet _set; + public void Store(FrozenSet set) => _set = (Int32FrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(int item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs new file mode 100644 index 0000000000000..12485d4a1bf67 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a base class for frozen sets that store their values in a dedicated array. + internal abstract class ItemsFrozenSet : FrozenSetInternalBase + where TThisWrapper : struct, FrozenSetInternalBase.IGenericSpecializedWrapper + { + private protected readonly FrozenHashTable _hashTable; + private protected readonly T[] _items; + + protected ItemsFrozenSet(HashSet source, IEqualityComparer comparer) : base(comparer) + { + Debug.Assert(source.Count != 0); + + T[] entries = new T[source.Count]; + source.CopyTo(entries); + + _items = new T[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + o => o is null ? 0 : comparer.GetHashCode(o), + (index, item) => _items[index] = item); + } + + /// + private protected sealed override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected sealed override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected sealed override int CountCore => _hashTable.Count; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs new file mode 100644 index 0000000000000..c6c699881abe5 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a base class for frozen dictionaries that store their keys and values in dedicated arrays. + internal abstract class KeysAndValuesFrozenDictionary : FrozenDictionary, IDictionary + where TKey : notnull + { + private protected readonly FrozenHashTable _hashTable; + private protected readonly TKey[] _keys; + private protected readonly TValue[] _values; + + protected KeysAndValuesFrozenDictionary(Dictionary source, IEqualityComparer comparer) : base(comparer) + { + Debug.Assert(source.Count != 0); + + KeyValuePair[] entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _keys = new TKey[entries.Length]; + _values = new TValue[entries.Length]; + + _hashTable = FrozenHashTable.Create( + entries, + pair => comparer.GetHashCode(pair.Key), + (index, pair) => + { + _keys[index] = pair.Key; + _values[index] = pair.Value; + }); + } + + /// + private protected sealed override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected sealed override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected sealed override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected sealed override int CountCore => _hashTable.Count; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs new file mode 100644 index 0000000000000..9f670de54c399 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary implementation where strings are grouped by their lengths. + internal sealed class LengthBucketsFrozenDictionary : FrozenDictionary + { + /// Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space. + private const double EmptyLengthsRatio = 0.2; + /// The maximum number of items allowed per bucket. The larger the value, the longer it can take to search a bucket, which is sequentially examined. + private const int MaxPerLength = 5; + + private readonly KeyValuePair[][] _lengthBuckets; + private readonly int _minLength; + private readonly string[] _keys; + private readonly TValue[] _values; + private readonly bool _ignoreCase; + + private LengthBucketsFrozenDictionary( + string[] keys, TValue[] values, KeyValuePair[][] lengthBuckets, int minLength, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + _keys = keys; + _values = values; + _lengthBuckets = lengthBuckets; + _minLength = minLength; + _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase); + } + + internal static LengthBucketsFrozenDictionary? TryCreateLengthBucketsFrozenSet(Dictionary source, IEqualityComparer comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + // Iterate through all of the inputs, bucketing them based on the length of the string. + var groupedByLength = new Dictionary>>(); + int minLength = int.MaxValue, maxLength = int.MinValue; + foreach (KeyValuePair pair in source) + { + string s = pair.Key; + + if (s.Length < minLength) minLength = s.Length; + if (s.Length > maxLength) maxLength = s.Length; + + if (!groupedByLength.TryGetValue(s.Length, out List>? list)) + { + groupedByLength[s.Length] = list = new List>(MaxPerLength); + } + + // If we've already hit the max per-bucket limit, bail. + if (list.Count == MaxPerLength) + { + return null; + } + + list.Add(pair); + } + + // If there would be too much empty space in the lookup array, bail. + int spread = maxLength - minLength + 1; + if (groupedByLength.Count / (double)spread < EmptyLengthsRatio) + { + return null; + } + + string[] keys = new string[source.Count]; + TValue[] values = new TValue[keys.Length]; + var lengthBuckets = new KeyValuePair[maxLength - minLength + 1][]; + + // Iterate through each bucket, filling the keys/values arrays, and creating a lookup array such that + // given a string length we can index into that array to find the array of string,int pairs: the string + // is the key and the int is the index into the keys/values array for the corresponding entry. + int index = 0; + foreach (KeyValuePair>> group in groupedByLength) + { + KeyValuePair[] length = lengthBuckets[group.Key - minLength] = new KeyValuePair[group.Value.Count]; + int i = 0; + foreach (KeyValuePair pair in group.Value) + { + length[i] = new KeyValuePair(pair.Key, index); + keys[index] = pair.Key; + values[index] = pair.Value; + + i++; + index++; + } + } + + return new LengthBucketsFrozenDictionary(keys, values, lengthBuckets, minLength, comparer); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected override int CountCore => _keys.Length; + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) + { + // If the length doesn't have an associated bucket, the key isn't in the dictionary. + int length = key.Length - _minLength; + if (length >= 0) + { + // Get the bucket for this key's length. If it's null, the key isn't in the dictionary. + KeyValuePair[][] lengths = _lengthBuckets; + if ((uint)length < (uint)lengths.Length && lengths[length] is KeyValuePair[] subset) + { + // Now iterate through every key in the bucket to see whether this is a match. + if (_ignoreCase) + { + foreach (KeyValuePair kvp in subset) + { + if (StringComparer.OrdinalIgnoreCase.Equals(key, kvp.Key)) + { + return ref _values[kvp.Value]; + } + } + } + else + { + foreach (KeyValuePair kvp in subset) + { + if (key == kvp.Key) + { + return ref _values[kvp.Value]; + } + } + } + } + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs new file mode 100644 index 0000000000000..e7eb59975f711 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set implementation where strings are grouped by their lengths. + internal sealed class LengthBucketsFrozenSet : FrozenSetInternalBase + { + /// Allowed ratio between buckets with values and total buckets. Under this ratio, this implementation won't be used due to too much wasted space. + private const double EmptyLengthsRatio = 0.2; + /// The maximum number of items allowed per bucket. The larger the value, the longer it can take to search a bucket, which is sequentially examined. + private const int MaxPerLength = 5; + + private readonly KeyValuePair[][] _lengthBuckets; + private readonly int _minLength; + private readonly string[] _items; + private readonly bool _ignoreCase; + + private LengthBucketsFrozenSet(string[] items, KeyValuePair[][] lengthBuckets, int minLength, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + _items = items; + _lengthBuckets = lengthBuckets; + _minLength = minLength; + _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase); + } + + internal static LengthBucketsFrozenSet? TryCreateLengthBucketsFrozenSet(HashSet source, IEqualityComparer comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + // Iterate through all of the inputs, bucketing them based on the length of the string. + var groupedByLength = new Dictionary>(); + int minLength = int.MaxValue, maxLength = int.MinValue; + foreach (string s in source) + { + Debug.Assert(s is not null, "This implementation should not be used with null source values."); + + if (s.Length < minLength) minLength = s.Length; + if (s.Length > maxLength) maxLength = s.Length; + + if (!groupedByLength.TryGetValue(s.Length, out List? list)) + { + groupedByLength[s.Length] = list = new List(MaxPerLength); + } + + // If we've already hit the max per-bucket limit, bail. + if (list.Count == MaxPerLength) + { + return null; + } + + list.Add(s); + } + + // If there would be too much empty space in the lookup array, bail. + int spread = maxLength - minLength + 1; + if (groupedByLength.Count / (double)spread < EmptyLengthsRatio) + { + return null; + } + + string[] items = new string[source.Count]; + var lengthBuckets = new KeyValuePair[maxLength - minLength + 1][]; + + // Iterate through each bucket, filling the items array, and creating a lookup array such that + // given a string length we can index into that array to find the array of string,int pairs: the string + // is the key and the int is the index into the items array for the corresponding entry. + int index = 0; + foreach (KeyValuePair> group in groupedByLength) + { + KeyValuePair[] length = lengthBuckets[group.Key - minLength] = new KeyValuePair[group.Value.Count]; + int i = 0; + foreach (string value in group.Value) + { + length[i] = new KeyValuePair(value, index); + items[index] = value; + + i++; + index++; + } + } + + return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected override int CountCore => _items.Length; + + /// + private protected override int FindItemIndex(string? item) + { + if (item is not null) // this implementation won't be constructed from null values, but Contains may still be called with one + { + // If the length doesn't have an associated bucket, the key isn't in the set. + int length = item.Length - _minLength; + if (length >= 0) + { + // Get the bucket for this key's length. If it's null, the key isn't in the set. + KeyValuePair[][] lengths = _lengthBuckets; + if ((uint)length < (uint)lengths.Length && lengths[length] is KeyValuePair[] subset) + { + // Now iterate through every key in the bucket to see whether this is a match. + if (_ignoreCase) + { + foreach (KeyValuePair kvp in subset) + { + if (StringComparer.OrdinalIgnoreCase.Equals(item, kvp.Key)) + { + return kvp.Value; + } + } + } + else + { + foreach (KeyValuePair kvp in subset) + { + if (item == kvp.Key) + { + return kvp.Value; + } + } + } + } + } + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private LengthBucketsFrozenSet _set; + public void Store(FrozenSet set) => _set = (LengthBucketsFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(string item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs new file mode 100644 index 0000000000000..34265384492fe --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenDictionary.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary optimized for ordinal (case-sensitive or case-insensitive) lookup of strings. + /// The type of values in the dictionary. + internal sealed class OrdinalStringFrozenDictionary : FrozenDictionary + { + private readonly FrozenHashTable _hashTable; + private readonly string[] _keys; + private readonly TValue[] _values; + private readonly StringComparerBase _partialComparer; + private readonly int _minimumLength; + private readonly int _maximumLengthDiff; + + internal OrdinalStringFrozenDictionary(Dictionary source, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + var entries = new KeyValuePair[source.Count]; + ((ICollection>)source).CopyTo(entries, 0); + + _keys = new string[entries.Length]; + _values = new TValue[entries.Length]; + + _partialComparer = ComparerPicker.Pick( + Array.ConvertAll(entries, pair => pair.Key), + ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase), + out _minimumLength, + out _maximumLengthDiff); + + _hashTable = FrozenHashTable.Create( + entries, + pair => _partialComparer.GetHashCode(pair.Key), + (index, pair) => + { + _keys[index] = pair.Key; + _values[index] = pair.Value; + }); + } + + /// + private protected override ImmutableArray KeysCore => new ImmutableArray(_keys); + + /// + private protected override ImmutableArray ValuesCore => new ImmutableArray(_values); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) + { + if ((uint)(key.Length - _minimumLength) <= (uint)_maximumLengthDiff) + { + StringComparerBase partialComparer = _partialComparer; + + int hashCode = partialComparer.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (partialComparer.Equals(key, _keys[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't) + { + return ref _values[index]; + } + } + + index++; + } + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs new file mode 100644 index 0000000000000..0c6ff0d5bcee7 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/OrdinalStringFrozenSet.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set optimized for ordinal (case-sensitive or case-insensitive) lookup of strings. + internal sealed class OrdinalStringFrozenSet : FrozenSetInternalBase + { + private readonly FrozenHashTable _hashTable; + private readonly string[] _items; + private readonly StringComparerBase _partialComparer; + private readonly int _minimumLength; + private readonly int _maximumLengthDiff; + + internal OrdinalStringFrozenSet(HashSet source, IEqualityComparer comparer) : + base(comparer) + { + Debug.Assert(source.Count != 0); + Debug.Assert(comparer == EqualityComparer.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase); + + string[] entries = new string[source.Count]; + source.CopyTo(entries); + + _items = new string[entries.Length]; + + _partialComparer = ComparerPicker.Pick( + entries, + ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase), + out _minimumLength, + out _maximumLengthDiff); + + _hashTable = FrozenHashTable.Create( + entries, + _partialComparer.GetHashCode, + (index, item) => _items[index] = item); + } + + /// + private protected override ImmutableArray ItemsCore => new ImmutableArray(_items); + + /// + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items); + + /// + private protected override int CountCore => _hashTable.Count; + + /// + private protected override int FindItemIndex(string item) + { + if (item is not null && // this implementation won't be used for null values + (uint)(item.Length - _minimumLength) <= (uint)_maximumLengthDiff) + { + StringComparerBase partialComparer = _partialComparer; + + int hashCode = partialComparer.GetHashCode(item); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (partialComparer.Equals(item, _items[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't) + { + return index; + } + } + + index++; + } + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private OrdinalStringFrozenSet _set; + public void Store(FrozenSet set) => _set = (OrdinalStringFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(string item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs new file mode 100644 index 0000000000000..2ea18d247f8c0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs @@ -0,0 +1,242 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + internal static class ComparerPicker + { + /// + /// Pick an optimal comparer for the set of strings and case-sensitivity mode. + /// + /// + /// The idea here is to find the shortest substring slice across all the input strings which yields a set of + /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to + /// perform the dictionary lookup. Keeping the slices as small as possible minimizes the number of characters + /// involved in hashing, speeding up the whole process. + /// + /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good + /// enough uniqueness factor. We look at all the strings both left-justified and right-justified as this maximizes + /// the opportunities to find unique slices, especially in the case of many strings with the same prefix or suffix. + /// + /// In whatever slice we end up with, if all the characters involved in the slice are ASCII and we're doing case-insensitive + /// operations, then we can select an ASCII-specific case-insensitive comparer which yields faster overall performance. + /// + /// Warning: This code may reorganize (e.g. sort) the entries in the input array. It will not delete or add anything though. + /// + public static StringComparerBase Pick(ReadOnlySpan uniqueStrings, bool ignoreCase, out int minimumLength, out int maximumLengthDiff) + { + Debug.Assert(uniqueStrings.Length != 0); + + // First, try to pick a substring comparer. + // if we couldn't find a good substring comparer, fallback to a full string comparer. + StringComparerBase? c = + PickSubstringComparer(uniqueStrings, ignoreCase) ?? + PickFullStringComparer(uniqueStrings, ignoreCase); + + // Calculate the trivial rejection boundaries. + int min = int.MaxValue, max = 0; + foreach (string s in uniqueStrings) + { + if (s.Length < min) + { + min = s.Length; + } + + if (s.Length > max) + { + max = s.Length; + } + } + + minimumLength = min; + maximumLengthDiff = max - min; + return c; + } + + private static StringComparerBase? PickSubstringComparer(ReadOnlySpan uniqueStrings, bool ignoreCase) + { + const double SufficientUniquenessFactor = 0.95; // 95% is good enough + + // What is the shortest string? This represent the maximum substring length we consider + int maxSubstringLength = int.MaxValue; + foreach (string s in uniqueStrings) + { + if (s.Length < maxSubstringLength) + { + maxSubstringLength = s.Length; + } + } + + SubstringComparerBase leftComparer = ignoreCase ? new LeftJustifiedCaseInsensitiveSubstringComparer() : new LeftJustifiedSubstringComparer(); + SubstringComparerBase rightComparer = ignoreCase ? new RightJustifiedCaseInsensitiveSubstringComparer() : new RightJustifiedSubstringComparer(); + + // try to find the minimal unique substring to use for comparisons + var leftSet = new HashSet(new ComparerWrapper(leftComparer)); + var rightSet = new HashSet(new ComparerWrapper(rightComparer)); + for (int count = 1; count <= maxSubstringLength; count++) + { + for (int index = 0; index <= maxSubstringLength - count; index++) + { + leftComparer.Index = index; + leftComparer.Count = count; + + double factor = GetUniquenessFactor(leftSet, uniqueStrings); + if (factor >= SufficientUniquenessFactor) + { + if (ignoreCase) + { + foreach (string ss in uniqueStrings) + { + if (!IsAllAscii(ss.AsSpan(leftComparer.Index, leftComparer.Count))) + { + // keep the slower non-ascii comparer since we have some non-ascii text + return leftComparer; + } + } + + // optimize for all-ascii case + return new LeftJustifiedCaseInsensitiveAsciiSubstringComparer + { + Index = leftComparer.Index, + Count = leftComparer.Count, + }; + } + + // Optimize the single char case + if (leftComparer.Count == 1) + { + return new LeftJustifiedSingleCharComparer + { + Index = leftComparer.Index, + Count = 1, + }; + } + + return leftComparer; + } + + rightComparer.Index = -index - count; + rightComparer.Count = count; + + factor = GetUniquenessFactor(rightSet, uniqueStrings); + if (factor >= SufficientUniquenessFactor) + { + if (ignoreCase) + { + foreach (string ss in uniqueStrings) + { + if (!IsAllAscii(ss.AsSpan(ss.Length + rightComparer.Index, rightComparer.Count))) + { + // keep the slower non-ascii comparer since we have some non-ascii text + return rightComparer; + } + } + + // optimize for all-ascii case + return new RightJustifiedCaseInsensitiveAsciiSubstringComparer + { + Index = rightComparer.Index, + Count = rightComparer.Count, + }; + } + + // Optimize the single char case + if (rightComparer.Count == 1) + { + return new RightJustifiedSingleCharComparer + { + Index = rightComparer.Index, + Count = 1, + }; + } + + return rightComparer; + } + } + } + + return null; + } + + private static StringComparerBase PickFullStringComparer(ReadOnlySpan uniqueStrings, bool ignoreCase) + { + if (!ignoreCase) + { + return new FullStringComparer(); + } + + foreach (string s in uniqueStrings) + { + if (!IsAllAscii(s.AsSpan())) + { + return new FullCaseInsensitiveStringComparer(); + } + } + + return new FullCaseInsensitiveAsciiStringComparer(); + } + + private sealed class ComparerWrapper : IEqualityComparer + { + private readonly SubstringComparerBase _comp; + + public ComparerWrapper(SubstringComparerBase comp) => _comp = comp; + + public bool Equals(string? x, string? y) => _comp.EqualsPartial(x, y); + public int GetHashCode([DisallowNull] string obj) => _comp.GetHashCode(obj); + } + + // TODO https://github.com/dotnet/runtime/issues/28230: + // Replace this once Ascii.IsValid exists. + internal static unsafe bool IsAllAscii(ReadOnlySpan s) + { + fixed (char* src = s) + { + uint* ptrUInt32 = (uint*)src; + int length = s.Length; + + while (length > 3) + { + if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1])) + { + return false; + } + + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + char ch = *ptrChar++; + if (ch >= 0x7f) + { + return false; + } + } + } + + return true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0; + } + + private static double GetUniquenessFactor(HashSet set, ReadOnlySpan uniqueStrings) + { + set.Clear(); + foreach (string s in uniqueStrings) + { + set.Add(s); + } + + return set.Count / (double)uniqueStrings.Length; + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs new file mode 100644 index 0000000000000..5c3b60a8b92f0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer for ordinal case-insensitive ascii-only string comparisons. + /// + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class FullCaseInsensitiveAsciiStringComparer : StringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan()); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs new file mode 100644 index 0000000000000..67add016b6c05 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer for ordinal case-insensitive string comparisons. + /// + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class FullCaseInsensitiveStringComparer : StringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan()); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs new file mode 100644 index 0000000000000..1c9ead8804de0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer for ordinal string comparisons. + /// + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class FullStringComparer : StringComparerBase + { + public override bool Equals(string? x, string? y) => string.Equals(x, y); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan()); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs new file mode 100644 index 0000000000000..3fa6ba9d9ae99 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks from the start of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class LeftJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs new file mode 100644 index 0000000000000..6faa142f133d6 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks from the start of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs new file mode 100644 index 0000000000000..43aca9823b39c --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a single char of the input strings. + /// + /// + /// This comparer looks from the start of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class LeftJustifiedSingleCharComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => string.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x![Index] == y![Index]; + public override int GetHashCode(string s) => s[Index]; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs new file mode 100644 index 0000000000000..160c524a3f017 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks from the start of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class LeftJustifiedSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => string.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count)); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs new file mode 100644 index 0000000000000..cb6f72c757121 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks at the end of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class RightJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs new file mode 100644 index 0000000000000..37578f6eddaf3 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks at the end of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class RightJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase); + public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs new file mode 100644 index 0000000000000..2508342b4db32 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a single character of the input strings. + /// + /// + /// This comparer looks at the end of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class RightJustifiedSingleCharComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => string.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x![x.Length + Index] == y![y.Length + Index]; + public override int GetHashCode(string s) => s[s.Length + Index]; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs new file mode 100644 index 0000000000000..2c1367affb251 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + /// + /// A comparer that operates over a portion of the input strings. + /// + /// + /// This comparer looks at the end of input strings. + /// + /// This code doesn't perform any error checks on the input as it assumes + /// the data is always valid. This is ensured by precondition checks before + /// a key is used to perform a dictionary lookup. + /// + internal sealed class RightJustifiedSubstringComparer : SubstringComparerBase + { + public override bool Equals(string? x, string? y) => string.Equals(x, y); + public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count)); + public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs new file mode 100644 index 0000000000000..da80dc8c125fc --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace System.Collections.Frozen +{ + // We define this rather than using IEqualityComparer, since virtual dispatch is faster than interface dispatch + internal abstract class StringComparerBase : EqualityComparer + { + // TODO https://github.com/dotnet/runtime/issues/77679: + // Replace these once non-randomized implementations are available. + + protected static unsafe int GetHashCodeOrdinal(ReadOnlySpan s) + { + int length = s.Length; + fixed (char* src = &MemoryMarshal.GetReference(s)) + { + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ ptrUInt32[0]; + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ ptrUInt32[1]; + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *ptrChar++; + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } + } + + // useful if the string only contains ASCII characterss + protected static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan s) + { + int length = s.Length; + fixed (char* src = &MemoryMarshal.GetReference(s)) + { + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + // We "normalize to lowercase" every char by ORing with 0x0020. This casts + // a very wide net because it will change, e.g., '^' to '~'. But that should + // be ok because we expect this to be very rare in practice. + const uint NormalizeToLowercase = 0x0020_0020u; // valid both for big-endian and for little-endian + + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ (ptrUInt32[0] | NormalizeToLowercase); + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (ptrUInt32[1] | NormalizeToLowercase); + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ (*ptrChar | NormalizeToLowercase); + ptrChar++; + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } + } + + protected static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan s) + { + int length = s.Length; + + char[]? rentedArray = null; + Span scratch = length <= 256 ? + stackalloc char[256] : + (rentedArray = ArrayPool.Shared.Rent(length)); + + length = s.ToUpperInvariant(scratch); // NOTE: this really should be the (non-existent) ToUpperOrdinal + + uint hash1 = (5381 << 16) + 5381; + uint hash2 = hash1; + + fixed (char* src = &MemoryMarshal.GetReference(scratch)) + { + uint* ptrUInt32 = (uint*)src; + while (length > 3) + { + hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptrUInt32[0]; + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptrUInt32[1]; + ptrUInt32 += 2; + length -= 4; + } + + char* ptrChar = (char*)ptrUInt32; + while (length-- > 0) + { + hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ *ptrChar++; + } + } + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + + return (int)(hash1 + (hash2 * 1_566_083_941)); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs new file mode 100644 index 0000000000000..4f382373dde66 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Frozen +{ + internal abstract class SubstringComparerBase : StringComparerBase + { + public int Index; + public int Count; + + public abstract bool EqualsPartial(string? x, string? y); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs new file mode 100644 index 0000000000000..5bf563de58ed6 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Collections.Frozen +{ + /// Provides a frozen dictionary optimized for value type keys using the default comparer. + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + internal sealed class ValueTypeDefaultComparerFrozenDictionary : KeysAndValuesFrozenDictionary, IDictionary + where TKey : notnull + { + internal ValueTypeDefaultComparerFrozenDictionary(Dictionary source) : + base(source, EqualityComparer.Default) + { + Debug.Assert(typeof(TKey).IsValueType); + } + + /// + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + int hashCode = EqualityComparer.Default.GetHashCode(key); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (EqualityComparer.Default.Equals(key, _keys[index])) + { + return ref _values[index]; + } + } + + index++; + } + + return ref Unsafe.NullRef(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs new file mode 100644 index 0000000000000..6eca1831646a0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Collections.Frozen +{ + /// Provides a frozen set optimized for value types using the default comparer. + /// The type of values in the dictionary. + internal sealed class ValueTypeDefaultComparerFrozenSet : ItemsFrozenSet.GSW> + { + internal ValueTypeDefaultComparerFrozenSet(HashSet source) : + base(source, EqualityComparer.Default) + { + Debug.Assert(typeof(T).IsValueType); + } + + /// + private protected override int FindItemIndex(T item) + { + int hashCode = EqualityComparer.Default.GetHashCode(item!); + _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex); + + while (index <= endIndex) + { + if (hashCode == _hashTable.HashCodes[index]) + { + if (EqualityComparer.Default.Equals(item, _items[index])) + { + return index; + } + } + + index++; + } + + return -1; + } + + internal struct GSW : IGenericSpecializedWrapper + { + private ValueTypeDefaultComparerFrozenSet _set; + public void Store(FrozenSet set) => _set = (ValueTypeDefaultComparerFrozenSet)set; + + public int Count => _set.Count; + public IEqualityComparer Comparer => _set.Comparer; + public int FindItemIndex(T item) => _set.FindItemIndex(item); + public Enumerator GetEnumerator() => _set.GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs index 17206683bbd23..3df4cd8e08b4a 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs @@ -18,7 +18,7 @@ internal sealed class ImmutableDictionaryDebuggerProxy : Immutable /// Initializes a new instance of the class. /// /// The enumerable to show in the debugger. - public ImmutableDictionaryDebuggerProxy(IImmutableDictionary dictionary) + public ImmutableDictionaryDebuggerProxy(IReadOnlyDictionary dictionary) : base(enumerable: dictionary) { } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs new file mode 100644 index 0000000000000..faf71460a5465 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/ThrowHelper.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Collections +{ + internal static class ThrowHelper + { + public static void ThrowIfNull(object arg, [CallerArgumentExpression("arg")] string? paramName = null) + { + if (arg is null) + { + ThrowArgumentNullException(paramName); + } + } + + [DoesNotReturn] + public static void ThrowIfDestinationTooSmall() => + throw new ArgumentException(SR.CapacityMustBeGreaterThanOrEqualToCount, "destination"); + + [DoesNotReturn] + public static void ThrowArgumentNullException(string? paramName) => + throw new ArgumentNullException(paramName); + + [DoesNotReturn] + public static void ThrowKeyNotFoundException() => + throw new KeyNotFoundException(); + + [DoesNotReturn] + public static void ThrowInvalidOperationException() => + throw new InvalidOperationException(); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs b/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs new file mode 100644 index 0000000000000..77f304f46fdcd --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Polyfills.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ +#if !NETCOREAPP2_0_OR_GREATER + internal static class KeyValuePairExtensions + { + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Deconstruct(this KeyValuePair source, out TKey key, out TValue value) + { + key = source.Key; + value = source.Value; + } + } +#endif + +#if !NET5_0_OR_GREATER + internal interface IReadOnlySet : IReadOnlyCollection + { + bool Contains(T item); + bool IsProperSubsetOf(IEnumerable other); + bool IsProperSupersetOf(IEnumerable other); + bool IsSubsetOf(IEnumerable other); + bool IsSupersetOf(IEnumerable other); + bool Overlaps(IEnumerable other); + bool SetEquals(IEnumerable other); + } +#endif +} + +namespace System.Numerics +{ +#if !NETCOREAPP3_0_OR_GREATER + internal static class BitOperations + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) => (value << offset) | (value >> (32 - offset)); + } +#endif +} + +namespace System.Runtime.CompilerServices +{ +#if !NETCOREAPP3_0_OR_GREATER + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; + + public string ParameterName { get; } + } +#endif +} diff --git a/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs b/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs index f1a9228e445f5..77a034a739141 100644 --- a/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs +++ b/src/libraries/System.Collections.Immutable/src/Validation/Requires.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -21,7 +20,7 @@ internal static class Requires /// The name of the parameter to include in any thrown exception. /// Thrown if is null [DebuggerStepThrough] - public static void NotNull([ValidatedNotNull]T value, string? parameterName) + public static void NotNull([NotNull]T value, string? parameterName) where T : class // ensures value-types aren't passed to a null checking method { if (value == null) @@ -39,7 +38,7 @@ public static void NotNull([ValidatedNotNull]T value, string? parameterName) /// The value of the parameter. /// Thrown if is null [DebuggerStepThrough] - public static T NotNullPassthrough([ValidatedNotNull]T value, string? parameterName) + public static T NotNullPassthrough([NotNull]T value, string? parameterName) where T : class // ensures value-types aren't passed to a null checking method { NotNull(value, parameterName); @@ -58,7 +57,7 @@ public static T NotNullPassthrough([ValidatedNotNull]T value, string? paramet /// may or may not be a class, but certainly cannot be null. /// [DebuggerStepThrough] - public static void NotNullAllowStructs([ValidatedNotNull]T value, string? parameterName) + public static void NotNullAllowStructs([NotNull]T value, string? parameterName) { if (null == value) { @@ -70,8 +69,9 @@ public static void NotNullAllowStructs([ValidatedNotNull]T value, string? par /// Throws an . /// /// The name of the parameter that was null. + [DoesNotReturn] [DebuggerStepThrough] - private static void FailArgumentNullException(string? parameterName) + public static void FailArgumentNullException(string? parameterName) { // Separating out this throwing operation helps with inlining of the caller throw new ArgumentNullException(parameterName); @@ -81,7 +81,7 @@ private static void FailArgumentNullException(string? parameterName) /// Throws an if a condition does not evaluate to true. /// [DebuggerStepThrough] - public static void Range(bool condition, string? parameterName, string? message = null) + public static void Range([DoesNotReturnIf(false)] bool condition, string? parameterName, string? message = null) { if (!condition) { @@ -92,6 +92,7 @@ public static void Range(bool condition, string? parameterName, string? message /// /// Throws an . /// + [DoesNotReturn] [DebuggerStepThrough] public static void FailRange(string? parameterName, string? message = null) { @@ -109,7 +110,7 @@ public static void FailRange(string? parameterName, string? message = null) /// Throws an if a condition does not evaluate to true. /// [DebuggerStepThrough] - public static void Argument(bool condition, string? parameterName, string? message) + public static void Argument([DoesNotReturnIf(false)] bool condition, string? parameterName, string? message) { if (!condition) { @@ -121,7 +122,7 @@ public static void Argument(bool condition, string? parameterName, string? messa /// Throws an if a condition does not evaluate to true. /// [DebuggerStepThrough] - public static void Argument(bool condition) + public static void Argument([DoesNotReturnIf(false)] bool condition) { if (!condition) { @@ -134,6 +135,7 @@ public static void Argument(bool condition) /// /// Specifies the type of the disposed object. /// The disposed object. + [DoesNotReturn] [DebuggerStepThrough] [MethodImpl(MethodImplOptions.NoInlining)] // inlining this on .NET < 4.5.2 on x64 causes InvalidProgramException. public static void FailObjectDisposed(TDisposed disposed) diff --git a/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs b/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs deleted file mode 100644 index a9b26011bc47a..0000000000000 --- a/src/libraries/System.Collections.Immutable/src/Validation/ValidatedNotNullAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace System.Collections.Immutable -{ - /// - /// Indicates to Code Analysis that a method validates a particular parameter. - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - internal sealed class ValidatedNotNullAttribute : Attribute - { - } -} diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs new file mode 100644 index 0000000000000..aaa24662f09a4 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTest.cs @@ -0,0 +1,480 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Tests; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Collections.Frozen.Tests +{ + public abstract class FrozenDictionary_Generic_Tests : IDictionary_Generic_Tests + { + protected override bool IsReadOnly => true; + protected override bool AddRemoveClear_ThrowsNotSupported => true; + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + protected override IDictionary GenericIDictionaryFactory(int count) + { + var d = new Dictionary(); + for (int i = 0; i < count; i++) + { + d.Add(CreateTKey(i), CreateTValue(i)); + } + return d.ToFrozenDictionary(GetKeyIEqualityComparer()); + } + + protected override IDictionary GenericIDictionaryFactory() => Enumerable.Empty>().ToFrozenDictionary(); + + protected override IDictionary GenericIDictionaryFactory(IEqualityComparer comparer) => Enumerable.Empty>().ToFrozenDictionary(comparer); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override bool ResetImplemented => true; + protected override bool IDictionary_Generic_Keys_Values_Enumeration_ResetImplemented => true; + + protected override EnumerableOrder Order => EnumerableOrder.Unspecified; + + [Theory] + [InlineData(100_000)] + public void CreateVeryLargeDictionary_Success(int largeCount) + { + GenericIDictionaryFactory(largeCount); + } + + [Fact] + public void NullSource_ThrowsException() + { + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary()); + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary(null)); + AssertExtensions.Throws("source", () => ((Dictionary)null).ToFrozenDictionary(EqualityComparer.Default)); + + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null)); + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null, EqualityComparer.Default)); + AssertExtensions.Throws("keySelector", () => Enumerable.Empty().ToFrozenDictionary((Func)null, (Func)null, EqualityComparer.Default)); + + AssertExtensions.Throws("elementSelector", () => Enumerable.Empty().ToFrozenDictionary(i => i, (Func)null)); + AssertExtensions.Throws("elementSelector", () => Enumerable.Empty().ToFrozenDictionary(i => i, (Func)null, EqualityComparer.Default)); + } + + [Fact] + public void EmptySource_ProducedFrozenDictionaryEmpty() + { + Assert.Same(FrozenDictionary.Empty, new Dictionary().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, Enumerable.Empty>().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, Array.Empty>().ToFrozenDictionary()); + Assert.Same(FrozenDictionary.Empty, new List>().ToFrozenDictionary()); + + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenDictionary.Empty, new Dictionary().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, Enumerable.Empty>().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, Array.Empty>().ToFrozenDictionary(comparer)); + Assert.Same(FrozenDictionary.Empty, new List>().ToFrozenDictionary(comparer)); + } + } + + [Fact] + public void EmptyFrozenDictionary_Idempotent() + { + FrozenDictionary empty = FrozenDictionary.Empty; + + Assert.NotNull(empty); + Assert.Same(empty, FrozenDictionary.Empty); + } + + [Fact] + public void EmptyFrozenDictionary_OperationsAreNops() + { + FrozenDictionary empty = FrozenDictionary.Empty; + + Assert.Same(EqualityComparer.Default, empty.Comparer); + Assert.Equal(0, empty.Count); + Assert.Empty(empty.Keys); + Assert.Empty(empty.Values); + + TKey key = CreateTKey(0); + Assert.False(empty.ContainsKey(key)); + Assert.False(empty.TryGetValue(key, out TValue value)); + Assert.Equal(default, value); + Assert.True(Unsafe.IsNullRef(ref Unsafe.AsRef(in empty.GetValueRefOrNullRef(key)))); + Assert.Throws(() => empty[key]); + + empty.CopyTo(Span>.Empty); + KeyValuePair[] array = new KeyValuePair[1]; + empty.CopyTo(array); + Assert.Equal(default, array[0]); + + int count = 0; + foreach (KeyValuePair pair in empty) + { + count++; + } + Assert.Equal(0, count); + } + + [Fact] + public void FrozenDictionary_ToFrozenDictionary_Idempotent() + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenDictionary.Empty, FrozenDictionary.Empty.ToFrozenDictionary(comparer)); + } + + FrozenDictionary frozen = new Dictionary() { { CreateTKey(0), CreateTValue(0) } }.ToFrozenDictionary(); + Assert.Same(frozen, frozen.ToFrozenDictionary()); + Assert.NotSame(frozen, frozen.ToFrozenDictionary(NonDefaultEqualityComparer.Instance)); + } + + public static IEnumerable LookupItems_AllItemsFoundAsExpected_MemberData() + { + foreach (int size in new[] { 1, 2, 10, 999, 1024 }) + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + foreach (bool specifySameComparer in new[] { false, true }) + { + yield return new object[] { size, comparer, specifySameComparer }; + } + } + } + } + + [Fact] + public void ToFrozenDictionary_KeySelector_ResultsAreUsed() + { + TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray(); + + FrozenDictionary frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], NonDefaultEqualityComparer.Instance); + Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); + + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, frozen[keys[i]]); + } + } + + [Fact] + public void ToFrozenDictionary_KeySelectorAndValueSelector_ResultsAreUsed() + { + TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray(); + TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray(); + + FrozenDictionary frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], i => values[i], NonDefaultEqualityComparer.Instance); + Assert.Same(NonDefaultEqualityComparer.Instance, frozen.Comparer); + + for (int i = 0; i < 10; i++) + { + Assert.Equal(values[i], frozen[keys[i]]); + } + } + + [Theory] + [MemberData(nameof(LookupItems_AllItemsFoundAsExpected_MemberData))] + public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer comparer, bool specifySameComparer) + { + Dictionary original = + Enumerable.Range(0, size) + .Select(i => new KeyValuePair(CreateTKey(i), CreateTValue(i))) + .ToDictionary(p => p.Key, p => p.Value, comparer); + KeyValuePair[] originalPairs = original.ToArray(); + + FrozenDictionary frozen = specifySameComparer ? + original.ToFrozenDictionary(comparer) : + original.ToFrozenDictionary(); + + // Make sure creating the frozen dictionary didn't alter the original + Assert.Equal(originalPairs.Length, original.Count); + Assert.All(originalPairs, p => Assert.Equal(p.Value, original[p.Key])); + + // Make sure the frozen dictionary matches the original + Assert.Equal(original.Count, frozen.Count); + Assert.Equal(new HashSet>(original), new HashSet>(frozen)); + Assert.All(originalPairs, p => Assert.True(frozen.ContainsKey(p.Key))); + Assert.All(originalPairs, p => Assert.Equal(p.Value, frozen[p.Key])); + Assert.All(originalPairs, p => Assert.Equal(p.Value, frozen.GetValueRefOrNullRef(p.Key))); + if (specifySameComparer || + comparer is null || + comparer == EqualityComparer.Default) + { + Assert.Equal(original.Comparer, frozen.Comparer); + } + + // Generate additional items and ensure they match iff the original matches. + for (int i = size; i < size + 100; i++) + { + TKey key = CreateTKey(i); + if (original.ContainsKey(key)) + { + Assert.True(frozen.ContainsKey(key)); + } + else + { + Assert.Throws(() => frozen[key]); + Assert.False(frozen.TryGetValue(key, out TValue value)); + Assert.Equal(default, value); + Assert.True(Unsafe.IsNullRef(ref Unsafe.AsRef(in frozen.GetValueRefOrNullRef(key)))); + } + } + } + + [Fact] + public void MultipleValuesSameKey_LastInSourceWins() + { + TKey[] keys = Enumerable.Range(0, 2).Select(CreateTKey).ToArray(); + TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray(); + + foreach (bool reverse in new[] { false, true }) + { + IEnumerable> source = + from key in keys + from value in values + select new KeyValuePair(key, value); + + if (reverse) + { + source = source.Reverse(); + } + + FrozenDictionary frozen = source.ToFrozenDictionary(GetKeyIEqualityComparer()); + + Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[0]]); + Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[1]]); + } + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Keys_ContainsAllCorrectKeys(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Key); + + IReadOnlyDictionary rod = (IReadOnlyDictionary)dictionary; + Assert.True(expected.SequenceEqual(rod.Keys)); + Assert.All(expected, k => rod.ContainsKey(k)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void IReadOnlyDictionary_Generic_Values_ContainsAllCorrectValues(int count) + { + IDictionary dictionary = GenericIDictionaryFactory(count); + IEnumerable expected = dictionary.Select((pair) => pair.Value); + + IReadOnlyDictionary rod = (IReadOnlyDictionary)dictionary; + Assert.True(expected.SequenceEqual(rod.Values)); + + foreach (KeyValuePair pair in dictionary) + { + Assert.Equal(dictionary[pair.Key], rod[pair.Key]); + } + + Assert.All(dictionary, pair => + { + Assert.True(rod.TryGetValue(pair.Key, out TValue value)); + Assert.Equal(pair.Value, value); + }); + } + } + + public abstract class FrozenDictionary_Generic_Tests_string_string : FrozenDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) + { + return new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + } + + protected override string CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + + protected override string CreateTValue(int seed) => CreateTKey(seed); + } + + public class FrozenDictionary_Generic_Tests_string_string_Default : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => EqualityComparer.Default; + } + + public class FrozenDictionary_Generic_Tests_string_string_Ordinal : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => StringComparer.Ordinal; + } + + public class FrozenDictionary_Generic_Tests_string_string_OrdinalIgnoreCase : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => StringComparer.OrdinalIgnoreCase; + } + + public class FrozenDictionary_Generic_Tests_string_string_NonDefault : FrozenDictionary_Generic_Tests_string_string + { + public override IEqualityComparer GetKeyIEqualityComparer() => NonDefaultEqualityComparer.Instance; + } + + public class FrozenDictionary_Generic_Tests_ulong_ulong : FrozenDictionary_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override KeyValuePair CreateT(int seed) + { + ulong key = CreateTKey(seed); + ulong value = CreateTKey(~seed); + return new KeyValuePair(key, value); + } + + protected override ulong CreateTKey(int seed) + { + Random rand = new Random(seed); + ulong hi = unchecked((ulong)rand.Next()); + ulong lo = unchecked((ulong)rand.Next()); + return (hi << 32) | lo; + } + + protected override ulong CreateTValue(int seed) => CreateTKey(seed); + + [OuterLoop("Takes several seconds")] + [Theory] + [InlineData(8_000_000)] + public void CreateHugeDictionary_Success(int largeCount) + { + GenericIDictionaryFactory(largeCount); + } + } + + public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override KeyValuePair CreateT(int seed) + { + Random rand = new Random(seed); + return new KeyValuePair(rand.Next(), rand.Next()); + } + + protected override int CreateTKey(int seed) => new Random(seed).Next(); + + protected override int CreateTValue(int seed) => CreateTKey(seed); + } + + public class FrozenDictionary_Generic_Tests_SimpleClass_SimpleClass : FrozenDictionary_Generic_Tests + { + protected override KeyValuePair CreateT(int seed) + { + return new KeyValuePair(CreateTKey(seed), CreateTKey(seed + 500)); + } + + protected override SimpleClass CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return new SimpleClass { Value = Convert.ToBase64String(bytes1) }; + } + + protected override SimpleClass CreateTValue(int seed) => CreateTKey(seed); + } + + public class SimpleClass : IComparable + { + public string Value { get; set; } + + public int CompareTo(SimpleClass? other) => + other is null ? -1 : + Value.CompareTo(other.Value); + } + + public sealed class NonDefaultEqualityComparer : IEqualityComparer + { + public static NonDefaultEqualityComparer Instance { get; } = new(); + public bool Equals(TKey? x, TKey? y) => EqualityComparer.Default.Equals(x, y); + public int GetHashCode([DisallowNull] TKey obj) => EqualityComparer.Default.GetHashCode(obj); + } + + public class FrozenDictionary_NonGeneric_Tests : IDictionary_NonGeneric_Tests + { + protected override IDictionary NonGenericIDictionaryFactory() => FrozenDictionary.Empty; + + protected override IDictionary NonGenericIDictionaryFactory(int count) + { + var d = new Dictionary(); + for (int i = 0; i < count; i++) + { + d.Add(CreateTKey(i), CreateTValue(i)); + } + return d.ToFrozenDictionary(); + } + + protected override ICollection NonGenericICollectionFactory(int count) => NonGenericIDictionaryFactory(count); + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTKey(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + /// + /// Creates an object that is dependent on the seed given. The object may be either + /// a value type or a reference type, chosen based on the value of the seed. + /// + protected override object CreateTValue(int seed) => CreateTKey(seed); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => new List(); + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override bool IsReadOnly => true; + + protected override bool ResetImplemented => true; + + protected override bool IDictionary_NonGeneric_Keys_Values_Enumeration_ResetImplemented => true; + + protected override bool SupportsSerialization => false; + + protected override bool ExpectedIsFixedSize => true; + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectReferenceType_ThrowType => typeof(ArgumentException); + + protected override Type ICollection_NonGeneric_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + [Fact] + public void ICollection_CopyTo_MultipleArrayTypesSupported() + { + FrozenDictionary frozen = new Dictionary() + { + { "hello", 123 }, + { "world", 456 } + }.ToFrozenDictionary(); + + var kvpArray = new KeyValuePair[4]; + ((ICollection)frozen).CopyTo(kvpArray, 1); + Assert.Equal(new KeyValuePair(null, 0), kvpArray[0]); + Assert.Equal(new KeyValuePair("hello", 123), kvpArray[1]); + Assert.Equal(new KeyValuePair("world", 456), kvpArray[2]); + Assert.Equal(new KeyValuePair(null, 0), kvpArray[3]); + + var deArray = new DictionaryEntry[4]; + ((ICollection)frozen).CopyTo(deArray, 2); + Assert.Equal(new DictionaryEntry(null, null), deArray[0]); + Assert.Equal(new DictionaryEntry(null, null), deArray[1]); + Assert.Equal(new DictionaryEntry("hello", 123), deArray[2]); + Assert.Equal(new DictionaryEntry("world", 456), deArray[3]); + } + } +} diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs new file mode 100644 index 0000000000000..c0be30bbe8252 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Tests; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xunit; + +namespace System.Collections.Frozen.Tests +{ + public abstract class FrozenSet_Generic_Tests : ISet_Generic_Tests + { + protected override bool ResetImplemented => true; + + protected override ISet GenericISetFactory() => Array.Empty().ToFrozenSet(); + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Array.Empty(); + + protected override bool IsReadOnly => true; + + protected override EnumerableOrder Order => EnumerableOrder.Unspecified; + + protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException); + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override ISet GenericISetFactory(int count) + { + var s = new HashSet(); + for (int i = 0; i < count; i++) + { + s.Add(CreateT(i)); + } + return s.ToFrozenSet(GetIEqualityComparer()); + } + + [Theory] + [InlineData(100_000)] + public void CreateVeryLargeSet_Success(int largeCount) + { + GenericISetFactory(largeCount); + } + + [Fact] + public void NullSource_ThrowsException() + { + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet()); + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet(null)); + AssertExtensions.Throws("source", () => ((HashSet)null).ToFrozenSet(EqualityComparer.Default)); + } + + [Fact] + public void EmptySource_ProducedFrozenSetEmpty() + { + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, Enumerable.Empty().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, Array.Empty().ToFrozenSet()); + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet()); + + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, Enumerable.Empty().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, Array.Empty().ToFrozenSet(comparer)); + Assert.Same(FrozenSet.Empty, new List().ToFrozenSet(comparer)); + } + } + + [Fact] + public void EmptyFrozenSet_Idempotent() + { + FrozenSet empty = FrozenSet.Empty; + + Assert.NotNull(empty); + Assert.Same(empty, FrozenSet.Empty); + } + + [Fact] + public void EmptyFrozenSet_OperationsAreNops() + { + FrozenSet empty = FrozenSet.Empty; + + Assert.Same(EqualityComparer.Default, empty.Comparer); + Assert.Equal(0, empty.Count); + Assert.Empty(empty.Items); + + T item = CreateT(0); + Assert.False(empty.Contains(item)); + + empty.CopyTo(Span.Empty); + T[] array = new T[1]; + empty.CopyTo(array); + Assert.Equal(default, array[0]); + + int count = 0; + foreach (T value in empty) + { + count++; + } + Assert.Equal(0, count); + } + + [Fact] + public void FrozenSet_ToFrozenSet_Idempotent() + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + Assert.Same(FrozenSet.Empty, FrozenSet.Empty.ToFrozenSet(comparer)); + } + + FrozenSet frozen = new HashSet() { { CreateT(0) } }.ToFrozenSet(); + Assert.Same(frozen, frozen.ToFrozenSet()); + Assert.NotSame(frozen, frozen.ToFrozenSet(NonDefaultEqualityComparer.Instance)); + } + + public static IEnumerable LookupItems_AllItemsFoundAsExpected_MemberData() + { + foreach (int size in new[] { 1, 2, 10, 999, 1024 }) + { + foreach (IEqualityComparer comparer in new IEqualityComparer[] { null, EqualityComparer.Default, NonDefaultEqualityComparer.Instance }) + { + foreach (bool specifySameComparer in new[] { false, true }) + { + yield return new object[] { size, comparer, specifySameComparer }; + } + } + } + } + + [Theory] + [MemberData(nameof(LookupItems_AllItemsFoundAsExpected_MemberData))] + public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer comparer, bool specifySameComparer) + { + HashSet original = new HashSet(Enumerable.Range(0, size).Select(CreateT), comparer); + T[] originalItems = original.ToArray(); + + FrozenSet frozen = specifySameComparer ? + original.ToFrozenSet(comparer) : + original.ToFrozenSet(); + + // Make sure creating the frozen dictionary didn't alter the original + Assert.Equal(originalItems.Length, original.Count); + Assert.All(originalItems, p => Assert.True(frozen.Contains(p))); + + // Make sure the frozen dictionary matches the original + Assert.Equal(original.Count, frozen.Count); + Assert.Equal(original, new HashSet(frozen)); + Assert.All(originalItems, p => Assert.True(frozen.Contains(p))); + if (specifySameComparer || + comparer is null || + comparer == EqualityComparer.Default) + { + Assert.Equal(original.Comparer, frozen.Comparer); + } + + // Generate additional items and ensure they match iff the original matches. + for (int i = size; i < size + 100; i++) + { + T item = CreateT(i); + Assert.Equal(original.Contains(item), frozen.Contains(item)); + } + } + } + + public abstract class FrozenSet_Generic_Tests_string : FrozenSet_Generic_Tests + { + protected override string CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return Convert.ToBase64String(bytes1); + } + } + + public class FrozenSet_Generic_Tests_string_Default : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => EqualityComparer.Default; + } + + public class FrozenSet_Generic_Tests_string_Ordinal : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => StringComparer.Ordinal; + } + + public class FrozenSet_Generic_Tests_string_OrdinalIgnoreCase : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => StringComparer.OrdinalIgnoreCase; + + [Fact] + public void TryGetValue_FindsExpectedResult() + { + FrozenSet frozen = new[] { "abc" }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); + + Assert.False(frozen.TryGetValue("ab", out string actualValue)); + Assert.Null(actualValue); + + Assert.True(frozen.TryGetValue("ABC", out actualValue)); + Assert.Equal("abc", actualValue); + } + } + + public class FrozenSet_Generic_Tests_string_NonDefault : FrozenSet_Generic_Tests_string + { + protected override IEqualityComparer GetIEqualityComparer() => NonDefaultEqualityComparer.Instance; + } + + public class FrozenSet_Generic_Tests_ulong : FrozenSet_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override ulong CreateT(int seed) + { + Random rand = new Random(seed); + ulong hi = unchecked((ulong)rand.Next()); + ulong lo = unchecked((ulong)rand.Next()); + return (hi << 32) | lo; + } + } + + public class FrozenSet_Generic_Tests_int : FrozenSet_Generic_Tests + { + protected override bool DefaultValueAllowed => true; + + protected override int CreateT(int seed) => new Random(seed).Next(); + } + + public class FrozenSet_Generic_Tests_SimpleClass : FrozenSet_Generic_Tests + { + protected override SimpleClass CreateT(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes1 = new byte[stringLength]; + rand.NextBytes(bytes1); + return new SimpleClass { Value = Convert.ToBase64String(bytes1) }; + } + } + + public class FrozenSet_NonGeneric_Tests : ICollection_NonGeneric_Tests + { + protected override ICollection NonGenericICollectionFactory() => + Array.Empty().ToFrozenSet(); + + protected override ICollection NonGenericICollectionFactory(int count) + { + var set = new HashSet(); + var rand = new Random(42); + while (set.Count < count) + { + set.Add(rand.Next().ToString(CultureInfo.InvariantCulture)); + } + return set.ToFrozenSet(); + } + + protected override bool IsReadOnly => true; + + protected override bool Enumerator_Current_UndefinedOperation_Throws => true; + + protected override bool ResetImplemented => true; + + protected override IEnumerable GetModifyEnumerables(ModifyOperation operations) => Array.Empty(); + + protected override void AddToCollection(ICollection collection, int numberOfItemsToAdd) => throw new NotImplementedException(); + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectReferenceType_ThrowType => typeof(InvalidCastException); + + protected override Type ICollection_NonGeneric_CopyTo_ArrayOfIncorrectValueType_ThrowType => typeof(InvalidCastException); + + protected override Type ICollection_NonGeneric_CopyTo_NonZeroLowerBound_ThrowType => typeof(ArgumentOutOfRangeException); + } +} diff --git a/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj b/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj index fc886aaeaac34..f5e57da3629f5 100644 --- a/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj +++ b/src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj @@ -8,9 +8,7 @@ - + @@ -38,43 +36,28 @@ - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs index a70b9bf5c0c89..510ba278f30d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs @@ -28,7 +28,7 @@ internal static partial class HashHelpers // 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. - private static readonly int[] s_primes = + internal static readonly int[] s_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,