diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 42f96a523..89aca274b 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 steps: diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 1f1475b67..b9630f8f0 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a47ee6dbe..339bd74cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - cron: '1 0 * * 1' # Mondays at 00:01 env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x PROJECT_NAME: Lynx DOTNET_CLI_TELEMETRY_OPTOUT: 1 diff --git a/.github/workflows/on-demand-tests.yml b/.github/workflows/on-demand-tests.yml index 8cb4032dc..b38619c1e 100644 --- a/.github/workflows/on-demand-tests.yml +++ b/.github/workflows/on-demand-tests.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 steps: diff --git a/.github/workflows/perft.yml b/.github/workflows/perft.yml index d60740b98..d3a8fc303 100644 --- a/.github/workflows/perft.yml +++ b/.github/workflows/perft.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 steps: @@ -55,7 +55,7 @@ jobs: if: github.event.inputs.divide env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba7a97e3f..b63bd15ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ on: required: false env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x DOTNET_CLI_TELEMETRY_OPTOUT: 1 jobs: diff --git a/.gitignore b/.gitignore index 1190701fc..f26ca224f 100644 --- a/.gitignore +++ b/.gitignore @@ -376,4 +376,11 @@ tmp/ *.zip # EPD files -*.epd \ No newline at end of file +*.epd + +# Verify exclusions +**/Snapshots/*.g.received.cs + +# Exclude all source generators output but ours +**/Generated/* +!**/Generated/Lynx.Generator/ diff --git a/Directory.Build.props b/Directory.Build.props index 6b023ad0c..15eea50c6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - net8.0 + net9.0 preview Enable enable @@ -13,13 +13,11 @@ - - diff --git a/Lynx.sln b/Lynx.sln index 1689eca4b..03a0a3065 100644 --- a/Lynx.sln +++ b/Lynx.sln @@ -30,6 +30,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dev_test", "dev_test", "{D6 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lynx.Test", "tests\Lynx.Test\Lynx.Test.csproj", "{A8D2A6F0-BDE8-4562-8B94-4638D1ABE359}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lynx.Generator", "src\Lynx.Generator\Lynx.Generator.csproj", "{B3CA6E5A-C0B1-4324-804B-E8E593638A43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lynx.Generator.Test", "tests\Lynx.Generator.Test\Lynx.Generator.Test.csproj", "{3C59CEB2-7CFB-4D4A-8686-78AB918D9A89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +60,14 @@ Global {A8D2A6F0-BDE8-4562-8B94-4638D1ABE359}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8D2A6F0-BDE8-4562-8B94-4638D1ABE359}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8D2A6F0-BDE8-4562-8B94-4638D1ABE359}.Release|Any CPU.Build.0 = Release|Any CPU + {B3CA6E5A-C0B1-4324-804B-E8E593638A43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3CA6E5A-C0B1-4324-804B-E8E593638A43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3CA6E5A-C0B1-4324-804B-E8E593638A43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3CA6E5A-C0B1-4324-804B-E8E593638A43}.Release|Any CPU.Build.0 = Release|Any CPU + {3C59CEB2-7CFB-4D4A-8686-78AB918D9A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C59CEB2-7CFB-4D4A-8686-78AB918D9A89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C59CEB2-7CFB-4D4A-8686-78AB918D9A89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C59CEB2-7CFB-4D4A-8686-78AB918D9A89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,6 +76,7 @@ Global {4288CBA1-156C-435D-845F-4D60A20B10A4} = {D656F9EF-DEF3-42C9-BB12-09961D43B844} {25C9C733-F43B-4E8D-BD89-FC0D9CDFED36} = {D656F9EF-DEF3-42C9-BB12-09961D43B844} {A8D2A6F0-BDE8-4562-8B94-4638D1ABE359} = {D656F9EF-DEF3-42C9-BB12-09961D43B844} + {3C59CEB2-7CFB-4D4A-8686-78AB918D9A89} = {D656F9EF-DEF3-42C9-BB12-09961D43B844} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {44D1B1AC-75A9-4AB8-9FF9-A4A182D84F0F} diff --git a/README.md b/README.md index 3e75a6ff6..1159cec8d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Lynx is a chess engine developed by [@eduherminio](https://github.com/eduherminio). -It's written in C# (.NET 8). +It's written in C# (.NET 9). You can find Lynx: @@ -27,7 +27,7 @@ Here are the ones 'properly' rated over at least a few hundred of games: | Version | Date | Estimated
elo0| [CCRL](https://www.computerchess.org.uk/ccrl/4040/) | [CCRL
Blitz](https://www.computerchess.org.uk/ccrl/404/) | [MCERL](https://www.chessengeria.eu/mcerl) | [CEGT
40/20](http://www.cegt.net/40_40%20Rating%20List/40_40%20All%20Versions/rangliste.html) | [CEGT
40/4](http://www.cegt.net/40_4_Ratinglist/40_4_AllVersion/rangliste.html) | [CEGT
5+3 pb](http://www.cegt.net/5Plus3Rating/5Plus3AllVersion/rangliste.html) | |---|---|---|---|---|---|---|---|---| -| [1.7.0](https://github.com/lynx-chess/Lynx/releases/tag/v1.7.0) | 2024-10-05 | [3101](https://github.com/lynx-chess/Lynx/commit/06da9363b7f38dce5690e8c2c0dcd2914cdfaa30#commitcomment-147596793) | | | | 2974 | 2936 | | +| [1.7.0](https://github.com/lynx-chess/Lynx/releases/tag/v1.7.0) | 2024-10-05 | [3101](https://github.com/lynx-chess/Lynx/commit/06da9363b7f38dce5690e8c2c0dcd2914cdfaa30#commitcomment-147596793) | [3111](https://www.computerchess.org.uk/ccrl/4040/cgi/engine_details.cgi?print=Details&each_game=0&eng=Lynx%201.7.0%2064-bit#Lynx_1_7_0_64-bit) | [3127](https://www.computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?print=Details&each_game=1&eng=Lynx%201.7.0%2064-bit#Lynx_1_7_0_64-bit) | | 2974 | 2936 | | | [1.6.0](https://github.com/lynx-chess/Lynx/releases/tag/v1.6.0) | 2024-08-15 | [2952](https://github.com/lynx-chess/Lynx/commit/a230d0518bf2743ec0dd27931928719e43ac5334#commitcomment-145399551) | [2981](https://www.computerchess.org.uk/ccrl/4040/cgi/engine_details.cgi?print=Details&each_game=0&eng=Lynx%201.6.0%2064-bit#Lynx_1_6_0_64-bit)* | | 3039 | | | | | [1.5.1](https://github.com/lynx-chess/Lynx/releases/tag/v1.5.1) | 2024-06-21 | [2830](https://github.com/lynx-chess/Lynx/commit/47e7b8799cfac433c1004213e51daf35ae0fcd97#commitcomment-143384223) | [2851](https://www.computerchess.org.uk/ccrl/4040/cgi/engine_details.cgi?print=Details&each_game=0&eng=Lynx%201.5.1%2064-bit#Lynx_1_5_1_64-bit) | | | | 2660 | 2690 | | [1.5.0](https://github.com/lynx-chess/Lynx/releases/tag/v1.5.0) | 2024-06-09 | [2817](https://github.com/lynx-chess/Lynx/commit/70f23d96a2789ef22440cd0955a8b9557eb2682f#commitcomment-142930835) | | [2817](https://www.computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?print=Details&each_game=1&eng=Lynx%201.5.0%2064-bit#Lynx_1_5_0_64-bit) | | | | | @@ -63,7 +63,7 @@ However, you can also choose to build Lynx yourself. ### Requirements -- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0). You can find instructions about how to install it in your preferred OS/Distro either [here](https://docs.microsoft.com/en-us/dotnet/core/install/) or [here](https://github.com/dotnet/core/tree/main/release-notes/8.0). +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0). You can find instructions about how to install it in your preferred OS/Distro either [here](https://docs.microsoft.com/en-us/dotnet/core/install/) or [here](https://github.com/dotnet/core/tree/main/release-notes/9.0). If you're a Linux user and are new to .NET ecosystem, the conversation in [this issue](https://github.com/lynx-chess/Lynx/issues/33) may help. diff --git a/src/Lynx.Cli/Lynx.Cli.csproj b/src/Lynx.Cli/Lynx.Cli.csproj index 020af1dac..61e9bb2a8 100644 --- a/src/Lynx.Cli/Lynx.Cli.csproj +++ b/src/Lynx.Cli/Lynx.Cli.csproj @@ -2,7 +2,6 @@ Exe - true true $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -15,16 +14,23 @@ true true true + true true false + + true + 0 + + + false - - - + + + diff --git a/src/Lynx.Generator/GeneratedPackGenerator.cs b/src/Lynx.Generator/GeneratedPackGenerator.cs new file mode 100644 index 000000000..50535886c --- /dev/null +++ b/src/Lynx.Generator/GeneratedPackGenerator.cs @@ -0,0 +1,228 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; + +namespace Lynx.Generator; + +[Generator(LanguageNames.CSharp)] +public class GeneratedPackGenerator : IIncrementalGenerator +{ + private const string OurAttribute = "Lynx.Generator.GeneratedPackAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static postInitializationContext => + postInitializationContext.AddSource("GeneratedPackAttribute.g.cs", AttributeSource())); + + IncrementalValueProvider> pipeline = context.SyntaxProvider + .ForAttributeWithMetadataName( + fullyQualifiedMetadataName: OurAttribute, + predicate: static (syntaxNode, _) => IsGeneratedPackAttribute(syntaxNode), + transform: static (context, _) => GetModel(context) + ).Where(static m => m is not null).Collect(); + + context.RegisterSourceOutput(pipeline, static (context, models) => + Execute(context, models)); + } + + private static SourceText AttributeSource() => SourceText.From(""" + using System; + + namespace Lynx.Generator + { + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } + } + """, Encoding.UTF8); + + private static bool IsGeneratedPackAttribute(SyntaxNode syntaxNode) => + syntaxNode is VariableDeclaratorSyntax; + + private static Model? GetModel(GeneratorAttributeSyntaxContext context) + { + var (mg, eg) = ExtractPackedValueFromAttribute(context.TargetNode, context.SemanticModel); + + if (mg != Model.DefaultShortValue && eg != Model.DefaultShortValue) + { + var classSymbol = context.TargetSymbol.ContainingSymbol; + + return new Model( + Namespace: classSymbol.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)) ?? string.Empty, + ClassName: classSymbol.Name, + PropertyName: context.TargetSymbol.Name[1..], + MG: mg, + EG: eg); + } + + return null; + + /// + /// Heavily inspired by https://andrewlock.net/creating-a-source-generator-part-4-customising-generated-code-with-marker-attributes/ + /// Code in https://github.com/andrewlock/blog-examples/tree/c35edf1c1f0e1f9adf84c215e2ce7ab644b374f5/NetEscapades.EnumGenerators4 + /// + static (short MG, short EG) ExtractPackedValueFromAttribute(SyntaxNode variableDeclarationSyntax, SemanticModel semanticModel) + { + (short, short)defaultResult = (Model.DefaultShortValue, Model.DefaultShortValue); + + short mg = Model.DefaultShortValue; + short eg = Model.DefaultShortValue; + + if (semanticModel.GetDeclaredSymbol(variableDeclarationSyntax) is not IFieldSymbol variableSymbol) + { + return defaultResult; + } + + INamedTypeSymbol? ourAttribute = semanticModel.Compilation.GetTypeByMetadataName(OurAttribute); + if (ourAttribute is null) + { + return defaultResult; + } + + // Loop through all of the attributes on the enum until we find the [EnumExtensions] attribute + foreach (var group in variableSymbol.GetAttributes().GroupBy(static attr => attr.AttributeClass, SymbolEqualityComparer.Default)) + { + // Verify that this is our attribute + if (!ourAttribute.Equals(group.Key, SymbolEqualityComparer.Default)) + { + continue; + } + + // More than one of our attributes -> invalid for our use case + if(group.Count() > 1) + { + return defaultResult; + } + + AttributeData attributeData = group.First(); + + // Constructor arguments + if (!attributeData.ConstructorArguments.IsEmpty) + { + ImmutableArray args = attributeData.ConstructorArguments; + + // Make sure we don't have any errors, otherwise don't do generation + foreach (TypedConstant arg in args) + { + if (arg.Kind == TypedConstantKind.Error) + { + return defaultResult; + } + } + + // Make sure both arguments are present + if (args.Length != 2 || args[0].IsNull || args[1].IsNull) + { + return defaultResult; + } + + mg = (short)args[0].Value!; + eg = (short)args[1].Value!; + } + + // Named arguments + if (!attributeData.NamedArguments.IsEmpty) + { + foreach (KeyValuePair arg in attributeData.NamedArguments) + { + TypedConstant typedConstant = arg.Value; + + // Make sure we don't have any errors, otherwise don't do generation + if (typedConstant.Kind == TypedConstantKind.Error) + { + return defaultResult; + } + else + { + // Use the constructor argument or property name to infer which value is set + switch (arg.Key) + { + case "mg": + mg = typedConstant.IsNull + ? Model.DefaultShortValue + : (short)typedConstant.Value!; + break; + case "eg": + eg = typedConstant.IsNull + ? Model.DefaultShortValue + : (short)typedConstant.Value!; + break; + } + } + } + } + + break; + } + + if (mg == Model.DefaultShortValue || eg == Model.DefaultShortValue) + { + return defaultResult; + } + + return (mg, eg); + } + } + + private static void Execute(SourceProductionContext context, ImmutableArray models) + { + foreach (var group in models.GroupBy(m => m?.Namespace)) + { + Debug.Assert(group.Key is not null); + + foreach (var subgroup in group.GroupBy(m => m?.ClassName)) + { + Debug.Assert(subgroup.Key is not null); + + var sb = new StringBuilder(); + sb.Append("namespace ").Append(group.Key).AppendLine(";"); + sb.AppendLine(); + sb.Append("static partial class ").AppendLine(subgroup.Key); + sb.AppendLine("{"); + + foreach (var model in subgroup) + { + Debug.Assert(model is not null); + +#pragma warning disable S2589 // Boolean expressions should not be gratuitous - Debug.Assert check only exists on release mode + if (model is null) + { + continue; + } +#pragma warning restore S2589 // Boolean expressions should not be gratuitous + + sb.AppendLine($$""" + /// + /// + /// + {{string.Format("public const int {0} = {1};", model.PropertyName, Utils.Pack(model.MG, model.EG))}} + + """); + } + + sb.AppendLine("}"); + + var sourceText = SourceText.From(sb.ToString(), Encoding.UTF8); + context.AddSource($"{subgroup.Key}.g.cs", sourceText); + } + } + } + + private sealed record Model(string Namespace, string ClassName, string PropertyName, short MG, short EG) + { + public const short DefaultShortValue = 32_323; + } +} diff --git a/src/Lynx.Generator/Helpers/IsExternalInit.cs b/src/Lynx.Generator/Helpers/IsExternalInit.cs new file mode 100644 index 000000000..c2efa938c --- /dev/null +++ b/src/Lynx.Generator/Helpers/IsExternalInit.cs @@ -0,0 +1,10 @@ +#pragma warning disable S2094 // Classes should not be empty + +namespace System.Runtime.CompilerServices; + +/// +/// https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809 +/// +internal static class IsExternalInit; + +#pragma warning restore S2094 // Classes should not be empty diff --git a/src/Lynx.Generator/Helpers/Range.cs b/src/Lynx.Generator/Helpers/Range.cs new file mode 100644 index 000000000..3db71d4f2 --- /dev/null +++ b/src/Lynx.Generator/Helpers/Range.cs @@ -0,0 +1,280 @@ +// Based on https://www.meziantou.net/how-to-use-csharp-8-indices-and-ranges-in-dotnet-standard-2-0-and-dotn.htm + +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs + +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable S3427 // Method overloads with default parameter values should not overlap + public Index(int value, bool fromEnd = false) +#pragma warning restore S3427 // Method overloads with default parameter values should not overlap + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? obj) => obj is Index index && _value == (index)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? obj) => + obj is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return (Start.GetHashCode() * 31) + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// + public static T[] GetSubArray(T[] array, Range range) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + +#pragma warning disable S2955 // Generic parameters not constrained to reference types should not be compared to "null" + if (default(T) != null || typeof(T[]) == array.GetType()) + { + // We know the type of the array to be exactly T[]. + + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } +#pragma warning restore S2955 // Generic parameters not constrained to reference types should not be compared to "null" + } + } +} diff --git a/src/Lynx.Generator/Lynx.Generator.csproj b/src/Lynx.Generator/Lynx.Generator.csproj new file mode 100644 index 000000000..d7b19dfc3 --- /dev/null +++ b/src/Lynx.Generator/Lynx.Generator.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0 + true + false + + false + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + <_Parameter1>$(AssemblyName).Test + + + + + $(NoWarn),IDE0130 + + + diff --git a/src/Lynx.Generator/Utils.cs b/src/Lynx.Generator/Utils.cs new file mode 100644 index 000000000..370fa1044 --- /dev/null +++ b/src/Lynx.Generator/Utils.cs @@ -0,0 +1,12 @@ +namespace Lynx.Generator; + +internal static class Utils +{ + /// + /// https://minuskelvin.net/chesswiki/content/packed-eval.html + /// + public static int Pack(short mg, short eg) + { + return (eg << 16) + mg; + } +} diff --git a/src/Lynx/Configuration.cs b/src/Lynx/Configuration.cs index 7a7060fcc..b5824c241 100644 --- a/src/Lynx/Configuration.cs +++ b/src/Lynx/Configuration.cs @@ -142,7 +142,7 @@ public int TranspositionTableSize [SPSA(1, 10, 0.5)] public int NMP_DepthDivisor { get; set; } = 3; - [SPSA(150, 350, 10)] + [SPSA(50, 350, 15)] public int NMP_StaticEvalBetaDivisor { get; set; } = 100; [SPSA(1, 10, 0.5)] diff --git a/src/Lynx/FENParser.cs b/src/Lynx/FENParser.cs index 8f4ac2089..048566138 100644 --- a/src/Lynx/FENParser.cs +++ b/src/Lynx/FENParser.cs @@ -1,6 +1,7 @@ using Lynx.Model; using NLog; using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using ParseResult = (ulong[] PieceBitBoards, ulong[] OccupancyBitBoards, int[] board, Lynx.Model.Side Side, byte Castle, Lynx.Model.BoardSquare EnPassant, @@ -30,7 +31,7 @@ public static ParseResult ParseFEN(ReadOnlySpan fen) try { - success = ParseBoard(fen, pieceBitBoards, occupancyBitBoards, board); + ParseBoard(fen, pieceBitBoards, occupancyBitBoards, board); var unparsedStringAsSpan = fen[fen.IndexOf(' ')..]; Span parts = stackalloc Range[5]; @@ -72,26 +73,16 @@ public static ParseResult ParseFEN(ReadOnlySpan fen) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ParseBoard(ReadOnlySpan fen, BitBoard[] pieceBitBoards, BitBoard[] occupancyBitBoards, int[] board) + private static void ParseBoard(ReadOnlySpan fen, BitBoard[] pieceBitBoards, BitBoard[] occupancyBitBoards, int[] board) { - bool success = true; var rankIndex = 0; var end = fen.IndexOf('/'); - while ( - end != -1 -#if DEBUG - && success -#endif - ) + while (end != -1) { var match = fen[..end]; - ParseBoardSection(pieceBitBoards, board, rankIndex, match -#if DEBUG - , ref success -#endif - ); + ParseBoardSection(pieceBitBoards, board, rankIndex, match); PopulateOccupancies(pieceBitBoards, occupancyBitBoards); fen = fen[(end + 1)..]; @@ -99,20 +90,10 @@ private static bool ParseBoard(ReadOnlySpan fen, BitBoard[] pieceBitBoards ++rankIndex; } - ParseBoardSection(pieceBitBoards, board, rankIndex, fen[..fen.IndexOf(' ')] -#if DEBUG - , ref success -#endif - ); + ParseBoardSection(pieceBitBoards, board, rankIndex, fen[..fen.IndexOf(' ')]); PopulateOccupancies(pieceBitBoards, occupancyBitBoards); - return success; - - static void ParseBoardSection(BitBoard[] pieceBitBoards, int[] board, int rankIndex, ReadOnlySpan boardfenSection -#if DEBUG - , ref bool success -#endif - ) + static void ParseBoardSection(BitBoard[] pieceBitBoards, int[] board, int rankIndex, ReadOnlySpan boardfenSection) { int fileIndex = 0; @@ -147,13 +128,7 @@ static void ParseBoardSection(BitBoard[] pieceBitBoards, int[] board, int rankIn else { fileIndex += ch - '0'; -#if DEBUG - if (fileIndex < 1 || fileIndex > 8) - { - System.Diagnostics.Debug.Fail($"Error parsing char {ch} in fen {boardfenSection.ToString()}"); - success = false; - } -#endif + Debug.Assert(fileIndex >= 1 && fileIndex <= 8, $"Error parsing char {ch} in fen {boardfenSection.ToString()}"); } } } diff --git a/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/EvaluationParams.g.cs b/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/EvaluationParams.g.cs new file mode 100644 index 000000000..d7a53a61b --- /dev/null +++ b/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/EvaluationParams.g.cs @@ -0,0 +1,50 @@ +namespace Lynx; + +static partial class EvaluationParams +{ + /// + /// + /// + public const int IsolatedPawnPenalty = -917521; + + /// + /// + /// + public const int OpenFileRookBonus = 131112; + + /// + /// + /// + public const int SemiOpenFileRookBonus = 524303; + + /// + /// + /// + public const int SemiOpenFileKingPenalty = 327656; + + /// + /// + /// + public const int OpenFileKingPenalty = 131007; + + /// + /// + /// + public const int KingShieldBonus = -196585; + + /// + /// + /// + public const int BishopPairBonus = 4718622; + + /// + /// + /// + public const int PieceProtectedByPawnBonus = 983052; + + /// + /// + /// + public const int PieceAttackedByPawnPenalty = -2162735; + +} diff --git a/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/GeneratedPackAttribute.g.cs b/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/GeneratedPackAttribute.g.cs new file mode 100644 index 000000000..997f37d5c --- /dev/null +++ b/src/Lynx/Generated/Lynx.Generator/Lynx.Generator.GeneratedPackGenerator/GeneratedPackAttribute.g.cs @@ -0,0 +1,17 @@ +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/src/Lynx/Lynx.csproj b/src/Lynx/Lynx.csproj index c365cbb7f..72850ad82 100644 --- a/src/Lynx/Lynx.csproj +++ b/src/Lynx/Lynx.csproj @@ -10,8 +10,8 @@ - - + + @@ -39,6 +39,24 @@ + + + + + + + + + + true + Generated + + + + + + + true snupkg diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 46f66a752..f20c925e9 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -588,7 +588,7 @@ public int Phase() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int TaperedEvaluation(TaperedEvaluationTerm taperedEvaluationTerm, int phase) + internal static int TaperedEvaluation(int taperedEvaluationTerm, int phase) { return ((Utils.UnpackMG(taperedEvaluationTerm) * phase) + (Utils.UnpackEG(taperedEvaluationTerm) * (24 - phase))) / 24; } diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 245bcf99d..b0986c3e2 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -65,12 +65,7 @@ internal static Move[] GenerateAllMoves(Position position, bool capturesOnly = f [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span GenerateAllMoves(Position position, Span movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); int localIndex = 0; @@ -96,12 +91,7 @@ public static Span GenerateAllMoves(Position position, Span movePool [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Move[] GenerateAllCaptures(Position position, Move[] movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); int localIndex = 0; @@ -127,12 +117,8 @@ public static Move[] GenerateAllCaptures(Position position, Move[] movePool) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span GenerateAllCaptures(Position position, Span movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); + int localIndex = 0; @@ -166,13 +152,7 @@ internal static void GenerateAllPawnMoves(ref int localIndex, Span movePoo var sourceRank = (sourceSquare >> 3) + 1; -#if DEBUG - if (sourceRank == 1 || sourceRank == 8) - { - _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); - continue; - } -#endif + Debug.Assert(sourceRank != 1 && sourceRank != 8, $"There's a non-promoted {position.Side} pawn in rank {sourceRank})"); // Pawn pushes var singlePushSquare = sourceSquare + pawnPush; @@ -252,13 +232,7 @@ internal static void GeneratePawnCapturesAndPromotions(ref int localIndex, Span< var sourceRank = (sourceSquare >> 3) + 1; -#if DEBUG - if (sourceRank == 1 || sourceRank == 8) - { - _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); - continue; - } -#endif + Debug.Assert(sourceRank != 1 && sourceRank != 8, $"There's a non-promoted {position.Side} pawn in rank {sourceRank})"); // Pawn pushes var singlePushSquare = sourceSquare + pawnPush; @@ -464,12 +438,7 @@ internal static void GeneratePieceCaptures(ref int localIndex, Span movePo [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CanGenerateAtLeastAValidMove(Position position) { -#if DEBUG - if (position.Side == Side.Both) - { - return false; - } -#endif + Debug.Assert(position.Side != Side.Both); var offset = Utils.PieceOffset(position.Side); @@ -488,7 +457,7 @@ public static bool CanGenerateAtLeastAValidMove(Position position) } catch (Exception e) { - Debug.Fail($"Error in {nameof(CanGenerateAtLeastAValidMove)}", e.StackTrace); + _logger.Error(e, $"Error in {nameof(CanGenerateAtLeastAValidMove)}"); return false; } #endif @@ -511,13 +480,8 @@ private static bool IsAnyPawnMoveValid(Position position, int offset) var sourceRank = (sourceSquare >> 3) + 1; -#if DEBUG - if (sourceRank == 1 || sourceRank == 8) - { - _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); - continue; - } -#endif + Debug.Assert(sourceRank != 1 && sourceRank != 8, $"There's a non-promoted {position.Side} pawn in rank {sourceRank})"); + // Pawn pushes var singlePushSquare = sourceSquare + pawnPush; if (!position.OccupancyBitBoards[2].GetBit(singlePushSquare)) diff --git a/src/Lynx/PSQT.cs b/src/Lynx/PSQT.cs index a5328d365..214af71bf 100644 --- a/src/Lynx/PSQT.cs +++ b/src/Lynx/PSQT.cs @@ -1,5 +1,7 @@ using Lynx.Model; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using static Lynx.TunableEvalParameters; namespace Lynx; @@ -99,17 +101,9 @@ static EvaluationPSQTs() public static int PSQT(int friendEnemy, int bucket, int piece, int square) { var index = PSQTIndex(friendEnemy, bucket, piece, square); + Debug.Assert(index >= 0 && index < _packedPSQT.Length); - unsafe - { - // Since _tt is a pinned array - // This is no-op pinning as it does not influence the GC compaction - // https://tooslowexception.com/pinned-object-heap-in-net-5/ - fixed (int* psqtPtr = &_packedPSQT[0]) - { - return psqtPtr[index]; - } - } + return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_packedPSQT), index); } /// diff --git a/src/Lynx/TunableEvalParameters.cs b/src/Lynx/TunableEvalParameters.cs index 4a787dab8..917dfa039 100644 --- a/src/Lynx/TunableEvalParameters.cs +++ b/src/Lynx/TunableEvalParameters.cs @@ -1,6 +1,7 @@ // 2024-9-19 13:24:58 6 using static Lynx.Utils; +using Lynx.Generator; namespace Lynx; @@ -6011,29 +6012,42 @@ internal static partial class TunableEvalParameters ]; } -public static class EvaluationParams +public static partial class EvaluationParams { - public static readonly TaperedEvaluationTerm IsolatedPawnPenalty = Pack(-17, -14); +#pragma warning disable IDE0051, IDE0052, CS0169 // Remove unread private members - public static readonly TaperedEvaluationTerm OpenFileRookBonus = Pack(40, 2); + [GeneratedPack(-17, -14)] + private static readonly int _IsolatedPawnPenalty; - public static readonly TaperedEvaluationTerm SemiOpenFileRookBonus = Pack(15, 8); + [GeneratedPack(40, 2)] + private static readonly int _OpenFileRookBonus; - public static readonly TaperedEvaluationTerm SemiOpenFileKingPenalty = Pack(-24, 5); + [GeneratedPack(15, 8)] + private static readonly int _SemiOpenFileRookBonus; - public static readonly TaperedEvaluationTerm OpenFileKingPenalty = Pack(-65, 2); + [GeneratedPack(-24, 5)] + private static readonly int _SemiOpenFileKingPenalty; - public static readonly TaperedEvaluationTerm KingShieldBonus = Pack(23, -3); + [GeneratedPack(-65, 2)] + private static readonly int _OpenFileKingPenalty; - public static readonly TaperedEvaluationTerm BishopPairBonus = Pack(30, 72); + [GeneratedPack(23, -3)] + private static readonly int _KingShieldBonus; - public static readonly TaperedEvaluationTerm PieceProtectedByPawnBonus = Pack(12, 15); + [GeneratedPack(30, 72)] + private static readonly int _BishopPairBonus; - public static readonly TaperedEvaluationTerm PieceAttackedByPawnPenalty = Pack(-47, -33); + [GeneratedPack(12, 15)] + private static readonly int _PieceProtectedByPawnBonus; - public static readonly TaperedEvaluationTermByRank PawnPhalanxBonus = + [GeneratedPack(-47, -33)] + private static readonly int _PieceAttackedByPawnPenalty; + +#pragma warning restore IDE0051, IDE0052, CS0169 // Remove unread private members + + public static readonly TaperedEvaluationTermByRank PawnPhalanxBonus = [ - Pack(0, 0), + Pack(0, 0), Pack(1, 2), Pack(10, 10), Pack(22, 24), diff --git a/src/Lynx/UCIHandler.cs b/src/Lynx/UCIHandler.cs index dbd94a23c..e604481dd 100644 --- a/src/Lynx/UCIHandler.cs +++ b/src/Lynx/UCIHandler.cs @@ -131,17 +131,17 @@ static ReadOnlySpan ExtractCommandItems(string rawCommand) private void HandlePosition(ReadOnlySpan command) { - var sw = Stopwatch.StartNew(); #if DEBUG + var sw = Stopwatch.StartNew(); _engine.Game.CurrentPosition.Print(); #endif _engine.AdjustPosition(command); + #if DEBUG _engine.Game.CurrentPosition.Print(); -#endif - _logger.Info("Position parsing took {0}ms", sw.ElapsedMilliseconds); +#endif } private void HandleStop() => _engine.StopSearching(); diff --git a/src/Lynx/Utils.cs b/src/Lynx/Utils.cs index ae7f5eb80..3697dc20e 100644 --- a/src/Lynx/Utils.cs +++ b/src/Lynx/Utils.cs @@ -36,10 +36,7 @@ public static int PieceOffset(int side) /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int PieceOffset(bool isWhite) - { - return 6 - (6 * Unsafe.As(ref isWhite)); - } + public static int PieceOffset(bool isWhite) => isWhite ? 0 : 6; /// /// Side.Black -> Side.White diff --git a/src/Lynx/ZobristTable.cs b/src/Lynx/ZobristTable.cs index 09d56cfe3..f173d0e7b 100644 --- a/src/Lynx/ZobristTable.cs +++ b/src/Lynx/ZobristTable.cs @@ -1,4 +1,5 @@ using Lynx.Model; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace Lynx; @@ -35,12 +36,8 @@ public static ulong EnPassantHash(int enPassantSquare) return default; } -#if DEBUG - if (Constants.EnPassantCaptureSquares.Length <= enPassantSquare || Constants.EnPassantCaptureSquares[enPassantSquare] == 0) - { - throw new ArgumentException($"{Constants.Coordinates[enPassantSquare]} is not a valid en-passant square"); - } -#endif + Debug.Assert(Constants.EnPassantCaptureSquares.Length > enPassantSquare && Constants.EnPassantCaptureSquares[enPassantSquare] != 0, + $"{Constants.Coordinates[enPassantSquare]} is not a valid en-passant square"); var file = enPassantSquare & 0x07; // enPassantSquare % 8 diff --git a/tests/Lynx.Generator.Test/GeneratedPackTest.cs b/tests/Lynx.Generator.Test/GeneratedPackTest.cs new file mode 100644 index 000000000..cef822dd1 --- /dev/null +++ b/tests/Lynx.Generator.Test/GeneratedPackTest.cs @@ -0,0 +1,174 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; + +namespace Lynx.Generator.Test; + +public class GeneratedPackTest +{ + [Test] + public Task StaticClass() + { + const string source = @" +using namespace Lynx.Generator; + +public static partial class TestClass +{ + [GeneratedPack(1, 2)] + public static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task NonStaticClass() + { + const string source = @" +using namespace Lynx.Generator; + +public partial class TestClass +{ + [GeneratedPack(-1, 2)] + public static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task PrivateStaticReadonlyField() + { + const string source = @" +using namespace Lynx.Generator; + +public partial class TestClass +{ + [GeneratedPack(-1, -2)] + private static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task NamedArguments() + { + const string source = @" +using namespace Lynx.Generator; + +public partial class TestClass +{ + [GeneratedPack(mg : -1, eg : -2)] + private static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task MultipleNamespacesAndClasses() + { + const string source = @" +using namespace Lynx.Generator; + +namespace Namespace1 +{ + public partial class TestClass1_1 + { + [GeneratedPack(0, 1)] + private static readonly int _TestConstant1_1; + } + + public partial class TestClass1_2 + { + [GeneratedPack(2, 3)] + private static readonly int _TestConstant1_2; + } +} + +namespace Namespace2 +{ + public partial class TestClass2_1 + { + [GeneratedPack(4, 5)] + private static readonly int _TestConstant2_1; + } +}"; + + return Verify(source); + } + + [Test] + public Task NoNamespaceImport_ShouldNotGenerateConstant() + { + const string source = @" +public partial class TestClass +{ + [GeneratedPack(1, 2)] + private static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task TwoAttributes_ShouldNotGenerateConstant() + { + const string source = @" +using namespace Lynx.Generator; + +public partial class TestClass +{ + [GeneratedPack(1, 2)] + [GeneratedPack(1, 3)] + private static readonly int _TestConstant; +}"; + + return Verify(source); + } + + [Test] + public Task NoAttributes_ShouldNotGenerateConstant() + { + const string source = @" +using namespace Lynx.Generator; + +public partial class TestClass +{ + private static readonly int _TestConstant; +}"; + + return Verify(source); + } + + /// + /// Based on: https://andrewlock.net/creating-a-source-generator-part-2-testing-an-incremental-generator-with-snapshot-testing/ + /// https://github.com/andrewlock/blog-examples/blob/c35edf1c1f0e1f9adf84c215e2ce7ab644b374f5/NetEscapades.EnumGenerators2/tests/NetEscapades.EnumGenerators.Tests/cs + /// + private static Task Verify(string source) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + // Create references for assemblies we require + // We could add multiple references if required + IEnumerable references = + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + ]; + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "Tests", + syntaxTrees: [syntaxTree], + references: references); + + GeneratedPackGenerator generator = new GeneratedPackGenerator(); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + + driver = driver.RunGenerators(compilation); + + return Verifier + .Verify(driver) + .UseDirectory("Snapshots"); + } +} diff --git a/tests/Lynx.Generator.Test/Lynx.Generator.Test.csproj b/tests/Lynx.Generator.Test/Lynx.Generator.Test.csproj new file mode 100644 index 000000000..379711277 --- /dev/null +++ b/tests/Lynx.Generator.Test/Lynx.Generator.Test.csproj @@ -0,0 +1,38 @@ + + + + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + $(NoWarn),NUnit2005 + + + diff --git a/tests/Lynx.Generator.Test/ModuleInitializer.cs b/tests/Lynx.Generator.Test/ModuleInitializer.cs new file mode 100644 index 000000000..7563b2688 --- /dev/null +++ b/tests/Lynx.Generator.Test/ModuleInitializer.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace Lynx.Generator.Test; +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() => + VerifySourceGenerators.Initialize(); +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackArrayTest.StaticClass#TestClass.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackArrayTest.StaticClass#TestClass.g.verified.cs new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackArrayTest.StaticClass#TestClass.g.verified.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_1.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_1.g.verified.cs new file mode 100644 index 000000000..72084b4c3 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_1.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass1_1.g.cs +namespace Lynx.Generator.Namespace1; + +static partial class TestClass1_1 +{ + /// + /// + /// + public const int TestConstant1_1 = 65536; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_2.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_2.g.verified.cs new file mode 100644 index 000000000..48c6df895 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass1_2.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass1_2.g.cs +namespace Lynx.Generator.Namespace1; + +static partial class TestClass1_2 +{ + /// + /// + /// + public const int TestConstant1_2 = 196610; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass2_1.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass2_1.g.verified.cs new file mode 100644 index 000000000..674877b14 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.MultipleNamespacesAndClasses#TestClass2_1.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass2_1.g.cs +namespace Lynx.Generator.Namespace2; + +static partial class TestClass2_1 +{ + /// + /// + /// + public const int TestConstant2_1 = 327684; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#TestClass.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#TestClass.g.verified.cs new file mode 100644 index 000000000..bb203032a --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NamedArguments#TestClass.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass.g.cs +namespace Lynx.Generator; + +static partial class TestClass +{ + /// + /// + /// + public const int TestConstant = -131073; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoNamespaceImport_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoNamespaceImport_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NoNamespaceImport_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#TestClass.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#TestClass.g.verified.cs new file mode 100644 index 000000000..1e66c825c --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.NonStaticClass#TestClass.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass.g.cs +namespace Lynx.Generator; + +static partial class TestClass +{ + /// + /// + /// + public const int TestConstant = 131071; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#TestClass.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#TestClass.g.verified.cs new file mode 100644 index 000000000..bb203032a --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.PrivateStaticReadonlyField#TestClass.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass.g.cs +namespace Lynx.Generator; + +static partial class TestClass +{ + /// + /// + /// + public const int TestConstant = -131073; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#TestClass.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#TestClass.g.verified.cs new file mode 100644 index 000000000..bb0793d82 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.StaticClass#TestClass.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: TestClass.g.cs +namespace Lynx.Generator; + +static partial class TestClass +{ + /// + /// + /// + public const int TestConstant = 131073; + +} diff --git a/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.TwoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.TwoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs new file mode 100644 index 000000000..15344d827 --- /dev/null +++ b/tests/Lynx.Generator.Test/Snapshots/GeneratedPackTest.TwoAttributes_ShouldNotGenerateConstant#GeneratedPackAttribute.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: GeneratedPackAttribute.g.cs +using System; + +namespace Lynx.Generator +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal sealed class GeneratedPackAttribute : Attribute + { + public short MG { get; } + public short EG { get; } + + public GeneratedPackAttribute(short mg, short eg) + { + MG = mg; + EG = eg; + } + } +} \ No newline at end of file diff --git a/tests/Lynx.Generator.Test/UtilsTest.cs b/tests/Lynx.Generator.Test/UtilsTest.cs new file mode 100644 index 000000000..936e85543 --- /dev/null +++ b/tests/Lynx.Generator.Test/UtilsTest.cs @@ -0,0 +1,12 @@ +namespace Lynx.Generator.Test; + +public class UtilsTest +{ + [TestCase(1, 2, 131073)] + [TestCase(-1, 2, 131071)] + [TestCase(-1, -2, -131073)] + public void Pack(short mg, short eg, int packedValue) + { + Assert.AreEqual(packedValue, Utils.Pack(mg, eg)); + } +} diff --git a/tests/Lynx.Test/Lynx.Test.csproj b/tests/Lynx.Test/Lynx.Test.csproj index 472d28d32..9232b8c66 100644 --- a/tests/Lynx.Test/Lynx.Test.csproj +++ b/tests/Lynx.Test/Lynx.Test.csproj @@ -7,18 +7,19 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + all runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/tests/Lynx.Test/MoveGeneration/GeneratePawnMovesTest.cs b/tests/Lynx.Test/MoveGeneration/GeneratePawnMovesTest.cs index 8ac288c7b..44a2a6b87 100644 --- a/tests/Lynx.Test/MoveGeneration/GeneratePawnMovesTest.cs +++ b/tests/Lynx.Test/MoveGeneration/GeneratePawnMovesTest.cs @@ -233,10 +233,6 @@ public void DoubleEnPassant(string fen) Assert.AreEqual(2, moves.Count(m => m.IsEnPassant() && m.IsCapture())); } -#if DEBUG - [TestCase("PPPPPPPP/8/8/8/8/8/8/8 w - - 0 1", Description = "Last rank")] - [TestCase("8/8/8/8/8/8/8/pppppppp b - - 0 1", Description = "Last rank")] -#endif [TestCase("8/8/8/p1p1p1p1/PpPpPpPp/1P1P1P1P/8/8 w - - 0 1", Description = "Blocked position")] [TestCase("8/8/8/p1p1p1p1/PpPpPpPp/1P1P1P1P/8/8 b - - 0 1", Description = "Blocked position")] [TestCase("8/8/8/N7/P7/1p6/8/8 w - - 0 1", Description = "Backwards/inverse capture")] diff --git a/tests/Lynx.Test/PerftTest.cs b/tests/Lynx.Test/PerftTest.cs index 501e935bd..cb0d1cbc3 100644 --- a/tests/Lynx.Test/PerftTest.cs +++ b/tests/Lynx.Test/PerftTest.cs @@ -258,6 +258,22 @@ public void Kz04pxPositions(string fen, long _, long nodesD1, long nodesD2 = 0, } } + /// + /// martinn = Motor's author + /// + // Motor vs Avalanche + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 1, 1)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 2, 48)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 3, 1_060)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 4, 42_723)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 5, 981_168)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 6, 37_765_954)] + [TestCase("6r1/2q2pp1/1PB2k2/3P1P2/5Q1B/8/6K1/7R b - - 0 1", 7, 891_192_699)] // ~40s + public void MartinnPositions(string fen, int depth, long expectedNumberOfNodes) + { + Validate(fen, depth, expectedNumberOfNodes); + } + [Test] public void PositionWithHighestKnownNumberOfMoves() => Validate("R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1 w - - 0 1", 1, 218); diff --git a/tests/Lynx.Test/ZobristTableTest.cs b/tests/Lynx.Test/ZobristTableTest.cs index 339a69c02..98378f3ab 100644 --- a/tests/Lynx.Test/ZobristTableTest.cs +++ b/tests/Lynx.Test/ZobristTableTest.cs @@ -66,17 +66,6 @@ public void EnPassantHash() } Assert.AreEqual(16, enPassantSquares.Count()); - -#if DEBUG - for (int square = 0; square < 64; ++square) - { - var rank = square / 8; - if (rank != 2 && rank != 5) - { - Assert.Throws(() => ZobristTable.EnPassantHash(square)); - } - } -#endif } [Test]