// Copyright (c) 2022 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using FisheryLib.Collections; namespace FisheryLib; [PublicAPI] public static class CollectionExtensions { public static bool TryGetItem(this List list, int index, [NotNullWhen(true)] out T? item) { if (list._size > index) { item = list._items[index]!; return true; } item = default; return false; } /// /// Taken from Span.this[] and slower than List.this[] due to the ArrayAdjustment /// internal static T GetItemUnchecked(this List list, int index) => Unsafe.Add( ref Unsafe.AddByteOffset(ref Unsafe.As>(list._items).Data, PerTypeValues.ArrayAdjustment), index)!; [StructLayout(LayoutKind.Sequential)] private sealed class Pinnable { public T? Data; } private static class PerTypeValues { public static readonly IntPtr ArrayAdjustment = MeasureArrayAdjustment(); private static IntPtr MeasureArrayAdjustment() { var array = new T[1]; return Unsafe.ByteOffset(ref Unsafe.As>(array).Data!, ref array[0]); } } public static bool TryGetItem(this T[] array, int index, out T? item) { if (array.Length > index) { item = array[index]; return true; } item = default; return false; } public static T[] ToArray(this IEnumerable enumerable, int length) { var array = new T[length]; if (enumerable is IList iList) array.Fill(iList); else array.FillUsingEnumerable(enumerable); return array; } public static TElement[] ToArray(this TEnumerable enumerable, TElement[] destination) where TEnumerable : IList { Guard.IsNotNull(destination); destination.Fill(enumerable); return destination; } public static T[] ToArray(this IEnumerable enumerable, T[] destination) { Guard.IsNotNull(destination); if (enumerable is IList iList) destination.Fill(iList); else destination.FillUsingEnumerable(enumerable); return destination; } private static void FillUsingEnumerable(this T[] array, IEnumerable enumerable, int startIndex = 0, int count = -1) { var i = 0u; foreach (var item in enumerable) { if (i >= (uint)count || startIndex >= array.Length) break; array[startIndex++] = item; i++; } } public static void Fill(this TElement[] array, TCollection collection, int startIndex = 0, int count = -1) where TCollection : IList { Guard.IsNotNull(collection); if (count <= 0) count = collection.Count; if (count <= array.Length - startIndex) { collection.CopyTo(array, startIndex); return; } for (var listIndex = 0; listIndex < count && startIndex < array.Length; startIndex++, listIndex++) array[startIndex] = collection[listIndex]; } public static void Fill(this T[] array, IEnumerable enumerable, int startIndex = 0, int count = -1) { Guard.IsNotNull(enumerable); if (enumerable is IList iList) array.Fill(iList, startIndex, count); else array.FillUsingEnumerable(enumerable, startIndex, count); } public static ref TValue GetReference(this Dictionary dictionary, TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(key) & int.MaxValue; // HashCode.Get can handle null, & int.MaxValue removes any sign var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) return ref entry.value; bucket = entry.next; } } return ref ThrowKeyNotFoundException(key); } public static ref TValue GetReference(this Dictionary dictionary, ref TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && Equality.EqualsByRef(ref entry.key, ref key)) return ref entry.value; bucket = entry.next; } } return ref ThrowKeyNotFoundException(key); } /// /// Returns a reference to the value field of an entry if the key exists and an Unsafe.NullRef{TValue} if not. /// Must be checked with Unsafe.IsNullRef before assigning to a regular var or field without ref. /// /// The instance /// The key /// ref TValue or Unsafe.NullRef{TValue} public static ref TValue TryGetReferenceUnsafe(this Dictionary dictionary, TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) return ref entry.value; bucket = entry.next; } } return ref Unsafe.NullRef(); } public static ref TValue TryGetReferenceUnsafe(this Dictionary dictionary, ref TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && Equality.EqualsByRef(ref entry.key, ref key)) return ref entry.value; bucket = entry.next; } } return ref Unsafe.NullRef(); } public static TValue? TryGetValue(this Dictionary dictionary, ref TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && Equality.EqualsByRef(ref entry.key, ref key)) return entry.value; bucket = entry.next; } } return default; } [return: MaybeNull] public static TValue TryGetValue(this Dictionary dictionary, TKey key) { if (dictionary.buckets != null) { var keyCode = HashCode.Get(key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) return entry.value; bucket = entry.next; } } return default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TValue GetOrAddReference(this Dictionary dictionary, TKey key) where TValue : new() { var keyCode = HashCode.Get(key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) { var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) return ref entry.value; bucket = entry.next; } } dictionary.Add(key, Reflection.New()); goto StartOfLookup; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TValue GetOrAddReference(this Dictionary dictionary, ref TKey key) where TValue : new() { var keyCode = HashCode.Get(ref key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) { var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && Equality.EqualsByRef(ref entry.key, ref key)) return ref entry.value; bucket = entry.next; } } dictionary.Add(key, Reflection.New()); goto StartOfLookup; } // public static bool TryGetOrAddReference(this Dictionary dictionary, TKey key, // ref TValue reference) // { // var keyCode = HashCode.Get(ref key) & int.MaxValue; // var result = true; // // StartOfLookup: // if (dictionary.buckets != null) // { // var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; // while (bucket >= 0) // { // ref var entry = ref dictionary.entries[bucket]; // if (entry.hashCode == keyCode && entry.key.Equals(key)) // { // reference.Value = ref entry.value; // return result; // } // // bucket = entry.next; // } // } // // result = false; // dictionary.Add(key, Reflection.New()); // goto StartOfLookup; // } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TValue GetOrAdd(this Dictionary dictionary, TKey key) where TValue : new() { var keyCode = HashCode.Get(key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) { var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) return entry.value; bucket = entry.next; } } dictionary.Add(key, Reflection.New()); goto StartOfLookup; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TValue GetOrAdd(this Dictionary dictionary, ref TKey key) where TValue : new() { var keyCode = HashCode.Get(ref key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) { var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && Equality.EqualsByRef(ref entry.key, ref key)) return entry.value; bucket = entry.next; } } dictionary.Add(key, Reflection.New()); goto StartOfLookup; } public static bool TryGetOrAddValue(this Dictionary dictionary, TKey key, out TValue value) { var keyCode = HashCode.Get(ref key) & int.MaxValue; var result = true; StartOfLookup: if (dictionary.buckets != null) { var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) { ref var entry = ref dictionary.entries[bucket]; if (entry.hashCode == keyCode && entry.key.Equals(key)) { value = entry.value; return result; } bucket = entry.next; } } result = false; dictionary.Add(key, Reflection.New()); goto StartOfLookup; } public static int RemoveWhere(this Dictionary dictionary, Predicate> predicate) { Guard.IsNotNull(dictionary); Guard.IsNotNull(predicate); var entries = dictionary.entries; if (entries is null) return 0; var removedCount = 0; var entriesCount = entries.Length; for (var i = 0; i < entriesCount; i++) { ref var entry = ref entries[i]; if (entry.hashCode < 0) // valid entries are stored with & int.MaxValue on hashCode continue; // predicate might modify collection, turning a ref invalid var keyValuePair = new KeyValuePair(entry.key, entry.value); if (!predicate(keyValuePair)) continue; if (dictionary.Remove(keyValuePair.Key)) removedCount++; } return removedCount; } [DoesNotReturn] private static ref V ThrowKeyNotFoundException(T key) { Guard.IsNotNull(key); ThrowKeyNotFoundException(key); return ref Unsafe.NullRef(); } [DoesNotReturn] private static void ThrowKeyNotFoundException(object key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref int Version(this List list) => ref list._version; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddRangeFast(this List list, List range) { if (range._size < 1) return; list.EnsureCapacity(list._size + range._size); range.UnsafeCopyTo(list, list._size); list._size += range._size; list._version++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsafeCopyTo(this List source, List destination, int destinationStartIndex) => UnsafeBlockCopy(ref source._items[0], ref destination._items[destinationStartIndex], source._size); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsafeCopyTo(this T[] source, T[] destination, int destinationStartIndex) => UnsafeBlockCopy(ref source[0], ref destination[destinationStartIndex], source.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void UnsafeBlockCopy(ref T source, ref T destination, int count) => Unsafe.CopyBlock(ref Unsafe.As(ref destination), ref Unsafe.As(ref source), (uint)((nint)sizeof(T) * count)); /// /// Returns a list's internal _items array. Keep in mind list.Version() needs to change every time items are /// modified. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T[] ItemsUnchecked(this List list) => list._items; /// /// Returns a span pointing to a list's internal _items array. Keep in mind list.Version() needs to change every /// time items are modified. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span AsSpanUnchecked(this List list) => new(list._items, 0, list._size); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan AsReadOnlySpan(this List list) => new(list._items, 0, list._size); public static void ReplaceContentsWith(this List list, [AllowNull] List collection) { if (ReferenceEquals(list, collection)) return; if (collection is null || (collection._size is var collectionCount && collectionCount == 0)) { list.Clear(); return; } if (collectionCount <= list._size) { if (collectionCount < list._size) list.RemoveRange(collectionCount, list._size - collectionCount); collection.UnsafeCopyTo(list, 0); } else { list._items = new T[collectionCount]; collection.UnsafeCopyTo(list, 0); list._size = collectionCount; } list._version++; } public static void ReplaceContentsWith(this List list, [AllowNull] TOther collection) where TOther : ICollection { if (typeof(TOther) == typeof(List)) list.ReplaceContentsWith(Unsafe.As>(collection)); if (ReferenceEquals(list, collection)) return; if (collection is null || (collection.Count is var collectionCount && collectionCount == 0)) { list.Clear(); return; } if (collectionCount <= list._size) { if (collectionCount < list._size) list.RemoveRange(collectionCount, list._size - collectionCount); collection.CopyTo(list._items, 0); } else { list._items = new TThis[collectionCount]; collection.CopyTo(list._items, 0); list._size = collectionCount; } list._version++; } public static FishTable ToFishTable(this IEnumerable source, Func keySelector) => source.ToFishTable(keySelector, static item => item); public static FishTable ToFishTable(this IEnumerable source, Func keySelector, Func valueSelector) { Guard.IsNotNull(source); Guard.IsNotNull(keySelector); Guard.IsNotNull(valueSelector); var capacity = 0; if (source is ICollection iCollection) capacity = (int)(iCollection.Count * 1.5f); var fishTable = new FishTable(capacity); fishTable.AddRange(source, keySelector, valueSelector); return fishTable; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clear(this T[] array) => Array.Clear(array, 0, array.Length); // public static List Copy(this List source) // { // Guard.IsNotNull(source); // // var destination = new List(source._size); // // if (source._size == 0) // return destination; // // // Unsafe.CopyBlock(ref Unsafe.As(ref destination._items[0]), // // ref Unsafe.As(ref source._items[0]), (uint)sizeof(T) * (uint)source._size); // // FisheryLib.CollectionExtensions.UnsafeBlockCopy(ref source._items[0], // does a type check that causes issues // ref destination._items[0], source._size); // when called from transpiled methods // // destination._size = source._size; // // return destination; // } public static unsafe List Copy(this List source) { Guard.IsNotNull(source); var destination = new List(source._size); if (source._size == 0) return destination; fixed (void* destinationPointer = &destination._items[0], sourcePointer = &source._items[0]) Unsafe.CopyBlock(destinationPointer, sourcePointer, (uint)(source._size * sizeof(T))); destination._size = source._size; return destination; } public static void Fill(this T[] array, T value) { Guard.IsNotNull(array); var i = array.Length - 1; if (i < 0) return; do { array.UnsafeStore(i, value); } while (--i >= 0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T UnsafeLoad(this T[] array, int index) => Array.UnsafeLoad(array, index); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsafeStore(this T[] array, int index, T value) => Array.UnsafeStore(array, index, value); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TTo UnsafeCast(this TFrom obj) => Array.UnsafeMov(obj); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddRangeFast(this List list, T[] range) { Guard.IsNotNull(list); Guard.IsNotNull(range); if (range.Length < 1) return; list.EnsureCapacity(list._size + range.Length); range.UnsafeCopyTo(list._items, list._size); list._size += range.Length; list._version++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Add(this List list, string value) { Guard.IsNotNull(list); Guard.IsNotNull(value); if (value.Length < 1) return; list.EnsureCapacity(list._size + value.Length); UnsafeBlockCopy(ref value.m_firstChar, ref list._items[list._size], value.Length); list._size += value.Length; list._version++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static StringBuilder Append(this StringBuilder builder, List value) => builder.Append(value._items, 0, value._size); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static StringBuilder Append(this StringBuilder builder, T? value) // ReSharper disable once RedundantToStringCallForValueType // ReSharper disable once CompareNonConstrainedGenericWithNull // ReSharper disable once HeapView.PossibleBoxingAllocation => value == null ? builder : builder.Append(value.ToString()); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T GetReference(this List list, int index) { Guard.IsInRange(index, 0, list.Count); list._version++; return ref list._items[index]; } [Obsolete("Renamed to GetReferenceUnsafe")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T GetReferenceUnverifiable(this List list, int index) => ref list.GetReferenceUnsafe(index); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref T GetReferenceUnsafe(this List list, int index) => ref list._items[index]; public static IList TryNew(IEnumerable enumerable, Type type) => GetListExtensionMethods(type).New(enumerable); public static void TryAddRange(this IList list, T collection, Type type) where T : ICollection => GetListExtensionMethods(type).AddRange(list, collection); public static Array TryToArray(this IList list, Type type) => GetListExtensionMethods(type).ToArray(list); public static void RemoveAtFastUnordered(this List list, int index) { ref var lastBucket = ref list._items[list._size - 1]; list[index] = lastBucket; lastBucket = default!; list._size--; } public static bool RemoveFastUnordered(this List list, T item) { var index = list.IndexOf(item); if (index < 0) return false; list.RemoveAtFastUnordered(index); return true; } public static void InsertFastUnordered(this List list, int index, T item) { if ((uint)index > (uint)list._size) ThrowHelper.ThrowArgumentOutOfRangeException(); if (list._size == list._items.Length) list.EnsureCapacity(list._size + 1); ref var targetBucket = ref list._items[index]; list._items[list._size] = targetBucket; targetBucket = item; list._size++; list._version++; } public static unsafe uint GetSizeEstimate(this Dictionary dictionary) => dictionary.GetType().ComputeManagedObjectSizeEstimate() + ((uint)sizeof(Dictionary.Entry) * (uint)(dictionary.entries?.Length ?? 0)) + (sizeof(int) * (uint)(dictionary.buckets?.Length ?? 0)); internal static unsafe uint ComputeManagedObjectSizeEstimate(this Type type) { var objectHeader = (uint)sizeof(void*) * 2u; var cachedSize = CachedManagedObjectSizes.GetOrAdd(type.TypeHandle.Value); // can't use GetOrAddReference here due to recursion if (cachedSize.Size == uint.MaxValue) UpdateCachedManagedObjectSize(type, ref cachedSize); return objectHeader + cachedSize.Size; } private static void UpdateCachedManagedObjectSize(Type type, ref ManagedObjectSize cachedSize) { cachedSize.Size = 0; var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); for (var i = fields.Length; i-- > 0;) { var fieldType = fields[i].FieldType; if (!typeof(ValueType).IsAssignableFrom(fieldType) || fieldType == typeof(ValueType)) // IsValueType and IsSubclassOf somehow cause StackOverflows here cachedSize.Size += (uint)IntPtr.Size; else if (fieldType.IsPrimitive) cachedSize.Size += (uint)Marshal.SizeOf(fieldType); else if (fieldType != type) cachedSize.Size += fieldType.ComputeManagedObjectSizeEstimate(); else cachedSize.Size += (uint)IntPtr.Size; // fuck it } var remainder = cachedSize.Size % (uint)IntPtr.Size; if (remainder != 0u) cachedSize.Size += (uint)IntPtr.Size - remainder; CachedManagedObjectSizes[type.TypeHandle.Value] = cachedSize; } private static FishTable CachedManagedObjectSizes => _cachedManagedObjectSizes ??= new(); [ThreadStatic] private static FishTable? _cachedManagedObjectSizes; private struct ManagedObjectSize { public uint Size = uint.MaxValue; [UsedImplicitly] public ManagedObjectSize() { } } public static GenericBase GetListExtensionMethods(Type type) => ListExtensionCache.TryGetValue(type) ?? CreateListExtensionMethods(type); private static GenericBase CreateListExtensionMethods(Type type) { var methods = (GenericBase)Activator.CreateInstance(typeof(Generic<>).MakeGenericType(type)); ListExtensionCache[type] = methods; return methods; } private static Dictionary ListExtensionCache => _listExtensionCache ??= new(); [ThreadStatic] private static Dictionary? _listExtensionCache; public abstract class GenericBase { public abstract void AddRange(IList list, ICollection collection); public abstract Array ToArray(IList list); public abstract IList New(IEnumerable enumerable); } public class Generic : GenericBase { [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void AddRange(IList list, ICollection collection) => ((List)list).AddRange((IEnumerable)collection); [MethodImpl(MethodImplOptions.AggressiveInlining)] public override Array ToArray(IList list) => ((List)list).ToArray(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public override IList New(IEnumerable enumerable) => new List((IEnumerable)enumerable); } }