From bb1723579c26c9a21227796dd194d5a3f946723c Mon Sep 17 00:00:00 2001 From: Panos Date: Wed, 29 Mar 2023 23:55:16 +0300 Subject: [PATCH 1/4] Add: Match methods with state object. Add `Match` and `Match` for variant and specific methods which accept a `state` object and pass it to the provided functions. --- src/UnionGeneration/UnionSourceBuilder.cs | 182 +++++++++++++++++++++- 1 file changed, 181 insertions(+), 1 deletion(-) diff --git a/src/UnionGeneration/UnionSourceBuilder.cs b/src/UnionGeneration/UnionSourceBuilder.cs index ebe9036..e7f5597 100644 --- a/src/UnionGeneration/UnionSourceBuilder.cs +++ b/src/UnionGeneration/UnionSourceBuilder.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Net.NetworkInformation; +using System.Text; namespace Dunet.UnionGeneration; @@ -122,6 +123,51 @@ UnionDeclaration union builder.AppendLine(" );"); builder.AppendLine(); + // public abstract TMatchOutput Match( + // TState state, + // System.Func, TMatchOutput> @unionVariant1, + // System.Func, TMatchOutput> @unionVariant2, + // ... + // ); + builder.AppendLine(" public abstract TMatchOutput Match("); + builder.AppendLine(" TState state,"); + for (int i = 0; i < union.Variants.Count; ++i) + { + var variant = union.Variants[i]; + builder.Append($" System.Func {variant.Identifier.ToMethodParameterCase()}"); + if (i < union.Variants.Count - 1) + { + builder.Append(","); + } + builder.AppendLine(); + } + builder.AppendLine(" );"); + + // public abstract void Match( + // TState state, + // System.Action> @unionVariant1, + // System.Action> @unionVariant2, + // ... + // ); + builder.AppendLine(" public abstract void Match("); + builder.AppendLine(" TState state,"); + for (int i = 0; i < union.Variants.Count; ++i) + { + var variant = union.Variants[i]; + builder.Append($" System.Action {variant.Identifier.ToMethodParameterCase()}"); + if (i < union.Variants.Count - 1) + { + builder.Append(","); + } + builder.AppendLine(); + } + builder.AppendLine(" );"); + builder.AppendLine(); + return builder; } @@ -164,6 +210,42 @@ UnionDeclaration union builder.AppendLine(); + foreach (var variant in union.Variants) + { + // public abstract TMatchOutput MatchSpecific( + // TState state, + // System.Func, TMatchOutput> @specific, + // System.Func @else + // ); + builder.AppendLine($" public abstract TMatchOutput Match{variant.Identifier}("); + builder.AppendLine(" TState state,"); + builder.Append($" System.Func {variant.Identifier.ToMethodParameterCase()},"); + builder.AppendLine($" System.Func @else"); + builder.AppendLine(" );"); + } + + builder.AppendLine(); + + foreach (var variant in union.Variants) + { + // public abstract void MatchSpecific( + // TState state, + // System.Action> @specific, + // System.Action @else + // ); + builder.AppendLine($" public abstract void Match{variant.Identifier}("); + builder.AppendLine(" TState state,"); + builder.Append($" System.Action {variant.Identifier.ToMethodParameterCase()},"); + builder.AppendLine($" System.Action @else"); + builder.AppendLine(" );"); + } + + builder.AppendLine(); + return builder; } @@ -213,6 +295,50 @@ VariantDeclaration variant } builder.AppendLine($" ) => {variant.Identifier.ToMethodParameterCase()}(this);"); + // public override TMatchOutput Match( + // TState state, + // System.Func, TMatchOutput> @unionVariant1, + // System.Func, TMatchOutput> @unionVariant2, + // ... + // ) => unionVariantX(state, this); + builder.AppendLine(" public override TMatchOutput Match("); + builder.AppendLine(" TState state,"); + for (int i = 0; i < union.Variants.Count; ++i) + { + var variantParam = union.Variants[i]; + builder.Append($" System.Func {variantParam.Identifier.ToMethodParameterCase()}"); + if (i < union.Variants.Count - 1) + { + builder.Append(","); + } + builder.AppendLine(); + } + builder.AppendLine($" ) => {variant.Identifier.ToMethodParameterCase()}(state, this);"); + + // public override void Match( + // TState state, + // System.Action> @unionVariant1, + // System.Action> @unionVariant2, + // ... + // ) => unionVariantX(state, this); + builder.AppendLine(" public override void Match("); + builder.AppendLine(" TState state,"); + for (int i = 0; i < union.Variants.Count; ++i) + { + var variantParam = union.Variants[i]; + builder.Append($" System.Action {variantParam.Identifier.ToMethodParameterCase()}"); + if (i < union.Variants.Count - 1) + { + builder.Append(","); + } + builder.AppendLine(); + } + builder.AppendLine($" ) => {variant.Identifier.ToMethodParameterCase()}(state, this);"); + return builder; } @@ -272,6 +398,60 @@ VariantDeclaration variant } } + // public override TMatchOutput MatchVariantX( + // TState state, + // System.Func, TMatchOutput> @unionVariantX, + // System.Func @else, + // ... + // ) => unionVariantX(state, this); + foreach (var specificVariant in union.Variants) + { + builder.AppendLine( + $" public override TMatchOutput Match{specificVariant.Identifier}(" + ); + builder.AppendLine(" TState state,"); + builder.Append($" System.Func {specificVariant.Identifier.ToMethodParameterCase()}," + ); + builder.AppendLine($" System.Func @else"); + builder.Append(" ) => "); + if (specificVariant.Identifier == variant.Identifier) + { + builder.AppendLine($"{specificVariant.Identifier.ToMethodParameterCase()}(state, this);"); + } + else + { + builder.AppendLine("@else(state);"); + } + } + + // public override void MatchVariantX( + // TState state, + // System.Action> @unionVariantX, + // System.Action @else, + // ... + // ) => unionVariantX(state, this); + foreach (var specificVariant in union.Variants) + { + builder.AppendLine($" public override void Match{specificVariant.Identifier}("); + builder.AppendLine(" TState state,"); + builder.Append($" System.Action {specificVariant.Identifier.ToMethodParameterCase()},"); + builder.AppendLine($" System.Action @else"); + builder.Append(" ) => "); + if (specificVariant.Identifier == variant.Identifier) + { + builder.AppendLine($"{specificVariant.Identifier.ToMethodParameterCase()}(state, this);"); + } + else + { + builder.AppendLine("@else(state);"); + } + } + builder.AppendLine(" }"); builder.AppendLine(); From 8d5d8d95f2dd107d4cee9c2f8b55ed23670f466d Mon Sep 17 00:00:00 2001 From: Panos Date: Thu, 30 Mar 2023 00:26:22 +0300 Subject: [PATCH 2/4] Fix: UnionMayBeEmpty test Refactor the method to generate properly for an empty union and ensure all current tests are passing. --- src/UnionGeneration/UnionSourceBuilder.cs | 24 +++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/UnionGeneration/UnionSourceBuilder.cs b/src/UnionGeneration/UnionSourceBuilder.cs index e7f5597..9706a39 100644 --- a/src/UnionGeneration/UnionSourceBuilder.cs +++ b/src/UnionGeneration/UnionSourceBuilder.cs @@ -130,7 +130,8 @@ UnionDeclaration union // ... // ); builder.AppendLine(" public abstract TMatchOutput Match("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); for (int i = 0; i < union.Variants.Count; ++i) { var variant = union.Variants[i]; @@ -152,7 +153,8 @@ UnionDeclaration union // ... // ); builder.AppendLine(" public abstract void Match("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); for (int i = 0; i < union.Variants.Count; ++i) { var variant = union.Variants[i]; @@ -218,7 +220,8 @@ UnionDeclaration union // System.Func @else // ); builder.AppendLine($" public abstract TMatchOutput Match{variant.Identifier}("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); builder.Append($" System.Func {variant.Identifier.ToMethodParameterCase()},"); @@ -236,7 +239,8 @@ UnionDeclaration union // System.Action @else // ); builder.AppendLine($" public abstract void Match{variant.Identifier}("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); builder.Append($" System.Action {variant.Identifier.ToMethodParameterCase()},"); @@ -302,7 +306,8 @@ VariantDeclaration variant // ... // ) => unionVariantX(state, this); builder.AppendLine(" public override TMatchOutput Match("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); for (int i = 0; i < union.Variants.Count; ++i) { var variantParam = union.Variants[i]; @@ -324,7 +329,8 @@ VariantDeclaration variant // ... // ) => unionVariantX(state, this); builder.AppendLine(" public override void Match("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); for (int i = 0; i < union.Variants.Count; ++i) { var variantParam = union.Variants[i]; @@ -409,7 +415,8 @@ VariantDeclaration variant builder.AppendLine( $" public override TMatchOutput Match{specificVariant.Identifier}(" ); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); builder.Append($" System.Func("); - builder.AppendLine(" TState state,"); + builder.Append($" TState state"); + builder.AppendLine(union.Variants.Count > 0 ? "," : string.Empty); builder.Append($" System.Action {specificVariant.Identifier.ToMethodParameterCase()},"); From a90d113d1d2759749a0dab1d42aca51baf95c87e Mon Sep 17 00:00:00 2001 From: Panos Date: Fri, 31 Mar 2023 20:08:03 +0300 Subject: [PATCH 3/4] Add: Tests for new `state` methods Add tests for `Variant` and `Specific` methods with `state` object. --- .../MatchMethodWithStateTests.cs | 127 ++++++++++++++++++ .../MatchSpecificUnionValueWithStateTests.cs | 92 +++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 test/UnionGeneration/MatchMethodWithStateTests.cs create mode 100644 test/UnionGeneration/MatchSpecificUnionValueWithStateTests.cs diff --git a/test/UnionGeneration/MatchMethodWithStateTests.cs b/test/UnionGeneration/MatchMethodWithStateTests.cs new file mode 100644 index 0000000..39d5367 --- /dev/null +++ b/test/UnionGeneration/MatchMethodWithStateTests.cs @@ -0,0 +1,127 @@ +namespace Dunet.Test.UnionGeneration; + +public sealed class MatchMethodWithStateTests +{ + [Fact] + public void CanUseUnionTypesInDedicatedMatchMethod() + { + // Arrange. + var source = """ +using Dunet; + +Shape shape = new Shape.Rectangle(3, 4); +double state = 2d; + +var area = shape.Match( + state, + static (s, circle) => s + 3.14 * circle.Radius * circle.Radius, + static (s, rectangle) => s + rectangle.Length * rectangle.Width, + static (s, triangle) => s + triangle.Base * triangle.Height / 2 +); + +[Union] +partial record Shape +{ + partial record Circle(double Radius); + partial record Rectangle(double Length, double Width); + partial record Triangle(double Base, double Height); +} +"""; + // Act. + var result = Compiler.Compile(source); + + // Assert. + using var scope = new AssertionScope(); + result.CompilationErrors.Should().BeEmpty(); + result.GenerationErrors.Should().BeEmpty(); + } + + [Theory] + [InlineData("Shape shape = new Shape.Rectangle(3, 4);", 14d)] + [InlineData("Shape shape = new Shape.Circle(1);", 5.14d)] + [InlineData("Shape shape = new Shape.Triangle(4, 2);", 6d)] + public void MatchMethodCallsCorrectFunctionArgument( + string shapeDeclaration, + double expectedArea + ) + { + // Arrange. + var source = $$""" +using Dunet; + +static double GetArea() +{ + {{shapeDeclaration}} + double state = 2d; + return shape.Match( + state, + static (s, circle) => s + 3.14 * circle.Radius * circle.Radius, + static (s, rectangle) => s + rectangle.Length * rectangle.Width, + static (s, triangle) => s + triangle.Base * triangle.Height / 2 + ); +} + +[Union] +partial record Shape +{ + partial record Circle(double Radius); + partial record Rectangle(double Length, double Width); + partial record Triangle(double Base, double Height); +} +"""; + // Act. + var result = Compiler.Compile(source); + var actualArea = result.Assembly?.ExecuteStaticMethod("GetArea"); + + // Assert. + using var scope = new AssertionScope(); + result.CompilationErrors.Should().BeEmpty(); + result.GenerationErrors.Should().BeEmpty(); + actualArea.Should().BeApproximately(expectedArea, 0.0000000001d); + } + + [Theory] + [InlineData("Keyword keyword = new Keyword.New();" , "string state = \"new\";", "new")] + [InlineData("Keyword keyword = new Keyword.Base();", "string state = \"base\";", "base")] + [InlineData("Keyword keyword = new Keyword.Null();", "string state = \"null\";", "null")] + public void CanMatchOnUnionVariantsNamedAfterKeywords( + string keywordDeclaration, + string stateDeclaration, + string expectedKeyword + ) + { + // Arrange. + var source = $$""" +using Dunet; + +static string GetKeyword() +{ + {{keywordDeclaration}} + {{stateDeclaration}} + return keyword.Match( + state, + static (s, @new) => s, + static (s, @base) => s, + static (s, @null) => s + ); +} + +[Union] +partial record Keyword +{ + partial record New; + partial record Base; + partial record Null; +} +"""; + // Act. + var result = Compiler.Compile(source); + var actualKeyword = result.Assembly?.ExecuteStaticMethod("GetKeyword"); + + // Assert. + using var scope = new AssertionScope(); + result.CompilationErrors.Should().BeEmpty(); + result.GenerationErrors.Should().BeEmpty(); + actualKeyword.Should().Be(expectedKeyword); + } +} diff --git a/test/UnionGeneration/MatchSpecificUnionValueWithStateTests.cs b/test/UnionGeneration/MatchSpecificUnionValueWithStateTests.cs new file mode 100644 index 0000000..8b6d60a --- /dev/null +++ b/test/UnionGeneration/MatchSpecificUnionValueWithStateTests.cs @@ -0,0 +1,92 @@ +namespace Dunet.Test.UnionGeneration; + +public sealed class MatchSpecificUnionValueWithStateTests +{ + [Theory] + [InlineData("Shape shape = new Shape.Rectangle(3, 4);", 1d)] + [InlineData("Shape shape = new Shape.Circle(1);", 5.14d)] + [InlineData("Shape shape = new Shape.Triangle(4, 2);", 1d)] + public void SpecificMatchMethodCallsCorrectFunctionArgument( + string shapeDeclaration, + double expectedArea + ) + { + // Arrange. + var source = $$""" +using Dunet; + +static double GetArea() +{ + {{shapeDeclaration}} + double state = 2d; + return shape.MatchCircle( + state, + static (s, circle) => s + 3.14 * circle.Radius * circle.Radius, + static s => -1 + s + ); +} + +[Union] +partial record Shape +{ + partial record Circle(double Radius); + partial record Rectangle(double Length, double Width); + partial record Triangle(double Base, double Height); +} +"""; + // Act. + var result = Compiler.Compile(source); + var actualArea = result.Assembly?.ExecuteStaticMethod("GetArea"); + + // Assert. + using var scope = new AssertionScope(); + result.CompilationErrors.Should().BeEmpty(); + result.GenerationErrors.Should().BeEmpty(); + actualArea.Should().BeApproximately(expectedArea, 0.0000000001d); + } + + [Theory] + [InlineData("Shape shape = new Shape.Rectangle(3, 4);", 1d)] + [InlineData("Shape shape = new Shape.Circle(1);", 1d)] + [InlineData("Shape shape = new Shape.Triangle(4, 2);", 6d)] + public void SpecificMatchMethodCallsCorrectActionArgument( + string shapeDeclaration, + double expectedArea + ) + { + // Arrange. + var source = $$""" +using Dunet; + +static double GetArea() +{ + double value = 0d; + {{shapeDeclaration}} + double state = 2d; + shape.MatchTriangle( + state, + (s, triangle) => { value = s + 0.5 * triangle.Base * triangle.Height; }, + s => { value = -1 + s; } + ); + return value; +} + +[Union] +partial record Shape +{ + partial record Circle(double Radius); + partial record Rectangle(double Length, double Width); + partial record Triangle(double Base, double Height); +} +"""; + // Act. + var result = Compiler.Compile(source); + var actualArea = result.Assembly?.ExecuteStaticMethod("GetArea"); + + // Assert. + using var scope = new AssertionScope(); + result.CompilationErrors.Should().BeEmpty(); + result.GenerationErrors.Should().BeEmpty(); + actualArea.Should().BeApproximately(expectedArea, 0.0000000001d); + } +} From d05fbc55789ba4236dec14e6a23dc082a022e442 Mon Sep 17 00:00:00 2001 From: Domn Werner Date: Fri, 31 Mar 2023 18:43:21 -0700 Subject: [PATCH 4/4] Update src/UnionGeneration/UnionSourceBuilder.cs --- src/UnionGeneration/UnionSourceBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnionGeneration/UnionSourceBuilder.cs b/src/UnionGeneration/UnionSourceBuilder.cs index 9706a39..320a026 100644 --- a/src/UnionGeneration/UnionSourceBuilder.cs +++ b/src/UnionGeneration/UnionSourceBuilder.cs @@ -1,4 +1,3 @@ -using System.Net.NetworkInformation; using System.Text; namespace Dunet.UnionGeneration;