From 03910b7fb10df48d404daae35346c322a475fbbe Mon Sep 17 00:00:00 2001 From: GGG Date: Sun, 28 Jan 2024 01:02:40 -0300 Subject: [PATCH] Add the Green Node cache. These are also added as invisible classes because they should only be used by generated code as well, so we won't pollute the autocomplete suggestions with them. --- Tsu.Trees.RedGreen/src/Internal/GreenCache.cs | 222 +++++++++ Tsu.Trees.RedGreen/src/Utilities/Hash.cs | 441 ++++++++++++++++++ 2 files changed, 663 insertions(+) create mode 100644 Tsu.Trees.RedGreen/src/Internal/GreenCache.cs create mode 100644 Tsu.Trees.RedGreen/src/Utilities/Hash.cs diff --git a/Tsu.Trees.RedGreen/src/Internal/GreenCache.cs b/Tsu.Trees.RedGreen/src/Internal/GreenCache.cs new file mode 100644 index 0000000..31d5f96 --- /dev/null +++ b/Tsu.Trees.RedGreen/src/Internal/GreenCache.cs @@ -0,0 +1,222 @@ +using System.ComponentModel; +using System.Diagnostics; +using Tsu.Trees.RedGreen.Utilities; + +namespace Tsu.Trees.RedGreen.Internal; + +/// +/// A global cache for all green nodes. +/// +/// +/// +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class GreenCache + where TGreenRoot : class, IGreenNode + where TRedRoot : class + where TKind : Enum +{ + private const int CacheSizeBits = 16; + private const int CacheSize = 1 << CacheSizeBits; + private const int CacheMask = CacheSize - 1; + + private readonly struct Entry + { + public readonly int hash; + public readonly TGreenRoot? node; + + internal Entry(int hash, TGreenRoot node) + { + this.hash = hash; + this.node = node; + } + } + + private static readonly Entry[] s_cache = new Entry[CacheSize]; + + /// + /// Adds a node to the cache. + /// + /// + /// + public static void AddNode(TGreenRoot node, int hash) + { + if (AllChildrenInCache(node)) + { + Debug.Assert(node.GetCacheHash() == hash); + + var idx = hash & CacheMask; + s_cache[idx] = new Entry(hash, node); + } + } + + private static bool CanBeCached(TGreenRoot? child1) => + child1 == null || child1.IsCacheable; + + private static bool CanBeCached(TGreenRoot? child1, TGreenRoot? child2) => + CanBeCached(child1) && CanBeCached(child2); + + private static bool CanBeCached(TGreenRoot? child1, TGreenRoot? child2, TGreenRoot? child3) => + CanBeCached(child1) && CanBeCached(child2) && CanBeCached(child3); + + private static bool ChildInCache(TGreenRoot? child) + { + // for the purpose of this function consider that + // null nodes, tokens and trivias are cached somewhere else. + // TODO: should use slotCount + if (child == null || child.SlotCount == 0) return true; + + int hash = child.GetCacheHash(); + int idx = hash & CacheMask; + return s_cache[idx].node == child; + } + + private static bool AllChildrenInCache(TGreenRoot node) + { + // TODO: should use slotCount + var cnt = node.SlotCount; + for (int i = 0; i < cnt; i++) + { + if (!ChildInCache(node.GetSlot(i))) + { + return false; + } + } + + return true; + } + + /// + /// Attempts to get the cached version of the given node from the cache. + /// + /// + /// + /// + /// + public static TGreenRoot? TryGetNode(int kind, TGreenRoot? child1, out int hash) + { + if (CanBeCached(child1)) + { + int h = hash = GetCacheHash(kind, child1); + int idx = h & CacheMask; + var e = s_cache[idx]; + if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, child1)) + { + return e.node; + } + } + else + { + hash = -1; + } + + return null; + } + + /// + /// Attempts to get the cached version of the given node from the cache. + /// + /// + /// + /// + /// + /// + public static TGreenRoot? TryGetNode(int kind, TGreenRoot? child1, TGreenRoot? child2, out int hash) + { + if (CanBeCached(child1, child2)) + { + int h = hash = GetCacheHash(kind, child1, child2); + int idx = h & CacheMask; + var e = s_cache[idx]; + if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, child1, child2)) + { + return e.node; + } + } + else + { + hash = -1; + } + + return null; + } + + /// + /// Attempts to get the cached version of the given node from the cache. + /// + /// + /// + /// + /// + /// + /// + public static TGreenRoot? TryGetNode(int kind, TGreenRoot? child1, TGreenRoot? child2, TGreenRoot? child3, out int hash) + { + if (CanBeCached(child1, child2, child3)) + { + int h = hash = GetCacheHash(kind, child1, child2, child3); + int idx = h & CacheMask; + var e = s_cache[idx]; + if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, child1, child2, child3)) + { + return e.node; + } + } + else + { + hash = -1; + } + + return null; + } + + private static int GetCacheHash(int kind, TGreenRoot? child1) + { + int code = kind; + + // the only child is never null + // https://github.com/dotnet/roslyn/issues/41539 + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child1!), code); + + // ensure nonnegative hash + return code & int.MaxValue; + } + + private static int GetCacheHash(int kind, TGreenRoot? child1, TGreenRoot? child2) + { + int code = kind; + + if (child1 != null) + { + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child1), code); + } + if (child2 != null) + { + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child2), code); + } + + // ensure nonnegative hash + return code & int.MaxValue; + } + + private static int GetCacheHash(int kind, TGreenRoot? child1, TGreenRoot? child2, TGreenRoot? child3) + { + int code = kind; + + if (child1 != null) + { + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child1), code); + } + if (child2 != null) + { + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child2), code); + } + if (child3 != null) + { + code = Hash.Combine(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(child3), code); + } + + // ensure nonnegative hash + return code & int.MaxValue; + } +} \ No newline at end of file diff --git a/Tsu.Trees.RedGreen/src/Utilities/Hash.cs b/Tsu.Trees.RedGreen/src/Utilities/Hash.cs new file mode 100644 index 0000000..eacb022 --- /dev/null +++ b/Tsu.Trees.RedGreen/src/Utilities/Hash.cs @@ -0,0 +1,441 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// Sourced from: https://github.com/dotnet/roslyn/blob/cda55873bfd7f16ac6176f467f98ddf8f644e101/src/Compilers/Core/Portable/InternalUtilities/Hash.cs + +using System.Collections.Immutable; +using System.ComponentModel; + +namespace Tsu.Trees.RedGreen.Utilities; + +/// +/// Internal hashing utilities used by generated code. +/// +/// +/// This should never be used directly unless if you know what you're doing. +/// It is left undocumented on purpose to discourage usage. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class Hash +{ + #pragma warning disable CS1591 // We don't document "internal" interfaces. + + public static int Combine(int newKey, int currentKey) + { + return unchecked((currentKey * (int) 0xA5555529) + newKey); + } + + public static int Combine(bool newKeyPart, int currentKey) + { + return Combine(currentKey, newKeyPart ? 1 : 0); + } + + /// + /// This is how VB Anonymous Types combine hash values for fields. + /// PERF: Do not use with enum types because that involves multiple + /// unnecessary boxing operations. Unfortunately, we can't constrain + /// T to "non-enum", so we'll use a more restrictive constraint. + /// + public static int Combine(T newKeyPart, int currentKey) where T : class? + { + int hash = unchecked(currentKey * (int) 0xA5555529); + + if (newKeyPart != null) + { + return unchecked(hash + newKeyPart.GetHashCode()); + } + + return hash; + } + + public static int CombineValues(IEnumerable? values, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + public static int CombineValues(ImmutableDictionary values, int maxItemsToHash = int.MaxValue) + where TKey : notnull + { + if (values == null) + return 0; + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + break; + + hashCode = Combine(value.GetHashCode(), hashCode); + } + + return hashCode; + } + + public static int CombineValues(T[]? values, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var maxSize = Math.Min(maxItemsToHash, values.Length); + var hashCode = 0; + + for (int i = 0; i < maxSize; i++) + { + T value = values[i]; + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + public static int CombineValues(ImmutableArray values, int maxItemsToHash = int.MaxValue) + { + if (values.IsDefaultOrEmpty) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + public static int CombineValues(IEnumerable? values, StringComparer stringComparer, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + if (value != null) + { + hashCode = Combine(stringComparer.GetHashCode(value), hashCode); + } + } + + return hashCode; + } + + public static int CombineValues(ImmutableArray values, StringComparer stringComparer, int maxItemsToHash = int.MaxValue) + { + if (values == null) + return 0; + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + break; + + if (value != null) + hashCode = Combine(stringComparer.GetHashCode(value), hashCode); + } + + return hashCode; + } + + /// + /// The offset bias value used in the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + internal const int FnvOffsetBias = unchecked((int) 2166136261); + + /// + /// The generative factor used in the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + internal const int FnvPrime = 16777619; + + /// + /// Compute the FNV-1a hash of a sequence of bytes + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes + /// The FNV-1a hash of + public static int GetFNVHashCode(byte[] data) + { + int hashCode = FnvOffsetBias; + + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the FNV-1a hash of a sequence of bytes and determines if the byte + /// sequence is valid ASCII and hence the hash code matches a char sequence + /// encoding the same text. + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes that are likely to be ASCII text. + /// True if the sequence contains only characters in the ASCII range. + /// The FNV-1a hash of + public static int GetFNVHashCode(ReadOnlySpan data, out bool isAscii) + { + int hashCode = FnvOffsetBias; + + byte asciiMask = 0; + + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + asciiMask |= b; + hashCode = unchecked((hashCode ^ b) * FnvPrime); + } + + isAscii = (asciiMask & 0x80) == 0; + return hashCode; + } + + /// + /// Compute the FNV-1a hash of a sequence of bytes + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes + /// The FNV-1a hash of + public static int GetFNVHashCode(ImmutableArray data) + { + int hashCode = FnvOffsetBias; + + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here + /// for 16-bit Unicode chars on the understanding that the majority of chars will + /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits + /// for generating hash codes. + /// + public static int GetFNVHashCode(ReadOnlySpan data) + { + return CombineFNVHash(FnvOffsetBias, data); + } + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here + /// for 16-bit Unicode chars on the understanding that the majority of chars will + /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits + /// for generating hash codes. + /// + /// The input string + /// The start index of the first character to hash + /// The number of characters, beginning with to hash + /// The FNV-1a hash code of the substring beginning at and ending after characters. + public static int GetFNVHashCode(string text, int start, int length) + => GetFNVHashCode(text.AsSpan(start, length)); + + public static int GetCaseInsensitiveFNVHashCode(string text) + { + return GetCaseInsensitiveFNVHashCode(text.AsSpan(0, text.Length)); + } + + public static int GetCaseInsensitiveFNVHashCode(ReadOnlySpan data) + { + int hashCode = FnvOffsetBias; + + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ char.ToLower(data[i])) * FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The start index of the first character to hash + /// The FNV-1a hash code of the substring beginning at and ending at the end of the string. + public static int GetFNVHashCode(string text, int start) + { + return GetFNVHashCode(text, start, length: text.Length - start); + } + + /// + /// Compute the hashcode of a string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The FNV-1a hash code of + public static int GetFNVHashCode(string text) + { + return CombineFNVHash(FnvOffsetBias, text); + } + + /// + /// Compute the hashcode of a string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The FNV-1a hash code of + public static int GetFNVHashCode(System.Text.StringBuilder text) + { + int hashCode = FnvOffsetBias; + +#if NETCOREAPP3_1_OR_GREATER + foreach (var chunk in text.GetChunks()) + { + hashCode = CombineFNVHash(hashCode, chunk.Span); + } +#else + // StringBuilder.GetChunks is not available in this target framework. Since there is no other direct access + // to the underlying storage spans of StringBuilder, we fall back to using slower per-character operations. + int end = text.Length; + + for (int i = 0; i < end; i++) + { + hashCode = unchecked((hashCode ^ text[i]) * Hash.FnvPrime); + } +#endif + + return hashCode; + } + + /// + /// Compute the hashcode of a sub string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string as a char array + /// The start index of the first character to hash + /// The number of characters, beginning with to hash + /// The FNV-1a hash code of the substring beginning at and ending after characters. + public static int GetFNVHashCode(char[] text, int start, int length) + { + int hashCode = FnvOffsetBias; + int end = start + length; + + for (int i = start; i < end; i++) + { + hashCode = unchecked((hashCode ^ text[i]) * FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the hashcode of a single character using the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: In general, this isn't any more useful than "char.GetHashCode". However, + /// it may be needed if you need to generate the same hash code as a string or + /// substring with just a single character. + /// + /// The character to hash + /// The FNV-1a hash code of the character. + public static int GetFNVHashCode(char ch) + { + return CombineFNVHash(FnvOffsetBias, ch); + } + + /// + /// Combine a string with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The string to combine + /// The result of combining with using the FNV-1a algorithm + public static int CombineFNVHash(int hashCode, string text) + { + foreach (char ch in text) + { + hashCode = unchecked((hashCode ^ ch) * FnvPrime); + } + + return hashCode; + } + + /// + /// Combine a char with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The new character to combine + /// The result of combining with using the FNV-1a algorithm + public static int CombineFNVHash(int hashCode, char ch) + { + return unchecked((hashCode ^ ch) * FnvPrime); + } + + /// + /// Combine a string with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The string to combine + /// The result of combining with using the FNV-1a algorithm + public static int CombineFNVHash(int hashCode, ReadOnlySpan data) + { + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * FnvPrime); + } + + return hashCode; + } +} \ No newline at end of file