From f09b927a3b78ec4e920dd7ee7d571bdd1ea7dec9 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:19:59 +0300 Subject: [PATCH 01/20] feature: introduce source generator for enum code generation --- Directory.Packages.props | 3 + Garnet.sln | 13 ++- libs/common/EnumUtils.cs | 5 ++ libs/common/Garnet.common.csproj | 5 ++ libs/gen/EnumsSourceGenerator.cs | 133 +++++++++++++++++++++++++++++++ libs/gen/Garnet.gen.csproj | 21 +++++ 6 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 libs/gen/EnumsSourceGenerator.cs create mode 100644 libs/gen/Garnet.gen.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 411e403aed..900adbcfd7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,5 +23,8 @@ + + + \ No newline at end of file diff --git a/Garnet.sln b/Garnet.sln index 9e96aea436..871439e19d 100644 --- a/Garnet.sln +++ b/Garnet.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31808.319 MinimumVisualStudioVersion = 10.0.40219.1 @@ -96,6 +96,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandInfoUpdater", "playg EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "playground\SampleModule\SampleModule.csproj", "{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Garnet.gen", "libs\gen\Garnet.gen.csproj", "{E248012E-1D19-4114-924F-EFC9E859C4CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -280,6 +282,14 @@ Global {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|Any CPU.Build.0 = Release|Any CPU {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.ActiveCfg = Release|Any CPU {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.Build.0 = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.Build.0 = Debug|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.Build.0 = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.ActiveCfg = Release|Any CPU + {E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -309,6 +319,7 @@ Global {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD} = {346A5A53-51E4-4A75-B7E6-491D950382CE} {9BE474A2-1547-43AC-B4F2-FB48A01FA995} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F} = {69A71E2C-00E3-42F3-854E-BE157A24834E} + {E248012E-1D19-4114-924F-EFC9E859C4CD} = {147FCE31-EC09-4C90-8E4D-37CA87ED18C3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2C02C405-4798-41CA-AF98-61EDFEF6772E} diff --git a/libs/common/EnumUtils.cs b/libs/common/EnumUtils.cs index 07adc9f380..2c87d77303 100644 --- a/libs/common/EnumUtils.cs +++ b/libs/common/EnumUtils.cs @@ -8,6 +8,11 @@ namespace Garnet.common { + [AttributeUsage(AttributeTargets.Enum)] + public sealed class GenerateEnumUtilsAttribute : Attribute + { + } + /// /// Utilities for enums /// diff --git a/libs/common/Garnet.common.csproj b/libs/common/Garnet.common.csproj index f006085359..b42ab5c234 100644 --- a/libs/common/Garnet.common.csproj +++ b/libs/common/Garnet.common.csproj @@ -17,4 +17,9 @@ + + + + + \ No newline at end of file diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs new file mode 100644 index 0000000000..9a4f6a3d7a --- /dev/null +++ b/libs/gen/EnumsSourceGenerator.cs @@ -0,0 +1,133 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Garnet; + +[Generator] +public class EnumsSourceGenerator : ISourceGenerator +{ + const string GeneratedClassName = "EnumUtils"; + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new EnumsSyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + var syntaxReceiver = context.SyntaxReceiver as EnumsSyntaxReceiver; + if (syntaxReceiver is null) return; + + foreach (var enumDeclaration in syntaxReceiver.Enums) + { + var enumName = enumDeclaration.Identifier.Text; + + var values = enumDeclaration.Members + .Select(m => ( + m.Identifier.Text, + Value: m.EqualsValue?.Value?.ToString(), + Description: m.AttributeLists + .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") + .SingleOrDefault() + )) + .ToList(); + + var classBuilder = new StringBuilder(); + classBuilder.AppendLine("// "); + classBuilder.AppendLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); + + classBuilder.AppendLine("using System;"); + classBuilder.AppendLine("using System.ComponentModel;"); + classBuilder.AppendLine("using System.Numerics;"); + classBuilder.AppendLine(); + classBuilder.AppendLine("namespace namespace Garnet;"); + classBuilder.AppendLine(); + classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); + classBuilder.AppendLine("{"); + classBuilder.AppendLine(GenerateTryParseEnumFromDescriptionMethod(enumName, values)); + classBuilder.AppendLine(); + classBuilder.AppendLine(GenerateGetEnumDescriptionsMethod(enumName, values)); + classBuilder.AppendLine("}"); + var classSource = classBuilder.ToString(); + context.AddSource($"{GeneratedClassName}.{enumName}.g.cs", classSource); + } + } + + private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) + { + var method = new StringBuilder(); + method.AppendLine($" public static bool TryParse{enumName}FromDescription(string description, out {enumName} result)"); + method.AppendLine(" {"); + method.AppendLine(" result = default;"); + method.AppendLine(" switch (description)"); + method.AppendLine(" {"); + foreach (var (name, _, description) in values) + { + bool hasDescription = false; + var descriptionValue = description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString(); + if (descriptionValue is not null) + { + hasDescription = true; + method.AppendLine($" case {descriptionValue}:"); + } + + if (!hasDescription) continue; + method.AppendLine($" result = {enumName}.{name};"); + method.AppendLine(" return true;"); + } + method.AppendLine(" }"); + method.AppendLine(); + method.AppendLine(" return false;"); + method.AppendLine(" }"); + method.AppendLine(); + + return method.ToString(); + } + private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) + { + var method = new StringBuilder(); + method.AppendLine($" public static string[] Get{enumName}Descriptions({enumName} value)"); + method.AppendLine(" {"); + method.AppendLine(" var setFlags = BitOperations.PopCount((uint)value);"); + method.AppendLine(" if (setFlags == 0) return Array.Empty();"); + method.AppendLine(" if (setFlags == 1)"); + method.AppendLine(" {"); + method.AppendLine(" return value switch"); + method.AppendLine(" {"); + foreach (var (name, _, description) in values) + { + if (description is null) continue; + method.AppendLine($" {enumName}.{name} => [ {description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()} ],"); + } + method.AppendLine(" _ => Array.Empty(),"); + method.AppendLine(" };"); + method.AppendLine(" }"); + method.AppendLine(); + method.AppendLine(" var descriptions = new string[setFlags];"); + method.AppendLine(" var index = 0;"); + foreach (var (name, _, description) in values) + { + if (description is null) continue; + method.AppendLine($" if ((value & {enumName}.{name}) != 0) descriptions[index++] = {description.ArgumentList?.Arguments.FirstOrDefault()?.ToString()};"); + } + method.AppendLine(); + method.AppendLine(" return descriptions;"); + method.AppendLine(" }"); + + return method.ToString(); + } + + private class EnumsSyntaxReceiver : ISyntaxReceiver + { + public List Enums { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is EnumDeclarationSyntax enumDeclarationSyntax + && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumUtils"))) + { + Enums.Add(enumDeclarationSyntax); + } + } + } +} \ No newline at end of file diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj new file mode 100644 index 0000000000..a26451360d --- /dev/null +++ b/libs/gen/Garnet.gen.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + enable + enable + Latest + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 8dee215bddb16992c1f12fd6ff5b53f46ecec1e2 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:23:55 +0300 Subject: [PATCH 02/20] feature: introduce source generator for enum code generation --- libs/common/Garnet.common.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/common/Garnet.common.csproj b/libs/common/Garnet.common.csproj index b42ab5c234..7e934b1319 100644 --- a/libs/common/Garnet.common.csproj +++ b/libs/common/Garnet.common.csproj @@ -5,6 +5,7 @@ ../../Garnet.snk false true + true From 150a6652ea1079c660727d591c02d73bc79ccc7b Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:24:43 +0300 Subject: [PATCH 03/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 9a4f6a3d7a..9261b77301 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -35,6 +35,7 @@ public void Execute(GeneratorExecutionContext context) var classBuilder = new StringBuilder(); classBuilder.AppendLine("// "); classBuilder.AppendLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); + classBuilder.AppendLine("// "); classBuilder.AppendLine("using System;"); classBuilder.AppendLine("using System.ComponentModel;"); From 03caf87da4e75d07cdd929cc34babe6ea16fa129 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:33:18 +0300 Subject: [PATCH 04/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 2 +- libs/gen/Garnet.gen.csproj | 3 +-- libs/server/Resp/RespCommandInfoFlags.cs | 3 +++ libs/server/Resp/RespCommandKeySpecification.cs | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 9261b77301..aeaaf11264 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -41,7 +41,7 @@ public void Execute(GeneratorExecutionContext context) classBuilder.AppendLine("using System.ComponentModel;"); classBuilder.AppendLine("using System.Numerics;"); classBuilder.AppendLine(); - classBuilder.AppendLine("namespace namespace Garnet;"); + classBuilder.AppendLine("namespace namespace Garnet.common;"); classBuilder.AppendLine(); classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); classBuilder.AppendLine("{"); diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj index a26451360d..9ab2d884ae 100644 --- a/libs/gen/Garnet.gen.csproj +++ b/libs/gen/Garnet.gen.csproj @@ -1,7 +1,6 @@  - - netstandard2.0 + enable enable Latest diff --git a/libs/server/Resp/RespCommandInfoFlags.cs b/libs/server/Resp/RespCommandInfoFlags.cs index d2cfcd9c30..7656d5a530 100644 --- a/libs/server/Resp/RespCommandInfoFlags.cs +++ b/libs/server/Resp/RespCommandInfoFlags.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Net.NetworkInformation; +using Garnet.common; namespace Garnet.server { @@ -11,6 +12,7 @@ namespace Garnet.server /// RESP command flags /// [Flags] + [GenerateEnumUtils] public enum RespCommandFlags { None = 0, @@ -62,6 +64,7 @@ public enum RespCommandFlags /// RESP ACL categories /// [Flags] + [GenerateEnumUtils] public enum RespAclCategories { None = 0, diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs index f11292b8a9..d8964a7de0 100644 --- a/libs/server/Resp/RespCommandKeySpecification.cs +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -100,6 +100,7 @@ public string ToRespFormat() /// RESP key specification flags /// [Flags] + [GenerateEnumUtils] public enum KeySpecificationFlags : ushort { None = 0, From cceb42f0926e83e7d7e3892fe287dd6d90486e3f Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:35:30 +0300 Subject: [PATCH 05/20] feature: introduce source generator for enum code generation --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 900adbcfd7..bc1046f3ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,9 @@ - + + + @@ -24,7 +26,5 @@ - - \ No newline at end of file From 9e288b3c0939e080baa916356a7a96755ce390b4 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:40:32 +0300 Subject: [PATCH 06/20] feature: introduce source generator for enum code generation --- libs/gen/Garnet.gen.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj index 9ab2d884ae..a9bf84db92 100644 --- a/libs/gen/Garnet.gen.csproj +++ b/libs/gen/Garnet.gen.csproj @@ -1,6 +1,7 @@  + netstandard2.0 enable enable Latest From 0ecb009ab353c07504c85e46806d306f00a09f5c Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 11 Aug 2024 22:46:58 +0300 Subject: [PATCH 07/20] feature: introduce source generator for enum code generation --- libs/common/Garnet.common.csproj | 6 ------ libs/server/Garnet.server.csproj | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/common/Garnet.common.csproj b/libs/common/Garnet.common.csproj index 7e934b1319..f006085359 100644 --- a/libs/common/Garnet.common.csproj +++ b/libs/common/Garnet.common.csproj @@ -5,7 +5,6 @@ ../../Garnet.snk false true - true @@ -18,9 +17,4 @@ - - - - - \ No newline at end of file diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index 2bbe3b854c..aec257d9a7 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -5,6 +5,7 @@ ../../Garnet.snk false true + true @@ -26,4 +27,9 @@ + + + + + \ No newline at end of file From 1da53f7f870f3a3db6d18a52291fb2bd17d01976 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 12 Aug 2024 08:04:38 +0300 Subject: [PATCH 08/20] feature: introduce source generator for enum code generation --- libs/common/EnumUtils.cs | 118 ------------------ libs/common/GenerateEnumUtilsAttribute.cs | 12 ++ libs/gen/EnumsSourceGenerator.cs | 4 +- libs/server/Resp/RespCommandInfoParser.cs | 6 +- .../Resp/RespCommandKeySpecification.cs | 2 +- libs/server/Resp/RespCommandsInfo.cs | 4 +- 6 files changed, 21 insertions(+), 125 deletions(-) delete mode 100644 libs/common/EnumUtils.cs create mode 100644 libs/common/GenerateEnumUtilsAttribute.cs diff --git a/libs/common/EnumUtils.cs b/libs/common/EnumUtils.cs deleted file mode 100644 index 2c87d77303..0000000000 --- a/libs/common/EnumUtils.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; - -namespace Garnet.common -{ - [AttributeUsage(AttributeTargets.Enum)] - public sealed class GenerateEnumUtilsAttribute : Attribute - { - } - - /// - /// Utilities for enums - /// - public static class EnumUtils - { - private static readonly Dictionary> EnumNameToDescriptionCache = new(); - private static readonly Dictionary>> EnumDescriptionToNameCache = new(); - - /// - /// Gets a mapping between an enum's string value to its description, for each of the enum's values - /// - /// Enum type - /// A dictionary mapping between the enum's string value to its description - public static IDictionary GetEnumNameToDescription() where T : Enum - { - // Check if mapping is already in the cache. If not, add it to the cache. - if (!EnumNameToDescriptionCache.ContainsKey(typeof(T))) - AddTypeToCache(); - - return EnumNameToDescriptionCache[typeof(T)]; - } - - /// - /// If enum does not have the 'Flags' attribute, gets an array of size 1 with the description of the enum's value. - /// If no description exists, returns the ToString() value of the input value. - /// If enum has the 'Flags' attribute, gets an array with all the descriptions of the flags which are turned on in the input value. - /// If no description exists, returns the ToString() value of the flag. - /// - /// Enum type - /// Enum value - /// Array of descriptions - public static string[] GetEnumDescriptions(T value) where T : Enum - { - var nameToDesc = GetEnumNameToDescription(); - return value.ToString().Split(',').Select(f => nameToDesc.ContainsKey(f.Trim()) ? nameToDesc[f.Trim()] : f).ToArray(); - } - - /// - /// Gets an enum's values based on the description attribute - /// - /// Enum type - /// Enum description - /// Enum values - /// True if matched more than one value successfully - public static bool TryParseEnumsFromDescription(string strVal, out IEnumerable vals) where T : struct, Enum - { - vals = new List(); - - if (!EnumDescriptionToNameCache.ContainsKey(typeof(T))) - AddTypeToCache(); - - if (!EnumDescriptionToNameCache[typeof(T)].ContainsKey(strVal)) - return false; - - foreach (var enumName in EnumDescriptionToNameCache[typeof(T)][strVal]) - { - if (Enum.TryParse(enumName, out T enumVal)) - { - ((List)vals).Add(enumVal); - } - } - - return ((List)vals).Count > 0; - } - - /// - /// Gets an enum's value based on its description attribute - /// If more than one values match the same description, returns the first one - /// - /// Enum type - /// Enum description - /// Enum value - /// True if successful - public static bool TryParseEnumFromDescription(string strVal, out T val) where T : struct, Enum - { - var isSuccessful = TryParseEnumsFromDescription(strVal, out IEnumerable vals); - val = isSuccessful ? vals.First() : default; - return isSuccessful; - } - - - private static void AddTypeToCache() - { - var valToDesc = new Dictionary(); - var descToVals = new Dictionary>(); - - foreach (var flagFieldInfo in typeof(T).GetFields()) - { - var descAttr = (DescriptionAttribute)flagFieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault(); - if (descAttr != null) - { - valToDesc.Add(flagFieldInfo.Name, descAttr.Description); - if (!descToVals.ContainsKey(descAttr.Description)) - descToVals.Add(descAttr.Description, new List()); - descToVals[descAttr.Description].Add(flagFieldInfo.Name); - } - } - - EnumNameToDescriptionCache.Add(typeof(T), valToDesc); - EnumDescriptionToNameCache.Add(typeof(T), descToVals); - } - } -} \ No newline at end of file diff --git a/libs/common/GenerateEnumUtilsAttribute.cs b/libs/common/GenerateEnumUtilsAttribute.cs new file mode 100644 index 0000000000..d60fd1dbb7 --- /dev/null +++ b/libs/common/GenerateEnumUtilsAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; + +namespace Garnet.common +{ + [AttributeUsage(AttributeTargets.Enum)] + public sealed class GenerateEnumUtilsAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index aeaaf11264..c21f7e4f3e 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -20,6 +20,7 @@ public void Execute(GeneratorExecutionContext context) foreach (var enumDeclaration in syntaxReceiver.Enums) { + var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); var enumName = enumDeclaration.Identifier.Text; var values = enumDeclaration.Members @@ -40,8 +41,9 @@ public void Execute(GeneratorExecutionContext context) classBuilder.AppendLine("using System;"); classBuilder.AppendLine("using System.ComponentModel;"); classBuilder.AppendLine("using System.Numerics;"); + classBuilder.AppendLine($"using {namespaceDeclaration!.Name};"); classBuilder.AppendLine(); - classBuilder.AppendLine("namespace namespace Garnet.common;"); + classBuilder.AppendLine("namespace Garnet.common;"); classBuilder.AppendLine(); classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); classBuilder.AppendLine("{"); diff --git a/libs/server/Resp/RespCommandInfoParser.cs b/libs/server/Resp/RespCommandInfoParser.cs index 6c6534e133..a242e79f20 100644 --- a/libs/server/Resp/RespCommandInfoParser.cs +++ b/libs/server/Resp/RespCommandInfoParser.cs @@ -46,7 +46,7 @@ public static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, IReadOnlyDic for (var flagIdx = 0; flagIdx < flagCount; flagIdx++) { if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + || !EnumUtils.TryParseRespCommandFlagsFromDescription(strFlag, out var flag)) return false; flags |= flag; } @@ -69,7 +69,7 @@ public static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, IReadOnlyDic for (var aclCatIdx = 0; aclCatIdx < aclCatCount; aclCatIdx++) { if (!RespReadUtils.ReadSimpleString(out var strAclCat, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strAclCat.TrimStart('@'), out var aclCat)) + || !EnumUtils.TryParseRespAclCategoriesFromDescription(strAclCat.TrimStart('@'), out var aclCat)) return false; aclCategories |= aclCat; } @@ -153,7 +153,7 @@ internal static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out RespCo for (var flagIdx = 0; flagIdx < flagsCount; flagIdx++) { if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) - || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + || !EnumUtils.TryParseKeySpecificationFlagsFromDescription(strFlag, out var flag)) return false; flags |= flag; } diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs index d8964a7de0..2340f0a11b 100644 --- a/libs/server/Resp/RespCommandKeySpecification.cs +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -40,7 +40,7 @@ public KeySpecificationFlags Flags init { this.flags = value; - this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + this.respFormatFlags = EnumUtils.GetKeySpecificationFlagsDescriptions(this.flags); } } diff --git a/libs/server/Resp/RespCommandsInfo.cs b/libs/server/Resp/RespCommandsInfo.cs index 6e7073923e..5f66b5ed4e 100644 --- a/libs/server/Resp/RespCommandsInfo.cs +++ b/libs/server/Resp/RespCommandsInfo.cs @@ -52,7 +52,7 @@ public RespCommandFlags Flags init { this.flags = value; - this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + this.respFormatFlags = EnumUtils.GetRespCommandFlagsDescriptions(this.flags); } } @@ -80,7 +80,7 @@ public RespAclCategories AclCategories init { this.aclCategories = value; - this.respFormatAclCategories = EnumUtils.GetEnumDescriptions(this.aclCategories); + this.respFormatAclCategories = EnumUtils.GetRespAclCategoriesDescriptions(this.aclCategories); } } From 69d506b1a454b790d4d3c841c02cc55c4967b9e5 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 12 Aug 2024 08:07:07 +0300 Subject: [PATCH 09/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index c21f7e4f3e..e7a8ca2ca0 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -100,7 +100,7 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s foreach (var (name, _, description) in values) { if (description is null) continue; - method.AppendLine($" {enumName}.{name} => [ {description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()} ],"); + method.AppendLine($" {enumName}.{name} => [{description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()}],"); } method.AppendLine(" _ => Array.Empty(),"); method.AppendLine(" };"); From 245492af148d7eb268bf8f237b105215e774efaa Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 12 Aug 2024 08:14:51 +0300 Subject: [PATCH 10/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 17 ++++++++++++++++- libs/gen/Garnet.gen.csproj | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index e7a8ca2ca0..3008838083 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -45,6 +45,9 @@ public void Execute(GeneratorExecutionContext context) classBuilder.AppendLine(); classBuilder.AppendLine("namespace Garnet.common;"); classBuilder.AppendLine(); + classBuilder.AppendLine("/// "); + classBuilder.AppendLine($"/// Utility methods for enums."); + classBuilder.AppendLine("/// "); classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); classBuilder.AppendLine("{"); classBuilder.AppendLine(GenerateTryParseEnumFromDescriptionMethod(enumName, values)); @@ -59,6 +62,12 @@ public void Execute(GeneratorExecutionContext context) private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) { var method = new StringBuilder(); + method.AppendLine($" /// "); + method.AppendLine($" /// Tries to parse the enum value from the description."); + method.AppendLine($" /// "); + method.AppendLine($" /// Enum description."); + method.AppendLine($" /// Enum value."); + method.AppendLine($" /// True if successful."); method.AppendLine($" public static bool TryParse{enumName}FromDescription(string description, out {enumName} result)"); method.AppendLine(" {"); method.AppendLine(" result = default;"); @@ -89,6 +98,12 @@ private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) { var method = new StringBuilder(); + method.AppendLine($" /// "); + method.AppendLine($" /// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); + method.AppendLine($" /// If no description exists, returns the ToString() value of the input value."); + method.AppendLine($" /// "); + method.AppendLine($" /// Enum value."); + method.AppendLine($" /// Array of descriptions."); method.AppendLine($" public static string[] Get{enumName}Descriptions({enumName} value)"); method.AppendLine(" {"); method.AppendLine(" var setFlags = BitOperations.PopCount((uint)value);"); @@ -102,7 +117,7 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s if (description is null) continue; method.AppendLine($" {enumName}.{name} => [{description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()}],"); } - method.AppendLine(" _ => Array.Empty(),"); + method.AppendLine($" _ => [value.ToString()],"); method.AppendLine(" };"); method.AppendLine(" }"); method.AppendLine(); diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj index a9bf84db92..fc5f20fa1f 100644 --- a/libs/gen/Garnet.gen.csproj +++ b/libs/gen/Garnet.gen.csproj @@ -5,6 +5,7 @@ enable enable Latest + true From 39e5b5db0d2dda4f6968ea0f9a6036a960873181 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 12 Aug 2024 08:35:10 +0300 Subject: [PATCH 11/20] feature: introduce source generator for enum code generation --- libs/common/GenerateEnumUtilsAttribute.cs | 2 +- libs/gen/EnumsSourceGenerator.cs | 5 +++-- libs/server/Resp/RespCommandInfoFlags.cs | 4 ++-- libs/server/Resp/RespCommandKeySpecification.cs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libs/common/GenerateEnumUtilsAttribute.cs b/libs/common/GenerateEnumUtilsAttribute.cs index d60fd1dbb7..03889c0ccc 100644 --- a/libs/common/GenerateEnumUtilsAttribute.cs +++ b/libs/common/GenerateEnumUtilsAttribute.cs @@ -6,7 +6,7 @@ namespace Garnet.common { [AttributeUsage(AttributeTargets.Enum)] - public sealed class GenerateEnumUtilsAttribute : Attribute + public sealed class GenerateEnumDescriptionUtilsAttribute : Attribute { } } \ No newline at end of file diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 3008838083..5f341db638 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -98,6 +98,7 @@ private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) { var method = new StringBuilder(); + var defaultEnumValue = values.First(v => v.Value is "0"); method.AppendLine($" /// "); method.AppendLine($" /// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); method.AppendLine($" /// If no description exists, returns the ToString() value of the input value."); @@ -107,7 +108,7 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s method.AppendLine($" public static string[] Get{enumName}Descriptions({enumName} value)"); method.AppendLine(" {"); method.AppendLine(" var setFlags = BitOperations.PopCount((uint)value);"); - method.AppendLine(" if (setFlags == 0) return Array.Empty();"); + method.AppendLine($" if (setFlags == 0) return [\"{defaultEnumValue.Name}\"];"); method.AppendLine(" if (setFlags == 1)"); method.AppendLine(" {"); method.AppendLine(" return value switch"); @@ -142,7 +143,7 @@ private class EnumsSyntaxReceiver : ISyntaxReceiver public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is EnumDeclarationSyntax enumDeclarationSyntax - && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumUtils"))) + && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumDescriptionUtils"))) { Enums.Add(enumDeclarationSyntax); } diff --git a/libs/server/Resp/RespCommandInfoFlags.cs b/libs/server/Resp/RespCommandInfoFlags.cs index 7656d5a530..f9513fbad1 100644 --- a/libs/server/Resp/RespCommandInfoFlags.cs +++ b/libs/server/Resp/RespCommandInfoFlags.cs @@ -12,7 +12,7 @@ namespace Garnet.server /// RESP command flags /// [Flags] - [GenerateEnumUtils] + [GenerateEnumDescriptionUtils] public enum RespCommandFlags { None = 0, @@ -64,7 +64,7 @@ public enum RespCommandFlags /// RESP ACL categories /// [Flags] - [GenerateEnumUtils] + [GenerateEnumDescriptionUtils] public enum RespAclCategories { None = 0, diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs index 2340f0a11b..93e14a35c2 100644 --- a/libs/server/Resp/RespCommandKeySpecification.cs +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -100,7 +100,7 @@ public string ToRespFormat() /// RESP key specification flags /// [Flags] - [GenerateEnumUtils] + [GenerateEnumDescriptionUtils] public enum KeySpecificationFlags : ushort { None = 0, From 82e9fafc4608f467a95c5e9bdef114e5097f1f3d Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 12 Aug 2024 09:18:27 +0300 Subject: [PATCH 12/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 33 ++++++++++++++++++++++++-------- libs/server/Garnet.server.csproj | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 5f341db638..87377200e3 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -23,10 +23,12 @@ public void Execute(GeneratorExecutionContext context) var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); var enumName = enumDeclaration.Identifier.Text; + var semanticModel = context.Compilation.GetSemanticModel(enumDeclaration.SyntaxTree); + var values = enumDeclaration.Members .Select(m => ( m.Identifier.Text, - Value: m.EqualsValue?.Value?.ToString(), + semanticModel.GetOperation(m.EqualsValue?.Value)!.ConstantValue.Value, Description: m.AttributeLists .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") .SingleOrDefault() @@ -59,7 +61,7 @@ public void Execute(GeneratorExecutionContext context) } } - private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) + private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, object? Value, AttributeSyntax Description)> values) { var method = new StringBuilder(); method.AppendLine($" /// "); @@ -95,10 +97,10 @@ private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, return method.ToString(); } - private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, string? Value, AttributeSyntax Description)> values) + + private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, object? Value, AttributeSyntax Description)> values) { var method = new StringBuilder(); - var defaultEnumValue = values.First(v => v.Value is "0"); method.AppendLine($" /// "); method.AppendLine($" /// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); method.AppendLine($" /// If no description exists, returns the ToString() value of the input value."); @@ -107,15 +109,23 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s method.AppendLine($" /// Array of descriptions."); method.AppendLine($" public static string[] Get{enumName}Descriptions({enumName} value)"); method.AppendLine(" {"); - method.AppendLine(" var setFlags = BitOperations.PopCount((uint)value);"); - method.AppendLine($" if (setFlags == 0) return [\"{defaultEnumValue.Name}\"];"); + foreach (var (name, value, description) in values) + { + if (!IsPow2(value)) + { + var toString = description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString() ?? $"\"{name}\""; + method.AppendLine($" if (value is {enumName}.{name}) return [{toString}];"); + } + } + method.AppendLine(" var setFlags = BitOperations.PopCount((ulong)value);"); method.AppendLine(" if (setFlags == 1)"); method.AppendLine(" {"); method.AppendLine(" return value switch"); method.AppendLine(" {"); - foreach (var (name, _, description) in values) + foreach (var (name, value, description) in values) { if (description is null) continue; + if (!IsPow2(value)) continue; method.AppendLine($" {enumName}.{name} => [{description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()}],"); } method.AppendLine($" _ => [value.ToString()],"); @@ -124,9 +134,10 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s method.AppendLine(); method.AppendLine(" var descriptions = new string[setFlags];"); method.AppendLine(" var index = 0;"); - foreach (var (name, _, description) in values) + foreach (var (name, value, description) in values) { if (description is null) continue; + if (!IsPow2(value)) continue; method.AppendLine($" if ((value & {enumName}.{name}) != 0) descriptions[index++] = {description.ArgumentList?.Arguments.FirstOrDefault()?.ToString()};"); } method.AppendLine(); @@ -136,6 +147,12 @@ private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(s return method.ToString(); } + static bool IsPow2(object? value) + { + if (value is int x) return x != 0 && (x & (x - 1)) == 0; + return false; + } + private class EnumsSyntaxReceiver : ISyntaxReceiver { public List Enums { get; } = new(); diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index aec257d9a7..d0869b0f9c 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -29,7 +29,7 @@ - + \ No newline at end of file From e81058ca21e03cc8585bbc3bd08418e93c84fe62 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Tue, 13 Aug 2024 09:06:10 +0300 Subject: [PATCH 13/20] migrate to IIncrementalGenerator --- libs/gen/EnumsSourceGenerator.cs | 115 +++++++++++++++---------------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 87377200e3..e4cabd9ebe 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -5,60 +5,67 @@ namespace Garnet; [Generator] -public class EnumsSourceGenerator : ISourceGenerator +public class EnumsSourceGenerator : IIncrementalGenerator { const string GeneratedClassName = "EnumUtils"; - public void Initialize(GeneratorInitializationContext context) + + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new EnumsSyntaxReceiver()); + var enumDetails = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => node is EnumDeclarationSyntax enumDeclarationSyntax + && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumDescriptionUtils")), + transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.Node, ctx.SemanticModel) + ); + + var enumUtils = enumDetails.Select((details, _) => Execute(details)); + + context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource)); } - public void Execute(GeneratorExecutionContext context) + private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclaration, SemanticModel semanticModel) { - var syntaxReceiver = context.SyntaxReceiver as EnumsSyntaxReceiver; - if (syntaxReceiver is null) return; + var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); + var enumName = enumDeclaration.Identifier.Text; + + var values = enumDeclaration.Members + .Select(m => ( + m.Identifier.Text, + semanticModel.GetOperation(m.EqualsValue!.Value)!.ConstantValue.Value, + Description: m.AttributeLists + .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") + .SingleOrDefault() + )) + .ToList(); + return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, values); + } - foreach (var enumDeclaration in syntaxReceiver.Enums) - { - var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); - var enumName = enumDeclaration.Identifier.Text; - - var semanticModel = context.Compilation.GetSemanticModel(enumDeclaration.SyntaxTree); - - var values = enumDeclaration.Members - .Select(m => ( - m.Identifier.Text, - semanticModel.GetOperation(m.EqualsValue?.Value)!.ConstantValue.Value, - Description: m.AttributeLists - .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") - .SingleOrDefault() - )) - .ToList(); - - var classBuilder = new StringBuilder(); - classBuilder.AppendLine("// "); - classBuilder.AppendLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); - classBuilder.AppendLine("// "); - - classBuilder.AppendLine("using System;"); - classBuilder.AppendLine("using System.ComponentModel;"); - classBuilder.AppendLine("using System.Numerics;"); - classBuilder.AppendLine($"using {namespaceDeclaration!.Name};"); - classBuilder.AppendLine(); - classBuilder.AppendLine("namespace Garnet.common;"); - classBuilder.AppendLine(); - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"/// Utility methods for enums."); - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); - classBuilder.AppendLine("{"); - classBuilder.AppendLine(GenerateTryParseEnumFromDescriptionMethod(enumName, values)); - classBuilder.AppendLine(); - classBuilder.AppendLine(GenerateGetEnumDescriptionsMethod(enumName, values)); - classBuilder.AppendLine("}"); - var classSource = classBuilder.ToString(); - context.AddSource($"{GeneratedClassName}.{enumName}.g.cs", classSource); - } + private static (string EnumName, string ClassSource) Execute(EnumDetails details) + { + var classBuilder = new StringBuilder(); + classBuilder.AppendLine("// "); + classBuilder.AppendLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); + classBuilder.AppendLine("// "); + + classBuilder.AppendLine("using System;"); + classBuilder.AppendLine("using System.ComponentModel;"); + classBuilder.AppendLine("using System.Numerics;"); + classBuilder.AppendLine($"using {details.Namespace};"); + classBuilder.AppendLine(); + classBuilder.AppendLine("namespace Garnet.common;"); + classBuilder.AppendLine(); + classBuilder.AppendLine("/// "); + classBuilder.AppendLine($"/// Utility methods for enums."); + classBuilder.AppendLine("/// "); + classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); + classBuilder.AppendLine("{"); + classBuilder.AppendLine(GenerateTryParseEnumFromDescriptionMethod(details.EnumName, details.Values)); + classBuilder.AppendLine(); + classBuilder.AppendLine(GenerateGetEnumDescriptionsMethod(details.EnumName, details.Values)); + classBuilder.AppendLine("}"); + + var classSource = classBuilder.ToString(); + return (details.EnumName, classSource); } private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, object? Value, AttributeSyntax Description)> values) @@ -153,17 +160,5 @@ static bool IsPow2(object? value) return false; } - private class EnumsSyntaxReceiver : ISyntaxReceiver - { - public List Enums { get; } = new(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is EnumDeclarationSyntax enumDeclarationSyntax - && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumDescriptionUtils"))) - { - Enums.Add(enumDeclarationSyntax); - } - } - } + private record struct EnumDetails(string Namespace, string EnumName, List<(string Name, object? Value, AttributeSyntax Description)> Values); } \ No newline at end of file From c64790d8ea57d0f4544c9783f68e51d4b958ab99 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 14 Aug 2024 09:17:14 +0300 Subject: [PATCH 14/20] feature: introduce source generator for enum code generation --- libs/common/GenerateEnumUtilsAttribute.cs | 3 + libs/gen/EnumsSourceGenerator.cs | 189 +++++++++++----------- 2 files changed, 100 insertions(+), 92 deletions(-) diff --git a/libs/common/GenerateEnumUtilsAttribute.cs b/libs/common/GenerateEnumUtilsAttribute.cs index 03889c0ccc..05042925a3 100644 --- a/libs/common/GenerateEnumUtilsAttribute.cs +++ b/libs/common/GenerateEnumUtilsAttribute.cs @@ -5,6 +5,9 @@ namespace Garnet.common { + /// + /// Specifies that utility methods for generating enum descriptions should be generated for the target enum. + /// [AttributeUsage(AttributeTargets.Enum)] public sealed class GenerateEnumDescriptionUtilsAttribute : Attribute { diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index e4cabd9ebe..c21f70879d 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.CodeDom.Compiler; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -12,13 +13,12 @@ public class EnumsSourceGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { var enumDetails = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: static (node, _) => node is EnumDeclarationSyntax enumDeclarationSyntax - && enumDeclarationSyntax.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "GenerateEnumDescriptionUtils")), - transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.Node, ctx.SemanticModel) + .ForAttributeWithMetadataName( + "Garnet.common.GenerateEnumDescriptionUtilsAttribute", + predicate: static (_, _) => true, + transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.TargetNode, ctx.SemanticModel) ); - - var enumUtils = enumDetails.Select((details, _) => Execute(details)); + var enumUtils = enumDetails.Select(static (details, _) => Execute(details)); context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource)); } @@ -34,7 +34,7 @@ private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclar semanticModel.GetOperation(m.EqualsValue!.Value)!.ConstantValue.Value, Description: m.AttributeLists .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") - .SingleOrDefault() + .SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString() )) .ToList(); return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, values); @@ -42,116 +42,121 @@ private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclar private static (string EnumName, string ClassSource) Execute(EnumDetails details) { - var classBuilder = new StringBuilder(); - classBuilder.AppendLine("// "); - classBuilder.AppendLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); - classBuilder.AppendLine("// "); - - classBuilder.AppendLine("using System;"); - classBuilder.AppendLine("using System.ComponentModel;"); - classBuilder.AppendLine("using System.Numerics;"); - classBuilder.AppendLine($"using {details.Namespace};"); - classBuilder.AppendLine(); - classBuilder.AppendLine("namespace Garnet.common;"); - classBuilder.AppendLine(); - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"/// Utility methods for enums."); - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"public static partial class {GeneratedClassName}"); - classBuilder.AppendLine("{"); - classBuilder.AppendLine(GenerateTryParseEnumFromDescriptionMethod(details.EnumName, details.Values)); - classBuilder.AppendLine(); - classBuilder.AppendLine(GenerateGetEnumDescriptionsMethod(details.EnumName, details.Values)); - classBuilder.AppendLine("}"); - - var classSource = classBuilder.ToString(); - return (details.EnumName, classSource); + using var classWriter = new IndentedTextWriter(new StringWriter()); + classWriter.WriteLine("// "); + classWriter.WriteLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator."); + classWriter.WriteLine("// "); + + classWriter.WriteLine("using System;"); + classWriter.WriteLine("using System.ComponentModel;"); + classWriter.WriteLine("using System.Numerics;"); + classWriter.WriteLine(); + classWriter.WriteLine($"namespace {details.Namespace};"); + classWriter.WriteLine(); + classWriter.WriteLine("/// "); + classWriter.WriteLine($"/// Utility methods for enums."); + classWriter.WriteLine("/// "); + classWriter.WriteLine($"public static partial class {GeneratedClassName}"); + classWriter.WriteLine("{"); + classWriter.Indent++; + GenerateTryParseEnumFromDescriptionMethod(classWriter, details); + classWriter.WriteLine(); + GenerateGetEnumDescriptionsMethod(classWriter, details); + classWriter.Indent--; + classWriter.WriteLine("}"); + + return (details.EnumName, classWriter.InnerWriter.ToString()); } - private static string GenerateTryParseEnumFromDescriptionMethod(string enumName, List<(string Name, object? Value, AttributeSyntax Description)> values) + private static void GenerateTryParseEnumFromDescriptionMethod(IndentedTextWriter classWriter, EnumDetails details) { - var method = new StringBuilder(); - method.AppendLine($" /// "); - method.AppendLine($" /// Tries to parse the enum value from the description."); - method.AppendLine($" /// "); - method.AppendLine($" /// Enum description."); - method.AppendLine($" /// Enum value."); - method.AppendLine($" /// True if successful."); - method.AppendLine($" public static bool TryParse{enumName}FromDescription(string description, out {enumName} result)"); - method.AppendLine(" {"); - method.AppendLine(" result = default;"); - method.AppendLine(" switch (description)"); - method.AppendLine(" {"); - foreach (var (name, _, description) in values) + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Tries to parse the enum value from the description."); + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Enum description."); + classWriter.WriteLine("/// Enum value."); + classWriter.WriteLine("/// True if successful."); + classWriter.WriteLine($"public static bool TryParse{details.EnumName}FromDescription(string description, out {details.EnumName} result)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + classWriter.WriteLine("result = default;"); + classWriter.WriteLine("switch (description)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, _, description) in details.Values) { bool hasDescription = false; - var descriptionValue = description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString(); - if (descriptionValue is not null) + if (description is not null) { hasDescription = true; - method.AppendLine($" case {descriptionValue}:"); + classWriter.WriteLine($"case {description}:"); } if (!hasDescription) continue; - method.AppendLine($" result = {enumName}.{name};"); - method.AppendLine(" return true;"); + classWriter.Indent++; + classWriter.WriteLine($"result = {details.EnumName}.{name};"); + classWriter.WriteLine("return true;"); + classWriter.Indent--; } - method.AppendLine(" }"); - method.AppendLine(); - method.AppendLine(" return false;"); - method.AppendLine(" }"); - method.AppendLine(); - - return method.ToString(); + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); + classWriter.WriteLine("return false;"); + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); } - private static string GenerateGetEnumDescriptionsMethod(string enumName, List<(string Name, object? Value, AttributeSyntax Description)> values) + private static void GenerateGetEnumDescriptionsMethod(IndentedTextWriter classWriter, EnumDetails details) { - var method = new StringBuilder(); - method.AppendLine($" /// "); - method.AppendLine($" /// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); - method.AppendLine($" /// If no description exists, returns the ToString() value of the input value."); - method.AppendLine($" /// "); - method.AppendLine($" /// Enum value."); - method.AppendLine($" /// Array of descriptions."); - method.AppendLine($" public static string[] Get{enumName}Descriptions({enumName} value)"); - method.AppendLine(" {"); - foreach (var (name, value, description) in values) + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Gets the descriptions of the set flags. Assumes the enum is a flags enum."); + classWriter.WriteLine("/// If no description exists, returns the ToString() value of the input value."); + classWriter.WriteLine("/// "); + classWriter.WriteLine("/// Enum value."); + classWriter.WriteLine("/// Array of descriptions."); + classWriter.WriteLine($"public static string[] Get{details.EnumName}Descriptions({details.EnumName} value)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, value, description) in details.Values) { if (!IsPow2(value)) { - var toString = description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString() ?? $"\"{name}\""; - method.AppendLine($" if (value is {enumName}.{name}) return [{toString}];"); + var toStringValue = description ?? $"\"{name}\""; + classWriter.WriteLine($"if (value is {details.EnumName}.{name}) return [{toStringValue}];"); } } - method.AppendLine(" var setFlags = BitOperations.PopCount((ulong)value);"); - method.AppendLine(" if (setFlags == 1)"); - method.AppendLine(" {"); - method.AppendLine(" return value switch"); - method.AppendLine(" {"); - foreach (var (name, value, description) in values) + classWriter.WriteLine("var setFlags = BitOperations.PopCount((ulong)value);"); + classWriter.WriteLine("if (setFlags == 1)"); + classWriter.WriteLine("{"); + classWriter.Indent++; + classWriter.WriteLine("return value switch"); + classWriter.WriteLine("{"); + classWriter.Indent++; + foreach (var (name, value, description) in details.Values) { if (description is null) continue; if (!IsPow2(value)) continue; - method.AppendLine($" {enumName}.{name} => [{description?.ArgumentList?.Arguments.FirstOrDefault()?.ToString()}],"); + classWriter.WriteLine($"{details.EnumName}.{name} => [{description}],"); } - method.AppendLine($" _ => [value.ToString()],"); - method.AppendLine(" };"); - method.AppendLine(" }"); - method.AppendLine(); - method.AppendLine(" var descriptions = new string[setFlags];"); - method.AppendLine(" var index = 0;"); - foreach (var (name, value, description) in values) + classWriter.WriteLine($"_ => [value.ToString()],"); + classWriter.Indent--; + classWriter.WriteLine("};"); + classWriter.Indent--; + classWriter.WriteLine("}"); + classWriter.WriteLine(); + classWriter.WriteLine("var descriptions = new string[setFlags];"); + classWriter.WriteLine("var index = 0;"); + foreach (var (name, value, description) in details.Values) { if (description is null) continue; if (!IsPow2(value)) continue; - method.AppendLine($" if ((value & {enumName}.{name}) != 0) descriptions[index++] = {description.ArgumentList?.Arguments.FirstOrDefault()?.ToString()};"); + classWriter.WriteLine($"if ((value & {details.EnumName}.{name}) != 0) descriptions[index++] = {description};"); } - method.AppendLine(); - method.AppendLine(" return descriptions;"); - method.AppendLine(" }"); - - return method.ToString(); + classWriter.WriteLine(); + classWriter.WriteLine("return descriptions;"); + classWriter.Indent--; + classWriter.WriteLine("}"); } static bool IsPow2(object? value) @@ -160,5 +165,5 @@ static bool IsPow2(object? value) return false; } - private record struct EnumDetails(string Namespace, string EnumName, List<(string Name, object? Value, AttributeSyntax Description)> Values); + private record struct EnumDetails(string Namespace, string EnumName, List<(string Name, object? Value, string? Description)> Values); } \ No newline at end of file From bdde7514c846d8cd479b4b5ac51d9b806176200b Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 14 Aug 2024 09:38:37 +0300 Subject: [PATCH 15/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 25 ++++++++++++++++++++----- libs/gen/Garnet.gen.csproj | 1 - libs/server/Garnet.server.csproj | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index c21f70879d..6dcf03703c 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -1,5 +1,4 @@ using System.CodeDom.Compiler; -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -17,7 +16,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) "Garnet.common.GenerateEnumDescriptionUtilsAttribute", predicate: static (_, _) => true, transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.TargetNode, ctx.SemanticModel) - ); + ).WithComparer(new EnumDetailsComparer()); var enumUtils = enumDetails.Select(static (details, _) => Execute(details)); context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource)); @@ -36,7 +35,7 @@ private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclar .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") .SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString() )) - .ToList(); + .ToArray(); return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, values); } @@ -65,7 +64,7 @@ private static (string EnumName, string ClassSource) Execute(EnumDetails details classWriter.Indent--; classWriter.WriteLine("}"); - return (details.EnumName, classWriter.InnerWriter.ToString()); + return (details.EnumName, classWriter.InnerWriter.ToString()!); } private static void GenerateTryParseEnumFromDescriptionMethod(IndentedTextWriter classWriter, EnumDetails details) @@ -165,5 +164,21 @@ static bool IsPow2(object? value) return false; } - private record struct EnumDetails(string Namespace, string EnumName, List<(string Name, object? Value, string? Description)> Values); + private record struct EnumDetails(string Namespace, string EnumName, (string Name, object? Value, string? Description)[] Values); + + private class EnumDetailsComparer : IEqualityComparer + { + public bool Equals(EnumDetails x, EnumDetails y) => x.EnumName.Equals(y.EnumName) && x.Namespace.Equals(y.Namespace) && x.Values.SequenceEqual(y.Values); + public int GetHashCode(EnumDetails obj) + { + var hash = new HashCode(); + hash.Add(obj.EnumName); + hash.Add(obj.Namespace); + foreach (var value in obj.Values) + { + hash.Add(value); + } + return hash.ToHashCode(); + } + } } \ No newline at end of file diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj index fc5f20fa1f..24ad6b6b35 100644 --- a/libs/gen/Garnet.gen.csproj +++ b/libs/gen/Garnet.gen.csproj @@ -1,7 +1,6 @@  - netstandard2.0 enable enable Latest diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index d0869b0f9c..3e0fce7070 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -29,7 +29,7 @@ - + \ No newline at end of file From bf44e7813f037aa23bca8766db274f13b94d815a Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 14 Aug 2024 09:43:52 +0300 Subject: [PATCH 16/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 6dcf03703c..f9bc8ebc70 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -169,7 +169,7 @@ private record struct EnumDetails(string Namespace, string EnumName, (string Nam private class EnumDetailsComparer : IEqualityComparer { public bool Equals(EnumDetails x, EnumDetails y) => x.EnumName.Equals(y.EnumName) && x.Namespace.Equals(y.Namespace) && x.Values.SequenceEqual(y.Values); - public int GetHashCode(EnumDetails obj) + public int GetHashCode(EnumDetails obj) { var hash = new HashCode(); hash.Add(obj.EnumName); From 123ecc84aecc0a2ac014fb1d265e239add283a67 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 25 Aug 2024 15:21:09 +0300 Subject: [PATCH 17/20] change tfm to netstandard --- libs/gen/EnumsSourceGenerator.cs | 11 +- libs/gen/EquatableArray.cs | 113 +++++++++ libs/gen/Garnet.gen.csproj | 3 +- libs/gen/HashCode.cs | 384 +++++++++++++++++++++++++++++++ libs/server/Garnet.server.csproj | 2 +- 5 files changed, 507 insertions(+), 6 deletions(-) create mode 100644 libs/gen/EquatableArray.cs create mode 100644 libs/gen/HashCode.cs diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index f9bc8ebc70..9697b7ad5d 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -1,4 +1,7 @@ -using System.CodeDom.Compiler; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.CodeDom.Compiler; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -16,7 +19,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) "Garnet.common.GenerateEnumDescriptionUtilsAttribute", predicate: static (_, _) => true, transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.TargetNode, ctx.SemanticModel) - ).WithComparer(new EnumDetailsComparer()); + ); var enumUtils = enumDetails.Select(static (details, _) => Execute(details)); context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource)); @@ -36,7 +39,7 @@ private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclar .SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString() )) .ToArray(); - return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, values); + return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, new EquatableArray<(string Name, object? Value, string? Description)>(values)); } private static (string EnumName, string ClassSource) Execute(EnumDetails details) @@ -164,7 +167,7 @@ static bool IsPow2(object? value) return false; } - private record struct EnumDetails(string Namespace, string EnumName, (string Name, object? Value, string? Description)[] Values); + private record struct EnumDetails(string Namespace, string EnumName, EquatableArray<(string Name, object? Value, string? Description)> Values); private class EnumDetailsComparer : IEqualityComparer { diff --git a/libs/gen/EquatableArray.cs b/libs/gen/EquatableArray.cs new file mode 100644 index 0000000000..9e7e3eccab --- /dev/null +++ b/libs/gen/EquatableArray.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Garnet; + +/// +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + public static readonly EquatableArray Empty = new([]); + + /// + /// The underlying array. + /// + private readonly T[]? _array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public EquatableArray(T[] array) + { + _array = array; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (_array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return _array.AsSpan(); + } + + /// + /// Gets the underlying array if there is one + /// + public T[]? GetArray() => _array; + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + } + + public int Count => _array?.Length ?? 0; + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} diff --git a/libs/gen/Garnet.gen.csproj b/libs/gen/Garnet.gen.csproj index 24ad6b6b35..795c1ed291 100644 --- a/libs/gen/Garnet.gen.csproj +++ b/libs/gen/Garnet.gen.csproj @@ -1,6 +1,7 @@  - + + netstandard2.0 enable enable Latest diff --git a/libs/gen/HashCode.cs b/libs/gen/HashCode.cs new file mode 100644 index 0000000000..a015da414b --- /dev/null +++ b/libs/gen/HashCode.cs @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Garnet; + +/// +/// Polyfill for .NET 6 HashCode +/// +internal struct HashCode +{ + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static uint GenerateGlobalSeed() + { + var buffer = new byte[sizeof(uint)]; + new Random().NextBytes(buffer); + return BitConverter.ToUInt32(buffer, 0); + } + + public static int Combine(T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 4; + + hash = QueueRound(hash, hc1); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 8; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + + uint hash = MixEmptyState(); + hash += 12; + + hash = QueueRound(hash, hc1); + hash = QueueRound(hash, hc2); + hash = QueueRound(hash, hc3); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 16; + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 20; + + hash = QueueRound(hash, hc5); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 24; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + uint hash = MixState(v1, v2, v3, v4); + hash += 28; + + hash = QueueRound(hash, hc5); + hash = QueueRound(hash, hc6); + hash = QueueRound(hash, hc7); + + hash = MixFinal(hash); + return (int)hash; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode() ?? 0); + uint hc2 = (uint)(value2?.GetHashCode() ?? 0); + uint hc3 = (uint)(value3?.GetHashCode() ?? 0); + uint hc4 = (uint)(value4?.GetHashCode() ?? 0); + uint hc5 = (uint)(value5?.GetHashCode() ?? 0); + uint hc6 = (uint)(value6?.GetHashCode() ?? 0); + uint hc7 = (uint)(value7?.GetHashCode() ?? 0); + uint hc8 = (uint)(value8?.GetHashCode() ?? 0); + + Initialize(out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round(v1, hc1); + v2 = Round(v2, hc2); + v3 = Round(v3, hc3); + v4 = Round(v4, hc4); + + v1 = Round(v1, hc5); + v2 = Round(v2, hc6); + v3 = Round(v3, hc7); + v4 = Round(v4, hc8); + + uint hash = MixState(v1, v2, v3, v4); + hash += 32; + + hash = MixFinal(hash); + return (int)hash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + private static uint MixEmptyState() + { + return s_seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + private void Add(int value) + { + // The original xxHash works as follows: + // 0. Initialize immediately. We can't do this in a struct (no + // default ctor). + // 1. Accumulate blocks of length 16 (4 uints) into 4 accumulators. + // 2. Accumulate remaining blocks of length 4 (1 uint) into the + // hash. + // 3. Accumulate remaining blocks of length 1 into the hash. + + // There is no need for #3 as this type only accepts ints. _queue1, + // _queue2 and _queue3 are basically a buffer so that when + // ToHashCode is called we can execute #2 correctly. + + // We need to initialize the xxHash32 state (_v1 to _v4) lazily (see + // #0) nd the last place that can be done if you look at the + // original code is just before the first block of 16 bytes is mixed + // in. The xxHash32 state is never used for streams containing fewer + // than 16 bytes. + + // To see what's really going on here, have a look at the Combine + // methods. + + uint val = (uint)value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + _queue1 = val; + else if (position == 1) + _queue2 = val; + else if (position == 2) + _queue3 = val; + else // position == 3 + { + if (previousLength == 3) + Initialize(out _v1, out _v2, out _v3, out _v4); + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + public int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + hash = QueueRound(hash, _queue3); + } + } + + hash = MixFinal(hash); + return (int)hash; + } + +#pragma warning disable 0809 + // Obsolete member 'memberA' overrides non-obsolete member 'memberB'. + // Disallowing GetHashCode and Equals is by design + + // * We decided to not override GetHashCode() to produce the hash code + // as this would be weird, both naming-wise as well as from a + // behavioral standpoint (GetHashCode() should return the object's + // hash code, not the one being computed). + + // * Even though ToHashCode() can be called safely multiple times on + // this implementation, it is not part of the contract. If the + // implementation has to change in the future we don't want to worry + // about people who might have incorrectly used this type. + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException("Hash code not supported"); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException("Equality not supported"); +#pragma warning restore 0809 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); +} \ No newline at end of file diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index 3e0fce7070..d0869b0f9c 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -29,7 +29,7 @@ - + \ No newline at end of file From c7dd32776498dc67ee36ca2440bffe7d99ba1f27 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 25 Aug 2024 15:27:55 +0300 Subject: [PATCH 18/20] merge --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index ba5fb65418..86a3da28ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + From b018d6258cea9c791d09e055ec0f4876f60840ae Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 25 Aug 2024 15:33:43 +0300 Subject: [PATCH 19/20] merge --- libs/gen/EquatableArray.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/gen/EquatableArray.cs b/libs/gen/EquatableArray.cs index 9e7e3eccab..4ee9665644 100644 --- a/libs/gen/EquatableArray.cs +++ b/libs/gen/EquatableArray.cs @@ -78,13 +78,13 @@ public ReadOnlySpan AsSpan() /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + return ((IEnumerable)(_array ?? [])).GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { - return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); + return ((IEnumerable)(_array ?? [])).GetEnumerator(); } public int Count => _array?.Length ?? 0; @@ -110,4 +110,4 @@ IEnumerator IEnumerable.GetEnumerator() { return !left.Equals(right); } -} +} \ No newline at end of file From bd88fe02a452ff7734edf2563c871c4c46696eb4 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sun, 20 Oct 2024 21:05:06 +0300 Subject: [PATCH 20/20] feature: introduce source generator for enum code generation --- libs/gen/EnumsSourceGenerator.cs | 56 ++++++++++++------------- libs/server/Resp/RespCommandArgument.cs | 6 ++- libs/server/Resp/RespCommandDocs.cs | 6 ++- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/libs/gen/EnumsSourceGenerator.cs b/libs/gen/EnumsSourceGenerator.cs index 9697b7ad5d..e9c2707113 100644 --- a/libs/gen/EnumsSourceGenerator.cs +++ b/libs/gen/EnumsSourceGenerator.cs @@ -28,18 +28,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclaration, SemanticModel semanticModel) { var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf(); + var flags = enumDeclaration.AttributeLists.Where(al => al.Attributes.Any(a => a?.Name?.ToString() == "Flags")).Any(); var enumName = enumDeclaration.Identifier.Text; - var values = enumDeclaration.Members .Select(m => ( m.Identifier.Text, - semanticModel.GetOperation(m.EqualsValue!.Value)!.ConstantValue.Value, + Value: m.EqualsValue is not null ? semanticModel.GetOperation(m.EqualsValue.Value)!.ConstantValue.Value : null, Description: m.AttributeLists .SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description") .SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString() )) .ToArray(); - return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, new EquatableArray<(string Name, object? Value, string? Description)>(values)); + + return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, flags, new EquatableArray<(string Name, object? Value, string? Description)>(values)); } private static (string EnumName, string ClassSource) Execute(EnumDetails details) @@ -120,25 +121,30 @@ private static void GenerateGetEnumDescriptionsMethod(IndentedTextWriter classWr classWriter.WriteLine($"public static string[] Get{details.EnumName}Descriptions({details.EnumName} value)"); classWriter.WriteLine("{"); classWriter.Indent++; - foreach (var (name, value, description) in details.Values) + if (details.Flags) { - if (!IsPow2(value)) + foreach (var (name, value, description) in details.Values) { - var toStringValue = description ?? $"\"{name}\""; - classWriter.WriteLine($"if (value is {details.EnumName}.{name}) return [{toStringValue}];"); + if (!IsPow2(value)) + { + var toStringValue = description ?? $"\"{name}\""; + classWriter.WriteLine($"if (value is {details.EnumName}.{name}) return [{toStringValue}];"); + } } + + classWriter.WriteLine("var setFlags = BitOperations.PopCount((ulong)value);"); + classWriter.WriteLine("if (setFlags is 1)"); + classWriter.WriteLine("{"); + classWriter.Indent++; } - classWriter.WriteLine("var setFlags = BitOperations.PopCount((ulong)value);"); - classWriter.WriteLine("if (setFlags == 1)"); - classWriter.WriteLine("{"); - classWriter.Indent++; + classWriter.WriteLine("return value switch"); classWriter.WriteLine("{"); classWriter.Indent++; foreach (var (name, value, description) in details.Values) { if (description is null) continue; - if (!IsPow2(value)) continue; + if (details.Flags && !IsPow2(value)) continue; classWriter.WriteLine($"{details.EnumName}.{name} => [{description}],"); } classWriter.WriteLine($"_ => [value.ToString()],"); @@ -147,13 +153,19 @@ private static void GenerateGetEnumDescriptionsMethod(IndentedTextWriter classWr classWriter.Indent--; classWriter.WriteLine("}"); classWriter.WriteLine(); + + if (!details.Flags) + { + return; + } + classWriter.WriteLine("var descriptions = new string[setFlags];"); classWriter.WriteLine("var index = 0;"); foreach (var (name, value, description) in details.Values) { if (description is null) continue; if (!IsPow2(value)) continue; - classWriter.WriteLine($"if ((value & {details.EnumName}.{name}) != 0) descriptions[index++] = {description};"); + classWriter.WriteLine($"if ((value & {details.EnumName}.{name}) is not 0) descriptions[index++] = {description};"); } classWriter.WriteLine(); classWriter.WriteLine("return descriptions;"); @@ -167,21 +179,5 @@ static bool IsPow2(object? value) return false; } - private record struct EnumDetails(string Namespace, string EnumName, EquatableArray<(string Name, object? Value, string? Description)> Values); - - private class EnumDetailsComparer : IEqualityComparer - { - public bool Equals(EnumDetails x, EnumDetails y) => x.EnumName.Equals(y.EnumName) && x.Namespace.Equals(y.Namespace) && x.Values.SequenceEqual(y.Values); - public int GetHashCode(EnumDetails obj) - { - var hash = new HashCode(); - hash.Add(obj.EnumName); - hash.Add(obj.Namespace); - foreach (var value in obj.Values) - { - hash.Add(value); - } - return hash.ToHashCode(); - } - } + private record struct EnumDetails(string Namespace, string EnumName, bool Flags, EquatableArray<(string Name, object? Value, string? Description)> Values); } \ No newline at end of file diff --git a/libs/server/Resp/RespCommandArgument.cs b/libs/server/Resp/RespCommandArgument.cs index b5c22b1b02..4bb8e65fc9 100644 --- a/libs/server/Resp/RespCommandArgument.cs +++ b/libs/server/Resp/RespCommandArgument.cs @@ -52,7 +52,7 @@ public RespCommandArgumentFlags ArgumentFlags init { argFlags = value; - respFormatArgFlags = EnumUtils.GetEnumDescriptions(argFlags); + respFormatArgFlags = EnumUtils.GetRespCommandArgumentFlagsDescriptions(default); } } @@ -97,7 +97,7 @@ public virtual string ToRespFormat() key = "type"; sb.Append($"${key.Length}\r\n{key}\r\n"); - var respType = EnumUtils.GetEnumDescriptions(this.Type)[0]; + var respType = EnumUtils.GetRespCommandArgumentTypeDescriptions(this.Type)[0]; sb.Append($"${respType.Length}\r\n{respType}\r\n"); ArgCount += 2; @@ -488,6 +488,7 @@ public override void Write(Utf8JsonWriter writer, RespCommandArgumentBase cmdArg /// /// An enum representing a RESP command argument's type /// + [GenerateEnumDescriptionUtils] public enum RespCommandArgumentType : byte { None, @@ -551,6 +552,7 @@ public enum RespCommandArgumentType : byte /// Argument flags /// [Flags] + [GenerateEnumDescriptionUtils] public enum RespCommandArgumentFlags : byte { None = 0, diff --git a/libs/server/Resp/RespCommandDocs.cs b/libs/server/Resp/RespCommandDocs.cs index 146cab3d2d..2a21835102 100644 --- a/libs/server/Resp/RespCommandDocs.cs +++ b/libs/server/Resp/RespCommandDocs.cs @@ -49,7 +49,7 @@ public RespCommandDocFlags DocFlags init { docFlags = value; - respFormatDocFlags = EnumUtils.GetEnumDescriptions(docFlags); + respFormatDocFlags = EnumUtils.GetRespCommandDocFlagsDescriptions(docFlags); } } @@ -236,7 +236,7 @@ public string ToRespFormat() key = "group"; sb.Append($"${key.Length}\r\n{key}\r\n"); - var respType = EnumUtils.GetEnumDescriptions(this.Group)[0]; + var respType = EnumUtils.GetRespCommandGroupDescriptions(this.Group)[0]; sb.Append($"${respType.Length}\r\n{respType}\r\n"); argCount += 2; @@ -303,6 +303,7 @@ public string ToRespFormat() /// /// Enum representing the functional group to which the command belongs /// + [GenerateEnumDescriptionUtils] public enum RespCommandGroup : byte { None, @@ -348,6 +349,7 @@ public enum RespCommandGroup : byte /// Documentation flags /// [Flags] + [GenerateEnumDescriptionUtils] public enum RespCommandDocFlags : byte { None = 0,