LuaValueDictionary.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System.Diagnostics;
  4. using System.Runtime.CompilerServices;
  5. namespace Lua.Internal;
  6. /// <summary>
  7. /// A minimal dictionary that uses LuaValue as key and value.
  8. /// Nil value counting is included.
  9. /// </summary>
  10. sealed class LuaValueDictionary
  11. {
  12. int[]? _buckets;
  13. Entry[]? _entries;
  14. int _count;
  15. int _freeList;
  16. int _freeCount;
  17. int _version;
  18. const int StartOfFreeList = -3;
  19. int _nilCount;
  20. public LuaValueDictionary(int capacity)
  21. {
  22. if (capacity < 0)
  23. {
  24. ThrowHelper.ThrowArgumentOutOfRangeException(nameof(capacity));
  25. }
  26. if (capacity > 0)
  27. {
  28. Initialize(capacity);
  29. }
  30. }
  31. public int Count => _count - _freeCount;
  32. public int NilCount => _nilCount;
  33. public LuaValue this[LuaValue key]
  34. {
  35. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  36. get
  37. {
  38. ref var value = ref FindValue(key, out _);
  39. if (!Unsafe.IsNullRef(ref value))
  40. {
  41. return value;
  42. }
  43. return default;
  44. }
  45. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  46. set => Insert(key, value);
  47. }
  48. public void Clear()
  49. {
  50. var count = _count;
  51. if (count > 0)
  52. {
  53. Debug.Assert(_buckets != null, "_buckets should be non-null");
  54. Debug.Assert(_entries != null, "_entries should be non-null");
  55. Array.Clear(_buckets, 0, _buckets.Length);
  56. _count = 0;
  57. _freeList = -1;
  58. _freeCount = 0;
  59. Array.Clear(_entries, 0, count);
  60. _nilCount = 0;
  61. }
  62. }
  63. public bool ContainsKey(LuaValue key)
  64. {
  65. return !Unsafe.IsNullRef(ref FindValue(key, out _));
  66. }
  67. public bool ContainsValue(LuaValue value)
  68. {
  69. var entries = _entries;
  70. for (var i = 0; i < _count; i++)
  71. {
  72. if (entries![i].next >= -1 && entries[i].value.Equals(value))
  73. {
  74. return true;
  75. }
  76. }
  77. return false;
  78. }
  79. public Enumerator GetEnumerator()
  80. {
  81. return new(this);
  82. }
  83. [MethodImpl(MethodImplOptions.NoInlining)]
  84. internal ref LuaValue FindValue(LuaValue key, out int index)
  85. {
  86. index = -1;
  87. ref var entry = ref Unsafe.NullRef<Entry>();
  88. if (_buckets != null)
  89. {
  90. Debug.Assert(_entries != null, "expected entries to be != null");
  91. {
  92. var hashCode = (uint)key.GetHashCode();
  93. var i = GetBucket(hashCode);
  94. var entriesLength = _entries.Length;
  95. ref var entriesRef = ref _entries[0];
  96. uint collisionCount = 0;
  97. // ValueType: Devirtualize with EqualityComparer<LuaValue>.Default intrinsic
  98. i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional.
  99. do
  100. {
  101. // Should be a while loop https://github.com/dotnet/runtime/issues/9422
  102. // Test in if to drop range check for following array access
  103. if ((uint)i >= (uint)entriesLength)
  104. {
  105. goto ReturnNotFound;
  106. }
  107. entry = ref Unsafe.Add(ref entriesRef, i);
  108. if (entry.hashCode == hashCode && entry.keyType == key.Type &&
  109. key.Type switch
  110. {
  111. LuaValueType.String => Unsafe.As<string>(entry.keyObject) == key.UnsafeReadString(),
  112. LuaValueType.Number or LuaValueType.Boolean => entry.keyNumber == key.UnsafeReadDouble(),
  113. _ => entry.keyObject == key.UnsafeReadObject()
  114. })
  115. {
  116. index = i;
  117. goto ReturnFound;
  118. }
  119. i = entry.next;
  120. collisionCount++;
  121. } while (collisionCount <= (uint)entriesLength);
  122. // The chain of entries forms a loop; which means a concurrent update has happened.
  123. // Break out of the loop and throw, rather than looping forever.
  124. goto ConcurrentOperation;
  125. }
  126. }
  127. goto ReturnNotFound;
  128. ConcurrentOperation:
  129. ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
  130. ReturnFound:
  131. ref var value = ref entry.value;
  132. Return:
  133. return ref value;
  134. ReturnNotFound:
  135. value = ref Unsafe.NullRef<LuaValue>();
  136. goto Return;
  137. }
  138. void Initialize(int capacity)
  139. {
  140. var newSize = 8;
  141. while (newSize < capacity)
  142. {
  143. newSize *= 2;
  144. }
  145. var size = newSize;
  146. var buckets = new int[size];
  147. var entries = new Entry[size];
  148. // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
  149. _freeList = -1;
  150. _buckets = buckets;
  151. _entries = entries;
  152. }
  153. void Insert(LuaValue key, LuaValue value)
  154. {
  155. if (value.Type is LuaValueType.Nil)
  156. {
  157. _nilCount++;
  158. }
  159. if (_buckets == null)
  160. {
  161. Initialize(0);
  162. }
  163. Debug.Assert(_buckets != null);
  164. var entries = _entries;
  165. Debug.Assert(entries != null, "expected entries to be non-null");
  166. var hashCode = (uint)key.GetHashCode();
  167. uint collisionCount = 0;
  168. ref var bucket = ref GetBucket(hashCode);
  169. var i = bucket - 1; // Value in _buckets is 1-based
  170. {
  171. ref var entry = ref Unsafe.NullRef<Entry>();
  172. while ((uint)i < (uint)entries.Length)
  173. {
  174. entry = ref entries[i];
  175. if (entry.hashCode == hashCode && entry.keyType == key.Type &&
  176. key.Type switch
  177. {
  178. LuaValueType.Number or LuaValueType.Boolean => entry.keyNumber == key.UnsafeReadDouble(),
  179. LuaValueType.String => Unsafe.As<string>(entry.keyObject) == key.UnsafeReadString(),
  180. _ => entry.keyObject == key.UnsafeReadObject()
  181. })
  182. {
  183. if (entry.value.Type is LuaValueType.Nil)
  184. {
  185. _nilCount--;
  186. }
  187. entry.value = value;
  188. return;
  189. }
  190. i = entry.next;
  191. collisionCount++;
  192. if (collisionCount > (uint)entries.Length)
  193. {
  194. // The chain of entries forms a loop; which means a concurrent update has happened.
  195. // Break out of the loop and throw, rather than looping forever.
  196. ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
  197. }
  198. }
  199. }
  200. int index;
  201. if (_freeCount > 0)
  202. {
  203. index = _freeList;
  204. Debug.Assert(StartOfFreeList - entries[_freeList].next >= -1, "shouldn't overflow because `next` cannot underflow");
  205. _freeList = StartOfFreeList - entries[_freeList].next;
  206. _freeCount--;
  207. }
  208. else
  209. {
  210. var count = _count;
  211. if (count == entries.Length)
  212. {
  213. Resize();
  214. bucket = ref GetBucket(hashCode);
  215. }
  216. index = count;
  217. _count = count + 1;
  218. entries = _entries;
  219. }
  220. {
  221. ref var entry = ref entries![index];
  222. entry.hashCode = hashCode;
  223. entry.next = bucket - 1; // Value in _buckets is 1-based
  224. entry.keyType = key.Type;
  225. entry.keyObject = key.UnsafeReadObject();
  226. entry.keyNumber = key.UnsafeReadDouble();
  227. entry.value = value;
  228. bucket = index + 1; // Value in _buckets is 1-based
  229. _version++;
  230. }
  231. }
  232. void Resize()
  233. {
  234. Resize(_entries!.Length * 2);
  235. }
  236. void Resize(int newSize)
  237. {
  238. // Value types never rehash
  239. Debug.Assert(_entries != null, "_entries should be non-null");
  240. Debug.Assert(newSize >= _entries.Length);
  241. var entries = new Entry[newSize];
  242. var count = _count;
  243. Array.Copy(_entries, entries, count);
  244. // Assign member variables after both arrays allocated to guard against corruption from OOM if second fails
  245. _buckets = new int[newSize];
  246. for (var i = 0; i < count; i++)
  247. {
  248. if (entries[i].next >= -1)
  249. {
  250. ref var bucket = ref GetBucket(entries[i].hashCode);
  251. entries[i].next = bucket - 1; // Value in _buckets is 1-based
  252. bucket = i + 1;
  253. }
  254. }
  255. _entries = entries;
  256. }
  257. public bool Remove(LuaValue key)
  258. {
  259. // The overload Remove(LuaValue key, out LuaValue value) is a copy of this method with one additional
  260. // statement to copy the value for entry being removed into the output parameter.
  261. // Code has been intentionally duplicated for performance reasons.
  262. if (_buckets != null)
  263. {
  264. Debug.Assert(_entries != null, "entries should be non-null");
  265. uint collisionCount = 0;
  266. var hashCode = (uint)key.GetHashCode();
  267. ref var bucket = ref GetBucket(hashCode);
  268. var entries = _entries;
  269. var last = -1;
  270. var i = bucket - 1; // Value in buckets is 1-based
  271. while (i >= 0)
  272. {
  273. ref var entry = ref entries[i];
  274. if (entry.hashCode == hashCode && entry.keyType == key.Type &&
  275. key.Type switch
  276. {
  277. LuaValueType.Number or LuaValueType.Boolean => entry.keyNumber == key.UnsafeReadDouble(),
  278. LuaValueType.String => Unsafe.As<string>(entry.keyObject) == key.UnsafeReadString(),
  279. _ => entry.keyObject == key.UnsafeReadObject()
  280. })
  281. {
  282. if (last < 0)
  283. {
  284. bucket = entry.next + 1; // Value in buckets is 1-based
  285. }
  286. else
  287. {
  288. entries[last].next = entry.next;
  289. }
  290. Debug.Assert(StartOfFreeList - _freeList < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646");
  291. entry.next = StartOfFreeList - _freeList;
  292. if (entry.value.Type is LuaValueType.Nil)
  293. {
  294. _nilCount--;
  295. }
  296. entry.keyType = default;
  297. entry.keyObject = null!;
  298. entry.value = default;
  299. _freeList = i;
  300. _freeCount++;
  301. return true;
  302. }
  303. last = i;
  304. i = entry.next;
  305. collisionCount++;
  306. if (collisionCount > (uint)entries.Length)
  307. {
  308. // The chain of entries forms a loop; which means a concurrent update has happened.
  309. // Break out of the loop and throw, rather than looping forever.
  310. ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
  311. }
  312. }
  313. }
  314. return false;
  315. }
  316. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  317. public bool TryGetValue(LuaValue key, out LuaValue value)
  318. {
  319. ref var valRef = ref FindValue(key, out _);
  320. if (!Unsafe.IsNullRef(ref valRef))
  321. {
  322. value = valRef;
  323. return true;
  324. }
  325. value = default;
  326. return false;
  327. }
  328. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  329. public bool TryGetNext(LuaValue key, out KeyValuePair<LuaValue, LuaValue> pair)
  330. {
  331. ref var valRef = ref FindValue(key, out var index);
  332. if (!Unsafe.IsNullRef(ref valRef))
  333. {
  334. var entries = _entries!;
  335. while (++index < _count)
  336. {
  337. ref var entry = ref entries[index];
  338. if (entry is { next: >= -1, value.Type: not LuaValueType.Nil })
  339. {
  340. pair = new(new(entry.keyType, entry.keyNumber, entry.keyObject), entry.value);
  341. return true;
  342. }
  343. }
  344. }
  345. pair = default;
  346. return false;
  347. }
  348. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  349. ref int GetBucket(uint hashCode)
  350. {
  351. var buckets = _buckets!;
  352. return ref buckets[hashCode & (buckets.Length - 1)];
  353. }
  354. struct Entry
  355. {
  356. public uint hashCode;
  357. public LuaValueType keyType;
  358. public object? keyObject;
  359. public double keyNumber;
  360. /// <summary>
  361. /// 0-based index of next entry in chain: -1 means end of chain
  362. /// also encodes whether this entry _itself_ is part of the free list by changing sign and subtracting 3,
  363. /// so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc.
  364. /// </summary>
  365. public int next;
  366. public LuaValue value; // Value of entry
  367. }
  368. internal int Version => _version;
  369. internal static bool MoveNext(LuaValueDictionary dictionary, int version, ref int index, out KeyValuePair<LuaValue, LuaValue> current)
  370. {
  371. if (version != dictionary._version)
  372. {
  373. ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
  374. }
  375. SearchNext:
  376. // Use unsigned comparison since we set index to dictionary.count+1 when the enumeration ends.
  377. // dictionary.count+1 could be negative if dictionary.count is int.MaxValue
  378. while ((uint)index < (uint)dictionary._count)
  379. {
  380. ref var entry = ref dictionary._entries![index++];
  381. if (entry.next >= -1)
  382. {
  383. if (entry.value.Type is LuaValueType.Nil)
  384. {
  385. goto SearchNext;
  386. }
  387. current = new(new(entry.keyType, entry.keyNumber, entry.keyObject), entry.value);
  388. return true;
  389. }
  390. }
  391. index = dictionary._count + 1;
  392. current = default;
  393. return false;
  394. }
  395. public struct Enumerator
  396. {
  397. readonly LuaValueDictionary dictionary;
  398. readonly int _version;
  399. int _index;
  400. KeyValuePair<LuaValue, LuaValue> _current;
  401. internal Enumerator(LuaValueDictionary dictionary)
  402. {
  403. this.dictionary = dictionary;
  404. _version = dictionary._version;
  405. _index = 0;
  406. _current = default;
  407. }
  408. public bool MoveNext()
  409. {
  410. return LuaValueDictionary.MoveNext(dictionary, _version, ref _index, out _current);
  411. }
  412. public KeyValuePair<LuaValue, LuaValue> Current => _current;
  413. }
  414. static class ThrowHelper
  415. {
  416. public static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported()
  417. {
  418. throw new InvalidOperationException("Concurrent operations are not supported");
  419. }
  420. public static void ThrowArgumentOutOfRangeException(string paramName)
  421. {
  422. throw new ArgumentOutOfRangeException(paramName);
  423. }
  424. public static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
  425. {
  426. throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");
  427. }
  428. }
  429. }