From 937fefcc68c70d884f25a108d77c96f997a3d98d Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 19:29:05 -0700 Subject: [PATCH 01/56] add dotnet11 test project --- SumSharp.sln | 10 ++++++++-- Tests.Net11/Tests.Net11.csproj | 21 +++++++++++++++++++++ Tests.Net11/UnitTest1.cs | 11 +++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Tests.Net11/Tests.Net11.csproj create mode 100644 Tests.Net11/UnitTest1.cs diff --git a/SumSharp.sln b/SumSharp.sln index aed0033..a592b21 100644 --- a/SumSharp.sln +++ b/SumSharp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35806.99 +# Visual Studio Version 18 +VisualStudioVersion = 18.8.12023.21 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumSharp", "SumSharp\SumSharp.csproj", "{F757C23E-EF40-420D-AE51-9A0491392968}" EndProject @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.AOT", "Tests.AOT\Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumSharp.Analyzer", "SumSharp.Analyzer\SumSharp.Analyzer.csproj", "{D50E46B6-6A70-4A3C-A89F-348A1566825F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Net11", "Tests.Net11\Tests.Net11.csproj", "{76BD98D1-AA76-4FED-8802-7EB256A1A187}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {D50E46B6-6A70-4A3C-A89F-348A1566825F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D50E46B6-6A70-4A3C-A89F-348A1566825F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D50E46B6-6A70-4A3C-A89F-348A1566825F}.Release|Any CPU.Build.0 = Release|Any CPU + {76BD98D1-AA76-4FED-8802-7EB256A1A187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76BD98D1-AA76-4FED-8802-7EB256A1A187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76BD98D1-AA76-4FED-8802-7EB256A1A187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76BD98D1-AA76-4FED-8802-7EB256A1A187}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests.Net11/Tests.Net11.csproj b/Tests.Net11/Tests.Net11.csproj new file mode 100644 index 0000000..24163d7 --- /dev/null +++ b/Tests.Net11/Tests.Net11.csproj @@ -0,0 +1,21 @@ + + + + net11.0 + enable + enable + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests.Net11/UnitTest1.cs b/Tests.Net11/UnitTest1.cs new file mode 100644 index 0000000..3af0a6d --- /dev/null +++ b/Tests.Net11/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace Tests.Net11 +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} From 48c2dbf751fe9f277048d55bb158f0cc6745c5c9 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 19:31:46 -0700 Subject: [PATCH 02/56] start work on test --- Tests.Net11/Tests.Net11.csproj | 6 ++++++ Tests.Net11/Union.cs | 22 ++++++++++++++++++++++ Tests.Net11/UnitTest1.cs | 11 ----------- 3 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 Tests.Net11/Union.cs delete mode 100644 Tests.Net11/UnitTest1.cs diff --git a/Tests.Net11/Tests.Net11.csproj b/Tests.Net11/Tests.Net11.csproj index 24163d7..1389664 100644 --- a/Tests.Net11/Tests.Net11.csproj +++ b/Tests.Net11/Tests.Net11.csproj @@ -14,6 +14,12 @@ + + + + + + diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs new file mode 100644 index 0000000..d8747ab --- /dev/null +++ b/Tests.Net11/Union.cs @@ -0,0 +1,22 @@ +using SumSharp; + +namespace Tests.Net11; + +public partial class Union +{ + [UnionCase("Int", typeof(int))] + [UnionCase("Float", typeof(float))] + [UnionCase("Other", "T")] + + partial class IntOrStringOrOther + { + + } + + + [Fact] + public void Test1() + { + + } +} diff --git a/Tests.Net11/UnitTest1.cs b/Tests.Net11/UnitTest1.cs deleted file mode 100644 index 3af0a6d..0000000 --- a/Tests.Net11/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Tests.Net11 -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} From e906861e33f5c57fe308e45e7de0f170ca24ee9a Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 19:33:49 -0700 Subject: [PATCH 03/56] add switch test --- Tests.Net11/Union.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index d8747ab..b906de4 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -17,6 +17,15 @@ partial class IntOrStringOrOther [Fact] public void Test1() { + IntOrStringOrOther x = 5; + var result = x switch + { + int i => true, + float f => false, + bool b => false, + }; + + Assert.True(result); } } From b084d009c6ed40902d06efda1138b6a7a3cef328 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 19:52:37 -0700 Subject: [PATCH 04/56] add union atttribute and interface --- SumSharp.Generator/SymbolHandler.cs | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index fc79579..eb8ae26 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -737,23 +737,44 @@ private void EmitFieldsAndConstructor() fieldNameTypeMap[caseData.FieldType!] = caseData.FieldName!; } - List interfaces = []; + string interfaces = ": "; if (!DisableValueEquality) { - interfaces.Add($"System.IEquatable<{Name}>"); + interfaces += $@" + System.IEquatable<{Name}>"; } if (IsDisposable) { - interfaces.Add("System.IDisposable"); + interfaces += @", + System.IDisposable"; } if (IsAsyncDisposable) { - interfaces.Add("System.IAsyncDisposable"); + interfaces += @", + System.IAsyncDisposable"; + } + + if (interfaces == ": ") + { + interfaces = $@" +#if NET11_0_OR_GREATER + : System.Runtime.CompilerServices.IUnion +#endif"; + } + else + { + interfaces += $@" +#if NET11_0_OR_GREATER + , System.Runtime.CompilerServices.IUnion +#endif"; } Builder.Append($@" -{Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name}{(interfaces.Count == 0 ? "" : $" : {string.Join(", ", interfaces)}")} +#if NET11_0_OR_GREATER +[System.Runtime.CompilerServices.Union] +#endif +{Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name}{interfaces} {{"); foreach (var field in fieldNameTypeMap) From 03a375ad57c5d21c09fd1a566c2d44403d5fe7ca Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 20:13:35 -0700 Subject: [PATCH 05/56] make test compile --- SumSharp.Generator/SymbolHandler.cs | 39 +++++++++++++++++++++++++++-- Tests.Net11/Tests.Net11.csproj | 1 + 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index eb8ae26..0ff3db3 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -623,6 +623,8 @@ public string Emit() EmitAs(); + EmitNativeUnion(); + EmitIs(); EmitMatch(); @@ -759,14 +761,14 @@ private void EmitFieldsAndConstructor() { interfaces = $@" #if NET11_0_OR_GREATER - : System.Runtime.CompilerServices.IUnion + : {Name}.IUnionMembers #endif"; } else { interfaces += $@" #if NET11_0_OR_GREATER - , System.Runtime.CompilerServices.IUnion + , {Name}.IUnionMembers #endif"; } @@ -1196,6 +1198,39 @@ public void EmitAs() public ValueTask<{caseData.TypeInfo.Name}> As{caseData.Name}Or(System.Func> defaultValueFactory) => Index == {caseData.Index} ? ValueTask.FromResult(As{caseData.Name}Unsafe) : new ValueTask<{caseData.TypeInfo.Name}>(defaultValueFactory());"); } } + + public void EmitNativeUnion() + { + Builder.Append($@" +#if NET11_0_OR_GREATER + public object{Nullable} Value + {{ + get + {{ + throw new System.NotImplementedException(); + }} + }} + + public interface IUnionMembers + {{ + public object{Nullable} Value {{ get; }}"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + continue; + } + + Builder.AppendLine($@" + public static {Name} Create({caseData.TypeInfo.Name} value) => throw new System.NotImplementedException();"); + } + + Builder.AppendLine($@" + }} +#endif"); + } + public void EmitIs() { foreach (var caseData in Cases) diff --git a/Tests.Net11/Tests.Net11.csproj b/Tests.Net11/Tests.Net11.csproj index 1389664..d330533 100644 --- a/Tests.Net11/Tests.Net11.csproj +++ b/Tests.Net11/Tests.Net11.csproj @@ -2,6 +2,7 @@ net11.0 + preview enable enable false From 3e7f4d5a5a5adc7455bf066839223cee09452f7e Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 20:16:30 -0700 Subject: [PATCH 06/56] implement creation members --- SumSharp.Generator/SymbolHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 0ff3db3..99cb793 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1201,7 +1201,7 @@ public void EmitAs() public void EmitNativeUnion() { - Builder.Append($@" + Builder.AppendLine($@" #if NET11_0_OR_GREATER public object{Nullable} Value {{ @@ -1223,7 +1223,7 @@ public interface IUnionMembers } Builder.AppendLine($@" - public static {Name} Create({caseData.TypeInfo.Name} value) => throw new System.NotImplementedException();"); + public static {Name} Create({caseData.TypeInfo.Name} value) => {Name}.{caseData.Name}(value);"); } Builder.AppendLine($@" From 450ddbb9c55bc22b79db20dbc1e4b05c89c200fa Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 1 Aug 2026 20:24:24 -0700 Subject: [PATCH 07/56] make test compile --- SumSharp.Generator/SymbolHandler.cs | 19 +++++++++++++++++-- Tests.Net11/Union.cs | 24 ++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 99cb793..78f6140 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1201,13 +1201,28 @@ public void EmitAs() public void EmitNativeUnion() { - Builder.AppendLine($@" + Builder.Append($@" #if NET11_0_OR_GREATER public object{Nullable} Value {{ get {{ - throw new System.NotImplementedException(); + return Index switch + {{"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + continue; + } + + Builder.Append($@" + {caseData.Index} => As{caseData.Name}Unsafe,"); + } + + Builder.AppendLine($@" + }}; }} }} diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index b906de4..bd73f84 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -15,17 +15,33 @@ partial class IntOrStringOrOther [Fact] - public void Test1() + public void SimpleSwitch() { IntOrStringOrOther x = 5; - var result = x switch + Assert.True(x switch { int i => true, float f => false, bool b => false, - }; + }); - Assert.True(result); + IntOrStringOrOther y = 3.4f; + + Assert.True(y switch + { + int i => false, + float f => true, + bool b => false, + }); + + IntOrStringOrOther z = true; + + Assert.True(z switch + { + int i => false, + float f => false, + bool b => true, + }); } } From 716e80718f051b892f73d51073610dd9f0d87b24 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 08:31:52 -0700 Subject: [PATCH 08/56] update test --- SumSharp.Generator/SymbolHandler.cs | 14 +++++++++----- Tests.Net11/Union.cs | 20 ++++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 78f6140..2203fe3 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1214,11 +1214,13 @@ public object{Nullable} Value { if (caseData.TypeInfo is null) { - continue; + } - - Builder.Append($@" + else + { + Builder.Append($@" {caseData.Index} => As{caseData.Name}Unsafe,"); + } } Builder.AppendLine($@" @@ -1236,9 +1238,11 @@ public interface IUnionMembers { continue; } - - Builder.AppendLine($@" + else + { + Builder.AppendLine($@" public static {Name} Create({caseData.TypeInfo.Name} value) => {Name}.{caseData.Name}(value);"); + } } Builder.AppendLine($@" diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index bd73f84..4cdf32d 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -17,31 +17,39 @@ partial class IntOrStringOrOther [Fact] public void SimpleSwitch() { - IntOrStringOrOther x = 5; + IntOrStringOrOther w = 5; - Assert.True(x switch + Assert.True(w switch { int i => true, float f => false, bool b => false, }); - IntOrStringOrOther y = 3.4f; + IntOrStringOrOther x = 3.4f; - Assert.True(y switch + Assert.True(x switch { int i => false, float f => true, bool b => false, }); - IntOrStringOrOther z = true; + IntOrStringOrOther y = true; - Assert.True(z switch + Assert.True(y switch { int i => false, float f => false, bool b => true, }); + + var z = IntOrStringOrOther.Other(4); + + Assert.True(z switch + { + int i => true, + float f => false, + }); } } From c2441d4b5bc7d1aa94c68f609762410bbcb52dca Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 08:43:18 -0700 Subject: [PATCH 09/56] update test again --- Tests.Net11/Union.cs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 4cdf32d..d5f3f92 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -5,7 +5,7 @@ namespace Tests.Net11; public partial class Union { [UnionCase("Int", typeof(int))] - [UnionCase("Float", typeof(float))] + [UnionCase("String", typeof(string))] [UnionCase("Other", "T")] partial class IntOrStringOrOther @@ -13,6 +13,24 @@ partial class IntOrStringOrOther } + /*[UnionCase("Case0", typeof(string))] + [UnionCase("Case1", typeof(string))] + partial class RepeatedCases + { + + }*/ + + [Fact] + public void Value() + { + IntOrStringOrOther w = 5; + + Assert.Equal(5, IntOrStringOrOther.Int(5).Value); + Assert.Equal("abc", IntOrStringOrOther.String("abc").Value); + Assert.Equal(true, IntOrStringOrOther.Other(true).Value); + Assert.Equal(4, IntOrStringOrOther.Other(4).Value); + } + [Fact] public void SimpleSwitch() @@ -21,17 +39,17 @@ public void SimpleSwitch() Assert.True(w switch { - int i => true, - float f => false, + int i => i == 5, + string s => false, bool b => false, }); - IntOrStringOrOther x = 3.4f; + IntOrStringOrOther x = "abc"; Assert.True(x switch { int i => false, - float f => true, + string s => s == "abc", bool b => false, }); @@ -40,16 +58,16 @@ public void SimpleSwitch() Assert.True(y switch { int i => false, - float f => false, - bool b => true, + string s => false, + bool b => b, }); var z = IntOrStringOrOther.Other(4); Assert.True(z switch { - int i => true, - float f => false, + int i => i == 4, + string s => false, }); } } From b3cea069b958708a9c1c91de8719bd33254cf117 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 08:51:59 -0700 Subject: [PATCH 10/56] add test for hasvalue --- SumSharp.Generator/SymbolHandler.cs | 27 +++++++++++++++++++++++++ Tests.Net11/Union.cs | 31 +++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 2203fe3..b918f5f 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using static SumSharp.Generator.SymbolHandler; namespace SumSharp.Generator; @@ -1228,6 +1229,32 @@ public object{Nullable} Value }} }} + public bool HasValue + {{ + get + {{ + return Index switch + {{"); + + foreach(var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + Builder.Append($@" + {caseData.Index} => throw new System.NotImplementedException(),"); + } + else + { + Builder.Append($@" + {caseData.Index} => throw new System.NotImplementedException(),"); + } + } + + Builder.AppendLine($@" + }}; + }} + }} + public interface IUnionMembers {{ public object{Nullable} Value {{ get; }}"); diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index d5f3f92..0c8fafd 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -13,6 +13,13 @@ partial class IntOrStringOrOther } + [UnionCase("Some", "T")] + [UnionCase("None")] + partial class Optional + { + + } + /*[UnionCase("Case0", typeof(string))] [UnionCase("Case1", typeof(string))] partial class RepeatedCases @@ -23,14 +30,34 @@ partial class RepeatedCases [Fact] public void Value() { - IntOrStringOrOther w = 5; - Assert.Equal(5, IntOrStringOrOther.Int(5).Value); Assert.Equal("abc", IntOrStringOrOther.String("abc").Value); Assert.Equal(true, IntOrStringOrOther.Other(true).Value); Assert.Equal(4, IntOrStringOrOther.Other(4).Value); } + [Fact] + public void HasValue() + { + Assert.True(IntOrStringOrOther.Int(5).HasValue); + Assert.True(IntOrStringOrOther.String("abc").HasValue); + Assert.True(IntOrStringOrOther.Other(true).HasValue); + Assert.True(IntOrStringOrOther.Other(4).HasValue); + Assert.True(IntOrStringOrOther.Other(3).HasValue); + + Assert.False(IntOrStringOrOther.Other(null).HasValue); + Assert.False(IntOrStringOrOther.String(null!).HasValue); + + Assert.True(Optional.Some(1).HasValue); + Assert.True(Optional.Some(1).HasValue); + Assert.True(Optional.Some("abc").HasValue); + + Assert.False(Optional.Some(null).HasValue); + Assert.False(Optional.Some(null!).HasValue); + + Assert.True(Optional.None.HasValue); + } + [Fact] public void SimpleSwitch() From afa4530e057a419b842497b6ee726cb45751812e Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 09:04:24 -0700 Subject: [PATCH 11/56] make hasvalue test pass --- SumSharp.Generator/SymbolHandler.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index b918f5f..b8e21b2 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1224,7 +1224,7 @@ public object{Nullable} Value } } - Builder.AppendLine($@" + Builder.Append($@" }}; }} }} @@ -1241,12 +1241,24 @@ public bool HasValue if (caseData.TypeInfo is null) { Builder.Append($@" - {caseData.Index} => throw new System.NotImplementedException(),"); + {caseData.Index} => true,"); } else { + var expression = ""; + + // Cannot use "is not null" pattern matching against non-generic value types + if (caseData.TypeInfo.IsAlwaysValueType && !caseData.TypeInfo.IsGeneric) + { + expression = $"As{caseData.Name}Unsafe != null"; + } + else + { + expression = $"As{caseData.Name}Unsafe is not null"; + } + Builder.Append($@" - {caseData.Index} => throw new System.NotImplementedException(),"); + {caseData.Index} => {expression},"); } } From 9b7af9d9811359a4c931c2c1cb612240a1424976 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 11:23:26 -0700 Subject: [PATCH 12/56] small update to tests --- Tests.Net11/Union.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 0c8fafd..59b7a6c 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -62,36 +62,28 @@ public void HasValue() [Fact] public void SimpleSwitch() { - IntOrStringOrOther w = 5; - - Assert.True(w switch + Assert.True(IntOrStringOrOther.Int(5) switch { int i => i == 5, string s => false, bool b => false, }); - IntOrStringOrOther x = "abc"; - - Assert.True(x switch + Assert.True(IntOrStringOrOther.String("abc") switch { int i => false, string s => s == "abc", bool b => false, }); - IntOrStringOrOther y = true; - - Assert.True(y switch + Assert.True(IntOrStringOrOther.Other(true) switch { int i => false, string s => false, bool b => b, }); - var z = IntOrStringOrOther.Other(4); - - Assert.True(z switch + Assert.True(IntOrStringOrOther.Other(4) switch { int i => i == 4, string s => false, From f86b39120f1e55cb2a310b80e609568ec76e57e4 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 11:54:56 -0700 Subject: [PATCH 13/56] start work on trygetvalue --- SumSharp.Generator/SymbolHandler.cs | 22 +++++++++++++++++- Tests.Net11/Union.cs | 35 ++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index b8e21b2..379c394 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1265,7 +1265,27 @@ public bool HasValue Builder.AppendLine($@" }}; }} - }} + }}"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + + } + else + { + var nonNullableTypeName = caseData.TypeInfo.Name.TrimEnd('?'); + + Builder.AppendLine($@" + public bool TryGetValue(out {nonNullableTypeName} value) + {{ + throw new System.NotImplementedException(); + }}"); + } + } + + Builder.AppendLine($@" public interface IUnionMembers {{ diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 59b7a6c..8ea4a54 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -13,6 +13,13 @@ partial class IntOrStringOrOther } + [UnionCase("Int", typeof(int?))] + [UnionCase("Bool", typeof(bool?))] + partial class NullableValueTypes + { + + } + [UnionCase("Some", "T")] [UnionCase("None")] partial class Optional @@ -48,6 +55,11 @@ public void HasValue() Assert.False(IntOrStringOrOther.Other(null).HasValue); Assert.False(IntOrStringOrOther.String(null!).HasValue); + Assert.True(NullableValueTypes.Int(1).HasValue); + Assert.True(NullableValueTypes.Bool(false).HasValue); + Assert.False(NullableValueTypes.Int(null).HasValue); + Assert.False(NullableValueTypes.Bool(null).HasValue); + Assert.True(Optional.Some(1).HasValue); Assert.True(Optional.Some(1).HasValue); Assert.True(Optional.Some("abc").HasValue); @@ -60,7 +72,7 @@ public void HasValue() [Fact] - public void SimpleSwitch() + public void Switch() { Assert.True(IntOrStringOrOther.Int(5) switch { @@ -88,5 +100,26 @@ public void SimpleSwitch() int i => i == 4, string s => false, }); + + Assert.True(IntOrStringOrOther.Other(null) switch + { + int i => false, + string s => false, + null => true, + }); + + Assert.True(NullableValueTypes.Int(null) switch + { + int i => false, + bool b => false, + null => true, + }); + + Assert.True(NullableValueTypes.Bool(null) switch + { + int i => false, + bool b => false, + null => true, + }); } } From a2d32cde273fa3bd4b868a97e50a9900eb236f28 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 15:19:37 -0700 Subject: [PATCH 14/56] add trygetvalue to interface --- SumSharp.Generator/SymbolHandler.cs | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 379c394..1acc3eb 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -21,6 +21,10 @@ public abstract class TypeInfo { public abstract string Name { get; } + public bool NullableAnnotation => Name.EndsWith("?"); + + public string NullableStrippedName => Name.TrimEnd('?'); + public abstract bool IsUnmanaged { get; } public abstract bool UseUnmanagedStorage { get; } @@ -1245,17 +1249,10 @@ public bool HasValue } else { - var expression = ""; - - // Cannot use "is not null" pattern matching against non-generic value types - if (caseData.TypeInfo.IsAlwaysValueType && !caseData.TypeInfo.IsGeneric) - { - expression = $"As{caseData.Name}Unsafe != null"; - } - else - { - expression = $"As{caseData.Name}Unsafe is not null"; - } + var expression = + caseData.TypeInfo.IsAlwaysValueType && !caseData.TypeInfo.NullableAnnotation ? + "true" : + $"As{caseData.Name}Unsafe is not null"; Builder.Append($@" {caseData.Index} => {expression},"); @@ -1275,12 +1272,12 @@ public bool HasValue } else { - var nonNullableTypeName = caseData.TypeInfo.Name.TrimEnd('?'); - - Builder.AppendLine($@" - public bool TryGetValue(out {nonNullableTypeName} value) + Builder.Append($@" + public bool TryGetValue(out {caseData.TypeInfo.NullableStrippedName} value) {{ - throw new System.NotImplementedException(); + throw new System.NotImplementedException();"); + + Builder.AppendLine($@" }}"); } } @@ -1289,7 +1286,9 @@ public bool TryGetValue(out {nonNullableTypeName} value) public interface IUnionMembers {{ - public object{Nullable} Value {{ get; }}"); + public object{Nullable} Value {{ get; }} + + public bool HasValue {{ get; }}"); foreach (var caseData in Cases) { @@ -1300,7 +1299,9 @@ public interface IUnionMembers else { Builder.AppendLine($@" - public static {Name} Create({caseData.TypeInfo.Name} value) => {Name}.{caseData.Name}(value);"); + public static {Name} Create({caseData.TypeInfo.Name} value) => {Name}.{caseData.Name}(value); + + public bool TryGetValue(out {caseData.TypeInfo.NullableStrippedName} value);"); } } From 49cf597ce6be7c5c1b154eae6a14fe65c67ff1a2 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 15:21:20 -0700 Subject: [PATCH 15/56] small change --- SumSharp.Generator/SymbolHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 1acc3eb..caf74e2 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1219,7 +1219,8 @@ public object{Nullable} Value { if (caseData.TypeInfo is null) { - + Builder.Append($@" + {caseData.Index} => throw new System.NotImplementedException(),"); } else { From c3b9337e2c7c2296038d2c913500fd7537f6d259 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sun, 2 Aug 2026 19:46:52 -0700 Subject: [PATCH 16/56] add trygetvalue generic stub and use it for equality --- SumSharp.Generator/SymbolHandler.cs | 105 ++++++++-------------------- 1 file changed, 31 insertions(+), 74 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index caf74e2..b7984eb 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -619,6 +619,8 @@ public string Emit() EmitUnmanagedStorageSize(); } + EmitTryGetValueGeneric(); + if (!DisableValueEquality) { EmitEquals(); @@ -866,6 +868,15 @@ public void EmitUnmanagedStorageSize() public static int UnmanagedStorageSize => _unmanagedStorageSize;"); } + + public void EmitTryGetValueGeneric() + { + Builder.AppendLine($@" + private bool TryGetValue(out TValue__ value) + {{ + throw new System.NotImplementedException(); + }}"); + } public void EmitEquals() { Builder.Append($@" @@ -947,86 +958,32 @@ public override int GetHashCode() foreach (var type in DistinctTypes) { - Builder.Append($@" + Builder.AppendLine($@" ///Compares a {XMLEscapedName} with a for equality using on the underlying value public static bool operator==({Name} left, {type.Name} right) {{ - return left.Index switch - {{"); - foreach (var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - Builder.Append($@" - {caseData.Index} => false,"); - - continue; - } - - switch (type.IsGeneric, caseData.TypeInfo.IsGeneric) - { - case (false, false): - - if (caseData.TypeInfo.IsAlwaysValueType) - { - Builder.Append($@" - {caseData.Index} => {(caseData.TypeInfo.Name == type.Name ? $"left.As{caseData.Name}Unsafe.Equals(right)" : "false")},"); - } - else - { - Builder.Append($@" - {caseData.Index} => {(caseData.TypeInfo.Name == type.Name ? $"left.As{caseData.Name}Unsafe is null ? right is null : left.As{caseData.Name}Unsafe.Equals(right)" : "false")},"); - } - - break; - case (false, true): - - if (type.IsAlwaysValueType) - { - Builder.Append($@" - {caseData.Index} => typeof({caseData.TypeInfo.Name}) == typeof({type.Name}) && left.As{caseData.Name}Unsafe{NullForgiving}.Equals(right),"); - } - else - { - Builder.Append($@" - {caseData.Index} => typeof({caseData.TypeInfo.Name}) == typeof({type.Name}) && (ReferenceEquals(null, left.As{caseData.Name}Unsafe) ? ReferenceEquals(null, right) : left.As{caseData.Name}Unsafe.Equals(right)),"); - } - - break; - case (true, false): - - Builder.Append($@" - {caseData.Index} => typeof({caseData.TypeInfo.Name}) == typeof({type.Name}) && left.As{caseData.Name}Unsafe.Equals(right),"); - - break; - - case (true, true): - { - var expression = new List(); - - if (caseData.TypeInfo.Name != type.Name) - { - expression.Add($"typeof({caseData.TypeInfo.Name}) == typeof({type.Name})"); - } - if (caseData.TypeInfo.IsAlwaysValueType || type.IsAlwaysValueType) - { - expression.Add($"left.As{caseData.Name}Unsafe{NullForgiving}.Equals(right)"); - } - else - { - expression.Add($"(ReferenceEquals(null, left.As{caseData.Name}Unsafe) ? ReferenceEquals(null, right) : left.As{caseData.Name}Unsafe.Equals(right))"); - } - - Builder.Append($@" - {caseData.Index} => {string.Join(" && ", expression)},"); + if (!left.TryGetValue<{type.Name}>(out var value)) + {{ + return false; + }}"); - } - break; - } + if (type.IsAlwaysValueType) + { + Builder.Append($@" + return value.Equals(right);"); + } + else if (type.IsAlwaysRefType) + { + Builder.Append($@" + return ReferenceEquals(null, value) ? ReferenceEquals(null, right) : value.Equals(right);"); + } + else + { + Builder.Append($@" + return typeof({type.Name}).IsValueType ? value{NullForgiving}.Equals(right) : (ReferenceEquals(null, value) ? ReferenceEquals(null, right) : value.Equals(right));"); } - Builder.AppendLine($@" - }}; + Builder.Append($@" }} ///Compares a with a {XMLEscapedName} for equality using on the underlying value public static bool operator==({type.Name} left, {Name} right) => right == left; From d8038d9205c4f9a37c3d05d9c1334fa999f64746 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 09:41:53 -0700 Subject: [PATCH 17/56] get trygetvaluegeneric working --- SumSharp.Generator/SymbolHandler.cs | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index b7984eb..4be979d 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -871,10 +871,41 @@ public void EmitUnmanagedStorageSize() public void EmitTryGetValueGeneric() { - Builder.AppendLine($@" + Builder.Append($@" private bool TryGetValue(out TValue__ value) {{ - throw new System.NotImplementedException(); + value = default!; + + switch (Index) + {{"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + Builder.Append($@" + case {caseData.Index}: break;"); + } + else + { + Builder.Append($@" + case {caseData.Index}: + if (typeof({caseData.TypeInfo.Name}) == typeof(TValue__)) + {{ + var temp = As{caseData.Name}Unsafe; + + value = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, TValue__>(ref System.Runtime.CompilerServices.Unsafe.AsRef(in temp)); + + return true; + }} + break;"); + } + } + + Builder.AppendLine($@" + }}; + + return false; }}"); } public void EmitEquals() From 8940e510e50cc4cae9e87942a27ac54d71c1fc25 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 10:07:56 -0700 Subject: [PATCH 18/56] get trygetvalue working --- SumSharp.Generator/SymbolHandler.cs | 52 +++++++++++++++++++++++++++-- Tests.Net11/Union.cs | 16 +++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 4be979d..7b46231 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1261,12 +1261,58 @@ public bool HasValue } else { - Builder.Append($@" + Builder.AppendLine($@" public bool TryGetValue(out {caseData.TypeInfo.NullableStrippedName} value) {{ - throw new System.NotImplementedException();"); + value = default!; - Builder.AppendLine($@" + if (!TryGetValue<{caseData.TypeInfo.Name}>(out var rawValue)) + {{ + return false; + }}"); + + if (caseData.TypeInfo.IsAlwaysValueType) + { + if (caseData.TypeInfo.NullableAnnotation) + { + Builder.Append($@" + if (!rawValue.HasValue) + {{ + return false; + }} + + value = rawValue.Value;"); + } + else + { + Builder.Append($@" + value = rawValue;"); + } + } + else if (caseData.TypeInfo.IsAlwaysRefType) + { + Builder.Append($@" + if (ReferenceEquals(null, rawValue)) + {{ + return false; + }} + + value = rawValue;"); + } + else + { + Builder.Append($@" + if (!typeof({caseData.TypeInfo.Name}).IsValueType && ReferenceEquals(null, rawValue)) + {{ + return false; + }} + + value = rawValue;"); + } + + Builder.AppendLine($@" + + return true; }}"); } } diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 8ea4a54..d3db87e 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -88,6 +88,14 @@ public void Switch() bool b => false, }); + Assert.True(IntOrStringOrOther.String(null!) switch + { + int i => false, + string s => false, + bool b => false, + null => true, + }); + Assert.True(IntOrStringOrOther.Other(true) switch { int i => false, @@ -108,6 +116,14 @@ public void Switch() null => true, }); + Assert.True(IntOrStringOrOther.Other(null!) switch + { + int i => false, + string s => false, + float[] => false, + null => true, + }); + Assert.True(NullableValueTypes.Int(null) switch { int i => false, From 33d28ed3362b57ebf5616f2a932b045ddc753c60 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 10:30:07 -0700 Subject: [PATCH 19/56] add test for createfailure --- SumSharp/CreateFailureException.cs | 22 ++++++++++++++++++++++ Tests.Net11/Union.cs | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 SumSharp/CreateFailureException.cs diff --git a/SumSharp/CreateFailureException.cs b/SumSharp/CreateFailureException.cs new file mode 100644 index 0000000..e16c9a3 --- /dev/null +++ b/SumSharp/CreateFailureException.cs @@ -0,0 +1,22 @@ +using System; + +namespace SumSharp; + +/// +/// Thrown when the a generated IUnionMembers.Create static factory method is called with a type that has more than +/// one matching case. +/// +/// The type of the case that failed to be constructed +/// The multiple cases that match the given type +public sealed class CreateFailureException(Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a case of type {caseType}. There are multiple candidate cases of this type") +{ + /// + /// The type of the case that failed to be constructed + /// + public Type CaseType => caseType; + + /// + /// The multiple cases that match the given type + /// + public string[] CandidateCaseNames => candidateCaseNames; +} diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index d3db87e..6eda6ed 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -17,7 +17,7 @@ partial class IntOrStringOrOther [UnionCase("Bool", typeof(bool?))] partial class NullableValueTypes { - + } [UnionCase("Some", "T")] @@ -27,6 +27,14 @@ partial class Optional } + [UnionCase("Case0", typeof(string))] + [UnionCase("Case1", typeof(string))] + [UnionCase("Case2", typeof(int))] + partial class RepeatedTypes + { + + } + /*[UnionCase("Case0", typeof(string))] [UnionCase("Case1", typeof(string))] partial class RepeatedCases @@ -138,4 +146,16 @@ public void Switch() null => true, }); } + + [Fact] + public void RepeatedTypesCreateFailure() + { + var ex = Assert.Throws(() => + { + RepeatedTypes x = "abc"; + }); + + Assert.Equal(typeof(string), ex.CaseType); + Assert.Equal(["Case0", "Case1"], ex.CandidateCaseNames); + } } From 900650737c445e8fc44810eccdcb986b3d47bfdd Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 11:27:57 -0700 Subject: [PATCH 20/56] make test pass --- SumSharp.Generator/SymbolHandler.cs | 105 +++++++++++++--------------- SumSharp/CreateFailureException.cs | 11 ++- Tests.Net11/Union.cs | 1 + 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 7b46231..b82c983 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -260,7 +260,8 @@ public CaseData(int index, string name, TypeInfo? typeInfo, bool storeAsObject, public CaseData[] UniqueCases { get; } - public TypeInfo[] DistinctTypes { get; } + // Cases grouped by type + public IGrouping[] CaseGroups { get; } public INamedTypeSymbol[] ContainingTypes; @@ -424,32 +425,21 @@ public SymbolHandler( }) .ToArray(); - var typeMap = new Dictionary(); - - foreach (var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - continue; - } - - typeMap[caseData.TypeInfo.Name] = caseData.TypeInfo; - } - - DistinctTypes = [..typeMap.Values]; + CaseGroups = + [..Cases + .Where(caseData => caseData.TypeInfo is not null) + .GroupBy(caseData => + caseData.TypeInfo!.IsTupleType ? + $"({string.Join(", ", caseData.TypeInfo.TupleTypeArgs)})" : // Removes custom field names + caseData.TypeInfo.Name)]; - if (storageStrategy == 0 && DistinctTypes.Length == 1 && !Cases.Any(caseData => caseData.StorageMode == 1)) + if (storageStrategy == 0 && CaseGroups.Length == 1 && !Cases.Any(caseData => caseData.StorageMode == 1)) { Cases = [.. Cases.Select(caseData => new CaseData(caseData.Index, caseData.Name, caseData.TypeInfo, false, caseData.StorageMode, FullUnmanagedStorageTypeName))]; } UniqueCases = - Cases - .Where(caseData => caseData.TypeInfo is not null) - .GroupBy(caseData => - caseData.TypeInfo!.IsTupleType ? - $"({string.Join(", ", caseData.TypeInfo.TupleTypeArgs)})" : // Removes custom field names - caseData.TypeInfo.Name) + CaseGroups .Where(group => group.Count() == 1) .SelectMany(group => group) .ToArray(); @@ -987,8 +977,10 @@ public override int GetHashCode() #if NET9_0_OR_GREATER"); } - foreach (var type in DistinctTypes) + foreach (var caseGroup in CaseGroups) { + var type = caseGroup.First().TypeInfo!; + Builder.AppendLine($@" ///Compares a {XMLEscapedName} with a for equality using on the underlying value public static bool operator==({Name} left, {type.Name} right) @@ -1253,68 +1245,63 @@ public bool HasValue }} }}"); - foreach (var caseData in Cases) + foreach (var caseGroup in CaseGroups) { - if (caseData.TypeInfo is null) - { + var typeInfo = caseGroup.First().TypeInfo!; - } - else - { - Builder.AppendLine($@" - public bool TryGetValue(out {caseData.TypeInfo.NullableStrippedName} value) + Builder.AppendLine($@" + public bool TryGetValue(out {typeInfo.NullableStrippedName} value) {{ value = default!; - if (!TryGetValue<{caseData.TypeInfo.Name}>(out var rawValue)) + if (!TryGetValue<{typeInfo.Name}>(out var rawValue)) {{ return false; }}"); - if (caseData.TypeInfo.IsAlwaysValueType) + if (typeInfo.IsAlwaysValueType) + { + if (typeInfo.NullableAnnotation) { - if (caseData.TypeInfo.NullableAnnotation) - { - Builder.Append($@" + Builder.Append($@" if (!rawValue.HasValue) {{ return false; }} value = rawValue.Value;"); - } - else - { - Builder.Append($@" - value = rawValue;"); - } } - else if (caseData.TypeInfo.IsAlwaysRefType) + else { Builder.Append($@" + value = rawValue;"); + } + } + else if (typeInfo.IsAlwaysRefType) + { + Builder.Append($@" if (ReferenceEquals(null, rawValue)) {{ return false; }} value = rawValue;"); - } - else - { - Builder.Append($@" - if (!typeof({caseData.TypeInfo.Name}).IsValueType && ReferenceEquals(null, rawValue)) + } + else + { + Builder.Append($@" + if (!typeof({typeInfo.Name}).IsValueType && ReferenceEquals(null, rawValue)) {{ return false; }} value = rawValue;"); - } + } - Builder.AppendLine($@" + Builder.AppendLine($@" return true; }}"); - } } Builder.AppendLine($@" @@ -1325,19 +1312,25 @@ public interface IUnionMembers public bool HasValue {{ get; }}"); - foreach (var caseData in Cases) + foreach (var caseGroup in CaseGroups) { - if (caseData.TypeInfo is null) + var firstCase = caseGroup.First(); + + if (caseGroup.Count() == 1) { - continue; + Builder.AppendLine($@" + public static {Name} Create({firstCase.TypeInfo!.Name} value) => {Name}.{firstCase.Name}(value);"); } else { - Builder.AppendLine($@" - public static {Name} Create({caseData.TypeInfo.Name} value) => {Name}.{caseData.Name}(value); + var candidateCases = caseGroup.Select(caseData => $"\"{caseData.Name}\""); - public bool TryGetValue(out {caseData.TypeInfo.NullableStrippedName} value);"); + Builder.AppendLine($@" + public static {Name} Create({firstCase.TypeInfo!.Name} value) => throw new global::SumSharp.CreateFailureException(typeof({Name}), typeof({firstCase.TypeInfo.Name}), [{string.Join(", ", candidateCases)}]);"); } + + Builder.AppendLine($@" + public bool TryGetValue(out {firstCase.TypeInfo!.NullableStrippedName} value);"); } Builder.AppendLine($@" diff --git a/SumSharp/CreateFailureException.cs b/SumSharp/CreateFailureException.cs index e16c9a3..60de2cb 100644 --- a/SumSharp/CreateFailureException.cs +++ b/SumSharp/CreateFailureException.cs @@ -3,13 +3,18 @@ namespace SumSharp; /// -/// Thrown when the a generated IUnionMembers.Create static factory method is called with a type that has more than -/// one matching case. +/// Thrown when the a generated IUnionMembers.Create static factory method is called with a type that has more than one matching case in the union /// +/// The type of the union that failed to be constructed /// The type of the case that failed to be constructed /// The multiple cases that match the given type -public sealed class CreateFailureException(Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a case of type {caseType}. There are multiple candidate cases of this type") +public sealed class CreateFailureException(Type unionType, Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a {unionType} with underlying type {caseType}. There are multiple candidate cases of this type") { + /// + /// The type of the union that failed to be constructed + /// + public Type UnionType => unionType; + /// /// The type of the case that failed to be constructed /// diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 6eda6ed..2a94056 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -155,6 +155,7 @@ public void RepeatedTypesCreateFailure() RepeatedTypes x = "abc"; }); + Assert.Equal(typeof(RepeatedTypes), ex.UnionType); Assert.Equal(typeof(string), ex.CaseType); Assert.Equal(["Case0", "Case1"], ex.CandidateCaseNames); } From f7f140f73572d0ab88ba317ee53f6382989fe7a1 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 11:54:43 -0700 Subject: [PATCH 21/56] add documentation to newly added functions --- SumSharp.Generator/SymbolHandler.cs | 17 ++++++++++++++--- ...reException.cs => AmbiguousCaseException.cs} | 2 +- Tests.Net11/Union.cs | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) rename SumSharp/{CreateFailureException.cs => AmbiguousCaseException.cs} (94%) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index b82c983..d5f0545 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1188,6 +1188,7 @@ public void EmitNativeUnion() { Builder.Append($@" #if NET11_0_OR_GREATER + ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed public object{Nullable} Value {{ get @@ -1214,6 +1215,7 @@ public object{Nullable} Value }} }} + ///True if the underlying value is not a null. False otherwise. If the active case is empty the value is considered to be not null public bool HasValue {{ get @@ -1250,6 +1252,9 @@ public bool HasValue var typeInfo = caseGroup.First().TypeInfo!; Builder.AppendLine($@" + ///Attempts to get a value of type from the union. Returns true if the union holds a non-null value of the type. + ///Returns false otherwise. + ///An out parameter that will be set to the underlying value, if present. public bool TryGetValue(out {typeInfo.NullableStrippedName} value) {{ value = default!; @@ -1316,21 +1321,27 @@ public interface IUnionMembers { var firstCase = caseGroup.First(); + var typeInfo = firstCase.TypeInfo!; + if (caseGroup.Count() == 1) { Builder.AppendLine($@" - public static {Name} Create({firstCase.TypeInfo!.Name} value) => {Name}.{firstCase.Name}(value);"); + ///Creates a That holds a value of type by invoking the case constructor + public static {Name} Create({typeInfo.Name} value) => {Name}.{firstCase.Name}(value);"); } else { var candidateCases = caseGroup.Select(caseData => $"\"{caseData.Name}\""); Builder.AppendLine($@" - public static {Name} Create({firstCase.TypeInfo!.Name} value) => throw new global::SumSharp.CreateFailureException(typeof({Name}), typeof({firstCase.TypeInfo.Name}), [{string.Join(", ", candidateCases)}]);"); + ///Always throws a . This method exists to satisfy the compiler's requirements that a static Create method exist for + /// each type the union can hold. There are multiple cases ({string.Join(", ", candidateCases)}) that can hold a value of type . + /// Use the appropriate case constructor directly rather than relying on a compiler provided conversion. + public static {Name} Create({typeInfo.Name} value) => throw new global::SumSharp.AmbiguousCaseException(typeof({Name}), typeof({typeInfo.Name}), [{string.Join(", ", candidateCases)}]);"); } Builder.AppendLine($@" - public bool TryGetValue(out {firstCase.TypeInfo!.NullableStrippedName} value);"); + public bool TryGetValue(out {typeInfo.NullableStrippedName} value);"); } Builder.AppendLine($@" diff --git a/SumSharp/CreateFailureException.cs b/SumSharp/AmbiguousCaseException.cs similarity index 94% rename from SumSharp/CreateFailureException.cs rename to SumSharp/AmbiguousCaseException.cs index 60de2cb..05f094b 100644 --- a/SumSharp/CreateFailureException.cs +++ b/SumSharp/AmbiguousCaseException.cs @@ -8,7 +8,7 @@ namespace SumSharp; /// The type of the union that failed to be constructed /// The type of the case that failed to be constructed /// The multiple cases that match the given type -public sealed class CreateFailureException(Type unionType, Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a {unionType} with underlying type {caseType}. There are multiple candidate cases of this type") +public sealed class AmbiguousCaseException(Type unionType, Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a {unionType} with underlying type {caseType}. There are multiple candidate cases of this type") { /// /// The type of the union that failed to be constructed diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 2a94056..c8c6208 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -148,9 +148,9 @@ public void Switch() } [Fact] - public void RepeatedTypesCreateFailure() + public void RepeatedTypesAmbiguousCaseException() { - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => { RepeatedTypes x = "abc"; }); From 6e582de95b1bae031af326db1185ca93d2ab19be Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Mon, 3 Aug 2026 11:57:25 -0700 Subject: [PATCH 22/56] add test for switch on repeated types --- Tests.Net11/Union.cs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index c8c6208..fe51ac7 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -147,6 +147,44 @@ public void Switch() }); } + [Fact] + public void RepeatedTypesSwitch() + { + Assert.True(RepeatedTypes.Case0("abc") switch + { + string s => s == "abc", + int i => false, + null => false, + }); + + Assert.True(RepeatedTypes.Case0(null!) switch + { + string s => false, + int i => false, + null => true, + }); + + Assert.True(RepeatedTypes.Case1("abc") switch + { + string s => s == "abc", + int i => false, + null => false, + }); + + Assert.True(RepeatedTypes.Case1(null!) switch + { + string s => false, + int i => false, + null => true, + }); + + Assert.True(RepeatedTypes.Case2(4) switch + { + string s => false, + int i => i == 4, + }); + } + [Fact] public void RepeatedTypesAmbiguousCaseException() { From c74ad10a5a5263ccc6138f90ae7097ed83340c91 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 15:43:08 -0700 Subject: [PATCH 23/56] simplify trygetvalue<> --- SumSharp.Generator/SymbolHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index d5f0545..e574448 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -884,7 +884,7 @@ private bool TryGetValue(out TValue__ value) {{ var temp = As{caseData.Name}Unsafe; - value = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, TValue__>(ref System.Runtime.CompilerServices.Unsafe.AsRef(in temp)); + value = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, TValue__>(ref temp); return true; }} From d1f56be0c4450a3bb130886f32e5c175cc6f5754 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 16:11:27 -0700 Subject: [PATCH 24/56] add partial struct declarations for empty cases --- SumSharp.Generator/SymbolHandler.cs | 17 +++++++++++++++-- Tests.Net11/Union.cs | 24 ++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index e574448..829f2ba 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -769,9 +769,22 @@ private void EmitFieldsAndConstructor() #endif"; } - Builder.Append($@" + Builder.AppendLine($@" #if NET11_0_OR_GREATER -[System.Runtime.CompilerServices.Union] +[System.Runtime.CompilerServices.Union]"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is not null) + { + continue; + } + + Builder.AppendLine($@" +public partial record struct {caseData.Name}"); + } + + Builder.Append($@" #endif {Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name}{interfaces} {{"); diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index fe51ac7..17333ba 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -35,12 +35,21 @@ partial class RepeatedTypes } - /*[UnionCase("Case0", typeof(string))] - [UnionCase("Case1", typeof(string))] - partial class RepeatedCases + [UnionCase("Case0", typeof(int[]))] + [UnionCase("Case1")] + [UnionCase("Case2")] + partial class EmptyCases1 + { + + } + + [UnionCase("Case0")] + [UnionCase("Case1")] + [UnionCase("Case2", typeof(int[]))] + partial class EmptyCases2 { - }*/ + } [Fact] public void Value() @@ -49,6 +58,11 @@ public void Value() Assert.Equal("abc", IntOrStringOrOther.String("abc").Value); Assert.Equal(true, IntOrStringOrOther.Other(true).Value); Assert.Equal(4, IntOrStringOrOther.Other(4).Value); + Assert.Equal(new None(), Optional.None.Value); + Assert.Equal(new Case1(), EmptyCases1.Case1.Value); + Assert.Equal(new Case2(), EmptyCases1.Case2.Value); + Assert.Equal(new Case0(), EmptyCases2.Case0.Value); + Assert.Equal(new Case1(), EmptyCases2.Case1.Value); } [Fact] @@ -76,6 +90,8 @@ public void HasValue() Assert.False(Optional.Some(null!).HasValue); Assert.True(Optional.None.HasValue); + Assert.True(EmptyCases.Case1.HasValue); + Assert.True(EmptyCases.Case2.HasValue); } From 608223bb0e72ba99f8790ebce0ae3bcb1468df7f Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 16:29:37 -0700 Subject: [PATCH 25/56] get empty case value implemented --- SumSharp.Generator/SymbolHandler.cs | 30 ++++++++++++++++++++--------- Tests.Net11/Union.cs | 6 ++++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 829f2ba..eaf321d 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using static SumSharp.Generator.SymbolHandler; @@ -260,6 +261,9 @@ public CaseData(int index, string name, TypeInfo? typeInfo, bool storeAsObject, public CaseData[] UniqueCases { get; } + public CaseData[] EmptyCases { get; } + + // Cases grouped by type public IGrouping[] CaseGroups { get; } @@ -444,6 +448,8 @@ public SymbolHandler( .SelectMany(group => group) .ToArray(); + EmptyCases = [..Cases.Where(caseData => caseData.TypeInfo is null)]; + var enableJsonSerializationData = symbol! .GetAttributes() @@ -770,9 +776,7 @@ private void EmitFieldsAndConstructor() } Builder.AppendLine($@" -#if NET11_0_OR_GREATER -[System.Runtime.CompilerServices.Union]"); - +#if NET11_0_OR_GREATER"); foreach (var caseData in Cases) { if (caseData.TypeInfo is not null) @@ -781,10 +785,11 @@ private void EmitFieldsAndConstructor() } Builder.AppendLine($@" -public partial record struct {caseData.Name}"); +public partial record struct {caseData.Name};"); } Builder.Append($@" +[System.Runtime.CompilerServices.Union] #endif {Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name}{interfaces} {{"); @@ -1214,7 +1219,7 @@ public object{Nullable} Value if (caseData.TypeInfo is null) { Builder.Append($@" - {caseData.Index} => throw new System.NotImplementedException(),"); + {caseData.Index} => new {caseData.Name}(),"); } else { @@ -1339,7 +1344,7 @@ public interface IUnionMembers if (caseGroup.Count() == 1) { Builder.AppendLine($@" - ///Creates a That holds a value of type by invoking the case constructor + ///Creates a that holds a value of type by invoking the case constructor public static {Name} Create({typeInfo.Name} value) => {Name}.{firstCase.Name}(value);"); } else @@ -1347,9 +1352,9 @@ public interface IUnionMembers var candidateCases = caseGroup.Select(caseData => $"\"{caseData.Name}\""); Builder.AppendLine($@" - ///Always throws a . This method exists to satisfy the compiler's requirements that a static Create method exist for - /// each type the union can hold. There are multiple cases ({string.Join(", ", candidateCases)}) that can hold a value of type . - /// Use the appropriate case constructor directly rather than relying on a compiler provided conversion. + ///Always throws a . This method exists to satisfy the compiler's requirements for .NET 11 union types. There are multiple + ///cases ({string.Join(", ", candidateCases)}) that can hold a value of type . Use the appropriate case constructor directly rather than relying on a compiler + ///provided conversion. public static {Name} Create({typeInfo.Name} value) => throw new global::SumSharp.AmbiguousCaseException(typeof({Name}), typeof({typeInfo.Name}), [{string.Join(", ", candidateCases)}]);"); } @@ -1357,6 +1362,13 @@ public interface IUnionMembers public bool TryGetValue(out {typeInfo.NullableStrippedName} value);"); } + foreach (var caseData in EmptyCases) + { + Builder.AppendLine($@" + ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types + public static {Name} Create({caseData.Name} _) => {Name}.{caseData.Name};"); + } + Builder.AppendLine($@" }} #endif"); diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 17333ba..4593b2d 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -90,8 +90,10 @@ public void HasValue() Assert.False(Optional.Some(null!).HasValue); Assert.True(Optional.None.HasValue); - Assert.True(EmptyCases.Case1.HasValue); - Assert.True(EmptyCases.Case2.HasValue); + Assert.True(EmptyCases1.Case1.HasValue); + Assert.True(EmptyCases1.Case2.HasValue); + Assert.True(EmptyCases2.Case0.HasValue); + Assert.True(EmptyCases2.Case1.HasValue); } From b984881f1b80be062f254d06fe83321fd04cad5d Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 16:45:42 -0700 Subject: [PATCH 26/56] add trygetvalue to interface for empty cases --- SumSharp.Generator/SymbolHandler.cs | 14 ++++++- Tests.Net11/Union.cs | 58 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index eaf321d..0818026 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1327,6 +1327,16 @@ public bool TryGetValue(out {typeInfo.NullableStrippedName} value) }}"); } + foreach (var caseData in EmptyCases) + { + Builder.AppendLine($@" + ///Returns true if the union is . Returns false otherwise. The parameter is always set to default + public bool TryGetValue(out {caseData.Name} value) + {{ + throw new System.NotImplementedException(); + }}"); + } + Builder.AppendLine($@" public interface IUnionMembers @@ -1366,7 +1376,9 @@ public interface IUnionMembers { Builder.AppendLine($@" ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({caseData.Name} _) => {Name}.{caseData.Name};"); + public static {Name} Create({caseData.Name} _) => {Name}.{caseData.Name}; + + public bool TryGetValue(out {caseData.Name} value);"); } Builder.AppendLine($@" diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 4593b2d..f829b3d 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -215,4 +215,62 @@ public void RepeatedTypesAmbiguousCaseException() Assert.Equal(typeof(string), ex.CaseType); Assert.Equal(["Case0", "Case1"], ex.CandidateCaseNames); } + + [Fact] + public void EmptyTypesSwitch() + { + Assert.True(Optional.Some("abc") switch + { + string s => true, + None => false, + }); + + Assert.True(Optional.None switch + { + string s => false, + None => true, + }); + + Assert.True(EmptyCases1.Case0([0]) switch + { + int[] ints => ints.Single() == 0, + Case1 => false, + Case2 => false, + }); + + Assert.True(EmptyCases1.Case1 switch + { + int[] ints => false, + Case1 => true, + Case2 => false, + }); + + Assert.True(EmptyCases1.Case2 switch + { + int[] ints => false, + Case1 => false, + Case2 => true, + }); + + Assert.True(EmptyCases2.Case0 switch + { + Case0 => true, + Case1 => false, + int[] ints => false, + }); + + Assert.True(EmptyCases2.Case1 switch + { + Case0 => false, + Case1 => true, + int[] ints => false, + }); + + Assert.True(EmptyCases2.Case2([1]) switch + { + Case0 => false, + Case1 => false, + int[] ints => ints.Single() == 1, + }); + } } From 4e3c2d5fe581d2c26f731a1a24776416ef9dbf7b Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 16:52:25 -0700 Subject: [PATCH 27/56] fix docs and make test pass --- SumSharp.Generator/SymbolHandler.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 0818026..f083662 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1206,7 +1206,6 @@ public void EmitNativeUnion() { Builder.Append($@" #if NET11_0_OR_GREATER - ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed public object{Nullable} Value {{ get @@ -1233,7 +1232,6 @@ public object{Nullable} Value }} }} - ///True if the underlying value is not a null. False otherwise. If the active case is empty the value is considered to be not null public bool HasValue {{ get @@ -1270,9 +1268,6 @@ public bool HasValue var typeInfo = caseGroup.First().TypeInfo!; Builder.AppendLine($@" - ///Attempts to get a value of type from the union. Returns true if the union holds a non-null value of the type. - ///Returns false otherwise. - ///An out parameter that will be set to the underlying value, if present. public bool TryGetValue(out {typeInfo.NullableStrippedName} value) {{ value = default!; @@ -1329,20 +1324,23 @@ public bool TryGetValue(out {typeInfo.NullableStrippedName} value) foreach (var caseData in EmptyCases) { - Builder.AppendLine($@" - ///Returns true if the union is . Returns false otherwise. The parameter is always set to default - public bool TryGetValue(out {caseData.Name} value) - {{ - throw new System.NotImplementedException(); - }}"); + Builder.Append($@" + public bool TryGetValue(out {caseData.Name} value) + {{ + value = default; + + return Index == {caseData.Index}; + }}"); } Builder.AppendLine($@" public interface IUnionMembers {{ + ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed public object{Nullable} Value {{ get; }} + ///True if the underlying value is not a null. False otherwise. If the active case is empty the value is considered to be not null public bool HasValue {{ get; }}"); foreach (var caseGroup in CaseGroups) @@ -1369,6 +1367,9 @@ public interface IUnionMembers } Builder.AppendLine($@" + ///Attempts to get a value of type from the union. Returns true if the union holds a non-null value of the type. + ///Returns false otherwise. + ///An out parameter that will be set to the underlying value, if present. public bool TryGetValue(out {typeInfo.NullableStrippedName} value);"); } @@ -1378,6 +1379,7 @@ public interface IUnionMembers ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types public static {Name} Create({caseData.Name} _) => {Name}.{caseData.Name}; + ///Returns true if the union is . Returns false otherwise. The parameter is always set to default public bool TryGetValue(out {caseData.Name} value);"); } From 59241b16a0e8ae2e3caff46b6aff832655d66e39 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 17:00:21 -0700 Subject: [PATCH 28/56] add documentation to empty structs --- SumSharp.Generator/SymbolHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index f083662..cd51f3a 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -785,6 +785,7 @@ private void EmitFieldsAndConstructor() } Builder.AppendLine($@" +///Used to implement .NET 11 union requirements for {XMLEscapedName}. Use this type when pattern matching using C#'s built-in switch statement public partial record struct {caseData.Name};"); } From 0be624c4667504cf82415d2df44bb7712f2555ed Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 17:01:01 -0700 Subject: [PATCH 29/56] update docs again --- SumSharp.Generator/SymbolHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index cd51f3a..1361e70 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -785,7 +785,7 @@ private void EmitFieldsAndConstructor() } Builder.AppendLine($@" -///Used to implement .NET 11 union requirements for {XMLEscapedName}. Use this type when pattern matching using C#'s built-in switch statement +///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement public partial record struct {caseData.Name};"); } From c44693b183510ba3ee4a7d7d93bb21f04ab2a39a Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 17:14:39 -0700 Subject: [PATCH 30/56] small improvement to equals implementation --- SumSharp.Generator/SymbolHandler.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 1361e70..6f4af11 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -924,6 +924,7 @@ public void EmitEquals() public bool Equals({Name}{NullableIfRef} other) {{ {(IsStruct ? "" : "if (other is null) return false;")} + {(IsStruct ? "" : "if (ReferenceEquals(this, other)) return true;")} if (Index != other.Index) return false; return Index switch @@ -1025,8 +1026,9 @@ public override int GetHashCode() return typeof({type.Name}).IsValueType ? value{NullForgiving}.Equals(right) : (ReferenceEquals(null, value) ? ReferenceEquals(null, right) : value.Equals(right));"); } - Builder.Append($@" + Builder.AppendLine($@" }} + ///Compares a with a {XMLEscapedName} for equality using on the underlying value public static bool operator==({type.Name} left, {Name} right) => right == left; From d82f07931688400572c46080394615b15231e7a3 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Tue, 4 Aug 2026 17:18:14 -0700 Subject: [PATCH 31/56] optional -> option --- Tests.Net11/Union.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index f829b3d..35cb7b0 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -22,7 +22,7 @@ partial class NullableValueTypes [UnionCase("Some", "T")] [UnionCase("None")] - partial class Optional + partial class Option { } @@ -58,7 +58,7 @@ public void Value() Assert.Equal("abc", IntOrStringOrOther.String("abc").Value); Assert.Equal(true, IntOrStringOrOther.Other(true).Value); Assert.Equal(4, IntOrStringOrOther.Other(4).Value); - Assert.Equal(new None(), Optional.None.Value); + Assert.Equal(new None(), Option.None.Value); Assert.Equal(new Case1(), EmptyCases1.Case1.Value); Assert.Equal(new Case2(), EmptyCases1.Case2.Value); Assert.Equal(new Case0(), EmptyCases2.Case0.Value); @@ -82,14 +82,14 @@ public void HasValue() Assert.False(NullableValueTypes.Int(null).HasValue); Assert.False(NullableValueTypes.Bool(null).HasValue); - Assert.True(Optional.Some(1).HasValue); - Assert.True(Optional.Some(1).HasValue); - Assert.True(Optional.Some("abc").HasValue); + Assert.True(Option.Some(1).HasValue); + Assert.True(Option.Some(1).HasValue); + Assert.True(Option.Some("abc").HasValue); - Assert.False(Optional.Some(null).HasValue); - Assert.False(Optional.Some(null!).HasValue); + Assert.False(Option.Some(null).HasValue); + Assert.False(Option.Some(null!).HasValue); - Assert.True(Optional.None.HasValue); + Assert.True(Option.None.HasValue); Assert.True(EmptyCases1.Case1.HasValue); Assert.True(EmptyCases1.Case2.HasValue); Assert.True(EmptyCases2.Case0.HasValue); @@ -219,13 +219,13 @@ public void RepeatedTypesAmbiguousCaseException() [Fact] public void EmptyTypesSwitch() { - Assert.True(Optional.Some("abc") switch + Assert.True(Option.Some("abc") switch { string s => true, None => false, }); - Assert.True(Optional.None switch + Assert.True(Option.None switch { string s => false, None => true, From 43f308f8442f63a25e8d4934fa8ab3f2d1e26f19 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 08:16:24 -0700 Subject: [PATCH 32/56] start work on adding structs for all cases [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 26 ++++- Tests.Net11/Union.cs | 147 +++++++++------------------- 2 files changed, 65 insertions(+), 108 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 6f4af11..fd637a6 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -32,6 +32,8 @@ public abstract class TypeInfo public abstract bool IsGeneric { get; } + public abstract string[] TypeArguments { get; } + public abstract bool IsAlwaysValueType { get; } public abstract bool IsAlwaysRefType { get; } @@ -56,6 +58,8 @@ public class NonArray(INamedTypeSymbol symbol) : TypeInfo public override bool IsGeneric => false; + public override string[] TypeArguments => []; + public override bool IsAlwaysValueType => symbol.IsValueType; public override bool IsAlwaysRefType => symbol.IsReferenceType; @@ -79,6 +83,8 @@ public class Array(IArrayTypeSymbol symbol) : TypeInfo public override bool IsGeneric => false; + public override string[] TypeArguments => []; + public override bool IsAlwaysValueType => false; public override bool IsAlwaysRefType => true; @@ -98,6 +104,8 @@ public class SimpleGenericTypeArgument(ITypeParameterSymbol symbol, bool useUnma public override bool IsGeneric => true; + public override string[] TypeArguments => [Name]; + public override bool IsAlwaysValueType => symbol.HasValueTypeConstraint || symbol.HasUnmanagedTypeConstraint; public override bool IsAlwaysRefType => symbol.HasReferenceTypeConstraint; @@ -173,6 +181,8 @@ static string RemoveFieldName(string input) public override bool IsGeneric => true; + public override string[] TypeArguments => []; + public override bool IsAlwaysValueType => ((genericTypeInfo & 1) == 0 && !isInterface) || IsUnmanaged || IsTupleType; public override bool IsAlwaysRefType => ((genericTypeInfo & 2) == 0 || isInterface) && !IsUnmanaged && !IsTupleType; @@ -779,14 +789,20 @@ private void EmitFieldsAndConstructor() #if NET11_0_OR_GREATER"); foreach (var caseData in Cases) { - if (caseData.TypeInfo is not null) + if (caseData.TypeInfo is null) { - continue; + Builder.AppendLine($@" + ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement + public readonly partial record struct {caseData.Name};"); } + else + { + var typeArguments = string.Join(", ", caseData.TypeInfo.TypeArguments.Intersect(TypeArguments)); - Builder.AppendLine($@" -///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement -public partial record struct {caseData.Name};"); + Builder.AppendLine($@" + ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement + public readonly record struct {caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}({caseData.TypeInfo.Name} Value);"); + } } Builder.Append($@" diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 35cb7b0..9aa01c9 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -13,7 +13,7 @@ partial class IntOrStringOrOther } - [UnionCase("Int", typeof(int?))] + [UnionCase("NullableInt", typeof(int?))] [UnionCase("Bool", typeof(bool?))] partial class NullableValueTypes { @@ -27,25 +27,17 @@ partial class Option } - [UnionCase("Case0", typeof(string))] - [UnionCase("Case1", typeof(string))] - [UnionCase("Case2", typeof(int))] - partial class RepeatedTypes - { - - } - - [UnionCase("Case0", typeof(int[]))] - [UnionCase("Case1")] - [UnionCase("Case2")] + [UnionCase("IntArray", typeof(int[]))] + [UnionCase("EmptyCase1")] + [UnionCase("EmptyCase2")] partial class EmptyCases1 { } - [UnionCase("Case0")] - [UnionCase("Case1")] - [UnionCase("Case2", typeof(int[]))] + [UnionCase("EmptyCase0")] + [UnionCase("EmptyCase1")] + [UnionCase("FloatArray", typeof(float[]))] partial class EmptyCases2 { @@ -59,10 +51,10 @@ public void Value() Assert.Equal(true, IntOrStringOrOther.Other(true).Value); Assert.Equal(4, IntOrStringOrOther.Other(4).Value); Assert.Equal(new None(), Option.None.Value); - Assert.Equal(new Case1(), EmptyCases1.Case1.Value); - Assert.Equal(new Case2(), EmptyCases1.Case2.Value); - Assert.Equal(new Case0(), EmptyCases2.Case0.Value); - Assert.Equal(new Case1(), EmptyCases2.Case1.Value); + Assert.Equal(new EmptyCase1(), EmptyCases1.EmptyCase1.Value); + Assert.Equal(new EmptyCase2(), EmptyCases1.EmptyCase2.Value); + Assert.Equal(new EmptyCase0(), EmptyCases2.EmptyCase0.Value); + Assert.Equal(new EmptyCase1(), EmptyCases2.EmptyCase1.Value); } [Fact] @@ -74,26 +66,26 @@ public void HasValue() Assert.True(IntOrStringOrOther.Other(4).HasValue); Assert.True(IntOrStringOrOther.Other(3).HasValue); - Assert.False(IntOrStringOrOther.Other(null).HasValue); - Assert.False(IntOrStringOrOther.String(null!).HasValue); + Assert.True(IntOrStringOrOther.Other(null).HasValue); + Assert.True(IntOrStringOrOther.String(null!).HasValue); - Assert.True(NullableValueTypes.Int(1).HasValue); + Assert.True(NullableValueTypes.NullableInt(1).HasValue); Assert.True(NullableValueTypes.Bool(false).HasValue); - Assert.False(NullableValueTypes.Int(null).HasValue); - Assert.False(NullableValueTypes.Bool(null).HasValue); + Assert.True(NullableValueTypes.NullableInt(null).HasValue); + Assert.True(NullableValueTypes.Bool(null).HasValue); Assert.True(Option.Some(1).HasValue); Assert.True(Option.Some(1).HasValue); Assert.True(Option.Some("abc").HasValue); - Assert.False(Option.Some(null).HasValue); - Assert.False(Option.Some(null!).HasValue); + Assert.True(Option.Some(null).HasValue); + Assert.True(Option.Some(null!).HasValue); Assert.True(Option.None.HasValue); - Assert.True(EmptyCases1.Case1.HasValue); - Assert.True(EmptyCases1.Case2.HasValue); - Assert.True(EmptyCases2.Case0.HasValue); - Assert.True(EmptyCases2.Case1.HasValue); + Assert.True(EmptyCases1.EmptyCase1.HasValue); + Assert.True(EmptyCases1.EmptyCase2.HasValue); + Assert.True(EmptyCases2.EmptyCase0.HasValue); + Assert.True(EmptyCases2.EmptyCase1.HasValue); } @@ -150,7 +142,7 @@ public void Switch() null => true, }); - Assert.True(NullableValueTypes.Int(null) switch + Assert.True(NullableValueTypes.NullableInt(null) switch { int i => false, bool b => false, @@ -165,57 +157,6 @@ public void Switch() }); } - [Fact] - public void RepeatedTypesSwitch() - { - Assert.True(RepeatedTypes.Case0("abc") switch - { - string s => s == "abc", - int i => false, - null => false, - }); - - Assert.True(RepeatedTypes.Case0(null!) switch - { - string s => false, - int i => false, - null => true, - }); - - Assert.True(RepeatedTypes.Case1("abc") switch - { - string s => s == "abc", - int i => false, - null => false, - }); - - Assert.True(RepeatedTypes.Case1(null!) switch - { - string s => false, - int i => false, - null => true, - }); - - Assert.True(RepeatedTypes.Case2(4) switch - { - string s => false, - int i => i == 4, - }); - } - - [Fact] - public void RepeatedTypesAmbiguousCaseException() - { - var ex = Assert.Throws(() => - { - RepeatedTypes x = "abc"; - }); - - Assert.Equal(typeof(RepeatedTypes), ex.UnionType); - Assert.Equal(typeof(string), ex.CaseType); - Assert.Equal(["Case0", "Case1"], ex.CandidateCaseNames); - } - [Fact] public void EmptyTypesSwitch() { @@ -231,46 +172,46 @@ public void EmptyTypesSwitch() None => true, }); - Assert.True(EmptyCases1.Case0([0]) switch + Assert.True(EmptyCases1.IntArray([0]) switch { int[] ints => ints.Single() == 0, - Case1 => false, - Case2 => false, + EmptyCase1 => false, + EmptyCase2 => false, }); - Assert.True(EmptyCases1.Case1 switch + Assert.True(EmptyCases1.EmptyCase1 switch { int[] ints => false, - Case1 => true, - Case2 => false, + EmptyCase1 => true, + EmptyCase2 => false, }); - Assert.True(EmptyCases1.Case2 switch + Assert.True(EmptyCases1.EmptyCase2 switch { int[] ints => false, - Case1 => false, - Case2 => true, + EmptyCase1 => false, + EmptyCase2 => true, }); - Assert.True(EmptyCases2.Case0 switch + Assert.True(EmptyCases2.EmptyCase0 switch { - Case0 => true, - Case1 => false, - int[] ints => false, + EmptyCase0 => true, + EmptyCase1 => false, + float[] floats => false, }); - Assert.True(EmptyCases2.Case1 switch + Assert.True(EmptyCases2.EmptyCase1 switch { - Case0 => false, - Case1 => true, - int[] ints => false, + EmptyCase0 => false, + EmptyCase1 => true, + float[] floats => false, }); - Assert.True(EmptyCases2.Case2([1]) switch + Assert.True(EmptyCases2.FloatArray([1.1f]) switch { - Case0 => false, - Case1 => false, - int[] ints => ints.Single() == 1, + EmptyCase0 => false, + EmptyCase1 => false, + float[] floats => floats.Single() == 1.1f, }); } } From cdd5d56072cad2b684188b22e0fce55f8b05d8b6 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 08:31:16 -0700 Subject: [PATCH 33/56] get value and hasvalue implemented [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 52 +++++++++-------------------- Tests.Net11/Union.cs | 8 ++--- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index fd637a6..eda5e71 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using static SumSharp.Generator.SymbolHandler; namespace SumSharp.Generator; @@ -273,6 +271,7 @@ public CaseData(int index, string name, TypeInfo? typeInfo, bool storeAsObject, public CaseData[] EmptyCases { get; } + public Dictionary Net11StructNameMap { get; } // Cases grouped by type public IGrouping[] CaseGroups { get; } @@ -460,6 +459,18 @@ public SymbolHandler( EmptyCases = [..Cases.Where(caseData => caseData.TypeInfo is null)]; + Net11StructNameMap = Cases.ToDictionary(caseData => caseData, caseData => + { + if (caseData.TypeInfo is null) + { + return caseData.Name; + } + + var typeArguments = string.Join(", ", caseData.TypeInfo.TypeArguments.Intersect(TypeArguments)); + + return $"{caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}"; + }); + var enableJsonSerializationData = symbol! .GetAttributes() @@ -797,11 +808,9 @@ private void EmitFieldsAndConstructor() } else { - var typeArguments = string.Join(", ", caseData.TypeInfo.TypeArguments.Intersect(TypeArguments)); - Builder.AppendLine($@" ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement - public readonly record struct {caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}({caseData.TypeInfo.Name} Value);"); + public readonly record struct {Net11StructNameMap[caseData]}({caseData.TypeInfo.Name} Value);"); } } @@ -1242,7 +1251,7 @@ public object{Nullable} Value else { Builder.Append($@" - {caseData.Index} => As{caseData.Name}Unsafe,"); + {caseData.Index} => new {Net11StructNameMap[caseData]}(As{caseData.Name}Unsafe),"); } } @@ -1251,36 +1260,7 @@ public object{Nullable} Value }} }} - public bool HasValue - {{ - get - {{ - return Index switch - {{"); - - foreach(var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - Builder.Append($@" - {caseData.Index} => true,"); - } - else - { - var expression = - caseData.TypeInfo.IsAlwaysValueType && !caseData.TypeInfo.NullableAnnotation ? - "true" : - $"As{caseData.Name}Unsafe is not null"; - - Builder.Append($@" - {caseData.Index} => {expression},"); - } - } - - Builder.AppendLine($@" - }}; - }} - }}"); + public bool HasValue => true;"); foreach (var caseGroup in CaseGroups) { diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 9aa01c9..e1b4563 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -46,10 +46,10 @@ partial class EmptyCases2 [Fact] public void Value() { - Assert.Equal(5, IntOrStringOrOther.Int(5).Value); - Assert.Equal("abc", IntOrStringOrOther.String("abc").Value); - Assert.Equal(true, IntOrStringOrOther.Other(true).Value); - Assert.Equal(4, IntOrStringOrOther.Other(4).Value); + Assert.Equal(new Int(5), IntOrStringOrOther.Int(5).Value); + Assert.Equal(new String("abc"), IntOrStringOrOther.String("abc").Value); + Assert.Equal(new Other(true), IntOrStringOrOther.Other(true).Value); + Assert.Equal(new Other(4), IntOrStringOrOther.Other(4).Value); Assert.Equal(new None(), Option.None.Value); Assert.Equal(new EmptyCase1(), EmptyCases1.EmptyCase1.Value); Assert.Equal(new EmptyCase2(), EmptyCases1.EmptyCase2.Value); From 48c7006b8fc1d906a4ae86f3608f2634722f5671 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 09:48:25 -0700 Subject: [PATCH 34/56] get case struct names in a different way --- SumSharp.Generator/SymbolHandler.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index eda5e71..f9389f2 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -30,8 +30,6 @@ public abstract class TypeInfo public abstract bool IsGeneric { get; } - public abstract string[] TypeArguments { get; } - public abstract bool IsAlwaysValueType { get; } public abstract bool IsAlwaysRefType { get; } @@ -56,8 +54,6 @@ public class NonArray(INamedTypeSymbol symbol) : TypeInfo public override bool IsGeneric => false; - public override string[] TypeArguments => []; - public override bool IsAlwaysValueType => symbol.IsValueType; public override bool IsAlwaysRefType => symbol.IsReferenceType; @@ -81,8 +77,6 @@ public class Array(IArrayTypeSymbol symbol) : TypeInfo public override bool IsGeneric => false; - public override string[] TypeArguments => []; - public override bool IsAlwaysValueType => false; public override bool IsAlwaysRefType => true; @@ -102,8 +96,6 @@ public class SimpleGenericTypeArgument(ITypeParameterSymbol symbol, bool useUnma public override bool IsGeneric => true; - public override string[] TypeArguments => [Name]; - public override bool IsAlwaysValueType => symbol.HasValueTypeConstraint || symbol.HasUnmanagedTypeConstraint; public override bool IsAlwaysRefType => symbol.HasReferenceTypeConstraint; @@ -179,8 +171,6 @@ static string RemoveFieldName(string input) public override bool IsGeneric => true; - public override string[] TypeArguments => []; - public override bool IsAlwaysValueType => ((genericTypeInfo & 1) == 0 && !isInterface) || IsUnmanaged || IsTupleType; public override bool IsAlwaysRefType => ((genericTypeInfo & 2) == 0 || isInterface) && !IsUnmanaged && !IsTupleType; @@ -461,14 +451,18 @@ public SymbolHandler( Net11StructNameMap = Cases.ToDictionary(caseData => caseData, caseData => { - if (caseData.TypeInfo is null) + if (caseData.TypeInfo is null || !caseData.TypeInfo.IsGeneric) { return caseData.Name; } + else + { + string[] parsedTypeArguments = [caseData.TypeInfo.Name]; - var typeArguments = string.Join(", ", caseData.TypeInfo.TypeArguments.Intersect(TypeArguments)); + var typeArguments = string.Join(", ", parsedTypeArguments.Intersect(TypeArguments)); - return $"{caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}"; + return $"{caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}"; + } }); var enableJsonSerializationData = From a0b2eebd3b3b6dd225ca7df4b5e2c882e03bd25a Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 10:02:51 -0700 Subject: [PATCH 35/56] update tests --- SumSharp.Generator/SymbolHandler.cs | 108 ++++----------------------- Tests.Net11/Union.cs | 111 +++++++--------------------- 2 files changed, 43 insertions(+), 176 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index f9389f2..09180ae 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1228,7 +1228,7 @@ public void EmitNativeUnion() { Builder.Append($@" #if NET11_0_OR_GREATER - public object{Nullable} Value + public object Value {{ get {{ @@ -1256,74 +1256,14 @@ public object{Nullable} Value public bool HasValue => true;"); - foreach (var caseGroup in CaseGroups) + foreach (var caseData in Cases) { - var typeInfo = caseGroup.First().TypeInfo!; - Builder.AppendLine($@" - public bool TryGetValue(out {typeInfo.NullableStrippedName} value) + public bool TryGetValue(out {Net11StructNameMap[caseData]} value) {{ - value = default!; - - if (!TryGetValue<{typeInfo.Name}>(out var rawValue)) - {{ - return false; - }}"); - - if (typeInfo.IsAlwaysValueType) - { - if (typeInfo.NullableAnnotation) - { - Builder.Append($@" - if (!rawValue.HasValue) - {{ - return false; - }} - - value = rawValue.Value;"); - } - else - { - Builder.Append($@" - value = rawValue;"); - } - } - else if (typeInfo.IsAlwaysRefType) - { - Builder.Append($@" - if (ReferenceEquals(null, rawValue)) - {{ - return false; - }} - - value = rawValue;"); - } - else - { - Builder.Append($@" - if (!typeof({typeInfo.Name}).IsValueType && ReferenceEquals(null, rawValue)) - {{ - return false; - }} - - value = rawValue;"); - } - - Builder.AppendLine($@" - - return true; + throw new System.NotImplementedException(); }}"); - } - - foreach (var caseData in EmptyCases) - { - Builder.Append($@" - public bool TryGetValue(out {caseData.Name} value) - {{ - value = default; - return Index == {caseData.Index}; - }}"); } Builder.AppendLine($@" @@ -1331,49 +1271,33 @@ public bool TryGetValue(out {caseData.Name} value) public interface IUnionMembers {{ ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed - public object{Nullable} Value {{ get; }} + public object Value {{ get; }} - ///True if the underlying value is not a null. False otherwise. If the active case is empty the value is considered to be not null + ///Always returns true. SumSharp unions are always considered non-null, even if the active case is empty public bool HasValue {{ get; }}"); - foreach (var caseGroup in CaseGroups) + foreach (var caseData in Cases) { - var firstCase = caseGroup.First(); - - var typeInfo = firstCase.TypeInfo!; - - if (caseGroup.Count() == 1) + if (caseData.TypeInfo is null) { Builder.AppendLine($@" - ///Creates a that holds a value of type by invoking the case constructor - public static {Name} Create({typeInfo.Name} value) => {Name}.{firstCase.Name}(value);"); + ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types + public static {Name} Create({Net11StructNameMap[caseData]} _) => {Name}.{caseData.Name};"); + } else { - var candidateCases = caseGroup.Select(caseData => $"\"{caseData.Name}\""); - Builder.AppendLine($@" - ///Always throws a . This method exists to satisfy the compiler's requirements for .NET 11 union types. There are multiple - ///cases ({string.Join(", ", candidateCases)}) that can hold a value of type . Use the appropriate case constructor directly rather than relying on a compiler - ///provided conversion. - public static {Name} Create({typeInfo.Name} value) => throw new global::SumSharp.AmbiguousCaseException(typeof({Name}), typeof({typeInfo.Name}), [{string.Join(", ", candidateCases)}]);"); + ///Creates a that holds a value of type by invoking the case constructor with .Value + ///This function exists to satisfy the compiler's requirements for .NET 11 union types + public static {Name} Create({Net11StructNameMap[caseData]} value) => {Name}.{caseData.Name}(value.Value);"); } Builder.AppendLine($@" - ///Attempts to get a value of type from the union. Returns true if the union holds a non-null value of the type. + ///Attempts to get a value of type from the union. Returns true if the union holds a {caseData.Name}. ///Returns false otherwise. ///An out parameter that will be set to the underlying value, if present. - public bool TryGetValue(out {typeInfo.NullableStrippedName} value);"); - } - - foreach (var caseData in EmptyCases) - { - Builder.AppendLine($@" - ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({caseData.Name} _) => {Name}.{caseData.Name}; - - ///Returns true if the union is . Returns false otherwise. The parameter is always set to default - public bool TryGetValue(out {caseData.Name} value);"); + public bool TryGetValue(out {Net11StructNameMap[caseData]} value);"); } Builder.AppendLine($@" diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index e1b4563..f1ce153 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -13,13 +13,6 @@ partial class IntOrStringOrOther } - [UnionCase("NullableInt", typeof(int?))] - [UnionCase("Bool", typeof(bool?))] - partial class NullableValueTypes - { - - } - [UnionCase("Some", "T")] [UnionCase("None")] partial class Option @@ -69,11 +62,6 @@ public void HasValue() Assert.True(IntOrStringOrOther.Other(null).HasValue); Assert.True(IntOrStringOrOther.String(null!).HasValue); - Assert.True(NullableValueTypes.NullableInt(1).HasValue); - Assert.True(NullableValueTypes.Bool(false).HasValue); - Assert.True(NullableValueTypes.NullableInt(null).HasValue); - Assert.True(NullableValueTypes.Bool(null).HasValue); - Assert.True(Option.Some(1).HasValue); Assert.True(Option.Some(1).HasValue); Assert.True(Option.Some("abc").HasValue); @@ -94,124 +82,79 @@ public void Switch() { Assert.True(IntOrStringOrOther.Int(5) switch { - int i => i == 5, - string s => false, - bool b => false, + Int(var i) => i == 5, + String(var s) => false, + Other(var b) => false, }); Assert.True(IntOrStringOrOther.String("abc") switch { - int i => false, - string s => s == "abc", - bool b => false, + Int(var i) => false, + String(var s) => s == "abc", + Other(var b) => false, }); Assert.True(IntOrStringOrOther.String(null!) switch { - int i => false, - string s => false, - bool b => false, - null => true, + Int(var i) => false, + String(var s) => false, + Other(var b) => false, }); Assert.True(IntOrStringOrOther.Other(true) switch { - int i => false, - string s => false, - bool b => b, + Int(var i) => false, + String(var s) => false, + Other(var b) => b, }); Assert.True(IntOrStringOrOther.Other(4) switch { - int i => i == 4, - string s => false, + Int(var i) => false, + String(var s) => false, + Other(var i) => i == 4, }); Assert.True(IntOrStringOrOther.Other(null) switch { - int i => false, - string s => false, - null => true, + Int(var i) => false, + String(var s) => false, + Other(var i) => !i.HasValue, }); Assert.True(IntOrStringOrOther.Other(null!) switch { - int i => false, - string s => false, - float[] => false, - null => true, - }); - - Assert.True(NullableValueTypes.NullableInt(null) switch - { - int i => false, - bool b => false, - null => true, - }); - - Assert.True(NullableValueTypes.Bool(null) switch - { - int i => false, - bool b => false, - null => true, + Int(var i) => false, + String(var s) => false, + Other(var f) => f is null, }); - } - [Fact] - public void EmptyTypesSwitch() - { Assert.True(Option.Some("abc") switch { - string s => true, + Some("abc") => true, + Some => false, None => false, }); Assert.True(Option.None switch { - string s => false, + Some => false, None => true, }); Assert.True(EmptyCases1.IntArray([0]) switch { - int[] ints => ints.Single() == 0, + IntArray([0]) => true, + IntArray => false, EmptyCase1 => false, EmptyCase2 => false, }); - Assert.True(EmptyCases1.EmptyCase1 switch - { - int[] ints => false, - EmptyCase1 => true, - EmptyCase2 => false, - }); - - Assert.True(EmptyCases1.EmptyCase2 switch - { - int[] ints => false, - EmptyCase1 => false, - EmptyCase2 => true, - }); - - Assert.True(EmptyCases2.EmptyCase0 switch - { - EmptyCase0 => true, - EmptyCase1 => false, - float[] floats => false, - }); - Assert.True(EmptyCases2.EmptyCase1 switch { EmptyCase0 => false, EmptyCase1 => true, - float[] floats => false, - }); - - Assert.True(EmptyCases2.FloatArray([1.1f]) switch - { - EmptyCase0 => false, - EmptyCase1 => false, - float[] floats => floats.Single() == 1.1f, + FloatArray => false, }); } } From fe2763796d03f33b15265178de452b34f5a21a71 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 10:09:20 -0700 Subject: [PATCH 36/56] make test pass --- SumSharp.Generator/SymbolHandler.cs | 13 +++++++++++-- Tests.Net11/Union.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 09180ae..62b9b88 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1249,7 +1249,7 @@ public object Value } } - Builder.Append($@" + Builder.AppendLine($@" }}; }} }} @@ -1261,7 +1261,16 @@ public object Value Builder.AppendLine($@" public bool TryGetValue(out {Net11StructNameMap[caseData]} value) {{ - throw new System.NotImplementedException(); + value = default; + + if (Index != {caseData.Index}) + {{ + return false; + }} + + {(caseData.TypeInfo is null ? "" : $"value = new(As{caseData.Name}Unsafe);")} + + return true; }}"); } diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index f1ce153..51ba487 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -97,7 +97,7 @@ public void Switch() Assert.True(IntOrStringOrOther.String(null!) switch { Int(var i) => false, - String(var s) => false, + String(var s) => s is null, Other(var b) => false, }); From ec99701544e3f8d29e78e9058bc355ec5151ad8a Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Wed, 5 Aug 2026 10:15:43 -0700 Subject: [PATCH 37/56] make iunionmembers interface explicitly implemented [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 6 ++-- Tests.Net11/Union.cs | 52 ++++++++++++----------------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 62b9b88..c0c96df 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -1228,7 +1228,7 @@ public void EmitNativeUnion() { Builder.Append($@" #if NET11_0_OR_GREATER - public object Value + object IUnionMembers.Value {{ get {{ @@ -1254,12 +1254,12 @@ public object Value }} }} - public bool HasValue => true;"); + bool IUnionMembers.HasValue => true;"); foreach (var caseData in Cases) { Builder.AppendLine($@" - public bool TryGetValue(out {Net11StructNameMap[caseData]} value) + bool IUnionMembers.TryGetValue(out {Net11StructNameMap[caseData]} value) {{ value = default; diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 51ba487..5c2407a 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -39,41 +39,30 @@ partial class EmptyCases2 [Fact] public void Value() { - Assert.Equal(new Int(5), IntOrStringOrOther.Int(5).Value); - Assert.Equal(new String("abc"), IntOrStringOrOther.String("abc").Value); - Assert.Equal(new Other(true), IntOrStringOrOther.Other(true).Value); - Assert.Equal(new Other(4), IntOrStringOrOther.Other(4).Value); - Assert.Equal(new None(), Option.None.Value); - Assert.Equal(new EmptyCase1(), EmptyCases1.EmptyCase1.Value); - Assert.Equal(new EmptyCase2(), EmptyCases1.EmptyCase2.Value); - Assert.Equal(new EmptyCase0(), EmptyCases2.EmptyCase0.Value); - Assert.Equal(new EmptyCase1(), EmptyCases2.EmptyCase1.Value); + Assert.Equal(new Int(5), ((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.Int(5)).Value); + Assert.Equal(new String("abc"), ((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.String("abc")).Value); + Assert.Equal(new Other(true), ((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.Other(true)).Value); + Assert.Equal(new Other(4), ((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.Other(4)).Value); + Assert.Equal(new None(), ((Option.IUnionMembers)Option.None).Value); + Assert.Equal(new EmptyCase1(), ((EmptyCases1.IUnionMembers)EmptyCases1.EmptyCase1).Value); + Assert.Equal(new EmptyCase2(), ((EmptyCases1.IUnionMembers)EmptyCases1.EmptyCase2).Value); + Assert.Equal(new EmptyCase0(), ((EmptyCases2.IUnionMembers)EmptyCases2.EmptyCase0).Value); + Assert.Equal(new EmptyCase1(), ((EmptyCases2.IUnionMembers)EmptyCases2.EmptyCase1).Value); } [Fact] public void HasValue() { - Assert.True(IntOrStringOrOther.Int(5).HasValue); - Assert.True(IntOrStringOrOther.String("abc").HasValue); - Assert.True(IntOrStringOrOther.Other(true).HasValue); - Assert.True(IntOrStringOrOther.Other(4).HasValue); - Assert.True(IntOrStringOrOther.Other(3).HasValue); - - Assert.True(IntOrStringOrOther.Other(null).HasValue); - Assert.True(IntOrStringOrOther.String(null!).HasValue); - - Assert.True(Option.Some(1).HasValue); - Assert.True(Option.Some(1).HasValue); - Assert.True(Option.Some("abc").HasValue); - - Assert.True(Option.Some(null).HasValue); - Assert.True(Option.Some(null!).HasValue); - - Assert.True(Option.None.HasValue); - Assert.True(EmptyCases1.EmptyCase1.HasValue); - Assert.True(EmptyCases1.EmptyCase2.HasValue); - Assert.True(EmptyCases2.EmptyCase0.HasValue); - Assert.True(EmptyCases2.EmptyCase1.HasValue); + Assert.True(((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.Int(5)).HasValue); + Assert.True(((IntOrStringOrOther.IUnionMembers)IntOrStringOrOther.Other(null)).HasValue); + + Assert.True(((Option.IUnionMembers)Option.Some(1)).HasValue); + Assert.True(((Option.IUnionMembers)Option.None).HasValue); + + Assert.True(((EmptyCases1.IUnionMembers)EmptyCases1.EmptyCase1).HasValue); + Assert.True(((EmptyCases1.IUnionMembers)EmptyCases1.EmptyCase2).HasValue); + Assert.True(((EmptyCases2.IUnionMembers)EmptyCases2.EmptyCase0).HasValue); + Assert.True(((EmptyCases2.IUnionMembers)EmptyCases2.EmptyCase1).HasValue); } @@ -97,7 +86,8 @@ public void Switch() Assert.True(IntOrStringOrOther.String(null!) switch { Int(var i) => false, - String(var s) => s is null, + String(null) => true, + String => false, Other(var b) => false, }); From 78321a6e6981dae48f6ca1d8e4c52a6a028eaea6 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 09:34:33 -0700 Subject: [PATCH 38/56] remove unneeded trygetvaluegeneric --- SumSharp.Generator/SymbolHandler.cs | 134 +++++++++++++++------------- 1 file changed, 74 insertions(+), 60 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index c0c96df..da8d958 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -630,8 +630,6 @@ public string Emit() EmitUnmanagedStorageSize(); } - EmitTryGetValueGeneric(); - if (!DisableValueEquality) { EmitEquals(); @@ -897,45 +895,6 @@ public void EmitUnmanagedStorageSize() public static int UnmanagedStorageSize => _unmanagedStorageSize;"); } - public void EmitTryGetValueGeneric() - { - Builder.Append($@" - private bool TryGetValue(out TValue__ value) - {{ - value = default!; - - switch (Index) - {{"); - - foreach (var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - Builder.Append($@" - case {caseData.Index}: break;"); - } - else - { - Builder.Append($@" - case {caseData.Index}: - if (typeof({caseData.TypeInfo.Name}) == typeof(TValue__)) - {{ - var temp = As{caseData.Name}Unsafe; - - value = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, TValue__>(ref temp); - - return true; - }} - break;"); - } - } - - Builder.AppendLine($@" - }}; - - return false; - }}"); - } public void EmitEquals() { Builder.Append($@" @@ -1020,32 +979,87 @@ public override int GetHashCode() { var type = caseGroup.First().TypeInfo!; - Builder.AppendLine($@" + Builder.Append($@" ///Compares a {XMLEscapedName} with a for equality using on the underlying value public static bool operator==({Name} left, {type.Name} right) {{ - if (!left.TryGetValue<{type.Name}>(out var value)) - {{ - return false; - }}"); - - if (type.IsAlwaysValueType) - { - Builder.Append($@" - return value.Equals(right);"); - } - else if (type.IsAlwaysRefType) - { - Builder.Append($@" - return ReferenceEquals(null, value) ? ReferenceEquals(null, right) : value.Equals(right);"); - } - else + switch (left.Index) + {{"); + foreach (var caseData in Cases) { - Builder.Append($@" - return typeof({type.Name}).IsValueType ? value{NullForgiving}.Equals(right) : (ReferenceEquals(null, value) ? ReferenceEquals(null, right) : value.Equals(right));"); + if (caseData.TypeInfo is null) + { + continue; + } + + if (caseData.TypeInfo.IsGeneric || type.IsGeneric) + { + if (caseData.TypeInfo.Name == type.Name) + { + Builder.Append($@" + case {caseData.Index}: return typeof({caseData.TypeInfo.Name}).IsValueType ? left.As{caseData.Name}Unsafe{NullForgiving}.Equals(right) : (ReferenceEquals(null, left.As{caseData.Name}Unsafe) ? ReferenceEquals(null, right) : left.As{caseData.Name}Unsafe.Equals(right));"); + } + else if (caseData.TypeInfo.IsAlwaysValueType || type.IsAlwaysValueType) + { + Builder.Append($@" + case {caseData.Index}: + if (typeof({caseData.TypeInfo.Name}) == typeof({type.Name})) + {{ + var leftValue = left.As{caseData.Name}Unsafe; + + return System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, {type.Name}>(ref leftValue){NullForgiving}.Equals(right); + }} + break;"); + } + else if (caseData.TypeInfo.IsAlwaysRefType || type.IsAlwaysRefType) + { + Builder.Append($@" + case {caseData.Index}: + if (typeof({caseData.TypeInfo.Name}) == typeof({type.Name})) + {{ + var leftValue = left.As{caseData.Name}Unsafe; + + var castedLeftValue = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, {type.Name}>(ref leftValue); + + return ReferenceEquals(null, castedLeftValue) ? ReferenceEquals(null, right) : castedLeftValue.Equals(right); + }} + break;"); + } + else + { + Builder.Append($@" + case {caseData.Index}: + if (typeof({caseData.TypeInfo.Name}) == typeof({type.Name})) + {{ + var leftValue = left.As{caseData.Name}Unsafe; + + var castedLeftValue = System.Runtime.CompilerServices.Unsafe.As<{caseData.TypeInfo.Name}, {type.Name}>(ref leftValue); + + return typeof({caseData.TypeInfo.Name}).IsValueType ? castedLeftValue{NullForgiving}.Equals(right) : (ReferenceEquals(null, castedLeftValue) ? ReferenceEquals(null, right) : castedLeftValue.Equals(right)); + }} + break;"); + } + } + else if (caseData.TypeInfo.Name == type.Name) + { + if (caseData.TypeInfo.IsAlwaysValueType) + { + Builder.Append($@" + case {caseData.Index}: return left.As{caseData.Name}Unsafe.Equals(right);"); + } + else + { + Builder.Append($@" + case {caseData.Index}: return ReferenceEquals(null, left.As{caseData.Name}Unsafe) ? ReferenceEquals(null, right) : left.As{caseData.Name}Unsafe.Equals(right);"); + } + } } Builder.AppendLine($@" + default: break; + }} + + return false; }} ///Compares a with a {XMLEscapedName} for equality using on the underlying value From eb518d93e3b2e356c04d481714311a1a0d7cf094 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 09:44:24 -0700 Subject: [PATCH 39/56] start work on complex generic --- SumSharp.Generator/SymbolHandler.cs | 2 +- Tests.Net11/Union.cs | 50 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index da8d958..ad0465d 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -809,7 +809,7 @@ private void EmitFieldsAndConstructor() Builder.Append($@" [System.Runtime.CompilerServices.Union] #endif -{Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name}{interfaces} +{Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name} {interfaces} {{"); foreach (var field in fieldNameTypeMap) diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 5c2407a..a0bddde 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -36,6 +36,22 @@ partial class EmptyCases2 } + public partial class OuterGeneric + { + public partial class InnerGeneric + { + [UnionCase("Case0", "T")] + [UnionCase("Case1", "U[]")] + [UnionCase("Case2", "Dictionary")] + [UnionCase("Case3", "W[]")] + [UnionCase("Case4", "X")] + public partial struct ComplexGeneric + { + + } + } + } + [Fact] public void Value() { @@ -147,4 +163,38 @@ public void Switch() FloatArray => false, }); } + + [Fact] + public void ComplexGeneric() + { + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case0("abc") switch + { + OuterGeneric.InnerGeneric.Case0("abc") => true, + _ => false + }); + + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case1([[1.0f], [2.0f, 3.0f]]) switch + { + OuterGeneric.InnerGeneric.Case1([[1.0f], [2.0f, 3.0f]]) => true, + _ => false + }); + + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case2(new() { [4] = [1, 2] }) switch + { + OuterGeneric.InnerGeneric.Case2(var dict) => dict[4] is [1, 2], + _ => false + }); + + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case3([5.5, 5.6]) switch + { + OuterGeneric.InnerGeneric.Case3([5.5, 5.6]) => true, + _ => false + }); + + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case4([1, 2, 3]) switch + { + OuterGeneric.InnerGeneric.Case4>([1, 2, 3]) => true, + _ => false + }); + } } From 7f4f71ae637398b54ec2cfe8ca962d0c611f92fc Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 10:49:00 -0700 Subject: [PATCH 40/56] more work on generic test [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 2 +- SumSharp.Generator/TypeNameParser.cs | 13 +++++++++++++ Tests.Net11/Union.cs | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 SumSharp.Generator/TypeNameParser.cs diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index ad0465d..8a1b6f6 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -459,7 +459,7 @@ public SymbolHandler( { string[] parsedTypeArguments = [caseData.TypeInfo.Name]; - var typeArguments = string.Join(", ", parsedTypeArguments.Intersect(TypeArguments)); + var typeArguments = string.Join(", ", TypeArguments.Intersect(parsedTypeArguments)); return $"{caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}"; } diff --git a/SumSharp.Generator/TypeNameParser.cs b/SumSharp.Generator/TypeNameParser.cs new file mode 100644 index 0000000..e75e8d8 --- /dev/null +++ b/SumSharp.Generator/TypeNameParser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SumSharp.Generator; + +internal class TypeNameParser +{ + public static string[] ParseTypeArguments(string typeName) + { + return [typeName]; + } +} diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index a0bddde..463045d 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -42,8 +42,8 @@ public partial class InnerGeneric { [UnionCase("Case0", "T")] [UnionCase("Case1", "U[]")] - [UnionCase("Case2", "Dictionary")] - [UnionCase("Case3", "W[]")] + [UnionCase("Case2", "Dictionary")] + [UnionCase("Case3", "(W[] WArray, bool Boolean)")] [UnionCase("Case4", "X")] public partial struct ComplexGeneric { @@ -167,33 +167,33 @@ public void Switch() [Fact] public void ComplexGeneric() { - Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case0("abc") switch + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case0("abc") switch { OuterGeneric.InnerGeneric.Case0("abc") => true, _ => false }); - Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case1([[1.0f], [2.0f, 3.0f]]) switch + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case1([[1.0f], [2.0f, 3.0f]]) switch { OuterGeneric.InnerGeneric.Case1([[1.0f], [2.0f, 3.0f]]) => true, _ => false }); - Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case2(new() { [4] = [1, 2] }) switch + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case2(new() { [4.0] = ([1, 2], [3, 4]) }) switch { - OuterGeneric.InnerGeneric.Case2(var dict) => dict[4] is [1, 2], + OuterGeneric.InnerGeneric.Case2(var dict) => dict[4.0] is ([1, 2], [3, 4]), _ => false }); - Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case3([5.5, 5.6]) switch + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case3(([[1], [2, 3]], false)) switch { - OuterGeneric.InnerGeneric.Case3([5.5, 5.6]) => true, + OuterGeneric.InnerGeneric.Case3(([[1], [2, 3]], false)) => true, _ => false }); - Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric>.Case4([1, 2, 3]) switch + Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case4(3.0) switch { - OuterGeneric.InnerGeneric.Case4>([1, 2, 3]) => true, + OuterGeneric.InnerGeneric.Case4(3.0) => true, _ => false }); } From 51f44dcf2ae65fae0ac6bc4f18ab49af0061cee8 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 10:58:05 -0700 Subject: [PATCH 41/56] start work on typenameparser --- SumSharp.Generator/SymbolHandler.cs | 2 +- Tests.Net11/Union.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 8a1b6f6..4ac87c0 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -457,7 +457,7 @@ public SymbolHandler( } else { - string[] parsedTypeArguments = [caseData.TypeInfo.Name]; + var parsedTypeArguments = TypeNameParser.ParseTypeArguments(caseData.TypeInfo.Name); var typeArguments = string.Join(", ", TypeArguments.Intersect(parsedTypeArguments)); diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 463045d..12330cd 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -38,14 +38,17 @@ partial class EmptyCases2 public partial class OuterGeneric { - public partial class InnerGeneric + public partial class InnerGeneric + where U : class + where V : unmanaged { [UnionCase("Case0", "T")] [UnionCase("Case1", "U[]")] [UnionCase("Case2", "Dictionary")] [UnionCase("Case3", "(W[] WArray, bool Boolean)")] [UnionCase("Case4", "X")] - public partial struct ComplexGeneric + public partial struct ComplexGeneric + where X : struct { } From d96c785bd6fcb75bbda49ad11589215de3ab6813 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 11:19:51 -0700 Subject: [PATCH 42/56] get typename parser working [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 2 +- SumSharp.Generator/TypeNameParser.cs | 159 ++++++++++++++++++++++++++- Tests.Net11/Union.cs | 6 +- 3 files changed, 157 insertions(+), 10 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 4ac87c0..f380a29 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -457,7 +457,7 @@ public SymbolHandler( } else { - var parsedTypeArguments = TypeNameParser.ParseTypeArguments(caseData.TypeInfo.Name); + var parsedTypeArguments = TypeNameParser.ExtractLeafTypes(caseData.TypeInfo.Name); var typeArguments = string.Join(", ", TypeArguments.Intersect(parsedTypeArguments)); diff --git a/SumSharp.Generator/TypeNameParser.cs b/SumSharp.Generator/TypeNameParser.cs index e75e8d8..644facb 100644 --- a/SumSharp.Generator/TypeNameParser.cs +++ b/SumSharp.Generator/TypeNameParser.cs @@ -1,13 +1,160 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; namespace SumSharp.Generator; - -internal class TypeNameParser +public static class TypeNameParser { - public static string[] ParseTypeArguments(string typeName) + public static List ExtractLeafTypes(string text) + { + var parser = new Parser(text); + var result = new List(); + + parser.ParseType(result); + + return result; + } + + private sealed class Parser { - return [typeName]; + private readonly string _text; + private int _pos; + + public Parser(string text) + { + _text = text; + } + + public void ParseType(List output) + { + SkipWhitespace(); + + if (Peek() == '(') + { + ParseTuple(output); + return; + } + + string identifier = ParseIdentifier(); + + SkipWhitespace(); + + // Generic? + if (Peek() == '<') + { + Consume('<'); + + while (true) + { + ParseType(output); + + SkipWhitespace(); + + if (Peek() == ',') + { + Consume(','); + continue; + } + + Consume('>'); + break; + } + } + else + { + output.Add(identifier); + } + + // Ignore array suffixes + while (true) + { + SkipWhitespace(); + + if (Peek() != '[') + break; + + Consume('['); + + while (Peek() != ']') + _pos++; + + Consume(']'); + } + + // Optional nullable suffix + if (Peek() == '?') + Consume('?'); + } + + private void ParseTuple(List output) + { + Consume('('); + + while (true) + { + ParseType(output); + + SkipWhitespace(); + + // Skip tuple field name if present + if (char.IsLetter(Peek()) || Peek() == '_') + { + ParseIdentifier(); + } + + SkipWhitespace(); + + if (Peek() == ',') + { + Consume(','); + continue; + } + + Consume(')'); + break; + } + } + + private string ParseIdentifier() + { + SkipWhitespace(); + + int start = _pos; + + while (_pos < _text.Length) + { + char c = _text[_pos]; + + if (char.IsLetterOrDigit(c) || c == '_' || c == '.') + { + _pos++; + } + else + { + break; + } + } + + return _text.Substring(start, _pos - start); + } + + private void SkipWhitespace() + { + while (_pos < _text.Length && char.IsWhiteSpace(_text[_pos])) + _pos++; + } + + private char Peek() + { + return _pos < _text.Length ? _text[_pos] : '\0'; + } + + private void Consume(char c) + { + if (Peek() != c) + throw new FormatException($"Expected '{c}'."); + + _pos++; + } } -} +} \ No newline at end of file diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 12330cd..3d9b20d 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -36,7 +36,7 @@ partial class EmptyCases2 } - public partial class OuterGeneric + public partial class OuterGeneric { public partial class InnerGeneric where U : class @@ -184,13 +184,13 @@ public void ComplexGeneric() Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case2(new() { [4.0] = ([1, 2], [3, 4]) }) switch { - OuterGeneric.InnerGeneric.Case2(var dict) => dict[4.0] is ([1, 2], [3, 4]), + OuterGeneric.InnerGeneric.Case2, double>(var dict) => dict[4.0] is ([1, 2], [3, 4]), _ => false }); Assert.True(OuterGeneric.InnerGeneric.ComplexGeneric, double>.Case3(([[1], [2, 3]], false)) switch { - OuterGeneric.InnerGeneric.Case3(([[1], [2, 3]], false)) => true, + OuterGeneric.InnerGeneric.Case3>(([[1], [2, 3]], false)) => true, _ => false }); From b821735d34cd6080d97ec834f4e4df17ba5955ba Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 11:26:48 -0700 Subject: [PATCH 43/56] small improvement to parser [skip ci] --- SumSharp.Generator/TypeNameParser.cs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/SumSharp.Generator/TypeNameParser.cs b/SumSharp.Generator/TypeNameParser.cs index 644facb..9fbc9ba 100644 --- a/SumSharp.Generator/TypeNameParser.cs +++ b/SumSharp.Generator/TypeNameParser.cs @@ -5,9 +5,9 @@ namespace SumSharp.Generator; public static class TypeNameParser { - public static List ExtractLeafTypes(string text) + public static List ExtractLeafTypes(string typeName) { - var parser = new Parser(text); + var parser = new Parser(typeName); var result = new List(); parser.ParseType(result); @@ -15,15 +15,9 @@ public static List ExtractLeafTypes(string text) return result; } - private sealed class Parser + private sealed class Parser(string typeName) { - private readonly string _text; - private int _pos; - - public Parser(string text) - { - _text = text; - } + private int _pos = 0; public void ParseType(List output) { @@ -121,9 +115,9 @@ private string ParseIdentifier() int start = _pos; - while (_pos < _text.Length) + while (_pos < typeName.Length) { - char c = _text[_pos]; + char c = typeName[_pos]; if (char.IsLetterOrDigit(c) || c == '_' || c == '.') { @@ -135,18 +129,18 @@ private string ParseIdentifier() } } - return _text.Substring(start, _pos - start); + return typeName.Substring(start, _pos - start); } private void SkipWhitespace() { - while (_pos < _text.Length && char.IsWhiteSpace(_text[_pos])) + while (_pos < typeName.Length && char.IsWhiteSpace(typeName[_pos])) _pos++; } private char Peek() { - return _pos < _text.Length ? _text[_pos] : '\0'; + return _pos < typeName.Length ? typeName[_pos] : '\0'; } private void Consume(char c) From 4d96e230e11008ae80a22c83ad0014e8b4ac1c8f Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Thu, 6 Aug 2026 11:29:33 -0700 Subject: [PATCH 44/56] remove some unneeded variables [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index f380a29..a2dc465 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -20,10 +20,6 @@ public abstract class TypeInfo { public abstract string Name { get; } - public bool NullableAnnotation => Name.EndsWith("?"); - - public string NullableStrippedName => Name.TrimEnd('?'); - public abstract bool IsUnmanaged { get; } public abstract bool UseUnmanagedStorage { get; } @@ -259,8 +255,6 @@ public CaseData(int index, string name, TypeInfo? typeInfo, bool storeAsObject, public CaseData[] UniqueCases { get; } - public CaseData[] EmptyCases { get; } - public Dictionary Net11StructNameMap { get; } // Cases grouped by type @@ -447,8 +441,6 @@ public SymbolHandler( .SelectMany(group => group) .ToArray(); - EmptyCases = [..Cases.Where(caseData => caseData.TypeInfo is null)]; - Net11StructNameMap = Cases.ToDictionary(caseData => caseData, caseData => { if (caseData.TypeInfo is null || !caseData.TypeInfo.IsGeneric) From 74e76d757629ded97e1187533c921291f1ff9b7c Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Fri, 7 Aug 2026 15:22:51 -0700 Subject: [PATCH 45/56] add generated code attribute [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 70 ++++++++++++++++------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index a2dc465..e9964e0 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; @@ -16,6 +17,8 @@ internal class SymbolHandler private const string IL2026SupressAttribute = "[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\"Trimming\", \"IL2026:RequiresUnreferencedCode\", Justification = \"It is the library consumer's responsibility to ensure the required types are preserved.\")]"; private const string IL3050SupressAttribute = "[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\"AOT\", \"IL3050:AotAnalysisWarning\", Justification = \"It is the library consumer's responsibility to ensure the required types are preserved.\")]"; + private static readonly string GeneratedCodeAttribute = $"[System.CodeDom.Compiler.GeneratedCode(\"SumSharp\", \"{Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion}\")]"; + public abstract class TypeInfo { public abstract string Name { get; } @@ -788,12 +791,14 @@ private void EmitFieldsAndConstructor() { Builder.AppendLine($@" ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement + {GeneratedCodeAttribute} public readonly partial record struct {caseData.Name};"); } else { Builder.AppendLine($@" ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement + {GeneratedCodeAttribute} public readonly record struct {Net11StructNameMap[caseData]}({caseData.TypeInfo.Name} Value);"); } } @@ -801,6 +806,7 @@ private void EmitFieldsAndConstructor() Builder.Append($@" [System.Runtime.CompilerServices.Union] #endif +{GeneratedCodeAttribute} {Accessibility} partial {GetDeclarationKind(IsStruct, IsRecord)} {Name} {interfaces} {{"); @@ -1760,6 +1766,7 @@ private void EmitStandardJsonConverter() { Builder.Append($@" ///System.Text.Json converter capable of serializing and deserializing a {XMLEscapedName} + {GeneratedCodeAttribute} public partial class StandardJsonConverter : System.Text.Json.Serialization.JsonConverter<{Name}> {{ {(UsingAOTCompilation ? IL2026SupressAttribute : "")} @@ -1874,6 +1881,7 @@ private void EmitNewtonsoftJsonConverter() { Builder.Append($@" ///Newtonsoft converter capable of serializing and deserializing a {XMLEscapedName} + {GeneratedCodeAttribute} public partial class NewtonsoftJsonConverter : Newtonsoft.Json.JsonConverter<{Name}> {{ public override {Name}{NullableIfRef} ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, {Name}{NullableIfRef} existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer) @@ -1989,6 +1997,7 @@ private void EmitEndClassDeclaration() private void EmitStaticClass() { Builder.Append($@" +{GeneratedCodeAttribute} {Accessibility} static partial class {NameWithoutTypeArguments} {{"); } @@ -2000,6 +2009,7 @@ private void EmitStandardJsonConverterFactory() Builder.Append($@" ///System.Text.Json converter capable of serializing and deserializing any {NameWithoutTypeArguments} {(UsingAOTCompilation ? IL3050SupressAttribute : "")} + {GeneratedCodeAttribute} public partial class StandardJsonConverter : System.Text.Json.Serialization.JsonConverterFactory {{ public override bool CanConvert(System.Type typeToConvert) @@ -2022,45 +2032,45 @@ private void EmitGenericNewtonsoftJsonConverter() var genericTypeDefinition = $"{NameWithoutTypeArguments}<{new string(',', TypeArguments.Length - 1)}>"; Builder.AppendLine($@" -///Newtonsoft converter capable of serializing and deserializing any {NameWithoutTypeArguments} -public class NewtonsoftJsonConverter : Newtonsoft.Json.JsonConverter -{{ - static readonly System.Collections.Concurrent.ConcurrentDictionary _converters = new(); - - private static Newtonsoft.Json.JsonConverter GetConverter(System.Type objectType) + ///Newtonsoft converter capable of serializing and deserializing any {NameWithoutTypeArguments} + {GeneratedCodeAttribute} + public class NewtonsoftJsonConverter : Newtonsoft.Json.JsonConverter {{ - return _converters.GetOrAdd(objectType, static objectType => + static readonly System.Collections.Concurrent.ConcurrentDictionary _converters = new(); + + private static Newtonsoft.Json.JsonConverter GetConverter(System.Type objectType) {{ - var converterType = typeof({genericTypeDefinition}.NewtonsoftJsonConverter).MakeGenericType(objectType.GetGenericArguments()); + return _converters.GetOrAdd(objectType, static objectType => + {{ + var converterType = typeof({genericTypeDefinition}.NewtonsoftJsonConverter).MakeGenericType(objectType.GetGenericArguments()); - return (Newtonsoft.Json.JsonConverter)System.Activator.CreateInstance(converterType); - }}); - }} + return (Newtonsoft.Json.JsonConverter)System.Activator.CreateInstance(converterType); + }}); + }} - public override bool CanConvert(System.Type objectType) - {{ - return objectType.IsGenericType && - objectType.GetGenericTypeDefinition() == typeof({genericTypeDefinition}); - }} + public override bool CanConvert(System.Type objectType) + {{ + return objectType.IsGenericType && + objectType.GetGenericTypeDefinition() == typeof({genericTypeDefinition}); + }} - public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object{Nullable} value, Newtonsoft.Json.JsonSerializer serializer) - {{ - if (value is null) + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object{Nullable} value, Newtonsoft.Json.JsonSerializer serializer) {{ - writer.WriteNull(); + if (value is null) + {{ + writer.WriteNull(); - return; - }} + return; + }} - GetConverter(value.GetType()).WriteJson(writer, value, serializer); - }} + GetConverter(value.GetType()).WriteJson(writer, value, serializer); + }} - public override object{Nullable} ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object{Nullable} existingValue, Newtonsoft.Json.JsonSerializer serializer) - {{ - return GetConverter(objectType).ReadJson(reader, objectType, existingValue, serializer); - }} -}} -"); + public override object{Nullable} ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object{Nullable} existingValue, Newtonsoft.Json.JsonSerializer serializer) + {{ + return GetConverter(objectType).ReadJson(reader, objectType, existingValue, serializer); + }} + }}"); } private void EmitEndStaticClass() From cb1185f153ae8a475644a8e5b2bebbbb88f8be3c Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Fri, 7 Aug 2026 15:54:59 -0700 Subject: [PATCH 46/56] small fix [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index e9964e0..0a839ac 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -791,7 +791,6 @@ private void EmitFieldsAndConstructor() { Builder.AppendLine($@" ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement - {GeneratedCodeAttribute} public readonly partial record struct {caseData.Name};"); } else @@ -854,8 +853,7 @@ public void EmitStaticConstructor() { var unmanagedTypes = Cases.Where(caseData => caseData.UseUnmanagedStorage) - .Select(caseData => caseData.TypeInfo!.Name) - .ToImmutableHashSet(); + .Select(caseData => caseData.TypeInfo!.Name); foreach (var type in unmanagedTypes) { @@ -881,7 +879,7 @@ static void CheckUnmanagedStorage() where TUnmanaged__ : unmanaged var _ = new StandardJsonConverter();"); } - Builder.AppendLine(@" + Builder.AppendLine(@" }"); } From 3b991d7f0f70d85442f70226771a758cb28ef1d4 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 12:35:19 -0700 Subject: [PATCH 47/56] update readme [skip ci] --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 963deb9..429c675 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A highly configurable C\# discriminated union library - [Empty cases](#empty-cases) - [Generic cases](#generic-cases) - [The `Match` function](#the-match-function) + - [.NET 11 union types and pattern matching](#net-11-union-types-and-pattern-matching) 4. [Motivation](#motivation) - [What about `OneOf`?](#what-about-oneof) - [Typical DU implementation approaches](#typical-du-implementation-approaches) @@ -39,12 +40,13 @@ A highly configurable C\# discriminated union library Discriminated unions, also known as sum types, are an invaluable tool for working with heterogenous data types in code. They help ensure safe data access patterns and can [make illegal states unrepresentable.](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/) -There are many discriminated union libraries available for C\#, such as [`OneOf`](https://github.com/mcintyre321/OneOf) which has received tens of millions of downloads. In my experience, all of them lack features commonly offered by discriminated union types in other languages. +There are many discriminated union libraries available for C\#, such as [`OneOf`](https://github.com/mcintyre321/OneOf) which has received tens of millions of downloads. In my experience, all of them lack features commonly offered by discriminated union types in other languages. Union types are being added to C# with the [.NET 11 release](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union), but these are not true DUs because they lack case names and thus cannot support multiple cases of the same type. `SumSharp` aims to be **the most powerful, expressive, and configurable C\# discriminated union library available**. Its goal is to provide features and syntax comparable to the discriminated union types natively offered by languages such as F\#, Rust, and Haskell. Although it's impossible to exactly replicate the functionality these other languages offer, `SumSharp` strives to get as close as possible. ### Features +- **Integration with .NET 11 union types, allowing pattern matching using C#'s built-in pattern matching syntax** - Unlimited number of cases - Support for class, struct, record, and record struct unions - Support for generic unions @@ -141,7 +143,7 @@ Case types can be generic. To define a generic case you must supply the **name** ```csharp [UnionCase("Some", "T")] [UnionCase("None")] -partial class Optional +partial class Option { } @@ -151,12 +153,14 @@ Note that generic types in general *must be fully qualified names unless you hav ### The `Match` function -`SumSharp` unions have a `Match` member function that provides functionality similar to the match statement in F\# (with the limitation that `SumSharp` does not offer partial matching). The parameters to `Match` are the handler functions for each case, in order. Each parameter has the same name as its corresponding case, allowing the use of named parameters to improve code readability and for the handlers to be specified out of order. To illustrate this, compare the syntax of performing a match on the `Optional` type defined in the last section to equivalent F\# code. +**If you are using .NET 11 or higher, `SumSharp` unions satisfy the compiler's requirements for a union type. In most cases using built-in C# pattern matching will be easier than using the `Match` function. See [.NET 11 union types and pattern matching](#net-11-union-types-and-pattern-matching)** + +`SumSharp` unions have a `Match` member function that provides functionality similar to the match statement in F\# (with the limitation that `SumSharp` does not offer partial matching). The parameters to `Match` are the handler functions for each case, in order. Each parameter has the same name as its corresponding case, allowing the use of named parameters to improve code readability and for the handlers to be specified out of order. To illustrate this, compare the syntax of performing a match on the `Option` type defined in the last section to equivalent F\# code. ```csharp -// Here myOptionalValue is an Optional +// Here myOptionValue is an Option // The "None" handler can come before the "Some" handler as long as they're both named -var result = myOptionalValue.Match( +var result = myOptionValue.Match( None: () => "", Some: x => x); ``` @@ -164,7 +168,7 @@ var result = myOptionalValue.Match( Corresponding F\# code would look like: ```fsharp -let result = match myOptionalValue with +let result = match myOptionValue with | None -> "" | Some x -> x ``` @@ -174,7 +178,7 @@ Handling each case is not required, but a warning will be emitted by the `SumSha If you only want to handle some subset of cases, you can provide a default handler to prevent a warning from being emitted. ```csharp -var result = myOptionalValue.Match( +var result = myOptionValue.Match( Some: x => x, _: () => ""); ``` @@ -182,18 +186,60 @@ var result = myOptionalValue.Match( Again, the corresponding F\# code would look like: ```fsharp -let result = match myOptionalValue with +let result = match myOptionValue with | Some x -> x | _ -> "" ``` The `SumSharp` analyzer will emit a warning if a default handler is provided for a `Match` that is already exhaustive. It will also emit a warning if any case handlers are specified by position rather than name. Specifying by name is preferred because it makes the code clearer and prevents bugs/compilation errors if the case ordering changes. +### .NET 11 union types and pattern matching + +If you are using .NET 11 or higher, `SumSharp` unions satisfy the compiler's requirements for a union type. All `SumSharp` unions implement [the non-boxing access pattern](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union#non-boxing-access-pattern) and [union member providers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union#union-member-providers). + +Because C# union types do not support case names, `SumSharp` generates wrapper structs for each case in the union and places them as the same namespace/nested type level as the union itself. Empty cases get empty `partial` structs. This means that two `SumSharp` unions that share the same namespace/type heirarchy **cannot share identical non-empty case names**. These structs are used when pattern matching using built-in C# syntax such as `switch` or `is`. An example using the `Option` type that was defined above: + +```csharp +var x = Option.Some(4); + +var value = x switch +{ + Some(var i) => i, + None => 0, +}; + +// prints "value is 4" +Console.WriteLine($"value is {value}"); + +// prints "x is 4" +if (x is Some(4)) +{ + Console.WriteLine($"x is 4"); +} +else if (x is None) +{ + Console.WriteLine("x is none"); +} + +``` + +#### Type union implementation details + +1. `SumSharp` unions are *never null*. A non-null `SumSharp` union will never match with the `null` pattern, even if the underlying data it stores is null. +2. The `IUnionMembers.Value` property is never null and will always return a boxed instance of one of the case structs. +3. The `IUnionMembers.HasValue` property always returns true. +4. The various `TryGetValue` overloads will wrap the underlying data in one of the case structs. +5. `SumSharp` unions implement their corresponding `IUnionMembers` interface explicitly. This means that the `Value` and `HasValue` properties and the `TryGetValue` methods cannot be used unless you explicitly cast it to an `IUnionMembers`. In general you should not need to use any of these: they exist to satisfy the compiler's requirements for custom union types. + --- ## Motivation -C\# unfortunately does not offer discriminated unions as a language feature. Although [a proposal](https://github.com/dotnet/csharplang/blob/18a527bcc1f0bdaf542d8b9a189c50068615b439/proposals/TypeUnions.md) has existed for a while, this feature doesn't seem to be coming in the near future. +### `SumSharp` vs .NET 11 union types + +The union types introduced by .NET 11 are not true disrciminated unions because they lack the ability to define case names, thus not allowing for multiple cases of the same type. They also always box value types by default, which is unnecessary and often undesireable. They do, however, provide highly convenient pattern matching syntax using C\#'s built-in pattern matching operations such as `switch` and `is`. + +As mentioned in the quick start guide, `SumSharp` unions satisfy the requirements for .NET 11 union types. Wrapper structs are defined for each case, allowing for pattern matching behavior that is similar to languages with first class DUs such as F\#. Thus, `SumSharp` works synergistically with C\#'s unions types. You don't need to choose between the two: using `SumSharp` gives you the best of both. ### What about `OneOf`? From 149e688d7ca4a9e7117a8245f5c784e78dc883f2 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 12:36:46 -0700 Subject: [PATCH 48/56] remove unneeded file [skip c] --- SumSharp/AmbiguousCaseException.cs | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 SumSharp/AmbiguousCaseException.cs diff --git a/SumSharp/AmbiguousCaseException.cs b/SumSharp/AmbiguousCaseException.cs deleted file mode 100644 index 05f094b..0000000 --- a/SumSharp/AmbiguousCaseException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace SumSharp; - -/// -/// Thrown when the a generated IUnionMembers.Create static factory method is called with a type that has more than one matching case in the union -/// -/// The type of the union that failed to be constructed -/// The type of the case that failed to be constructed -/// The multiple cases that match the given type -public sealed class AmbiguousCaseException(Type unionType, Type caseType, string[] candidateCaseNames) : Exception($"Failed to construct a {unionType} with underlying type {caseType}. There are multiple candidate cases of this type") -{ - /// - /// The type of the union that failed to be constructed - /// - public Type UnionType => unionType; - - /// - /// The type of the case that failed to be constructed - /// - public Type CaseType => caseType; - - /// - /// The multiple cases that match the given type - /// - public string[] CandidateCaseNames => candidateCaseNames; -} From ff646b71cc0dd6f409f6e04c4a6d5dcb530bdbf3 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 12:39:56 -0700 Subject: [PATCH 49/56] small fix to readme [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 429c675..7995047 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ Console.WriteLine($"value is {value}"); // prints "x is 4" if (x is Some(4)) { - Console.WriteLine($"x is 4"); + Console.WriteLine("x is 4"); } else if (x is None) { From 63fe5117db71b4df0e25e9fa0ead11fc46131b62 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 12:50:38 -0700 Subject: [PATCH 50/56] add table of contenst entry [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7995047..a79c173 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A highly configurable C\# discriminated union library - [The `Match` function](#the-match-function) - [.NET 11 union types and pattern matching](#net-11-union-types-and-pattern-matching) 4. [Motivation](#motivation) + - [SumSharp vs .NET 11 union types](#sumsharp-vs-net-11-union-types) - [What about `OneOf`?](#what-about-oneof) - [Typical DU implementation approaches](#typical-du-implementation-approaches) - [SumSharp's approach](#sumsharps-approach) From ec5368bb0c94e159dfb9ca8b0279be37fe15ce14 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 15:16:21 -0700 Subject: [PATCH 51/56] update readme again [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a79c173..5ee013e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ partial class StringOrDouble That's it! `SumSharp` will generate members for the `StringOrDouble` class that allow it to be used as a discriminated union type. These members include: +- `Value` and `HasValue` properties, and `TryGetValue` methods to satisfy requirements for a non-boxing .NET 11 union type - `String` and `Double` static functions that construct instances of `StringOrDouble` - `AsString` and `AsDouble` properties that return either the underlying string/double value or throw an `InvalidOperationException` - `IsString` and `IsDouble` boolean properties From c8c970401160a8d6ca4c79236e456e48bd701f12 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 15:46:38 -0700 Subject: [PATCH 52/56] fix issue with constraints on case structs [skip ci] --- SumSharp.Generator/SymbolHandler.cs | 63 ++++++++++++++++++++++++----- Tests.Net11/Union.cs | 3 +- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 0a839ac..9e90d92 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -258,7 +258,7 @@ public CaseData(int index, string name, TypeInfo? typeInfo, bool storeAsObject, public CaseData[] UniqueCases { get; } - public Dictionary Net11StructNameMap { get; } + public Dictionary Net11StructNameMap { get; } // Cases grouped by type public IGrouping[] CaseGroups { get; } @@ -448,15 +448,58 @@ public SymbolHandler( { if (caseData.TypeInfo is null || !caseData.TypeInfo.IsGeneric) { - return caseData.Name; + return (caseData.Name, ""); } else { var parsedTypeArguments = TypeNameParser.ExtractLeafTypes(caseData.TypeInfo.Name); - var typeArguments = string.Join(", ", TypeArguments.Intersect(parsedTypeArguments)); + var caseStructTypeArguments = TypeArguments.Intersect(parsedTypeArguments).ToArray(); - return $"{caseData.Name}{(typeArguments == "" ? "" : $"<{typeArguments}>")}"; + var caseStructTypeConstraints = + caseStructTypeArguments + .Select(typeArg => + { + var typeSymbol = (ITypeParameterSymbol)allGenericTypeArguments.Single(symbol => symbol.Name == typeArg); + + var constraints = new List(); + + if (typeSymbol.HasNotNullConstraint) + { + constraints.Add("notnull"); + } + if (typeSymbol.HasReferenceTypeConstraint) + { + constraints.Add("class"); + } + if (typeSymbol.HasUnmanagedTypeConstraint) + { + constraints.Add("unmanaged"); + } + if (typeSymbol.HasValueTypeConstraint) + { + constraints.Add("struct"); + } + if (typeSymbol.HasConstructorConstraint) + { + constraints.Add("new()"); + } + + if (constraints.Count == 0) + { + return ""; + } + + return $"where {typeArg} : {string.Join(", ", constraints)}"; + }) + .Where(contraints => contraints.Length > 0) + .ToArray(); + + var nameWithTypeArgs = $"{caseData.Name}{(caseStructTypeArguments.Length == 0 ? "" : $"<{string.Join(", ", caseStructTypeArguments)}>")}"; + + var constraints = string.Join(" ", caseStructTypeConstraints); + + return (nameWithTypeArgs, constraints); } }); @@ -798,7 +841,7 @@ private void EmitFieldsAndConstructor() Builder.AppendLine($@" ///Used to implement .NET 11 union requirements. Use this type when pattern matching using C#'s built-in switch statement {GeneratedCodeAttribute} - public readonly record struct {Net11StructNameMap[caseData]}({caseData.TypeInfo.Name} Value);"); + public readonly record struct {Net11StructNameMap[caseData].NameWithTypeArgs}({caseData.TypeInfo.Name} Value) {Net11StructNameMap[caseData].Constraints};"); } } @@ -1255,7 +1298,7 @@ object IUnionMembers.Value else { Builder.Append($@" - {caseData.Index} => new {Net11StructNameMap[caseData]}(As{caseData.Name}Unsafe),"); + {caseData.Index} => new {Net11StructNameMap[caseData].NameWithTypeArgs}(As{caseData.Name}Unsafe),"); } } @@ -1269,7 +1312,7 @@ object IUnionMembers.Value foreach (var caseData in Cases) { Builder.AppendLine($@" - bool IUnionMembers.TryGetValue(out {Net11StructNameMap[caseData]} value) + bool IUnionMembers.TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value) {{ value = default; @@ -1301,7 +1344,7 @@ public interface IUnionMembers { Builder.AppendLine($@" ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({Net11StructNameMap[caseData]} _) => {Name}.{caseData.Name};"); + public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} _) => {Name}.{caseData.Name};"); } else @@ -1309,14 +1352,14 @@ public interface IUnionMembers Builder.AppendLine($@" ///Creates a that holds a value of type by invoking the case constructor with .Value ///This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({Net11StructNameMap[caseData]} value) => {Name}.{caseData.Name}(value.Value);"); + public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} value) => {Name}.{caseData.Name}(value.Value);"); } Builder.AppendLine($@" ///Attempts to get a value of type from the union. Returns true if the union holds a {caseData.Name}. ///Returns false otherwise. ///An out parameter that will be set to the underlying value, if present. - public bool TryGetValue(out {Net11StructNameMap[caseData]} value);"); + public bool TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value);"); } Builder.AppendLine($@" diff --git a/Tests.Net11/Union.cs b/Tests.Net11/Union.cs index 3d9b20d..6297728 100644 --- a/Tests.Net11/Union.cs +++ b/Tests.Net11/Union.cs @@ -47,7 +47,8 @@ public partial class InnerGeneric [UnionCase("Case2", "Dictionary")] [UnionCase("Case3", "(W[] WArray, bool Boolean)")] [UnionCase("Case4", "X")] - public partial struct ComplexGeneric + public partial struct ComplexGeneric + where W : class, new() where X : struct { From 66d61997f21a59e9a95a72e073e69d3126bf53a9 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 15:48:41 -0700 Subject: [PATCH 53/56] fix constraints --- SumSharp.Generator/SymbolHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index 9e90d92..c061d20 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -468,18 +468,20 @@ public SymbolHandler( { constraints.Add("notnull"); } - if (typeSymbol.HasReferenceTypeConstraint) + else if (typeSymbol.HasReferenceTypeConstraint) { constraints.Add("class"); } + if (typeSymbol.HasUnmanagedTypeConstraint) { constraints.Add("unmanaged"); } - if (typeSymbol.HasValueTypeConstraint) + else if (typeSymbol.HasValueTypeConstraint) { constraints.Add("struct"); } + if (typeSymbol.HasConstructorConstraint) { constraints.Add("new()"); From 06932c175b8b3865314c7eaa806265b8b4a21226 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 15:53:51 -0700 Subject: [PATCH 54/56] fix build and test workflow --- .github/workflows/build-and-test.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c2e253a..6e3d434 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,10 +26,16 @@ jobs: dotnet-version: ${{ matrix.dotnet }} - name: Restore dependencies - run: dotnet restore +# run: dotnet restore + run: | + dotnet restore ./Tests/Tests.csproj + dotnet restore ./Tests.AOT/Tests.AOT.csproj - name: Build solution - run: dotnet build --configuration Release --no-restore +# run: dotnet build --configuration Release --no-restore + run: | + dotnet build ./Tests/Tests.csproj --configuration Release --no-restore + dotnet build ./Tests.AOT/Tests.AOT.csproj --configuration Release --no-restore - name: Run unit tests run: dotnet test ./Tests/Tests.csproj --configuration Release --no-build --verbosity normal From 52e551ece6187d50a0a9342f80abc85a36c31995 Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 16:01:31 -0700 Subject: [PATCH 55/56] small fix to readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5ee013e..5dd59df 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A highly configurable C\# discriminated union library --- -1. [Why use `SumSharp`?](#why-use-sumsharp) +1. [Why use SumSharp?](#why-use-sumsharp) 2. [Installation](#installation) 3. [Quick start](#quick-start) - [Creating a DU type](#creating-a-du-type) @@ -47,7 +47,7 @@ There are many discriminated union libraries available for C\#, such as [`OneOf` ### Features -- **Integration with .NET 11 union types, allowing pattern matching using C#'s built-in pattern matching syntax** +- **Integration with .NET 11 union types, allowing use of C#'s built-in pattern matching syntax** - Unlimited number of cases - Support for class, struct, record, and record struct unions - Support for generic unions @@ -227,11 +227,11 @@ else if (x is None) #### Type union implementation details -1. `SumSharp` unions are *never null*. A non-null `SumSharp` union will never match with the `null` pattern, even if the underlying data it stores is null. -2. The `IUnionMembers.Value` property is never null and will always return a boxed instance of one of the case structs. -3. The `IUnionMembers.HasValue` property always returns true. -4. The various `TryGetValue` overloads will wrap the underlying data in one of the case structs. -5. `SumSharp` unions implement their corresponding `IUnionMembers` interface explicitly. This means that the `Value` and `HasValue` properties and the `TryGetValue` methods cannot be used unless you explicitly cast it to an `IUnionMembers`. In general you should not need to use any of these: they exist to satisfy the compiler's requirements for custom union types. +* `SumSharp` unions are *never null*. A non-null `SumSharp` union will never match with the `null` pattern, even if the underlying data it stores is null. +* The `IUnionMembers.Value` property is never null and will always return a boxed instance of one of the case structs. +* The `IUnionMembers.HasValue` property always returns true. +* The various `TryGetValue` overloads will wrap the underlying data in one of the case structs. +* `SumSharp` unions implement their corresponding `IUnionMembers` interface explicitly. This means that the `Value` and `HasValue` properties and the `TryGetValue` methods cannot be used unless you explicitly cast it to an `IUnionMembers`. In general you should not need to use any of these: they exist to satisfy the compiler's requirements for custom union types. --- From 8b0f0a1c57471a047a9d6cb0681e74dbbe63f29d Mon Sep 17 00:00:00 2001 From: Christian Daley Date: Sat, 8 Aug 2026 16:14:12 -0700 Subject: [PATCH 56/56] small change --- SumSharp.Generator/SymbolHandler.cs | 185 ++++++++++++++-------------- 1 file changed, 93 insertions(+), 92 deletions(-) diff --git a/SumSharp.Generator/SymbolHandler.cs b/SumSharp.Generator/SymbolHandler.cs index c061d20..9efc1ee 100644 --- a/SumSharp.Generator/SymbolHandler.cs +++ b/SumSharp.Generator/SymbolHandler.cs @@ -677,10 +677,10 @@ public string Emit() EmitCaseConstructors(); - EmitAs(); - EmitNativeUnion(); + EmitAs(); + EmitIs(); EmitMatch(); @@ -1195,6 +1195,97 @@ private void EmitCaseConstructors() } } + public void EmitNativeUnion() + { + Builder.AppendLine($@" +#if NET11_0_OR_GREATER + public interface IUnionMembers + {{ + ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed + public object Value {{ get; }} + + ///Always returns true. SumSharp unions are always considered non-null, even if the active case is empty + public bool HasValue {{ get; }}"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + Builder.AppendLine($@" + ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types + public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} _) => {Name}.{caseData.Name};"); + + } + else + { + Builder.AppendLine($@" + ///Creates a that holds a value of type by invoking the case constructor with .Value + ///This function exists to satisfy the compiler's requirements for .NET 11 union types + public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} value) => {Name}.{caseData.Name}(value.Value);"); + } + + Builder.AppendLine($@" + ///Attempts to get a value of type from the union. Returns true if the union holds a {caseData.Name}. + ///Returns false otherwise. + ///An out parameter that will be set to the underlying value, if present. + public bool TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value);"); + } + + Builder.AppendLine($@" + }}"); + + Builder.Append($@" + object IUnionMembers.Value + {{ + get + {{ + return Index switch + {{"); + + foreach (var caseData in Cases) + { + if (caseData.TypeInfo is null) + { + Builder.Append($@" + {caseData.Index} => new {Net11StructNameMap[caseData].NameWithTypeArgs}(),"); + } + else + { + Builder.Append($@" + {caseData.Index} => new {Net11StructNameMap[caseData].NameWithTypeArgs}(As{caseData.Name}Unsafe),"); + } + } + + Builder.AppendLine($@" + }}; + }} + }} + + bool IUnionMembers.HasValue => true;"); + + foreach (var caseData in Cases) + { + Builder.AppendLine($@" + bool IUnionMembers.TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value) + {{ + value = default; + + if (Index != {caseData.Index}) + {{ + return false; + }} + + {(caseData.TypeInfo is null ? "" : $"value = new(As{caseData.Name}Unsafe);")} + + return true; + }}"); + + } + + Builder.AppendLine(@" +#endif"); + } + public void EmitAs() { foreach (var caseData in Cases) @@ -1279,96 +1370,6 @@ public void EmitAs() } } - public void EmitNativeUnion() - { - Builder.Append($@" -#if NET11_0_OR_GREATER - object IUnionMembers.Value - {{ - get - {{ - return Index switch - {{"); - - foreach (var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - Builder.Append($@" - {caseData.Index} => new {caseData.Name}(),"); - } - else - { - Builder.Append($@" - {caseData.Index} => new {Net11StructNameMap[caseData].NameWithTypeArgs}(As{caseData.Name}Unsafe),"); - } - } - - Builder.AppendLine($@" - }}; - }} - }} - - bool IUnionMembers.HasValue => true;"); - - foreach (var caseData in Cases) - { - Builder.AppendLine($@" - bool IUnionMembers.TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value) - {{ - value = default; - - if (Index != {caseData.Index}) - {{ - return false; - }} - - {(caseData.TypeInfo is null ? "" : $"value = new(As{caseData.Name}Unsafe);")} - - return true; - }}"); - - } - - Builder.AppendLine($@" - - public interface IUnionMembers - {{ - ///Returns the underlying value of the union as an {Nullable}. Value types will be boxed - public object Value {{ get; }} - - ///Always returns true. SumSharp unions are always considered non-null, even if the active case is empty - public bool HasValue {{ get; }}"); - - foreach (var caseData in Cases) - { - if (caseData.TypeInfo is null) - { - Builder.AppendLine($@" - ///Returns the singleton . The input value is ignored. This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} _) => {Name}.{caseData.Name};"); - - } - else - { - Builder.AppendLine($@" - ///Creates a that holds a value of type by invoking the case constructor with .Value - ///This function exists to satisfy the compiler's requirements for .NET 11 union types - public static {Name} Create({Net11StructNameMap[caseData].NameWithTypeArgs} value) => {Name}.{caseData.Name}(value.Value);"); - } - - Builder.AppendLine($@" - ///Attempts to get a value of type from the union. Returns true if the union holds a {caseData.Name}. - ///Returns false otherwise. - ///An out parameter that will be set to the underlying value, if present. - public bool TryGetValue(out {Net11StructNameMap[caseData].NameWithTypeArgs} value);"); - } - - Builder.AppendLine($@" - }} -#endif"); - } - public void EmitIs() { foreach (var caseData in Cases)