diff --git a/1.3/Assemblies/1Fishery.dll b/1.3/Assemblies/1Fishery.dll index ae26877..8062cf4 100644 Binary files a/1.3/Assemblies/1Fishery.dll and b/1.3/Assemblies/1Fishery.dll differ diff --git a/1.3/Assemblies/1Fishery.pdb b/1.3/Assemblies/1Fishery.pdb index efc3455..eed1a7c 100644 Binary files a/1.3/Assemblies/1Fishery.pdb and b/1.3/Assemblies/1Fishery.pdb differ diff --git a/1.4/Assemblies/1Fishery.dll b/1.4/Assemblies/1Fishery.dll index 25f63b3..1f30969 100644 Binary files a/1.4/Assemblies/1Fishery.dll and b/1.4/Assemblies/1Fishery.dll differ diff --git a/1.4/Assemblies/1Fishery.pdb b/1.4/Assemblies/1Fishery.pdb index 0904abb..6693544 100644 Binary files a/1.4/Assemblies/1Fishery.pdb and b/1.4/Assemblies/1Fishery.pdb differ diff --git a/About/Preview.dds b/About/Preview.dds new file mode 100644 index 0000000..fe95a25 Binary files /dev/null and b/About/Preview.dds differ diff --git a/About/Preview.png b/About/Preview.png new file mode 100644 index 0000000..4877302 Binary files /dev/null and b/About/Preview.png differ diff --git a/README.md b/README.md index 6ed94ad..1e31e56 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Fishery +![](About/Preview.png?raw=true) + Modding Library for RimWorld. Used by Performance Fish. Useful functions include various abstractions around IL in FishTranspiler, to make writing of transpilers easier and more robust. A couple collections like Dictionary and Span have extension methods for fast and convenient lookups. Some functionalities of Microsoft's Community Toolkit are included, expanded upon and modified to better fit Unity. Equality includes generic extension methods to use in place of Equals on structs without memory allocations. Convert provides a faster, generic alternative to System.Convert.ToType. FisheryLib.Reflection provides a couple reflection helpers, Aliases has a methodof function intended to be utilized with `global using static Aliases` to directly retrieve method infos from static method groups. Additionally this includes a fast and much more versatile System.Collections.Generic.Dictionary replacement in FisheryLib.Collections.FishTable, loosely based on Malte Skarupke's ska::bytell_hash_map, as well as several extensions and helpers for Mono.Cecil, which is utilized by Prepatcher. \ No newline at end of file diff --git a/Source/FisheryLib/Cecil/CecilExtensions.cs b/Source/FisheryLib/Cecil/CecilExtensions.cs index 2b2f4eb..cb6a432 100644 --- a/Source/FisheryLib/Cecil/CecilExtensions.cs +++ b/Source/FisheryLib/Cecil/CecilExtensions.cs @@ -3,9 +3,12 @@ // 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.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; using Mono.Collections.Generic; using Verse; using CallSite = Mono.Cecil.CallSite; @@ -334,8 +337,8 @@ public static class CecilExtensions public static void ReplaceBodyWith(this ILProcessor ilProcessor, MethodInfo replacement) { - var oldBody = ilProcessor.Body; - var module = oldBody.Method.Module; + var targetBody = ilProcessor.Body; + var module = targetBody.Method.Module; if (replacement.IsGenericMethod) replacement = replacement.GetGenericMethodDefinition(); @@ -348,26 +351,40 @@ public static class CecilExtensions var newMethodReference = module.ImportReference(replacement); var newBody = newMethodReference.Resolve().Body; + + newBody.SimplifyMacros(); + MethodBodyRocks.ComputeOffsets(newBody); - oldBody.Instructions.Clear(); + var targetInstructions = targetBody.instructions; + targetInstructions.Clear(); + foreach (var instruction in newBody.instructions) { - oldBody.Instructions.Add(new(instruction.OpCode, instruction.Operand switch + targetInstructions.Add(new(instruction.OpCode, instruction.Operand switch { FieldReference field => module.ImportReference(field, newMethodReference), MethodReference method => module.ImportReference(method, newMethodReference), TypeReference type => module.ImportReference(type, newMethodReference), + ParameterReference parameter => targetBody.GetParameter(parameter.Index), _ => instruction.Operand })); } + + MethodBodyRocks.ComputeOffsets(targetBody); + + foreach (var instruction in targetInstructions) + { + if (instruction.Operand is Instruction operand) + instruction.Operand = targetInstructions.First(ti => ti.Offset == operand.Offset); + } - oldBody.Variables.Clear(); + targetBody.Variables.Clear(); foreach (var variable in newBody.Variables) - oldBody.Variables.Add(new(module.ImportReference(variable.VariableType, newMethodReference))); + targetBody.Variables.Add(new(module.ImportReference(variable.VariableType, newMethodReference))); - oldBody.MaxStackSize = newBody.MaxStackSize; + targetBody.MaxStackSize = newBody.MaxStackSize; - oldBody.ExceptionHandlers.Clear(); + targetBody.ExceptionHandlers.Clear(); foreach (var exceptionHandler in newBody.ExceptionHandlers) { var handler = new ExceptionHandler(exceptionHandler.HandlerType) @@ -382,10 +399,13 @@ public static class CecilExtensions if (exceptionHandler.CatchType is { } catchType) handler.CatchType = module.ImportReference(catchType, newMethodReference); - oldBody.ExceptionHandlers.Add(handler); + targetBody.ExceptionHandlers.Add(handler); } - oldBody.Method.ImplAttributes = newBody.Method.ImplAttributes; + targetBody.Method.ImplAttributes = newBody.Method.ImplAttributes; + + targetBody.OptimizeMacros(); + newBody.OptimizeMacros(); } public static MethodDefinition GetMethod(this ILProcessor ilProcessor) => ilProcessor.Body.Method; @@ -533,6 +553,7 @@ public static class CecilExtensions // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract $"Argument is of invalid type: {instruction?.GetType().FullName ?? "NULL"}"); + [SuppressMessage("Maintainability", "CA1508")] private static Instruction CreateInstruction(OpCode opCode, object operand) => operand switch { @@ -554,6 +575,7 @@ public static class CecilExtensions ParameterDefinition parameter => Instruction.Create(opCode, parameter), _ => ThrowHelper.ThrowArgumentException(nameof(operand), // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - $"Operand for OpCode {opCode} is of invalid type: {operand?.GetType().FullName ?? "NULL"}") + $"Operand for OpCode {opCode} is of invalid type: { + operand?.GetType().FullName ?? operand?.GetType().Name ?? "NULL"}") }; } \ No newline at end of file diff --git a/Source/FisheryLib/Cecil/InstructionExtensions.cs b/Source/FisheryLib/Cecil/InstructionExtensions.cs new file mode 100644 index 0000000..32e9df0 --- /dev/null +++ b/Source/FisheryLib/Cecil/InstructionExtensions.cs @@ -0,0 +1,35 @@ +// 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 FisheryLib.Collections; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace FisheryLib.Cecil; + +public static class InstructionExtensions +{ + public static bool LoadsArgument(this Instruction instruction, int index) + => instruction.LoadsArgument() && GetLoadArgumentIndex(instruction) == index; + + public static bool LoadsArgument(this Instruction instruction) => LoadArgumentOpcodes.Contains(instruction.OpCode); + + private static int GetLoadArgumentIndex(Instruction instruction) + { + var opCode = instruction.OpCode; + return opCode == OpCodes.Ldarg_0 ? 0 + : opCode == OpCodes.Ldarg_1 ? 1 + : opCode == OpCodes.Ldarg_2 ? 2 + : opCode == OpCodes.Ldarg_3 ? 3 + : instruction.Operand is ParameterDefinition parameterDefinition ? parameterDefinition.Index + : Convert.Type(instruction.Operand); + } + + public static readonly FishSet LoadArgumentOpcodes = + [ + OpCodes.Ldarg, OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3, OpCodes.Ldarg_S, + OpCodes.Ldarga, OpCodes.Ldarga_S + ]; +} \ No newline at end of file diff --git a/Source/FisheryLib/Cecil/ReflectionExtensions.cs b/Source/FisheryLib/Cecil/ReflectionExtensions.cs index ab83ad7..d00df79 100644 --- a/Source/FisheryLib/Cecil/ReflectionExtensions.cs +++ b/Source/FisheryLib/Cecil/ReflectionExtensions.cs @@ -58,6 +58,8 @@ public static class ReflectionExtensions private static T? TryGetNamed(this IList memberInfos, string name, Func nameGetter) where T : class { + Guard.IsNotNull(memberInfos); + for (var i = 0; i < memberInfos.Count; i++) { if (nameGetter(memberInfos[i]) == name) @@ -69,6 +71,8 @@ public static class ReflectionExtensions public static TypeReference[] GetTypeReferences(this Type[] types, ModuleDefinition module) { + Guard.IsNotNull(types); + var result = new TypeReference[types.Length]; for (var i = types.Length; i-- > 0;) result[i] = module.ImportReference(types[i]); @@ -77,8 +81,14 @@ public static class ReflectionExtensions } public static bool IsGeneric(this TypeReference type) - => type.ContainsGenericParameter || type.HasGenericParameters || type.IsGenericInstance; + { + Guard.IsNotNull(type); + return type.ContainsGenericParameter || type.HasGenericParameters || type.IsGenericInstance; + } public static bool IsGeneric(this Type type) - => type.ContainsGenericParameters || type.IsGenericType || type.IsConstructedGenericType; + { + Guard.IsNotNull(type); + return type.ContainsGenericParameters || type.IsGenericType || type.IsConstructedGenericType; + } } \ No newline at end of file diff --git a/Source/FisheryLib/CollectionExtensions.cs b/Source/FisheryLib/CollectionExtensions.cs index c612a65..1c32f8a 100644 --- a/Source/FisheryLib/CollectionExtensions.cs +++ b/Source/FisheryLib/CollectionExtensions.cs @@ -145,7 +145,8 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(key) & 0x7FFFFFFF; // HashCode.Get can handle null, & 0x7FFFFFFF removes any sign + 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) @@ -165,7 +166,7 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) @@ -192,7 +193,7 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) @@ -212,7 +213,7 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) @@ -232,7 +233,7 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) @@ -253,7 +254,7 @@ public static class CollectionExtensions { if (dictionary.buckets != null) { - var keyCode = HashCode.Get(key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(key) & int.MaxValue; var bucket = dictionary.buckets[keyCode % dictionary.buckets.Length]; while (bucket >= 0) @@ -273,7 +274,7 @@ public static class CollectionExtensions public static ref TValue GetOrAddReference(this Dictionary dictionary, TKey key) where TValue : new() { - var keyCode = HashCode.Get(key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) @@ -297,7 +298,7 @@ public static class CollectionExtensions public static ref TValue GetOrAddReference(this Dictionary dictionary, ref TKey key) where TValue : new() { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) @@ -320,7 +321,7 @@ public static class CollectionExtensions // public static bool TryGetOrAddReference(this Dictionary dictionary, TKey key, // ref TValue reference) // { - // var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + // var keyCode = HashCode.Get(ref key) & int.MaxValue; // var result = true; // // StartOfLookup: @@ -349,7 +350,7 @@ public static class CollectionExtensions public static TValue GetOrAdd(this Dictionary dictionary, TKey key) where TValue : new() { - var keyCode = HashCode.Get(key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) @@ -373,7 +374,7 @@ public static class CollectionExtensions public static TValue GetOrAdd(this Dictionary dictionary, ref TKey key) where TValue : new() { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; StartOfLookup: if (dictionary.buckets != null) @@ -396,7 +397,7 @@ public static class CollectionExtensions public static bool TryGetOrAddValue(this Dictionary dictionary, TKey key, out TValue value) { - var keyCode = HashCode.Get(ref key) & 0x7FFFFFFF; + var keyCode = HashCode.Get(ref key) & int.MaxValue; var result = true; StartOfLookup: @@ -421,6 +422,38 @@ public static class CollectionExtensions 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) { @@ -640,7 +673,28 @@ public static class CollectionExtensions public static StringBuilder Append(this StringBuilder builder, List value) => builder.Append(value._items, 0, value._size); - public static ref T GetReferenceUnverifiable(this List list, int index) => ref list._items[index]; + [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); @@ -649,6 +703,40 @@ public static class CollectionExtensions 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)) @@ -657,7 +745,8 @@ public static class CollectionExtensions internal static unsafe uint ComputeManagedObjectSizeEstimate(this Type type) { var objectHeader = (uint)sizeof(void*) * 2u; - ref var cachedSize = ref CachedManagedObjectSizes.GetOrAddReference(type.TypeHandle.Value); + var cachedSize = CachedManagedObjectSizes.GetOrAdd(type.TypeHandle.Value); + // can't use GetOrAddReference here due to recursion if (cachedSize.Size == uint.MaxValue) UpdateCachedManagedObjectSize(type, ref cachedSize); @@ -689,6 +778,8 @@ public static class CollectionExtensions 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(); diff --git a/Source/FisheryLib/Collections/FishSet.cs b/Source/FisheryLib/Collections/FishSet.cs index ce432b3..5517cda 100644 --- a/Source/FisheryLib/Collections/FishSet.cs +++ b/Source/FisheryLib/Collections/FishSet.cs @@ -58,9 +58,9 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection get => _count; } - public object SyncRoot => throw new NotImplementedException(); + object ICollection.SyncRoot => this; - public bool IsSynchronized => throw new NotImplementedException(); + bool ICollection.IsSynchronized => false; public bool IsReadOnly => false; @@ -96,7 +96,13 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection get => Contains(key); [CollectionAccess(CollectionAccessType.UpdatedContent)] - set => InsertEntry(key, value); + set + { + if (value) + InsertEntry(key, ReplaceBehaviour.Fail); + else + Remove(key); + } } public FishSet() : this(0) @@ -121,7 +127,7 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection public FishSet(IEnumerable entries) : this(0) { foreach (var entry in entries) - InsertEntry(entry, false); + InsertEntry(entry, ReplaceBehaviour.Throw); } private int GetTailIndex(int entryIndex) => GetIndexForTail(entryIndex, _tails[(uint)entryIndex]); @@ -129,56 +135,6 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetIndexForTail(int entryIndex, uint tail) => (entryIndex + GetJumpDistance(tail)) & _wrapAroundMask; - #region Junk - - // private static long GetJumpDistanceLong(byte index) - // => index switch - // { - // 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, - // 8 => 8, 9 => 9, 10 => 10, 11 => 11, 12 => 12, 13 => 13, 14 => 14, - // 15 => 15, 16 => 21, 17 => 28, 18 => 36, 19 => 45, 20 => 55, 21 => 66, - // 22 => 78, 23 => 91, 24 => 105, 25 => 120, 26 => 136, 27 => 153, - // 28 => 171, 29 => 190, 30 => 210, 31 => 231, 32 => 253, 33 => 276, - // 34 => 300, 35 => 325, 36 => 351, 37 => 378, 38 => 406, 39 => 435, - // 40 => 465, 41 => 496, 42 => 528, 43 => 561, 44 => 595, 45 => 630, - // 46 => 666, 47 => 703, 48 => 741, 49 => 780, 50 => 820, 51 => 861, - // 52 => 903, 53 => 946, 54 => 990, 55 => 1035, 56 => 1081, 57 => 1128, - // 58 => 1176, 59 => 1225, 60 => 1275, 61 => 1326, 62 => 1378, 63 => 1431, - // 64 => 1485, 65 => 1540, 66 => 1596, 67 => 1653, 68 => 1711, 69 => 1770, - // 70 => 1830, 71 => 1891, 72 => 1953, 73 => 2016, 74 => 2080, 75 => 2145, - // 76 => 2211, 77 => 2278, 78 => 2346, 79 => 2415, 80 => 2485, 81 => 2556, - // 82 => 3741, 83 => 8385, 84 => 18915, 85 => 42486, 86 => 95703, 87 => 215496, - // 88 => 485605, 89 => 1091503, 90 => 2456436, 91 => 5529475, 92 => 12437578, - // 93 => 27986421, 94 => 62972253, 95 => 141700195, 96 => 318819126, 97 => 717314626, - // 98 => 1614000520, 99 => 3631437253, 100 => 8170829695, 101 => 18384318876, 102 => 41364501751, - // 103 => 93070021080, 104 => 209407709220, 105 => 471167588430, 106 => 1060127437995, 107 => 2385287281530, - // 108 => 5366895564381, 109 => 12075513791265, 110 => 27169907873235, 111 => 61132301007778, - // 112 => 137547673121001, 113 => 309482258302503, 114 => 696335090510256, 115 => 1566753939653640, - // 116 => 3525196427195653, 117 => 7931691866727775, 118 => 17846306747368716, - // 119 => 40154190394120111, 120 => 90346928493040500, 121 => 203280588949935750, - // 122 => 457381324898247375, 123 => 1029107980662394500, 124 => 2315492957028380766, - // 125 => 5209859150892887590, > 125 => throw new() - // }; - - // private static int GetJumpDistance(byte index) - // => index switch - // { - // 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, 10 => 10, 11 => 11, - // 12 => 12, 13 => 13, 14 => 14, 15 => 15, 16 => 21, 17 => 28, 18 => 36, 19 => 45, 20 => 55, 21 => 66, - // 22 => 78, 23 => 91, 24 => 105, 25 => 120, 26 => 136, 27 => 153, 28 => 171, 29 => 190, 30 => 210, 31 => 231, - // 32 => 253, 33 => 276, 34 => 300, 35 => 325, 36 => 351, 37 => 378, 38 => 406, 39 => 435, 40 => 465, - // 41 => 496, 42 => 528, 43 => 561, 44 => 595, 45 => 630, 46 => 666, 47 => 703, 48 => 741, 49 => 780, - // 50 => 820, 51 => 861, 52 => 903, 53 => 946, 54 => 990, 55 => 1035, 56 => 1081, 57 => 1128, 58 => 1176, - // 59 => 1225, 60 => 1275, 61 => 1326, 62 => 1378, 63 => 1431, 64 => 1485, 65 => 1540, 66 => 1596, 67 => 1653, - // 68 => 1711, 69 => 1770, 70 => 1830, 71 => 1891, 72 => 1953, 73 => 2016, 74 => 2080, 75 => 2145, 76 => 2211, - // 77 => 2278, 78 => 2346, 79 => 2415, 80 => 2485, 81 => 2556, 82 => 3741, 83 => 8385, 84 => 18915, - // 85 => 42486, 86 => 95703, 87 => 215496, 88 => 485605, 89 => 1091503, 90 => 2456436, 91 => 5529475, - // 92 => 12437578, 93 => 27986421, 94 => 62972253, 95 => 141700195, 96 => 318819126, 97 => 717314626, - // 98 => 1614000520, 255 => int.MaxValue, > 98 => ThrowForInvalidIndex(index) - // }; - - #endregion - private static int GetJumpDistance(uint index) => index switch { @@ -191,9 +147,6 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection private static int MultiplyWithPhi(uint index) => throw new NotImplementedException(); // (int)(uint)Math.Round(index * FishMath.PHI); - // private static int ThrowForInvalidIndex(byte index) - // => throw new($"Jump distance index for FishTable too large. Should be < 99, got {index} instead."); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(int bucketIndex) => bucketIndex != GetBucketIndexForKeyAt(bucketIndex); @@ -210,6 +163,12 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection _buckets[bucketIndex] = entry; _tails.SetSolo(bucketIndex); } + + private void SetBucketEmpty(int index) + { + _buckets[index] = default; + _tails.SetEmpty(index); + } private void SetEntryAsTail(int bucketIndex, T entry, int parentIndex, uint offset) { @@ -220,9 +179,9 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection private void SetParentTail(int entryIndex, uint newTail) => _tails[(uint)GetParentBucketIndex(entryIndex)] = newTail; - private bool InsertEntry(T entry, bool allowReplace = true, bool shifting = false) + private bool InsertEntry(T entry, ReplaceBehaviour replaceBehaviour, bool shifting = false) { - var addedNewEntry = InsertEntryInternal(entry, allowReplace); + var addedNewEntry = InsertEntryInternal(entry, replaceBehaviour); if (shifting) return addedNewEntry; @@ -247,7 +206,7 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection /// Returns true when adding a new entry, false for replacing an existing entry. AllowReplace: false causes throwing /// instead. Does not adjust Count, Version or invoke EntryAdded. /// - private bool InsertEntryInternal(T entry, bool allowReplace = true) + private bool InsertEntryInternal(T entry, ReplaceBehaviour replaceBehaviour) { StartOfMethod: var bucketIndex = GetBucketIndex(entry); @@ -259,7 +218,7 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection return true; } - if (TryReplaceValueOfMatchingKey(entry, allowReplace, ref bucket!)) + if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref bucket!)) return false; if (CheckResize()) @@ -268,53 +227,30 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection if (IsTail(bucket, bucketIndex)) { var previousEntry = bucket; - var hasTail = !_tails.IsSolo(bucketIndex); - var tailingEntries = hasTail ? new T[GetTailCount(bucketIndex)] : null; - - if (hasTail) - { - var i = 0; - var tailIndex = GetTailIndex(bucketIndex); - - while (true) - { - ref var tailingBucket = ref _buckets[tailIndex]; - tailingEntries![i++] = tailingBucket!; - tailingBucket = default; - - if (!_tails.IsSoloOrEmpty(tailIndex)) - { - var nextTailIndex = GetTailIndex(tailIndex); - _tails.SetEmpty(tailIndex); - tailIndex = nextTailIndex; - } - else - { - _tails.SetEmpty(tailIndex); - break; - } - } - } + var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryWithoutTail(bucketIndex, entry); - - InsertEntry(previousEntry, shifting: true); - if (hasTail) - { - for (var i = 0; i < tailingEntries!.Length; i++) - InsertEntry(tailingEntries[i], shifting: true); - } + InsertEntry(previousEntry, ReplaceBehaviour.Throw, true); + + if (tailingEntries != null) + InsertEntries(tailingEntries, ReplaceBehaviour.Throw, true); return true; } else { - return !TryFindTailIndexAndReplace(entry, ref bucketIndex, allowReplace) + return !TryFindTailIndexAndReplace(entry, ref bucketIndex, replaceBehaviour) && InsertAsTail(entry, bucketIndex); } } + private void InsertEntries(T[] tailingEntries, ReplaceBehaviour replaceBehaviour, bool shifting = false) + { + for (var i = 0; i < tailingEntries.Length; i++) + InsertEntry(tailingEntries[i], replaceBehaviour, shifting); + } + private int GetTailCount(int index) { var tailCount = 0; @@ -331,30 +267,36 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection private bool IsBucketEmpty(int bucketIndex) => IsBucketEmpty(bucketIndex, _tails, _buckets); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsBucketEmpty(int bucketIndex, Tails tails, T?[] buckets) => tails.IsEmpty(bucketIndex); + private static bool IsBucketEmpty(int bucketIndex, Tails tails, T?[] buckets) => tails.IsEmpty(bucketIndex); - private bool TryFindTailIndexAndReplace(T entry, ref int bucketIndex, bool allowReplace) + private bool TryFindTailIndexAndReplace(T entry, ref int bucketIndex, ReplaceBehaviour replaceBehaviour) { while (true) { if (!TryContinueWithTail(ref bucketIndex)) return false; - if (TryReplaceValueOfMatchingKey(entry, allowReplace, ref _buckets[bucketIndex]!)) + if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref _buckets[bucketIndex]!)) return true; } } - private unsafe bool TryReplaceValueOfMatchingKey(T entry, bool allowReplace, ref T bucket) + private unsafe bool TryReplaceValueOfMatchingKey(T entry, ReplaceBehaviour replaceBehaviour, ref T bucket) { if (!_equalityComparer(bucket, entry)) return false; - if (!allowReplace) - ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry); - - bucket = entry; - return true; + switch (replaceBehaviour) + { + case ReplaceBehaviour.Throw: + return ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry); + case ReplaceBehaviour.Replace: + bucket = entry; + goto default; + case ReplaceBehaviour.Fail: + default: + return true; + } } private bool InsertAsTail(T entry, int bucketIndex) @@ -382,56 +324,55 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection WarnForPossiblyExcessiveResizing(entry); Resize(); - InsertEntry(entry, false, true); + InsertEntry(entry, ReplaceBehaviour.Throw, true); return true; } var previousEntry = _buckets[bucketIndex]; - var hasTail = !_tails.IsSolo(bucketIndex); - - var tailingEntries = hasTail ? new T?[GetTailCount(bucketIndex)] : null; - - if (hasTail) - { - var k = 0; - var tailIndex = GetTailIndex(bucketIndex); - - while (true) - { - ref var tailingBucket = ref _buckets[tailIndex]; - tailingEntries![k] = tailingBucket; - - k++; - tailingBucket = default; - - if (!_tails.IsSoloOrEmpty(tailIndex)) - { - var nextTailIndex = GetTailIndex(tailIndex); - _tails.SetEmpty(tailIndex); - tailIndex = nextTailIndex; - } - else - { - _tails.SetEmpty(tailIndex); - break; - } - } - } + var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryAsTail(bucketIndex, entry, parentIndex, offset); - InsertEntry(previousEntry!, shifting: true); + InsertEntry(previousEntry!, ReplaceBehaviour.Throw, true); - if (hasTail) - { - for (var k = 0; k < tailingEntries!.Length; k++) - InsertEntry(tailingEntries[k]!, shifting: true); - } + if (tailingEntries != null) + InsertEntries(tailingEntries, ReplaceBehaviour.Throw, true); return true; } + private T[]? TryGetAndClearTailingEntries(int bucketIndex) + { + if (!HasTail(bucketIndex)) + return null; + + var tailingEntries = new T[GetTailCount(bucketIndex)]; + var i = 0; + var tailIndex = GetTailIndex(bucketIndex); + + while (true) + { + ref var tailingBucket = ref _buckets[tailIndex]; + tailingEntries[i++] = tailingBucket!; + tailingBucket = default; + + if (HasTail(tailIndex)) + { + var nextTailIndex = GetTailIndex(tailIndex); + _tails.SetEmpty(tailIndex); + tailIndex = nextTailIndex; + } + else + { + _tails.SetEmpty(tailIndex); + break; + } + } + + return tailingEntries; + } + [MethodImpl(MethodImplOptions.NoInlining)] private void WarnForPossiblyExcessiveResizing(T entry) => Log.Warning($"FishTable is resizing from a large number of clashing hashCodes. Last inserted key: '{ @@ -474,7 +415,7 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection for (var i = 0; i < oldBuckets.Length; i++) { if (!IsBucketEmpty(i, oldTails, oldBuckets)) - InsertEntry(oldBuckets[i]!, false, true); + InsertEntry(oldBuckets[i]!, ReplaceBehaviour.Throw, true); } } @@ -545,7 +486,7 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection public bool Add(T key) { Guard.IsNotNull(key); - return InsertEntry(key, false); + return InsertEntry(key, ReplaceBehaviour.Fail); } public void UnionWith(IEnumerable other) => throw new NotImplementedException(); @@ -578,36 +519,23 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection return true; } - private void EmplaceWithTails(int entryIndex) + private void EmplaceWithTailsBackward(int entryIndex) { - ref var bucket = ref _buckets[entryIndex]; - var entry = bucket; - - Span tailIndices = stackalloc int[GetTailCount(entryIndex)]; - - if (HasTail(entryIndex)) + if (IsBucketEmpty(entryIndex)) + Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace entry bucket"); + + while (true) { - tailIndices[0] = GetTailIndex(entryIndex); - for (var i = 1; i < tailIndices.Length; i++) - tailIndices[i] = GetTailIndex(tailIndices[i - 1]); - } - - if (IsTail(entry!, entryIndex)) - SetParentTail(entryIndex, Tails.SOLO); - - bucket = default; - _tails.SetEmpty(entryIndex); - InsertEntry(entry!, shifting: true); - - for (var i = 1; i < tailIndices.Length; i++) - { - var tailIndex = tailIndices[i]; - ref var tailBucket = ref _buckets[tailIndex]; - var tailEntry = tailBucket; + var tailIndex = HasTail(entryIndex) ? GetTailIndex(entryIndex) : -1; + var entry = _buckets[entryIndex]; - tailBucket = default; - _tails.SetEmpty(tailIndex); - InsertEntry(tailEntry!, shifting: true); + SetBucketEmpty(entryIndex); + InsertEntry(entry!, ReplaceBehaviour.Throw, true); + + if (tailIndex < 0) + return; + + entryIndex = tailIndex; } } @@ -621,20 +549,15 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection if (_equalityComparer(key, entry)) { - var tailIndex = -1; + var tailIndex = HasTail(bucketIndex) ? GetTailIndex(bucketIndex) : -1; + if (IsTail(entry!, bucketIndex)) - { - if (HasTail(bucketIndex)) - tailIndex = GetTailIndex(bucketIndex); - else - SetParentTail(bucketIndex, Tails.SOLO); - } + SetParentTail(bucketIndex, Tails.SOLO); - entry = default; - _tails.SetEmpty(bucketIndex); + SetBucketEmpty(bucketIndex); if (tailIndex != -1) - EmplaceWithTails(tailIndex); + EmplaceWithTailsBackward(tailIndex); return true; } @@ -714,17 +637,27 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection #endregion +#region enums + + private enum ReplaceBehaviour + { + Replace, + Throw, + Fail + } + +#endregion + + #region Structs - internal struct Tails + internal struct Tails(uint length) { public const uint EMPTY = 0u, SOLO = 1u; - private NibbleArray _values; - - public Tails(uint length) => _values = new(length); + private NibbleArray _values = new(length); public uint this[uint index] { @@ -837,10 +770,11 @@ public class FishSet : ISet, IReadOnlyCollection, ICollection } [DoesNotReturn] - internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + internal static bool ThrowAddingDuplicateWithKeyArgumentException(T key) { Guard.IsNotNull(key); ThrowAddingDuplicateWithKeyArgumentException((object)key); + return true; } [DoesNotReturn] diff --git a/Source/FisheryLib/Collections/FishTable.cs b/Source/FisheryLib/Collections/FishTable.cs index 718249d..4b3a74d 100644 --- a/Source/FisheryLib/Collections/FishTable.cs +++ b/Source/FisheryLib/Collections/FishTable.cs @@ -13,6 +13,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using FisheryLib.FunctionPointers; +using FisheryLib.Pools; using FisheryLib.Utility; using JetBrains.Annotations; @@ -44,20 +45,16 @@ public class FishTable : IDictionary, IDictionary, I _version; private float _maxLoadFactor = 0.5f; + + public KeyCollection Keys => new(this); - public ICollection Keys => throw new NotImplementedException(); - - public ICollection Values => throw new NotImplementedException(); + public ValueCollection Values => new(this); public int Version { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _version; - set - { - Guard.IsGreaterThan(value, _version); - _version = value; - } + set => _version = value; } public int Count @@ -73,9 +70,13 @@ public class FishTable : IDictionary, IDictionary, I + ((uint)sizeof(Entry) * (uint)_buckets.Length) + ((_tails.Length + 1u) >> 1); + public uint CollisionCount => (uint)GetTailingEntriesCount(); + public event Action>? EntryAdded, EntryRemoved; + + public Func ValueInitializer { get; set; } = static _ => Reflection.New(); public delegate void EntryEventHandler(TKey key, ref TValue value); @@ -132,12 +133,12 @@ public class FishTable : IDictionary, IDictionary, I public void Invoke(KeyValuePair pair); } - private record KeyEventHandler(Action Action) : IEventHandler + private sealed record KeyEventHandler(Action Action) : IEventHandler { public void Invoke(KeyValuePair pair) => Action(pair.Key); } - private record ValueEventHandler(Action Action) : IEventHandler + private sealed record ValueEventHandler(Action Action) : IEventHandler { public void Invoke(KeyValuePair pair) => Action(pair.Value); } @@ -152,21 +153,25 @@ public class FishTable : IDictionary, IDictionary, I _maxLoadFactor = value; } } - - ICollection IDictionary.Keys => throw new NotImplementedException(); - ICollection IDictionary.Values => throw new NotImplementedException(); + ICollection IDictionary.Keys => Keys; + + ICollection IDictionary.Values => Values; + + ICollection IDictionary.Keys => Keys; + + ICollection IDictionary.Values => Values; bool IDictionary.IsFixedSize => false; object ICollection.SyncRoot => this; // matching System.Collections.Generic.Dictionary bool ICollection.IsSynchronized => false; - - IEnumerable IReadOnlyDictionary.Keys => throw new NotImplementedException(); - - IEnumerable IReadOnlyDictionary.Values => throw new NotImplementedException(); - + + IEnumerable IReadOnlyDictionary.Keys => Keys; + + IEnumerable IReadOnlyDictionary.Values => Values; + object? IDictionary.this[object key] { get => this[AssertKeyType(key)]; @@ -213,7 +218,7 @@ public class FishTable : IDictionary, IDictionary, I } [CollectionAccess(CollectionAccessType.UpdatedContent)] - set => InsertEntry(key, value); + set => InsertEntry(key, value, ReplaceBehaviour.Replace); } public FishTable() : this(0) @@ -242,56 +247,6 @@ public class FishTable : IDictionary, IDictionary, I [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetIndexForTail(int entryIndex, uint tail) => (entryIndex + GetJumpDistance(tail)) & _wrapAroundMask; - - #region Junk - - // private static long GetJumpDistanceLong(byte index) - // => index switch - // { - // 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, - // 8 => 8, 9 => 9, 10 => 10, 11 => 11, 12 => 12, 13 => 13, 14 => 14, - // 15 => 15, 16 => 21, 17 => 28, 18 => 36, 19 => 45, 20 => 55, 21 => 66, - // 22 => 78, 23 => 91, 24 => 105, 25 => 120, 26 => 136, 27 => 153, - // 28 => 171, 29 => 190, 30 => 210, 31 => 231, 32 => 253, 33 => 276, - // 34 => 300, 35 => 325, 36 => 351, 37 => 378, 38 => 406, 39 => 435, - // 40 => 465, 41 => 496, 42 => 528, 43 => 561, 44 => 595, 45 => 630, - // 46 => 666, 47 => 703, 48 => 741, 49 => 780, 50 => 820, 51 => 861, - // 52 => 903, 53 => 946, 54 => 990, 55 => 1035, 56 => 1081, 57 => 1128, - // 58 => 1176, 59 => 1225, 60 => 1275, 61 => 1326, 62 => 1378, 63 => 1431, - // 64 => 1485, 65 => 1540, 66 => 1596, 67 => 1653, 68 => 1711, 69 => 1770, - // 70 => 1830, 71 => 1891, 72 => 1953, 73 => 2016, 74 => 2080, 75 => 2145, - // 76 => 2211, 77 => 2278, 78 => 2346, 79 => 2415, 80 => 2485, 81 => 2556, - // 82 => 3741, 83 => 8385, 84 => 18915, 85 => 42486, 86 => 95703, 87 => 215496, - // 88 => 485605, 89 => 1091503, 90 => 2456436, 91 => 5529475, 92 => 12437578, - // 93 => 27986421, 94 => 62972253, 95 => 141700195, 96 => 318819126, 97 => 717314626, - // 98 => 1614000520, 99 => 3631437253, 100 => 8170829695, 101 => 18384318876, 102 => 41364501751, - // 103 => 93070021080, 104 => 209407709220, 105 => 471167588430, 106 => 1060127437995, 107 => 2385287281530, - // 108 => 5366895564381, 109 => 12075513791265, 110 => 27169907873235, 111 => 61132301007778, - // 112 => 137547673121001, 113 => 309482258302503, 114 => 696335090510256, 115 => 1566753939653640, - // 116 => 3525196427195653, 117 => 7931691866727775, 118 => 17846306747368716, - // 119 => 40154190394120111, 120 => 90346928493040500, 121 => 203280588949935750, - // 122 => 457381324898247375, 123 => 1029107980662394500, 124 => 2315492957028380766, - // 125 => 5209859150892887590, > 125 => throw new() - // }; - - // private static int GetJumpDistance(byte index) - // => index switch - // { - // 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9, 10 => 10, 11 => 11, - // 12 => 12, 13 => 13, 14 => 14, 15 => 15, 16 => 21, 17 => 28, 18 => 36, 19 => 45, 20 => 55, 21 => 66, - // 22 => 78, 23 => 91, 24 => 105, 25 => 120, 26 => 136, 27 => 153, 28 => 171, 29 => 190, 30 => 210, 31 => 231, - // 32 => 253, 33 => 276, 34 => 300, 35 => 325, 36 => 351, 37 => 378, 38 => 406, 39 => 435, 40 => 465, - // 41 => 496, 42 => 528, 43 => 561, 44 => 595, 45 => 630, 46 => 666, 47 => 703, 48 => 741, 49 => 780, - // 50 => 820, 51 => 861, 52 => 903, 53 => 946, 54 => 990, 55 => 1035, 56 => 1081, 57 => 1128, 58 => 1176, - // 59 => 1225, 60 => 1275, 61 => 1326, 62 => 1378, 63 => 1431, 64 => 1485, 65 => 1540, 66 => 1596, 67 => 1653, - // 68 => 1711, 69 => 1770, 70 => 1830, 71 => 1891, 72 => 1953, 73 => 2016, 74 => 2080, 75 => 2145, 76 => 2211, - // 77 => 2278, 78 => 2346, 79 => 2415, 80 => 2485, 81 => 2556, 82 => 3741, 83 => 8385, 84 => 18915, - // 85 => 42486, 86 => 95703, 87 => 215496, 88 => 485605, 89 => 1091503, 90 => 2456436, 91 => 5529475, - // 92 => 12437578, 93 => 27986421, 94 => 62972253, 95 => 141700195, 96 => 318819126, 97 => 717314626, - // 98 => 1614000520, 255 => int.MaxValue, > 98 => ThrowForInvalidIndex(index) - // }; - - #endregion private static int GetJumpDistance(uint index) => index switch @@ -303,10 +258,8 @@ public class FishTable : IDictionary, IDictionary, I [MethodImpl(MethodImplOptions.NoInlining)] [DoesNotReturn] private static int MultiplyWithPhi(uint index) - => throw new NotImplementedException(); // (int)(uint)Math.Round(index * FishMath.PHI); - - // private static int ThrowForInvalidIndex(byte index) - // => throw new($"Jump distance index for FishTable too large. Should be < 99, got {index} instead."); + => throw new NotImplementedException($"Tried computing jump distance from a tails value of {index}"); + // => (int)(uint)Math.Round(index * FishMath.PHI); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsTail(int bucketIndex) @@ -325,6 +278,12 @@ public class FishTable : IDictionary, IDictionary, I _tails.SetSolo(bucketIndex); } + private void SetBucketEmpty(int index) + { + _buckets[index] = default; + _tails.SetEmpty(index); + } + private void SetEntryAsTail(int bucketIndex, in Entry entry, int parentIndex, uint offset) { SetEntryWithoutTail(bucketIndex, entry); @@ -334,22 +293,31 @@ public class FishTable : IDictionary, IDictionary, I private void SetParentTail(int entryIndex, uint newTail) => _tails[(uint)GetParentBucketIndex(entryIndex)] = newTail; - private void InsertEntry(TKey key, TValue? value, bool allowReplace = true, bool shifting = false) - => InsertEntry(new(key, value), allowReplace, shifting); + /// + /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not + /// adjust Count, Version or invoke EntryAdded. + /// + private bool InsertEntry(TKey key, TValue? value, ReplaceBehaviour replaceBehaviour, bool shifting = false) + => InsertEntry(new(key, value), replaceBehaviour, shifting); - private void InsertEntry(in Entry entry, bool allowReplace = true, bool shifting = false) + /// + /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not + /// adjust Count, Version or invoke EntryAdded. + /// + private bool InsertEntry(in Entry entry, ReplaceBehaviour replaceBehaviour, bool shifting = false) { - var addedNewEntry = InsertEntryInternal(entry, allowReplace); + var addedNewEntry = InsertEntryInternal(entry, replaceBehaviour); if (shifting) - return; + return addedNewEntry; _version++; if (!addedNewEntry) - return; + return false; OnEntryAdded(entry.AsKeyValuePair()); + return true; } private void OnEntryAdded(in KeyValuePair entry) @@ -360,10 +328,10 @@ public class FishTable : IDictionary, IDictionary, I } /// - /// Returns true when adding a new entry, false for replacing an existing entry. AllowReplace: false causes throwing - /// instead. Does not adjust Count, Version or invoke EntryAdded. + /// Returns true when adding a new entry, false for replacing an existing entry or failing to do so. Does not + /// adjust Count, Version or invoke EntryAdded. /// - private bool InsertEntryInternal(in Entry entry, bool allowReplace = true) + private bool InsertEntryInternal(in Entry entry, ReplaceBehaviour replaceBehaviour) { StartOfMethod: var bucketIndex = GetBucketIndex(entry.Key); @@ -375,7 +343,7 @@ public class FishTable : IDictionary, IDictionary, I return true; } - if (TryReplaceValueOfMatchingKey(entry, allowReplace, ref bucket)) + if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref bucket)) return false; if (CheckResize()) @@ -384,53 +352,30 @@ public class FishTable : IDictionary, IDictionary, I if (IsTail(ref bucket, bucketIndex)) { var previousEntry = bucket; - var hasTail = !_tails.IsSolo(bucketIndex); - var tailingEntries = hasTail ? new Entry[GetTailCount(bucketIndex)] : null; - - if (hasTail) - { - var i = 0; - var tailIndex = GetTailIndex(bucketIndex); - - while (true) - { - ref var tailingBucket = ref _buckets[tailIndex]; - tailingEntries![i++] = tailingBucket; - tailingBucket = default; - - if (!_tails.IsSoloOrEmpty(tailIndex)) - { - var nextTailIndex = GetTailIndex(tailIndex); - _tails.SetEmpty(tailIndex); - tailIndex = nextTailIndex; - } - else - { - _tails.SetEmpty(tailIndex); - break; - } - } - } + var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryWithoutTail(bucketIndex, entry); - - InsertEntry(previousEntry, shifting: true); - if (hasTail) - { - for (var i = 0; i < tailingEntries!.Length; i++) - InsertEntry(tailingEntries[i], shifting: true); - } + InsertEntry(previousEntry, ReplaceBehaviour.Throw, true); + + if (tailingEntries != null) + InsertEntries(tailingEntries, ReplaceBehaviour.Throw, true); return true; } else { - return !TryFindTailIndexAndReplace(entry, ref bucketIndex, allowReplace) + return !TryFindTailIndexAndReplace(entry, ref bucketIndex, replaceBehaviour) && InsertAsTail(entry, bucketIndex); } } + private void InsertEntries(Entry[] tailingEntries, ReplaceBehaviour replaceBehaviour, bool shifting = false) + { + for (var i = 0; i < tailingEntries.Length; i++) + InsertEntry(tailingEntries[i], replaceBehaviour, shifting); + } + private int GetTailCount(int index) { var tailCount = 0; @@ -444,6 +389,7 @@ public class FishTable : IDictionary, IDictionary, I return tailCount; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsBucketEmpty(int bucketIndex) => IsBucketEmpty(bucketIndex, _tails, _buckets); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -466,28 +412,35 @@ public class FishTable : IDictionary, IDictionary, I return false; // no good workaround without performance hit } - private bool TryFindTailIndexAndReplace(in Entry entry, ref int bucketIndex, bool allowReplace) + private bool TryFindTailIndexAndReplace(in Entry entry, ref int bucketIndex, ReplaceBehaviour replaceBehaviour) { while (true) { if (!TryContinueWithTail(ref bucketIndex)) return false; - if (TryReplaceValueOfMatchingKey(entry, allowReplace, ref _buckets[bucketIndex])) + if (TryReplaceValueOfMatchingKey(entry, replaceBehaviour, ref _buckets[bucketIndex])) return true; } } - private unsafe bool TryReplaceValueOfMatchingKey(in Entry entry, bool allowReplace, ref Entry bucket) + private unsafe bool TryReplaceValueOfMatchingKey(in Entry entry, ReplaceBehaviour replaceBehaviour, + ref Entry bucket) { if (!_keyEqualityComparer(bucket.Key, entry.Key)) return false; - if (!allowReplace) - ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry.Key); - - bucket.Value = entry.Value; - return true; + switch (replaceBehaviour) + { + case ReplaceBehaviour.Throw: + return ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(entry.Key); + case ReplaceBehaviour.Replace: + bucket.Value = entry.Value; + goto default; + case ReplaceBehaviour.Return: + default: + return true; + } } private bool InsertAsTail(in Entry entry, int bucketIndex) @@ -504,7 +457,7 @@ public class FishTable : IDictionary, IDictionary, I SetEntryAsTail(bucketIndex, entry, parentIndex, offset); return true; } - + if (IsTail(bucketIndex) && offset < _tails[(uint)GetParentBucketIndex(bucketIndex)]) break; @@ -515,56 +468,78 @@ public class FishTable : IDictionary, IDictionary, I WarnForPossiblyExcessiveResizing(entry); Resize(); - InsertEntry(entry, false, true); + InsertEntry(entry, ReplaceBehaviour.Throw, true); return true; } var previousEntry = _buckets[bucketIndex]; - var hasTail = !_tails.IsSolo(bucketIndex); - - var tailingEntries = hasTail ? new Entry[GetTailCount(bucketIndex)] : null; - - if (hasTail) - { - var k = 0; - var tailIndex = GetTailIndex(bucketIndex); - - while (true) - { - ref var tailingBucket = ref _buckets[tailIndex]; - tailingEntries![k] = tailingBucket; - - k++; - tailingBucket = default; - - if (!_tails.IsSoloOrEmpty(tailIndex)) - { - var nextTailIndex = GetTailIndex(tailIndex); - _tails.SetEmpty(tailIndex); - tailIndex = nextTailIndex; - } - else - { - _tails.SetEmpty(tailIndex); - break; - } - } - } + var tailingEntries = TryGetAndClearTailingEntries(bucketIndex); SetParentTail(bucketIndex, Tails.SOLO); SetEntryAsTail(bucketIndex, entry, parentIndex, offset); - - InsertEntry(previousEntry, shifting: true); + InsertEntry(previousEntry, ReplaceBehaviour.Throw, true); - if (hasTail) - { - for (var k = 0; k < tailingEntries!.Length; k++) - InsertEntry(tailingEntries[k], shifting: true); - } + if (tailingEntries != null) + InsertEntries(tailingEntries, ReplaceBehaviour.Throw, true); return true; } + private Entry[]? TryGetAndClearTailingEntries(int bucketIndex) + { + if (!HasTail(bucketIndex)) + return null; + + var tailingEntries = new Entry[GetTailCount(bucketIndex)]; + var i = 0; + var tailIndex = GetTailIndex(bucketIndex); + + while (true) + { + ref var tailingBucket = ref _buckets[tailIndex]; + tailingEntries[i++] = tailingBucket; + tailingBucket = default; + + if (HasTail(tailIndex)) + { + var nextTailIndex = GetTailIndex(tailIndex); + _tails.SetEmpty(tailIndex); + tailIndex = nextTailIndex; + } + else + { + _tails.SetEmpty(tailIndex); + break; + } + } + + return tailingEntries; + } + + // probably fails due to varying jump distances + /*private void EmplaceWithTailsForward(in Entry insertedEntry, int entryIndex) + { + var replacementEntry = insertedEntry; + + while (true) + { + if (entryIndex < 0) + { + InsertEntry(replacementEntry, false, true); + return; + } + + var tailIndex = HasTail(entryIndex) ? GetTailIndex(entryIndex) : -1; + var previousEntry = _buckets[entryIndex]; + + SetBucketEmpty(entryIndex); + InsertEntry(replacementEntry, false, true); + + replacementEntry = previousEntry; + entryIndex = tailIndex; + } + }*/ + [MethodImpl(MethodImplOptions.NoInlining)] private void WarnForPossiblyExcessiveResizing(Entry entry) => Log.Warning($"FishTable is resizing from a large number of clashing hashCodes. Last inserted key: '{ @@ -617,7 +592,7 @@ public class FishTable : IDictionary, IDictionary, I for (var i = 0; i < oldBuckets.Length; i++) { if (!IsBucketEmpty(i, oldTails, oldBuckets)) - InsertEntry(oldBuckets[i], false, true); + InsertEntry(oldBuckets[i], ReplaceBehaviour.Throw, true); } } @@ -663,6 +638,32 @@ public class FishTable : IDictionary, IDictionary, I } } + [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] + public unsafe bool TryGetOrAddValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + var result = true; + + StartOfLookup: + var bucketIndex = GetBucketIndex(key); + + while (true) + { + ref var bucket = ref _buckets[bucketIndex]; + + if (_keyEqualityComparer(key, bucket.Key)) + { + value = bucket.Value!; + return result; + } + + if (ContinueWithTailOrAddNew(ref bucketIndex, key)) + continue; + + result = false; + goto StartOfLookup; + } + } + [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe TValue GetOrAdd(TKey key) { @@ -699,6 +700,11 @@ public class FishTable : IDictionary, IDictionary, I } } + /// + /// Returns a reference to a value field of an existing entry if one exists, otherwise to a new entry. This + /// reference is only valid until the next time the collection gets modified. An invalid reference gives undefined + /// behaviour and does not throw. + /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe ref TValue GetOrAddReference(TKey key) { @@ -717,6 +723,11 @@ public class FishTable : IDictionary, IDictionary, I } } + /// + /// Returns a reference to a value field of an existing entry if one exists, otherwise to a new entry. This + /// reference is only valid until the next time the collection gets modified. An invalid reference gives undefined + /// behaviour and does not throw. + /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] public unsafe ref TValue GetOrAddReference(ref TKey key) { @@ -735,6 +746,11 @@ public class FishTable : IDictionary, IDictionary, I } } + /// + /// Returns a reference to a value field of an existing entry if one exists, otherwise throws a + /// KeyNotFoundException. This reference is only valid until the next time the collection gets modified. An invalid + /// reference gives undefined behaviour and does not throw. + /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue GetReference(TKey key) { @@ -751,6 +767,11 @@ public class FishTable : IDictionary, IDictionary, I } } + /// + /// Returns a reference to a value field of an existing entry if one exists, otherwise throws a + /// KeyNotFoundException. This reference is only valid until the next time the collection gets modified. An invalid + /// reference gives undefined behaviour and does not throw. + /// [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue GetReference(ref TKey key) { @@ -768,10 +789,11 @@ public class FishTable : IDictionary, IDictionary, I } /// - /// 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. + /// Returns a reference to a value field of an existing entry if one exists, otherwise an Unsafe.NullRef{TValue}. + /// Unsafe.IsNullRef should be used to check for Unsafe.NullRef before assigning to a regular var or field without + /// ref. Regular null checks throw. The returned reference is only valid until the next time the collection gets + /// modified. An invalid reference gives undefined behaviour and does not throw. /// - /// The key /// ref TValue or Unsafe.NullRef{TValue} [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue? TryGetReferenceUnsafe(TKey key) @@ -790,6 +812,13 @@ public class FishTable : IDictionary, IDictionary, I } } + /// + /// Returns a reference to a value field of an existing entry if one exists, otherwise an Unsafe.NullRef{TValue}. + /// Unsafe.IsNullRef should be used to check for Unsafe.NullRef before assigning to a regular var or field without + /// ref. Regular null checks throw. The returned reference is only valid until the next time the collection gets + /// modified. An invalid reference gives undefined behaviour and does not throw. + /// + /// ref TValue or Unsafe.NullRef{TValue} [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.ModifyExistingContent)] public unsafe ref TValue? TryGetReferenceUnsafe(ref TKey key) { @@ -824,6 +853,21 @@ public class FishTable : IDictionary, IDictionary, I } } + [CollectionAccess(CollectionAccessType.Read)] + public unsafe bool ContainsValue(TValue value) + { + var equalityComparer = Equals.Default; + + var length = _buckets.Length; + for (var i = 0; i < length; i++) + { + if (!IsBucketEmpty(i) && equalityComparer(_buckets[i].Value!, value)) + return true; + } + + return false; + } + #region GetBucketIndexMethods [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -838,8 +882,7 @@ public class FishTable : IDictionary, IDictionary, I [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBucketIndexForKeyAt(int index) => GetBucketIndex(_buckets[index].Key); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetParentBucketIndex(int childBucketIndex) + private int GetParentBucketIndex(int childBucketIndex, bool throwOnFailure = true) { var ancestorIndex = GetBucketIndexForKeyAt(childBucketIndex); @@ -847,9 +890,14 @@ public class FishTable : IDictionary, IDictionary, I while (true) { - if (i++ > 32) - ThrowHelper.ThrowFailedToFindParentInvalidOperationException(this, childBucketIndex); - + if (i++ > 32 || !HasTail(ancestorIndex)) + { + if (throwOnFailure) + ThrowHelper.ThrowFailedToFindParentInvalidOperationException(this, childBucketIndex); + else + return -1; + } + var nextIndex = GetTailIndex(ancestorIndex); if (nextIndex == childBucketIndex) break; @@ -860,6 +908,8 @@ public class FishTable : IDictionary, IDictionary, I return ancestorIndex; } + private bool HasParentBucket(int childBucketIndex) => GetParentBucketIndex(childBucketIndex, false) >= 0; + #endregion #region ContinueWithTailMethods @@ -874,7 +924,7 @@ public class FishTable : IDictionary, IDictionary, I if (TryContinueWithTail(ref entryIndex)) return true; - this[key] = Reflection.New(); + this[key] = ValueInitializer(key); return false; } @@ -888,7 +938,7 @@ public class FishTable : IDictionary, IDictionary, I if (TryContinueWithTail(ref entryIndex)) return true; - this[key] = Reflection.New(); + this[key] = ValueInitializer(key); return false; } @@ -931,7 +981,14 @@ public class FishTable : IDictionary, IDictionary, I public void Add(TKey key, TValue value) { Guard.IsNotNull(key); - InsertEntry(key, value, false); + InsertEntry(key, value, ReplaceBehaviour.Throw); + } + + [CollectionAccess(CollectionAccessType.Read | CollectionAccessType.UpdatedContent)] + public bool TryAdd(TKey key, TValue value) + { + Guard.IsNotNull(key); + return InsertEntry(key, value, ReplaceBehaviour.Return); } [CollectionAccess(CollectionAccessType.ModifyExistingContent)] @@ -944,39 +1001,68 @@ public class FishTable : IDictionary, IDictionary, I return true; } - private void EmplaceWithTails(int entryIndex) + [CollectionAccess(CollectionAccessType.ModifyExistingContent)] + public bool Remove(TKey key, out TValue? value) { - ref var bucket = ref _buckets[entryIndex]; - var entry = bucket; - - Span tailIndices = stackalloc int[GetTailCount(entryIndex)]; - - if (HasTail(entryIndex)) + if (!RemoveInternal(key, out var removedEntry)) { - tailIndices[0] = GetTailIndex(entryIndex); - for (var i = 1; i < tailIndices.Length; i++) - tailIndices[i] = GetTailIndex(tailIndices[i - 1]); + value = default; + return false; } - if (IsTail(ref entry, entryIndex)) - SetParentTail(entryIndex, Tails.SOLO); - - bucket = default; - _tails.SetEmpty(entryIndex); - InsertEntry(entry, shifting: true); - - for (var i = 1; i < tailIndices.Length; i++) - { - var tailIndex = tailIndices[i]; - ref var tailBucket = ref _buckets[tailIndex]; - var tailEntry = tailBucket; + value = removedEntry.Value!; + OnEntryRemoved(removedEntry.AsKeyValuePair()); + return true; + } - tailBucket = default; - _tails.SetEmpty(tailIndex); - InsertEntry(tailEntry, shifting: true); + [CollectionAccess(CollectionAccessType.ModifyExistingContent)] + public int RemoveWhere(Predicate> predicate) + { + Guard.IsNotNull(predicate); + + var removedCount = 0; + + var length = _buckets.Length; + for (var i = 0; i < length; i++) + { + if (IsBucketEmpty(i)) + continue; + + var entry = _buckets[i]; + + if (!predicate(entry.AsKeyValuePair())) + continue; + + if (!Remove(entry.Key)) + continue; + + removedCount++; + i--; // extra check in case of emplacement after removal } + + return removedCount; } + private void EmplaceWithTailsBackward(int entryIndex) + { + if (IsBucketEmpty(entryIndex)) + Utility.Diagnostics.ThrowHelper.ThrowInvalidOperationException("Tried to emplace entry bucket"); + + while (true) + { + var tailIndex = HasTail(entryIndex) ? GetTailIndex(entryIndex) : -1; + var entry = _buckets[entryIndex]; + + SetBucketEmpty(entryIndex); + InsertEntry(entry, ReplaceBehaviour.Throw, true); + + if (tailIndex < 0) + return; + + entryIndex = tailIndex; + } + } + private unsafe bool RemoveInternal(TKey key, out Entry removedEntry, bool checkValue = false, TValue? value = default) { @@ -984,30 +1070,24 @@ public class FishTable : IDictionary, IDictionary, I while (true) { - ref var entry = ref _buckets[bucketIndex]; + ref var bucket = ref _buckets[bucketIndex]; - if (_keyEqualityComparer(key, entry.Key)) + if (_keyEqualityComparer(key, bucket.Key)) { - if (checkValue && !value.Equals(entry.Value)) + if (checkValue && !value.Equals(bucket.Value)) goto OnFailure; - removedEntry = entry; + removedEntry = bucket; - var tailIndex = -1; - if (IsTail(ref entry, bucketIndex)) - { - if (HasTail(bucketIndex)) - tailIndex = GetTailIndex(bucketIndex); - else - SetParentTail(bucketIndex, Tails.SOLO); - } + var tailIndex = HasTail(bucketIndex) ? GetTailIndex(bucketIndex) : -1; + + if (IsTail(ref bucket, bucketIndex)) + SetParentTail(bucketIndex, Tails.SOLO); - entry.Key = default!; - entry.Value = default; - _tails.SetEmpty(bucketIndex); + SetBucketEmpty(bucketIndex); if (tailIndex != -1) - EmplaceWithTails(tailIndex); + EmplaceWithTailsBackward(tailIndex); return true; } @@ -1238,20 +1318,25 @@ public class FishTable : IDictionary, IDictionary, I #endregion +#region enums + + private enum ReplaceBehaviour + { + Replace, + Throw, + Return + } + +#endregion + #region Structs // [StructLayout(LayoutKind.Sequential, Pack = 1)] // matches KeyValuePair and gets reinterpret cast into that in the Enumerator - internal struct Entry + internal struct Entry(TKey key, TValue? value) { - public TKey Key; - public TValue? Value; - - public Entry(TKey key, TValue? value) - { - Key = key; - Value = value; - } + public TKey Key = key; + public TValue? Value = value; [Pure] [UnscopedRef] @@ -1259,20 +1344,34 @@ public class FishTable : IDictionary, IDictionary, I [SecuritySafeCritical] public ref KeyValuePair AsKeyValuePair() => ref Unsafe.As>(ref this); + + public override string ToString() + { + using var stringBuilder = new PooledStringBuilder(42); + + return stringBuilder + .Append("FishTable<") + .Append(typeof(TKey)) + .Append(", ") + .Append(typeof(TValue)) + .Append(">.Entry { Key = ") + .Append(Key) + .Append(", Value = ") + .Append(Value) + .Append(" }").ToString(); + } } - internal struct Tails + internal struct Tails(uint length) { public const uint EMPTY = 0u, SOLO = 1u; - private NibbleArray _values; + private NibbleArray _values = new(length); public uint Length => _values.Length; - public Tails(uint length) => _values = new(length); - public uint this[uint index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1399,6 +1498,180 @@ public class FishTable : IDictionary, IDictionary, I } } + [PublicAPI] + public readonly record struct KeyCollection(FishTable Parent) : ICollection, ICollection + { + public IEnumerator GetEnumerator() + { + foreach (var entry in Parent) + yield return entry.Key; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool Contains(TKey item) => Parent.ContainsKey(item); + + public void CopyTo(TKey[] array, int arrayIndex) + { + foreach (var entry in Parent) + array[arrayIndex++] = entry.Key; + } + + public void CopyTo(Array array, int index) + { + if (array is TKey[] typedArray) + { + CopyTo(typedArray, index); + } + else + { + foreach (var entry in Parent) + array.SetValue(entry.Key, index++); + } + } + + public bool Remove(TKey item) => Parent.Remove(item); + + public int RemoveWhere(Predicate predicate) + { + Guard.IsNotNull(predicate); + + var removedCount = 0; + var buckets = Parent._buckets; + var tails = Parent._tails; + var length = buckets.Length; + + for (var i = 0; i < length; i++) + { + if (Parent.IsBucketEmpty(i, tails, buckets)) + continue; + + var entry = buckets[i]; + + if (!predicate(entry.Key)) + continue; + + if (!Parent.Remove(entry.Key)) + continue; + + removedCount++; + i--; // extra check in case of emplacement after removal + } + + return removedCount; + } + + public int Count => Parent.Count; + + public bool IsReadOnly => true; + + void ICollection.Add(TKey item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => throw new NotSupportedException(); + } + + [PublicAPI] + public readonly record struct ValueCollection(FishTable Parent) : ICollection, ICollection + { + public IEnumerator GetEnumerator() + { + foreach (var entry in Parent) + yield return entry.Value; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool Contains(TValue item) => Parent.ContainsValue(item); + + public void CopyTo(TValue[] array, int arrayIndex) + { + foreach (var entry in Parent) + array[arrayIndex++] = entry.Value; + } + + public void CopyTo(Array array, int index) + { + if (array is TValue[] typedArray) + { + CopyTo(typedArray, index); + } + else + { + foreach (var entry in Parent) + array.SetValue(entry.Value, index++); + } + } + + public unsafe bool Remove(TValue item) + { + var buckets = Parent._buckets; + var tails = Parent._tails; + var equalityComparer = Equals.Default; + var length = buckets.Length; + + for (var i = 0; i < length; i++) + { + if (Parent.IsBucketEmpty(i, tails, buckets)) + continue; + + var entry = buckets[i]; + + if (!equalityComparer(entry.Value!, item)) + continue; + + if (Parent.Remove(entry.Key)) + return true; + } + + return false; + } + + public int RemoveWhere(Predicate predicate) + { + Guard.IsNotNull(predicate); + + var removedCount = 0; + var buckets = Parent._buckets; + var tails = Parent._tails; + var length = buckets.Length; + + for (var i = 0; i < length; i++) + { + if (Parent.IsBucketEmpty(i, tails, buckets)) + continue; + + var entry = buckets[i]; + + if (!predicate(entry.Value!)) + continue; + + if (!Parent.Remove(entry.Key)) + continue; + + removedCount++; + i--; // extra check in case of emplacement after removal + } + + return removedCount; + } + + public int Count => Parent.Count; + + public bool IsReadOnly => true; + + void ICollection.Add(TValue item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => throw new NotSupportedException(); + } + #endregion private static class ThrowHelper @@ -1439,10 +1712,11 @@ public class FishTable : IDictionary, IDictionary, I } [DoesNotReturn] - internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + internal static bool ThrowAddingDuplicateWithKeyArgumentException(T key) { Guard.IsNotNull(key); ThrowAddingDuplicateWithKeyArgumentException((object)key); + return true; } [DoesNotReturn] @@ -1461,10 +1735,50 @@ public class FishTable : IDictionary, IDictionary, I int childBucketIndex) { var entry = fishTable._buckets[childBucketIndex]; - throw new InvalidOperationException($"Failed to find parent index in FishTable for key: '{ - entry.Key}', hashCode: '{fishTable._keyHashCodeGetter(entry.Key)}', value: '{entry.Value}', count: '{ - fishTable._count}', bucket array length: '{fishTable._buckets.Length}', tailing entries: '{ - fishTable.GetTailingEntriesCount()}'"); + using var stringBuilder = new PooledStringBuilder(227); + + stringBuilder + .Append("Failed to find parent index in FishTable<") + .Append(typeof(TKey)) + .Append(", ") + .Append(typeof(TValue)) + .Append("> for key: '") + .Append(entry.Key) + .Append("', hashCode: '") + .Append(fishTable._keyHashCodeGetter(entry.Key)) + .Append("', value: '") + .Append(entry.Value) + .Append("', count: '") + .Append(fishTable._count) + .Append("', bucket array length: '") + .Append(fishTable._buckets.Length) + .Append("', total tailing entries count: '") + .Append(fishTable.GetTailingEntriesCount()) + .Append("', known chain of tails:"); + + var ancestorIndex = fishTable.GetBucketIndexForKeyAt(childBucketIndex); + var i = 0; + while (true) + { + var tailEntry = fishTable._buckets[ancestorIndex]; + + stringBuilder.Append("\n{ index: '") + .Append(ancestorIndex) + .Append("' key: '") + .Append(tailEntry.Key) + .Append("', hashCode: '") + .Append(fishTable._keyHashCodeGetter(tailEntry.Key)) + .Append("', value: '") + .Append(tailEntry.Value) + .Append(fishTable._tails.IsEmpty(ancestorIndex) ? " (empty) }" : " }"); + + if (i++ > 32 || !fishTable.HasTail(ancestorIndex)) + break; + + ancestorIndex = fishTable.GetTailIndex(ancestorIndex); + } + + throw new InvalidOperationException(stringBuilder.ToString()); } [DoesNotReturn] diff --git a/Source/FisheryLib/Collections/IndexedFishMap.cs b/Source/FisheryLib/Collections/IndexedFishMap.cs new file mode 100644 index 0000000..7791411 --- /dev/null +++ b/Source/FisheryLib/Collections/IndexedFishMap.cs @@ -0,0 +1,260 @@ +// 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 JetBrains.Annotations; + +namespace FisheryLib.Collections; + +/// +/// Indexable collection similar to a list, but with constant time Contains, Remove, Insert and IndexOf methods for any +/// argument type. Order of elements is not preserved on inserting and removing however. Displacement happens between +/// the target index and the last slot. X and Y are individually enforced to be unique +/// +[PublicAPI] +public class IndexedFishMap : IList<(TX X, TY Y)>, IList, IReadOnlyList<(TX X, TY Y)> + // IDictionary, IDictionary +{ + private readonly List<(TX X, TY Y)> _items = []; + private readonly FishTable _xIndices = new(); + private readonly FishTable _yIndices = new(); + + public (TX X, TY Y) this[int index] + { + get => _items[index]; + set + { + var previousValue = _items[index]; + _items[index] = value; + + _xIndices.Remove(previousValue.X); + _xIndices.Add(value.X, index); + + _yIndices.Remove(previousValue.Y); + _yIndices.Add(value.Y, index); + } + } + + public TY this[TX x] + { + get => _items[_xIndices[x]].Y; + set + { + Start: + if (!_xIndices.TryGetValue(x, out var index)) + index = _items.Count; + + if (!_yIndices.TryAdd(value, index)) + { + if (RemoveIfNotEqual(_yIndices, value, index)) + goto Start; + else + return; + } + + if (index == _items.Count) + { + _xIndices[x] = index; + _items.Add((x, value)); + } + else + { + _items[index] = (x, value); + } + } + } + + public TX this[TY y] + { + get => _items[_yIndices[y]].X; + set + { + Start: + if (!_yIndices.TryGetValue(y, out var index)) + index = _items.Count; + + if (!_xIndices.TryAdd(value, index)) + { + if (RemoveIfNotEqual(_xIndices, value, index)) + goto Start; + else + return; + } + + if (index == _items.Count) + { + _yIndices[y] = index; + _items.Add((value, y)); + } + else + { + _items[index] = (value, y); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool RemoveIfNotEqual(FishTable fishTable, TKey key, int index) + => fishTable[key] != index && fishTable.Remove(key); + + public List<(TX X, TY Y)> ReadOnlyList => _items; + + public List<(TX X, TY Y)>.Enumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator<(TX X, TY Y)> IEnumerable<(TX X, TY Y)>.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add((TX X, TY Y) item) + { + if (_xIndices.ContainsKey(item.X) || !_yIndices.TryAdd(item.Y, _items.Count)) + ThrowHelper.ThrowInvalidOperationException(); + + _xIndices.Add(item.X, _items.Count); + + _items.Add(item); + } + + public int Add(object? value) + { + Add(EnsureCorrectType(value)); + return _items.Count - 1; + } + + public bool TryAdd((TX X, TY Y) item) + { + if (_xIndices.ContainsKey(item.X) || !_yIndices.TryAdd(item.Y, _items.Count)) + return false; + + _xIndices.Add(item.X, _items.Count); + + _items.Add(item); + return true; + } + + public bool Contains(object? value) => IsCompatibleObject(value) && Contains(((TX X, TY Y))value); + + public void Clear() + { + _xIndices.Clear(); + _items.Clear(); + } + + public int IndexOf(object value) => IsCompatibleObject(value) ? IndexOf(((TX X, TY Y))value) : -1; + + public void Insert(int index, object value) => Insert(index, EnsureCorrectType(value)); + + public void Remove(object value) + { + if (IsCompatibleObject(value)) + Remove(((TX X, TY Y))value); + } + + public bool Contains((TX X, TY Y) item) => _xIndices.ContainsKey(item.X); + + public bool Contains(TX x) => _xIndices.ContainsKey(x); + + public bool Contains(TY y) => _yIndices.ContainsKey(y); + + public void CopyTo((TX X, TY Y)[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); + + public bool Remove((TX X, TY Y) item) + { + if (!_xIndices.ContainsKey(item.X) || !_yIndices.Remove(item.Y)) + return false; + + _xIndices.Remove(item.X, out var index); + + _items.RemoveAtFastUnordered(index); + if (index < _items.Count) + _xIndices[_items[index].X] = _yIndices[_items[index].Y] = index; + + return true; + } + + public bool Remove(TX x) + { + if (!_xIndices.Remove(x, out var index)) + return false; + + _yIndices.Remove(_items[index].Y); + + _items.RemoveAtFastUnordered(index); + if (index < _items.Count) + _xIndices[_items[index].X] = _yIndices[_items[index].Y] = index; + + return true; + } + + public bool Remove(TY y) + { + if (!_yIndices.Remove(y, out var index)) + return false; + + _xIndices.Remove(_items[index].X); + + _items.RemoveAtFastUnordered(index); + if (index < _items.Count) + _xIndices[_items[index].X] = _yIndices[_items[index].Y] = index; + + return true; + } + + public void CopyTo(Array array, int index) => ((IList)_items).CopyTo(array, index); + + public int Count => _items.Count; + + public object SyncRoot => this; + + public bool IsSynchronized => false; + + public bool IsReadOnly => false; + + public bool IsFixedSize => false; + + public int IndexOf((TX X, TY Y) item) => _xIndices.TryGetValue(item.X, out var index) ? index : -1; + + public int IndexOf(TX x) => _xIndices.TryGetValue(x, out var index) ? index : -1; + + public int IndexOf(TY y) => _yIndices.TryGetValue(y, out var index) ? index : -1; + + public void Insert(int index, (TX X, TY Y) item) + { + if (_xIndices.ContainsKey(item.X) || !_yIndices.TryAdd(item.Y, index)) + ThrowHelper.ThrowInvalidOperationException(); + + _xIndices.Add(item.X, index); + + _items.InsertFastUnordered(index, item); + + var lastIndex = _items.Count - 1; + _xIndices[_items[lastIndex].X] = _yIndices[_items[lastIndex].Y] = lastIndex; + } + + public void RemoveAt(int index) + { + var item = _items[index]; + _items.RemoveAtFastUnordered(index); + _xIndices.Remove(item.X); + _yIndices.Remove(item.Y); + + if (index < _items.Count) + _xIndices[_items[index].X] = _yIndices[_items[index].Y] = index; + } + + object IList.this[int index] + { + get => this[index]; + set => this[index] = EnsureCorrectType(value); + } + + private static bool IsCompatibleObject([NotNullWhen(true)] object? value) + => value is (TX, TY) || (value == null) & (default(TX) == null) & (default(TY) == null); + + private static (TX X, TY Y) EnsureCorrectType([NotNullWhen(true)] object? item) + => item is ValueTuple t ? t : ThrowHelper.ThrowArgumentException<(TX X, TY Y)>(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Collections/IndexedFishSet.cs b/Source/FisheryLib/Collections/IndexedFishSet.cs new file mode 100644 index 0000000..19c696c --- /dev/null +++ b/Source/FisheryLib/Collections/IndexedFishSet.cs @@ -0,0 +1,146 @@ +// 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 JetBrains.Annotations; + +namespace FisheryLib.Collections; + +/// +/// Indexable collection similar to a list, but with constant time Contains, Remove, Insert and IndexOf methods. Order +/// of elements is not preserved on inserting and removing however. Displacement happens between the target index and +/// the last slot +/// +[PublicAPI] +public class IndexedFishSet : IList, IList, IReadOnlyList +{ + private readonly List _items = []; + private readonly FishTable _indices = new(); + + public T this[int index] + { + get => _items[index]; + set + { + var previousValue = _items[index]; + _items[index] = value; + _indices.Remove(previousValue); + _indices.Add(value, index); + } + } + + public List ReadOnlyList => _items; + + public List.Enumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(T item) + { + if (!_indices.TryAdd(item, _items.Count)) + ThrowHelper.ThrowInvalidOperationException(); + + _items.Add(item); + } + + public int Add(object? value) + { + Add(EnsureCorrectType(value)); + return _items.Count - 1; + } + + public bool TryAdd(T item) + { + if (!_indices.TryAdd(item, _items.Count)) + return false; + + _items.Add(item); + return true; + } + + public bool Contains(object? value) => IsCompatibleObject(value) && Contains((T)value); + + public void Clear() + { + _indices.Clear(); + _items.Clear(); + } + + public int IndexOf(object value) => IsCompatibleObject(value) ? IndexOf((T)value) : -1; + + public void Insert(int index, object value) => Insert(index, EnsureCorrectType(value)); + + public void Remove(object value) + { + if (IsCompatibleObject(value)) + Remove((T)value); + } + + public bool Contains(T item) => _indices.ContainsKey(item); + + public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); + + public bool Remove(T item) + { + if (!_indices.Remove(item, out var index)) + return false; + + _items.RemoveAtFastUnordered(index); + if (index < _items.Count) + _indices[_items[index]] = index; + + return true; + } + + public void CopyTo(Array array, int index) => ((IList)_items).CopyTo(array, index); + + public int Count => _items.Count; + + public object SyncRoot => this; + + public bool IsSynchronized => false; + + public bool IsReadOnly => false; + + public bool IsFixedSize => false; + + public int IndexOf(T item) => _indices.TryGetValue(item, out var index) ? index : -1; + + public void Insert(int index, T item) + { + if (!_indices.TryAdd(item, index)) + ThrowHelper.ThrowInvalidOperationException(); + + _items.InsertFastUnordered(index, item); + + var lastIndex = _items.Count - 1; + _indices[_items[lastIndex]] = lastIndex; + } + + public void RemoveAt(int index) + { + var item = _items[index]; + _items.RemoveAtFastUnordered(index); + _indices.Remove(item); + + if (index < _items.Count) + _indices[_items[index]] = index; + } + + object IList.this[int index] + { + get => this[index]!; + set => this[index] = EnsureCorrectType(value); + } + + private static bool IsCompatibleObject([NotNullWhen(true)] object? value) + => value is T || (value == null && default(T) == null); + + private static T EnsureCorrectType([NotNullWhen(true)] object? item) + => item is T t ? t : ThrowHelper.ThrowArgumentException(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Collections/NibbleArray.cs b/Source/FisheryLib/Collections/NibbleArray.cs index 4edabf7..5a1b473 100644 --- a/Source/FisheryLib/Collections/NibbleArray.cs +++ b/Source/FisheryLib/Collections/NibbleArray.cs @@ -9,7 +9,7 @@ using JetBrains.Annotations; namespace FisheryLib.Collections; [PublicAPI] -public struct NibbleArray +public record struct NibbleArray { private uint _length; private byte[] _data; diff --git a/Source/FisheryLib/Diagnostics/Extensions/TypeExtensions.cs b/Source/FisheryLib/Diagnostics/Extensions/TypeExtensions.cs index ee06ed7..5847934 100644 --- a/Source/FisheryLib/Diagnostics/Extensions/TypeExtensions.cs +++ b/Source/FisheryLib/Diagnostics/Extensions/TypeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; @@ -15,7 +16,8 @@ public static class TypeExtensions /// /// The mapping of built-in types to their simple representation. /// - private static readonly IReadOnlyDictionary BuiltInTypesMap = new Dictionary + [SuppressMessage("Performance", "CA1859")] + private static readonly IReadOnlyDictionary _builtInTypesMap = new Dictionary { [typeof(bool)] = "bool", [typeof(byte)] = "byte", @@ -38,7 +40,7 @@ public static class TypeExtensions /// /// A thread-safe mapping of precomputed string representation of types. /// - private static readonly ConditionalWeakTable DisplayNames = new(); + private static readonly ConditionalWeakTable _displayNames = new(); /// /// Returns a simple string representation of a type. @@ -51,7 +53,7 @@ public static class TypeExtensions static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan typeArguments) { // Primitive types use the keyword name - if (BuiltInTypesMap.TryGetValue(type, out var typeName)) + if (_builtInTypesMap.TryGetValue(type, out var typeName)) return typeName!; // Array types are displayed as Foo[] @@ -131,7 +133,7 @@ public static class TypeExtensions } // Atomically get or build the display string for the current type. - return DisplayNames.GetValue(type, static t => + return _displayNames.GetValue(type, static t => { // By-ref types are displayed as T& if (t.IsByRef) diff --git a/Source/FisheryLib/Diagnostics/ThrowHelper.cs b/Source/FisheryLib/Diagnostics/ThrowHelper.cs index ea27bb2..3204723 100644 --- a/Source/FisheryLib/Diagnostics/ThrowHelper.cs +++ b/Source/FisheryLib/Diagnostics/ThrowHelper.cs @@ -7,12 +7,16 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Threading; +using JetBrains.Annotations; +// ReSharper disable InconsistentNaming namespace FisheryLib.Utility.Diagnostics; /// /// Helper methods to efficiently throw exceptions. /// +[PublicAPI] +[SuppressMessage("Usage", "CA2201")] public static partial class ThrowHelper { /// diff --git a/Source/FisheryLib/FishTranspiler.cs b/Source/FisheryLib/FishTranspiler.cs index d8f875a..3fd8737 100644 --- a/Source/FisheryLib/FishTranspiler.cs +++ b/Source/FisheryLib/FishTranspiler.cs @@ -3,6 +3,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Linq.Expressions; @@ -200,6 +201,7 @@ public static class FishTranspiler public static bool operator !=(CodeInstruction code, Container helper) => !(helper == code); + [SuppressMessage("Usage", "CA2225")] public static implicit operator CodeInstruction(Container helper) => helper.ToInstruction(); //public static implicit operator Container(CodeInstruction instruction) @@ -701,13 +703,15 @@ public static class FishTranspiler /// Pushes a supplied value of type int32 onto the evaluation stack as an int32. /// /// The value to push onto the evaluation stack - public static Container Constant(int integer) => new(GetLoadConstantOpCode(integer), GetOperandOfConstant(integer)); + public static Container Constant([SuppressMessage("Naming", "CA1720")] int integer) + => new(GetLoadConstantOpCode(integer), GetOperandOfConstant(integer)); /// /// Pushes a supplied value of type int64 onto the evaluation stack as an int64. /// /// The value to push onto the evaluation stack - public static Container Constant(long integer) => new(OpCodes.Ldc_I8, integer); + public static Container Constant([SuppressMessage("Naming", "CA1720")] long integer) + => new(OpCodes.Ldc_I8, integer); /// /// Pushes a supplied value of type float32 onto the evaluation stack as type F (float). @@ -1129,6 +1133,7 @@ public static class FishTranspiler /// Pushes a new object reference to a string literal stored in the metadata. /// /// The string literal + [SuppressMessage("Naming", "CA1720")] public static Container String(string text) { Guard.IsNotNull(text); @@ -2425,72 +2430,34 @@ public static class FishTranspiler public static bool StoresElement(this OpCode opcode) => StoreElementOpCodes.Contains(opcode); - public static readonly HashSet LoadElementOpCodes - = new() - { - OpCodes.Ldelem, - OpCodes.Ldelema, - OpCodes.Ldelem_I, - OpCodes.Ldelem_I1, - OpCodes.Ldelem_I2, - OpCodes.Ldelem_I4, - OpCodes.Ldelem_I8, - OpCodes.Ldelem_R4, - OpCodes.Ldelem_R8, - OpCodes.Ldelem_Ref, - OpCodes.Ldelem_U1, - OpCodes.Ldelem_U2, - OpCodes.Ldelem_U4 - }; + public static readonly HashSet LoadElementOpCodes = + [ + OpCodes.Ldelem, OpCodes.Ldelema, OpCodes.Ldelem_I, OpCodes.Ldelem_I1, OpCodes.Ldelem_I2, OpCodes.Ldelem_I4, + OpCodes.Ldelem_I8, OpCodes.Ldelem_R4, OpCodes.Ldelem_R8, OpCodes.Ldelem_Ref, OpCodes.Ldelem_U1, + OpCodes.Ldelem_U2, OpCodes.Ldelem_U4 + ]; - public static readonly HashSet StoreElementOpCodes - = new() - { - OpCodes.Stelem, - OpCodes.Stelem_I, - OpCodes.Stelem_I1, - OpCodes.Stelem_I2, - OpCodes.Stelem_I4, - OpCodes.Stelem_I8, - OpCodes.Stelem_R4, - OpCodes.Stelem_R8, - OpCodes.Stelem_Ref - }; + public static readonly HashSet StoreElementOpCodes = + [ + OpCodes.Stelem, OpCodes.Stelem_I, OpCodes.Stelem_I1, OpCodes.Stelem_I2, OpCodes.Stelem_I4, + OpCodes.Stelem_I8, OpCodes.Stelem_R4, OpCodes.Stelem_R8, OpCodes.Stelem_Ref + ]; public static bool LoadsByRef(this OpCode opcode) => LoadByRefOpCodes.Contains(opcode); public static bool StoresByRef(this OpCode opcode) => StoreByRefOpCodes.Contains(opcode); - public static readonly HashSet LoadByRefOpCodes - = new() - { - OpCodes.Ldobj, - OpCodes.Ldind_I, - OpCodes.Ldind_I1, - OpCodes.Ldind_I2, - OpCodes.Ldind_I4, - OpCodes.Ldind_I8, - OpCodes.Ldind_R4, - OpCodes.Ldind_R8, - OpCodes.Ldind_Ref, - OpCodes.Ldind_U1, - OpCodes.Ldind_U2, - OpCodes.Ldind_U4 - }; + public static readonly HashSet LoadByRefOpCodes = + [ + OpCodes.Ldobj, OpCodes.Ldind_I, OpCodes.Ldind_I1, OpCodes.Ldind_I2, OpCodes.Ldind_I4, OpCodes.Ldind_I8, + OpCodes.Ldind_R4, OpCodes.Ldind_R8, OpCodes.Ldind_Ref, OpCodes.Ldind_U1, OpCodes.Ldind_U2, OpCodes.Ldind_U4 + ]; - public static readonly HashSet StoreByRefOpCodes - = new() - { - OpCodes.Stobj, - OpCodes.Stind_I, - OpCodes.Stind_I1, - OpCodes.Stind_I2, - OpCodes.Stind_I4, - OpCodes.Stind_I8, - OpCodes.Stind_R4, - OpCodes.Stind_R8, - OpCodes.Stind_Ref - }; + public static readonly HashSet StoreByRefOpCodes = + [ + OpCodes.Stobj, OpCodes.Stind_I, OpCodes.Stind_I1, OpCodes.Stind_I2, OpCodes.Stind_I4, OpCodes.Stind_I8, + OpCodes.Stind_R4, OpCodes.Stind_R8, OpCodes.Stind_Ref + ]; public static bool LoadsField(this OpCode opcode) => opcode.LoadsInstanceField() || opcode.LoadsStaticField(); @@ -2538,100 +2505,38 @@ public static class FishTranspiler public static bool Compares(this OpCode opcode) => CompareOpCodes.Contains(opcode); public static readonly HashSet CompareOpCodes - = new() - { - OpCodes.Ceq, - OpCodes.Cgt, - OpCodes.Cgt_Un, - OpCodes.Clt, - OpCodes.Clt_Un - }; + = [OpCodes.Ceq, OpCodes.Cgt, OpCodes.Cgt_Un, OpCodes.Clt, OpCodes.Clt_Un]; public static bool Computes(this OpCode opcode) => ComputeOpCodes.Contains(opcode); - public static readonly HashSet ComputeOpCodes - = new() - { - OpCodes.Add, - OpCodes.Add_Ovf, - OpCodes.Add_Ovf_Un, - OpCodes.Sub, - OpCodes.Sub_Ovf, - OpCodes.Sub_Ovf_Un, - OpCodes.Mul, - OpCodes.Mul_Ovf, - OpCodes.Mul_Ovf_Un, - OpCodes.Div, - OpCodes.Div_Un, - OpCodes.Rem, - OpCodes.Rem_Un, - OpCodes.And, - OpCodes.Or, - OpCodes.Xor, - OpCodes.Shl, - OpCodes.Shr, - OpCodes.Shr_Un, - OpCodes.Not, - OpCodes.Neg - }; + public static readonly HashSet ComputeOpCodes = + [ + OpCodes.Add, OpCodes.Add_Ovf, OpCodes.Add_Ovf_Un, OpCodes.Sub, OpCodes.Sub_Ovf, OpCodes.Sub_Ovf_Un, + OpCodes.Mul, OpCodes.Mul_Ovf, OpCodes.Mul_Ovf_Un, OpCodes.Div, OpCodes.Div_Un, OpCodes.Rem, OpCodes.Rem_Un, + OpCodes.And, OpCodes.Or, OpCodes.Xor, OpCodes.Shl, OpCodes.Shr, OpCodes.Shr_Un, OpCodes.Not, OpCodes.Neg + ]; public static bool Converts(this OpCode opcode) => ConvertOpCodes.Contains(opcode); - public static readonly HashSet ConvertOpCodes - = new() - { - OpCodes.Conv_I, - OpCodes.Conv_I1, - OpCodes.Conv_I2, - OpCodes.Conv_I4, - OpCodes.Conv_I8, - OpCodes.Conv_Ovf_I, - OpCodes.Conv_Ovf_I1, - OpCodes.Conv_Ovf_I1_Un, - OpCodes.Conv_Ovf_I2, - OpCodes.Conv_Ovf_I2_Un, - OpCodes.Conv_Ovf_I4, - OpCodes.Conv_Ovf_I4_Un, - OpCodes.Conv_Ovf_I8, - OpCodes.Conv_Ovf_I8_Un, - OpCodes.Conv_Ovf_I_Un, - OpCodes.Conv_Ovf_U, - OpCodes.Conv_Ovf_U1, - OpCodes.Conv_Ovf_U1_Un, - OpCodes.Conv_Ovf_U2, - OpCodes.Conv_Ovf_U2_Un, - OpCodes.Conv_Ovf_U4, - OpCodes.Conv_Ovf_U4_Un, - OpCodes.Conv_Ovf_U8, - OpCodes.Conv_Ovf_U8_Un, - OpCodes.Conv_Ovf_U_Un, - OpCodes.Conv_R4, - OpCodes.Conv_R8, - OpCodes.Conv_R_Un, - OpCodes.Conv_U, - OpCodes.Conv_U1, - OpCodes.Conv_U2, - OpCodes.Conv_U4, - OpCodes.Conv_U8, - OpCodes.Box, - OpCodes.Unbox, - OpCodes.Unbox_Any - }; + public static readonly HashSet ConvertOpCodes = + [ + OpCodes.Conv_I, OpCodes.Conv_I1, OpCodes.Conv_I2, OpCodes.Conv_I4, OpCodes.Conv_I8, OpCodes.Conv_Ovf_I, + OpCodes.Conv_Ovf_I1, OpCodes.Conv_Ovf_I1_Un, OpCodes.Conv_Ovf_I2, OpCodes.Conv_Ovf_I2_Un, + OpCodes.Conv_Ovf_I4, OpCodes.Conv_Ovf_I4_Un, OpCodes.Conv_Ovf_I8, OpCodes.Conv_Ovf_I8_Un, + OpCodes.Conv_Ovf_I_Un, OpCodes.Conv_Ovf_U, OpCodes.Conv_Ovf_U1, OpCodes.Conv_Ovf_U1_Un, OpCodes.Conv_Ovf_U2, + OpCodes.Conv_Ovf_U2_Un, OpCodes.Conv_Ovf_U4, OpCodes.Conv_Ovf_U4_Un, OpCodes.Conv_Ovf_U8, + OpCodes.Conv_Ovf_U8_Un, OpCodes.Conv_Ovf_U_Un, OpCodes.Conv_R4, OpCodes.Conv_R8, OpCodes.Conv_R_Un, + OpCodes.Conv_U, OpCodes.Conv_U1, OpCodes.Conv_U2, OpCodes.Conv_U4, OpCodes.Conv_U8, OpCodes.Box, + OpCodes.Unbox, OpCodes.Unbox_Any + ]; public static bool LoadsArgument(this OpCode opcode) => LoadArgumentOpCodes.Contains(opcode); - public static readonly HashSet LoadArgumentOpCodes - = new() - { - OpCodes.Ldarg, - OpCodes.Ldarga, - OpCodes.Ldarga_S, - OpCodes.Ldarg_0, - OpCodes.Ldarg_1, - OpCodes.Ldarg_2, - OpCodes.Ldarg_3, - OpCodes.Ldarg_S - }; + public static readonly HashSet LoadArgumentOpCodes = + [ + OpCodes.Ldarg, OpCodes.Ldarga, OpCodes.Ldarga_S, OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, + OpCodes.Ldarg_3, OpCodes.Ldarg_S + ]; public static OpCode ToStoreArgument(this OpCode opcode) => opcode.StoresArgument() ? opcode @@ -2766,7 +2671,8 @@ public static class FishTranspiler public static object? GetOperandFromIndex(int index) => index > 3 ? index : null; - public static object? GetOperandOfConstant(int integer) => integer is < 9 and > -2 ? null : integer; + public static object? GetOperandOfConstant([SuppressMessage("Naming", "CA1720")] int integer) + => integer is < 9 and > -2 ? null : integer; public static object? GetOperandFromBuilder(LocalBuilder builder) { diff --git a/Source/FisheryLib/Fishery.cs b/Source/FisheryLib/Fishery.cs index ce6a945..947f372 100644 --- a/Source/FisheryLib/Fishery.cs +++ b/Source/FisheryLib/Fishery.cs @@ -14,20 +14,38 @@ global using CodeInstructions = System.Collections.Generic.IEnumerableenable True v1.4;v1.3 - 0.3.800.85 - 1701;1702;IL2121;CS8500;CS9082 + false + false + false + + + + 1701;1702;IL2121;CS8500;CS9082;CA1000;CA1002;CA1003;CA1707;CA1010;CA1715;CA1716;CA1724;CA1031;CA1033; + CA1034;CA1043;CA1051;CA1305;CA1310;CA1710;CA1711;CA1810;CA1812;CA1819;CA1851;CA2002;CA5394;CA1725;CA1062 + diff --git a/Source/FisheryLib/FunctionPointers/Convert.cs b/Source/FisheryLib/FunctionPointers/Convert.cs index 1b337aa..1fcd090 100644 --- a/Source/FisheryLib/FunctionPointers/Convert.cs +++ b/Source/FisheryLib/FunctionPointers/Convert.cs @@ -51,7 +51,7 @@ internal static class Convert try { var dm = new DynamicMethod($"ObjectConverter_{from}_{to}", to, - new[] { typeof(object), typeof(IFormatProvider) }, typeof(FromObjectTo), true); + [typeof(object), typeof(IFormatProvider)], typeof(FromObjectTo), true); var il = dm.GetILGenerator(); il.Emit(FishTranspiler.This); @@ -59,7 +59,7 @@ internal static class Convert if (from.IsValueType) { il.Emit(FishTranspiler.Call(typeof(FromObjectTo), nameof(UnboxSafely), - generics: new[] { from })); + generics: [from])); } il.Emit(FishTranspiler.Argument(1)); @@ -72,8 +72,9 @@ internal static class Convert } catch (Exception ex) { - throw new($"Failed to compile object converter from {from.FullDescription()} to { - to.FullDescription()}\n{ex}"); + return ThrowHelper.ThrowInvalidOperationException>( + $"Failed to compile object converter from {from.FullDescription()} to { + to.FullDescription()}\n{ex}"); } } @@ -83,7 +84,7 @@ internal static class Convert [MethodImpl(MethodImplOptions.AggressiveInlining)] [SecuritySafeCritical] private static ref T UnboxSafely(object obj) where T : struct - => ref Unsafe.Unbox(obj is T ? obj : default); + => ref Unsafe.Unbox(obj is T ? obj : default(T)); private record struct FuncWrapper { @@ -163,7 +164,7 @@ internal static class Convert private static MethodInfo GenerateWrapperMethod(Type from, Type to, MethodBase method) { var dm = new DynamicMethod($"Wrapper_op_{from.Name}_{to.Name}", to, - new[] { from.IsValueType ? from.MakeByRefType() : from, typeof(IFormatProvider) }, from, true); + [from.IsValueType ? from.MakeByRefType() : from, typeof(IFormatProvider)], from, true); var il = dm.GetILGenerator(); il.Emit(FishTranspiler.Argument(0)); diff --git a/Source/FisheryLib/FunctionPointers/Equals.cs b/Source/FisheryLib/FunctionPointers/Equals.cs index 0023d96..c02661a 100644 --- a/Source/FisheryLib/FunctionPointers/Equals.cs +++ b/Source/FisheryLib/FunctionPointers/Equals.cs @@ -3,6 +3,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; @@ -16,6 +17,7 @@ public static class Equals = (delegate*)FunctionPointers.Equals.EqualsMethods.GetFunctionPointer(typeof(T)); } +[SuppressMessage("Naming", "CA1720")] public static class Equals { [UsedImplicitly(ImplicitUseTargetFlags.Members)] @@ -37,6 +39,8 @@ public static class Equals public static bool IntPtr(IntPtr x, IntPtr y) => x == y; [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Globalization", "CA1307")] + [SuppressMessage("Globalization", "CA1309")] public static bool String(string x, string y) => string.Equals(x, y); [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Source/FisheryLib/FunctionPointers/EqualsByRef.cs b/Source/FisheryLib/FunctionPointers/EqualsByRef.cs index bc7439f..93d3ac6 100644 --- a/Source/FisheryLib/FunctionPointers/EqualsByRef.cs +++ b/Source/FisheryLib/FunctionPointers/EqualsByRef.cs @@ -3,6 +3,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; @@ -16,6 +17,7 @@ public static class EqualsByRef = (delegate*)EqualsByRef.EqualsMethods.GetFunctionPointer(typeof(T)); } +[SuppressMessage("Naming", "CA1720")] public static class EqualsByRef { [UsedImplicitly(ImplicitUseTargetFlags.Members)] @@ -37,6 +39,8 @@ public static class EqualsByRef public static bool IntPtr(ref IntPtr x, ref IntPtr y) => x == y; [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Globalization", "CA1307")] + [SuppressMessage("Globalization", "CA1309")] public static bool String(ref string x, ref string y) => string.Equals(x, y); [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Source/FisheryLib/FunctionPointers/GetHashCode.cs b/Source/FisheryLib/FunctionPointers/GetHashCode.cs index 5b2d503..05a8a3c 100644 --- a/Source/FisheryLib/FunctionPointers/GetHashCode.cs +++ b/Source/FisheryLib/FunctionPointers/GetHashCode.cs @@ -3,6 +3,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using JetBrains.Annotations; @@ -14,6 +15,7 @@ public static class GetHashCode = (delegate*)FunctionPointers.GetHashCode.HashCodeMethods.GetFunctionPointer(typeof(T)); } +[SuppressMessage("Naming", "CA1720")] public static class GetHashCode { [UsedImplicitly(ImplicitUseTargetFlags.Members)] diff --git a/Source/FisheryLib/FunctionPointers/GetHashCodeByRef.cs b/Source/FisheryLib/FunctionPointers/GetHashCodeByRef.cs index 15ac568..7b19c92 100644 --- a/Source/FisheryLib/FunctionPointers/GetHashCodeByRef.cs +++ b/Source/FisheryLib/FunctionPointers/GetHashCodeByRef.cs @@ -3,6 +3,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using JetBrains.Annotations; @@ -14,6 +15,7 @@ public static class GetHashCodeByRef = (delegate*)GetHashCodeByRef.HashCodeMethods.GetFunctionPointer(typeof(T)); } +[SuppressMessage("Naming", "CA1720")] public static class GetHashCodeByRef { [UsedImplicitly(ImplicitUseTargetFlags.Members)] diff --git a/Source/FisheryLib/FunctionPointers/MethodFactory.cs b/Source/FisheryLib/FunctionPointers/MethodFactory.cs index d1035d5..161c168 100644 --- a/Source/FisheryLib/FunctionPointers/MethodFactory.cs +++ b/Source/FisheryLib/FunctionPointers/MethodFactory.cs @@ -11,15 +11,15 @@ using System.Security; namespace FisheryLib.FunctionPointers; -public abstract class MethodFactory +public abstract class MethodFactory(MethodInfo[] methods, string namePrefix, bool byRef) { - protected readonly MethodInfo[] _methods; + protected readonly MethodInfo[] _methods = methods; protected readonly ConcurrentDictionary _methodsByType = new(); - protected readonly string _namePrefix; + protected readonly string _namePrefix = namePrefix; - protected readonly bool _byRef; + protected readonly bool _byRef = byRef; public MethodInfo GetGeneric(string name, params Type[] typeArguments) => GetNamed(name).MakeGenericMethod(typeArguments); @@ -64,14 +64,8 @@ public abstract class MethodFactory => type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly) .Where(predicate).ToArray(); - protected MethodFactory(MethodInfo[] methods, string namePrefix, bool byRef) - { - _byRef = byRef; - _methods = methods; - _namePrefix = namePrefix; - } - - public new class Equals : MethodFactory + public new class Equals(Type type, string dynamicMethodNamePrefix, bool byRef) + : MethodFactory(GetMethodInfoArraySafely(type), dynamicMethodNamePrefix, byRef) { protected override MethodInfo GetOrMakeMethodForType(Type type) => GetSpecializedMethod(type) @@ -160,7 +154,7 @@ public abstract class MethodFactory try { var dm = new DynamicMethod($"{_namePrefix}{type.Name}", typeof(bool), - _byRef ? new[] { type.MakeByRefType(), type.MakeByRefType() } : new[] { type, type }, type, true); + _byRef ? [type.MakeByRefType(), type.MakeByRefType()] : [type, type], type, true); var il = dm.GetILGenerator(); var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -232,16 +226,12 @@ public abstract class MethodFactory throw; } } - - public Equals(Type type, string dynamicMethodNamePrefix, bool byRef) - : base(GetMethodInfoArraySafely(type), dynamicMethodNamePrefix, byRef) - { - } private delegate TResult ByRefFunc(ref T1 arg1, ref T2 arg2); } - public class HashCode : MethodFactory + public class HashCode(Type type, string dynamicMethodNamePrefix, bool byRef) + : MethodFactory(GetMethodInfoArraySafely(type), dynamicMethodNamePrefix, byRef) { protected override MethodInfo GetOrMakeMethodForType(Type type) => GetSpecializedMethod(type) @@ -325,7 +315,7 @@ public abstract class MethodFactory try { var dm = new DynamicMethod($"{_namePrefix}{type.Name}", typeof(int), - _byRef ? new[] { type.MakeByRefType() } : new[] { type }, type, true); + _byRef ? [type.MakeByRefType()] : [type], type, true); var il = dm.GetILGenerator(); var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); @@ -382,11 +372,6 @@ public abstract class MethodFactory } } - public HashCode(Type type, string dynamicMethodNamePrefix, bool byRef) - : base(GetMethodInfoArraySafely(type), dynamicMethodNamePrefix, byRef) - { - } - private delegate int ByRefHashCodeGetter(ref T arg); private delegate int HashCodeGetter(T arg); } diff --git a/Source/FisheryLib/GUIScope.cs b/Source/FisheryLib/GUIScope.cs new file mode 100644 index 0000000..6dfb3bb --- /dev/null +++ b/Source/FisheryLib/GUIScope.cs @@ -0,0 +1,201 @@ +// Copyright (c) 2023 bradson +// This Source Code Form is subject to the terms of the MIT license. +// If a copy of the license was not distributed with this file, +// You can obtain one at https://opensource.org/licenses/MIT/. + +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using UnityEngine; +using Verse; + +namespace FisheryLib; + +[PublicAPI] +public static class GUIScope +{ + public readonly record struct WidgetGroup : IDisposable + { + public WidgetGroup(in Rect rect) => Widgets.BeginGroup(rect); + + public void Dispose() => Widgets.EndGroup(); + } + + public readonly record struct ScrollView : IDisposable + { + private const float SCROLL_BAR_WIDTH = 16f; + + private readonly ScrollViewStatus _scrollViewStatus; + + private readonly float _outRectHeight; + + public readonly Vector2 ViewSize; + + public ref float Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _scrollViewStatus.Height; + } + + public Vector2 Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _scrollViewStatus.Position; + } + + public ScrollView(in Rect outRect, ScrollViewStatus scrollViewStatus, bool showScrollbars = true) + { + _scrollViewStatus = scrollViewStatus; + _outRectHeight = outRect.height; + var viewRect = new Rect(0f, 0f, outRect.width, Math.Max(Height, _outRectHeight)); + if (Height - 0.1f >= outRect.height) + viewRect.width -= SCROLL_BAR_WIDTH; + + ViewSize = viewRect.size; + Height = 0f; + Widgets.BeginScrollView(outRect, ref _scrollViewStatus.Position, viewRect, showScrollbars); + } + + public void Dispose() => Widgets.EndScrollView(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CanCull(float entryHeight, float entryY) + => entryY + entryHeight < Position.y + || entryY > Position.y + _outRectHeight; + } + + public class ScrollViewStatus + { + public Vector2 Position; + public float Height; + } + + public readonly record struct ListingStandard : IDisposable + { + public Listing_Standard Listing + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public ListingStandard(in Rect rect, Listing_Standard? listing = null) + => (Listing = listing ?? new()).Begin(rect); + + public void Dispose() => Listing.End(); + } + + public readonly record struct ScrollableListingStandard : IDisposable + { + private readonly ListingStandard _listingStandard; + + private readonly ScrollView _scrollView; + + public Listing_Standard Listing + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _listingStandard.Listing; + } + + public Vector2 ScrollViewSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _scrollView.ViewSize; + } + + public Vector2 ScrollViewPosition + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _scrollView.Position; + } + + public ScrollableListingStandard(Rect outRect, ScrollViewStatus scrollViewStatus, + Listing_Standard? listing = null, bool showScrollbars = true) + { + _scrollView = new(outRect, scrollViewStatus, showScrollbars); + _listingStandard = new(new(0f, 0f, ScrollViewSize.x, float.PositiveInfinity), listing); + } + + public void Dispose() + { + _scrollView.Height = Listing.CurHeight + 12f; + _listingStandard.Dispose(); + _scrollView.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryCull(float entryHeight) + { + var canCull = _scrollView.CanCull(entryHeight, Listing.curY); + if (canCull) + Listing.curY += entryHeight; + + return canCull; + } + } + + public readonly record struct TextAnchor : IDisposable + { + private readonly UnityEngine.TextAnchor _default; + + public TextAnchor(UnityEngine.TextAnchor anchor) + { + _default = Text.Anchor; + Text.Anchor = anchor; + } + + public void Dispose() => Text.Anchor = _default; + } + + public readonly record struct WordWrap : IDisposable + { + private readonly bool _default; + + public WordWrap(bool wordWrap) + { + _default = Text.WordWrap; + Text.WordWrap = wordWrap; + } + + public void Dispose() => Text.WordWrap = _default; + } + + public readonly record struct Color : IDisposable + { + private readonly UnityEngine.Color _default; + + public Color(UnityEngine.Color color) + { + _default = GUI.color; + GUI.color = color; + } + + public void Dispose() => GUI.color = _default; + } + + public readonly record struct Font : IDisposable + { + private readonly GameFont _default; + + public Font(GameFont font) + { + _default = Text.Font; + Text.Font = font; + } + + public void Dispose() => Text.Font = _default; + } + + public readonly record struct FontSize : IDisposable + { + private readonly int _default; + + public FontSize(int size) + { + var curFontStyle = Text.CurFontStyle; + + _default = curFontStyle.fontSize; + curFontStyle.fontSize = size; + } + + public void Dispose() => Text.CurFontStyle.fontSize = _default; + } +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/FishPool.cs b/Source/FisheryLib/Pools/FishPool.cs new file mode 100644 index 0000000..e8ef6c3 --- /dev/null +++ b/Source/FisheryLib/Pools/FishPool.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2023 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.Runtime.CompilerServices; + +namespace FisheryLib.Pools; + +public static class FishPool where T : IFishPoolable +{ + private static T?[] _freeItems = new T[4]; + private static int _freeItemCount; + + public static T Get() + { + if (_freeItemCount == 0) + return Reflection.New(); + + var freeItem = _freeItems[--_freeItemCount]; + _freeItems[_freeItemCount] = default; + return freeItem!; + } + + public static void Return(T item) + { + item.Reset(); + + if (++_freeItemCount >= _freeItems.Length) + ExpandFreeItems(); + + _freeItems[_freeItemCount - 1] = item; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ExpandFreeItems() => Array.Resize(ref _freeItems, _freeItems.Length << 1); +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/IFishPoolable.cs b/Source/FisheryLib/Pools/IFishPoolable.cs new file mode 100644 index 0000000..5e0dcad --- /dev/null +++ b/Source/FisheryLib/Pools/IFishPoolable.cs @@ -0,0 +1,11 @@ +// 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/. + +namespace FisheryLib.Pools; + +public interface IFishPoolable +{ + public void Reset(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/PooledIList.cs b/Source/FisheryLib/Pools/PooledIList.cs new file mode 100644 index 0000000..25664d4 --- /dev/null +++ b/Source/FisheryLib/Pools/PooledIList.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2023 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.Runtime.CompilerServices; + +namespace FisheryLib.Pools; + +public record struct PooledIList() : IDisposable where T : IList, new() +{ + public T List = Get(); + + private static T?[] _freeItems = new T[4]; + private static int _freeItemCount; + + public void Dispose() + { + if (List == null) + return; + + Return(List); + List = default!; + } + + public static T Get() + { + if (_freeItemCount == 0) + return Reflection.New(); + + var freeItem = _freeItems[--_freeItemCount]; + _freeItems[_freeItemCount] = default; + return freeItem!; + } + + public static void Return(T item) + { + item.Clear(); + + if (++_freeItemCount >= _freeItems.Length) + ExpandFreeItems(); + + _freeItems[_freeItemCount - 1] = item; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ExpandFreeItems() => Array.Resize(ref _freeItems, _freeItems.Length << 1); +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/PooledStringBuilder.cs b/Source/FisheryLib/Pools/PooledStringBuilder.cs new file mode 100644 index 0000000..019b604 --- /dev/null +++ b/Source/FisheryLib/Pools/PooledStringBuilder.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2023 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.Security; +using System.Text; +using JetBrains.Annotations; + +namespace FisheryLib.Pools; + +public record struct PooledStringBuilder : IDisposable +{ + private FishPoolable _fishPoolable; + + public StringBuilder Builder => _fishPoolable.Builder; + + public StringBuilder Append(string? value) => Builder.Append(value); + public StringBuilder Append(char value) => Builder.Append(value); + public StringBuilder Append(char[] value) => Builder.Append(value); + public StringBuilder Append(PooledStringBuilderHandler value) => value.AppendToStringBuilder(Builder); + + public StringBuilder AppendLine(string? value) => Builder.AppendLine(value); + + public StringBuilder Insert(int index, string? value) => Builder.Insert(index, value); + public StringBuilder Insert(int index, char value) => Builder.Insert(index, value); + + public StringBuilder Remove(int startIndex, int length) => Builder.Remove(startIndex, length); + + public void Clear() => Builder.Clear(); + + public override string ToString() => Builder.ToString(); + + [SecuritySafeCritical] + public PooledStringBuilder() => _fishPoolable = FishPool.Get(); + + public PooledStringBuilder(int minimumSize) : this() + { + if (_fishPoolable.Builder.Capacity < minimumSize) + _fishPoolable.Builder.Capacity = minimumSize; + } + + [SecuritySafeCritical] + public void Dispose() + { + if (_fishPoolable.Builder == null!) + return; + + FishPool.Return(_fishPoolable); + _fishPoolable.Builder = null!; + } + + private struct FishPoolable : IFishPoolable + { + public StringBuilder Builder = new(); + + public void Reset() => Builder.Clear(); + + [UsedImplicitly] + public FishPoolable() {} + } +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/PooledStringBuilderHandler.cs b/Source/FisheryLib/Pools/PooledStringBuilderHandler.cs new file mode 100644 index 0000000..7d94643 --- /dev/null +++ b/Source/FisheryLib/Pools/PooledStringBuilderHandler.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2023 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.Runtime.CompilerServices; +using System.Text; +using JetBrains.Annotations; + +namespace FisheryLib.Pools; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +[InterpolatedStringHandler] +public record struct PooledStringBuilderHandler : IDisposable +{ + private PooledIList> _chars; + + public PooledStringBuilderHandler(int literalLength, int formattedCount) + { + _chars = new(); + _chars.List.EnsureCapacity(literalLength + (formattedCount * 8)); + } + + public void AppendLiteral(string s) => _chars.List.Add(s); + + public void AppendFormatted(T t) + { + if (t != null) + _chars.List.Add(t.ToString()); + } + + public StringBuilder AppendToStringBuilder(StringBuilder destination) + { + destination.Append(_chars.List); + Dispose(); + return destination; + } + + public void Dispose() => _chars.Dispose(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Pools/PooledStringHandler.cs b/Source/FisheryLib/Pools/PooledStringHandler.cs new file mode 100644 index 0000000..1f38bbf --- /dev/null +++ b/Source/FisheryLib/Pools/PooledStringHandler.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2023 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.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace FisheryLib.Pools; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +[InterpolatedStringHandler] +public record struct PooledStringHandler : IDisposable +{ + private PooledStringBuilder _builder; + + public PooledStringHandler(int literalLength, int formattedCount) + => _builder = new(literalLength + (formattedCount * 8)); + + public void AppendLiteral(string s) => _builder.Append(s); + + public void AppendFormatted(T t) + { + if (typeof(T) == typeof(PooledStringBuilderHandler)) + _builder.Append(Unsafe.As(ref t)); + else + _builder.Append(t?.ToString()); + } + + public string GetFormattedText() + { + var result = _builder.ToString(); + Dispose(); + return result; + } + + public void Dispose() => _builder.Dispose(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Reflection.cs b/Source/FisheryLib/Reflection.cs index 58e25d8..f5560d3 100644 --- a/Source/FisheryLib/Reflection.cs +++ b/Source/FisheryLib/Reflection.cs @@ -12,6 +12,7 @@ using System.Runtime.CompilerServices; using System.Security; using System.Text; using System.Threading.Tasks; +using FisheryLib.Pools; using JetBrains.Annotations; using Verse; @@ -20,6 +21,9 @@ namespace FisheryLib; [PublicAPI] public static class Reflection { + private static Assembly[] _allAssemblies = Array.Empty(); + private static object _allAssembliesLock = new(); + public static CodeInstructions GetCodeInstructions(Delegate method, ILGenerator? generator = null) => GetCodeInstructions(method.Method, generator); @@ -43,7 +47,37 @@ public static class Reflection yield return FishTranspiler.Return; } - public static Type? Type(string assembly, string type) => System.Type.GetType($"{type}, {assembly}"); + public static Assembly[] AllAssemblies + { + get + { + lock (_allAssembliesLock) + { + return _allAssemblies.Length == AppDomain.CurrentDomain.GetAssemblies().Length + ? _allAssemblies + : _allAssemblies = AccessTools.AllAssemblies().ToArray(); + } + } + } + + public static Type? Type(string assembly, string typeFullName) + => System.Type.GetType(string.Concat(typeFullName, ", ", assembly)); + + public static Type? Type(string assembly, string @namespace, string type) + => System.Type.GetType(StringHelper.Resolve($"{@namespace}.{type}, {assembly}")); + + public static Type? Type(string fullName) + { + var assemblies = AllAssemblies; + + for (var i = assemblies.Length; i-- > 0;) + { + if (assemblies[i].GetType(fullName) is { } type) + return type; + } + + return null; + } public static MethodInfo MethodInfo(T method) where T : Delegate => method.Method; @@ -323,7 +357,8 @@ public static class Reflection if (memberInfo is null) return "NULL"; - var stringBuilder = new StringBuilder(); + using var pooledStringBuilder = new PooledStringBuilder(); + var stringBuilder = pooledStringBuilder.Builder; switch (memberInfo) { @@ -346,7 +381,7 @@ public static class Reflection if (memberInfo is PropertyInfo propertyInfo1) AppendPropertyPostfix(stringBuilder, propertyInfo1); - return stringBuilder.ToString(); + return pooledStringBuilder.ToString(); } private static void AppendFieldPrefix(FieldInfo fieldInfo, StringBuilder stringBuilder) diff --git a/Source/FisheryLib/StringHelper.cs b/Source/FisheryLib/StringHelper.cs new file mode 100644 index 0000000..c4a508c --- /dev/null +++ b/Source/FisheryLib/StringHelper.cs @@ -0,0 +1,13 @@ +// 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 FisheryLib.Pools; + +namespace FisheryLib; + +public static class StringHelper +{ + public static string Resolve(PooledStringHandler interpolation) => interpolation.GetFormattedText(); +} \ No newline at end of file diff --git a/Source/FisheryLib/Utility.cs b/Source/FisheryLib/Utility.cs index 0c7d8d6..e7862d3 100644 --- a/Source/FisheryLib/Utility.cs +++ b/Source/FisheryLib/Utility.cs @@ -23,6 +23,7 @@ internal static class UtilityF } [DoesNotReturn] + [SuppressMessage("Usage", "CA2201")] internal static void Throw(string message) => throw new(message); [DoesNotReturn] @@ -37,10 +38,10 @@ internal static class UtilityF for (var i = 0; i < stacktrace.FrameCount; i++) { - var assembly = stacktrace.GetFrame(i).GetMethod().ReflectedType.Assembly; + var assembly = stacktrace.GetFrame(i).GetMethod().ReflectedType?.Assembly; fishAssembly ??= assembly; - if (assembly != fishAssembly) + if (assembly != fishAssembly && assembly != null) return assembly; } diff --git a/Source/FisheryLib/Utility/FishMath.cs b/Source/FisheryLib/Utility/FishMath.cs index 9a33491..c1084f8 100644 --- a/Source/FisheryLib/Utility/FishMath.cs +++ b/Source/FisheryLib/Utility/FishMath.cs @@ -35,10 +35,11 @@ public static class FishMath } private static byte[] _trailingZeroCountDeBruijn - = { + = + [ 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 - }; + ]; } \ No newline at end of file