From bdf6637e71c40db93309ceaeb5cc7853d9f06a75 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 13:40:35 +0200 Subject: [PATCH 01/57] Add proposal for enhanced enums in C# --- .../discriminated-unions/extended-enums.md | 600 ++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 meetings/working-groups/discriminated-unions/extended-enums.md diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md new file mode 100644 index 0000000000..ac558db8e6 --- /dev/null +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -0,0 +1,600 @@ +# Discriminated Unions and Enhanced Enums for C# + +This proposal extends C#'s union capabilities by introducing enhanced enums as algebraic data types. While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. + +## 1. Overview + +### Two Complementary Features + +C# will gain two separate features for different modeling needs: type unions (already specified) and enhanced enums (this proposal). These features work together but solve distinct problems in the type system. + +### At a Glance + +```csharp +// Type unions - one value, multiple possible types +union Result { string, ValidationError, NetworkException } + +// Enhanced enums - one type, multiple possible shapes +enum PaymentResult +{ + Success(string transactionId), + Declined(string reason), + PartialRefund(string originalId, decimal amount) +} +``` + +Type unions excel when you need to handle values that could be any of several existing types. Enhanced enums shine when modeling a single concept that can take different forms, each potentially carrying different data. + +## 2. Motivation and Design Philosophy + +### Distinct Problem Spaces + +Type unions and enhanced enums address fundamentally different modeling needs: + +**Type unions** bring together disparate existing types. You use them when the types already exist and you need to express "this or that" relationships. The focus is on the types themselves. + +**Enhanced enums** define a single type with multiple shapes or cases. You use them for algebraic data types where the focus is on the different forms a value can take, not on combining pre-existing types. + +### Limitations of Current Enums + +Today's C# enums have served us well but have significant limitations: + +1. **No associated data**: Cases are merely integral values, unable to carry additional information +2. **Not truly exhaustive**: Any integer can be cast to an enum type, breaking exhaustiveness guarantees +2. **Limited to integers**: Cannot use other primitive types like strings or doubles + +Enhanced enums address all these limitations while preserving the conceptual simplicity developers expect. + +### Building on Familiar Concepts + +By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide progressive disclosure. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. + +## 3. Type Unions (Brief Overview) + +### Core Concepts + +Type unions are fully specified in the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#summary). They provide: + +- Implicit conversions from case types to the union type +- Pattern matching that unwraps union contents +- Exhaustiveness checking in switch expressions +- Enhanced nullability tracking + +### Relationship to This Proposal + +This proposal leaves type unions unchanged. Enhanced enums are built independently, though both features share conceptual ground in making C#'s type system more expressive. Where unions excel at "or" relationships between types, enhanced enums excel at modeling variants within a single type. + +## 4. Enhanced Enums + +### Design Principles + +Enhanced enums follow these core principles: + +- **Progressive enhancement**: Simple enums stay simple; complexity is opt-in +- **Exhaustiveness**: The compiler knows all possible cases +- **Type safety**: Each case's data is strongly typed +- **Familiar syntax**: Builds on existing enum concepts + +### Syntax Extensions + +Enhanced enums extend the traditional enum syntax in three orthogonal ways: + +#### Extended Base Types + +Traditional enums only support integral types. Enhanced enums support any constant-bearing type: + +```csharp +enum Traditional : int { A = 1, B = 2 } +enum Extended : string { Active = "active", Inactive = "inactive" } +enum Extended : double { Pi = 3.14159, E = 2.71828 } +``` + +#### Shape Declarations + +A shape enum (ADT) is created by EITHER: +- Adding `class` or `struct` after `enum`, OR (inclusive) +- Having a parameter list on any enum member + +```csharp +enum class Result { Success, Failure } // shape enum via 'class' keyword +enum struct Result { Success, Failure } // shape enum via 'struct' keyword +enum Result { Success(), Failure() } // shape enum via parameter lists +``` + +When created via parameter lists alone, it defaults to `enum class` (reference type): + +```csharp +enum Result { Ok(int value), Error } // implicitly 'enum class' +// equivalent to: +enum class Result { Ok(int value), Error } +``` + +#### Data-Carrying Cases + +Shape enum members can have parameter lists to carry data: + +```csharp +enum Result +{ + Success(string id), + Failure(int code, string message) +} +``` + +#### Combination Rules + +- **Constant enums**: Can use extended base types but NOT have parameter lists. +- **Shape enums**: Can have parameter lists but NOT specify a base type +- **Mixing cases**: Cannot mix constant values and parameterized cases in the same enum + +```csharp +// ✓ Valid - constant enum with string base +enum Status : string { Active = "A", Inactive = "I" } + +// ✓ Valid - shape enum with data +enum class Result { Ok(int value), Error(string msg) } + +// ✗ Invalid - cannot mix constants and shapes +enum Bad { A = 1, B(string x) } + +// ✗ Invalid - shape enums cannot have base types +enum struct Bad : int { A, B } +``` + +For the complete formal grammar specification, see [Appendix A: Grammar Changes](#appendix-a-grammar-changes). + +### Constant Value Enums + +Enhanced constant enums extend traditional enums to support any primitive type that can have compile-time constants: + +```csharp +enum Priority : string +{ + Low = "low", + Medium = "medium", + High = "high" +} + +enum MathConstants : double +{ + Pi = 3.14159265359, + E = 2.71828182846, + GoldenRatio = 1.61803398875 +} +``` + +These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. + +### Shape Enums + +Shape enums are C#'s implementation of algebraic data types, allowing each case to carry different data. + +#### Basic Shape Declarations + +Shape enums can mix cases with and without data: + +```csharp +enum FileOperation +{ + Open(string path), + Close, + Read(byte[] buffer, int offset, int count), + Write(byte[] buffer) +} +``` + +Each case defines a constructor (and corresponding destructor) pattern. Cases without parameter lists are singletons, while cases with parameters create new instances. + +#### Reference vs Value Semantics + +**`enum class`** creates reference-type enums, stored on the heap: + +```csharp +enum class WebResponse +{ + Success(string content), + Error(int statusCode, string message), + Timeout +} +``` + +Benefits: +- Cheap to pass around (pointer-sized) +- No risk of struct tearing +- Natural null representation + +**`enum struct`** creates value-type enums, optimized for stack storage: + +```csharp +enum struct Option +{ + None, + Some(T value) +} +``` + +Benefits: +- No heap allocation +- Better cache locality +- Reduced GC pressure + +#### Members and Methods + +Enhanced enums can contain members just like unions: + +```csharp +enum class Result +{ + Success(T value), + Error(string message); + + public bool IsSuccess => this switch + { + Success(_) => true, + _ => false + }; + + public T GetValueOrDefault(T defaultValue) => this switch + { + Success(var value) => value, + _ => defaultValue + }; +} +``` + +Members are restricted to: +- Methods and properties (no additional state) +- Static members +- Nested types + +## 5. Pattern Matching + +### Enhanced Enum Patterns + +Enhanced enums support natural pattern matching syntax: + +```csharp +var message = operation switch +{ + Open(var path) => $"Opening {path}", + Close => "Closing file", + Read(_, var offset, var count) => $"Reading {count} bytes at {offset}", + Write(var buffer) => $"Writing {buffer.Length} bytes" +}; +``` + +The compiler understands the structure of each case and provides appropriate deconstruction. + +### Exhaustiveness + +Switch expressions over enhanced enums are exhaustive when all cases are handled: + +```csharp +enum Status { Active, Pending(DateTime since), Inactive } + +// Compiler knows this is exhaustive - no default needed +var description = status switch +{ + Active => "Currently active", + Pending(var date) => $"Pending since {date}", + Inactive => "Not active" +}; +``` + +### Comparison with Union Patterns + +Enhanced enums and type unions have different pattern matching behaviors: + +```csharp +// Union - patterns apply to the contained type +union Animal { Dog, Cat } +var sound = animal switch +{ + Dog d => d.Bark(), // Matches the Dog inside the union + Cat c => c.Meow() // Matches the Cat inside the union +}; + +// Enhanced enum - patterns match the enum's cases +enum Animal { Dog(string name), Cat(int lives) } +var description = animal switch +{ + Dog(var name) => $"Dog named {name}", // Matches the Dog case + Cat(var lives) => $"Cat with {lives} lives" // Matches the Cat case +}; +``` + +## 6. Translation Strategies + +### `enum class` Implementation + +Shape enums declared with `enum class` translate to abstract base classes with nested record types: + +```csharp +enum class Result +{ + Success(string value), + Failure(int code) +} + +// Translates to approximately: +abstract class Result : System.Enum +{ + private Result() { } + + public sealed record Success(string value) : Result; + public sealed record Failure(int code) : Result; +} +``` + +Singleton cases (those without parameters) use a shared instance: + +```csharp +enum class State { Ready, Processing, Complete } + +// Translates to approximately: +abstract class State : System.Enum +{ + private State() { } + + public sealed class Ready : State + { + public static readonly State Instance = new Ready(); + private Ready() { } + } + // Similar for Processing and Complete +} +``` + +### `enum struct` Implementation + +Shape enums declared with `enum struct` use a layout-optimized struct approach: + +```csharp +enum struct Option +{ + None, + Some(T value) +} + +// Translates to approximately: +struct Option : System.Enum +{ + private byte _discriminant; + private T _value; + + public bool IsNone => _discriminant == 0; + public bool IsSome => _discriminant == 1; + + public T GetSome() + { + if (_discriminant != 1) throw new InvalidOperationException(); + return _value; + } +} +``` + +For complex cases with multiple fields of different types, the compiler employs union-like storage optimization: + +```csharp +enum struct Message +{ + Text(string content), + Binary(byte[] data, int length), + Error(int code, string message) +} + +// Uses overlapping storage for fields, minimizing struct size +``` + +## 7. Examples and Use Cases + +### Migrating Traditional Enums + +Traditional enums can be progressively enhanced: + +```csharp +// Step 1: Traditional enum +enum OrderStatus { Pending = 1, Processing = 2, Shipped = 3, Delivered = 4 } + +// Step 2: Add data to specific states +enum OrderStatus +{ + Pending, + Processing(DateTime startedAt), + Shipped(string trackingNumber), + Delivered(DateTime deliveredAt) +} + +// Step 3: Add methods for common operations +enum OrderStatus +{ + Pending, + Processing(DateTime startedAt), + Shipped(string trackingNumber), + Delivered(DateTime deliveredAt); + + public bool IsComplete => this switch + { + Delivered(_) => true, + _ => false + }; +} +``` + +### Result and Option Types + +Enhanced enums make functional patterns natural: + +```csharp +enum class Result +{ + Ok(T value), + Error(E error); + + public Result Map(Func mapper) => this switch + { + Ok(var value) => new Ok(mapper(value)), + Error(var err) => new Error(err) + }; +} + +enum struct Option +{ + None, + Some(T value); + + public T GetOrDefault(T defaultValue) => this switch + { + Some(var value) => value, + None => defaultValue + }; +} +``` + +### State Machines + +Enhanced enums excel at modeling state machines with associated state data: + +```csharp +enum class ConnectionState +{ + Disconnected, + Connecting(DateTime attemptStarted, int attemptNumber), + Connected(IPEndPoint endpoint, DateTime connectedAt), + Reconnecting(IPEndPoint lastEndpoint, int retryCount, DateTime nextRetryAt), + Failed(string reason, Exception exception); + + public ConnectionState HandleTimeout() => this switch + { + Connecting(var started, var attempts) when attempts < 3 => + new Reconnecting(null, attempts + 1, DateTime.Now.AddSeconds(Math.Pow(2, attempts))), + Connecting(_, _) => + new Failed("Connection timeout", new TimeoutException()), + Connected(var endpoint, _) => + new Reconnecting(endpoint, 1, DateTime.Now.AddSeconds(1)), + _ => this + }; +} +``` + +## 8. Design Decisions and Trade-offs + +### Why Extend `enum` + +Extending the existing `enum` keyword rather than introducing new syntax provides several benefits: + +- **Familiarity**: Developers already understand enums conceptually +- **Progressive disclosure**: Simple cases remain simple +- **Cognitive load**: One concept (enums) instead of two (enums + ADTs) +- **Migration path**: Existing enums can be enhanced incrementally + +## 9. Performance Characteristics + +### Memory Layout + +**`enum class`**: +- Single pointer per instance (8 bytes on 64-bit) +- Heap allocation for each unique case instance +- Singleton pattern for parameter-less cases + +**`enum struct`**: +- Size equals discriminant (typically 1-4 bytes) plus largest case data +- Stack allocated or embedded in containing types +- Potential for struct tearing with concurrent access + +### Allocation Patterns + +```csharp +// Allocation per call +enum class Result { Ok(int value), Error(string message) } +var r1 = new Ok(42); // Heap allocation + +// No allocation +enum struct Result { Ok(int value), Error(string message) } +var r2 = new Ok(42); // Stack only +``` + +### Optimization Opportunities + +The compiler can optimize: +- Singleton cases to shared instances +- Small enum structs to fit in registers +- Pattern matching to jump tables +- Exhaustive switches to avoid default branches + +## 10. Runtime Representation + +Enhanced enums map to CLR types as follows: + +### Constant Enums +- Subclass `System.Enum` with appropriate backing field +- Metadata preserves enum semantics for reflection +- Compatible with existing enum APIs + +### Shape Enums +- **`enum class`**: Abstract class hierarchy with sealed nested classes +- **`enum struct`**: Struct with discriminant and union-style storage +- Custom attributes mark these as compiler-generated enhanced enums + +### Interop Considerations + +Enhanced enums maintain compatibility with: +- Existing `System.Enum` APIs where applicable +- Reflection-based frameworks +- Debugger visualization +- Binary serialization (with caveats for shape enums) + +### 10. Open Questions + +Several design decisions remain open: + +- **Case type accessibility**: Can users reference the generated nested types directly, or should they remain compiler-only? +- **Partial enums**: Should enhanced enums support `partial` for source generators? +- **Default values**: What should `default(EnumType)` produce for shape enums? +- **Serialization**: How should enhanced enums interact with System.Text.Json and other serializers? + +## Appendix A: Grammar Changes + +```antlr +enum_declaration + : attributes? enum_modifier* 'enum' enum_kind? identifier enum_base? enum_body ';'? + ; + +enum_kind + : 'class' + | 'struct' + ; + +enum_base + : ':' enum_underlying_type + ; + +enum_underlying_type + : integral_type + | 'bool' + | 'char' + | 'float' + | 'double' + | 'decimal' + | 'string' + | type_name // Must resolve to one of the above + ; + +enum_body + : '{' enum_member_declarations? '}' + | '{' enum_member_declarations ';' class_member_declarations '}' + ; + +enum_member_declarations + : enum_member_declaration (',' enum_member_declaration)* + ; + +enum_member_declaration + : attributes? identifier enum_member_initializer? + ; + +enum_member_initializer + : '=' constant_expression + | parameter_list + ; +``` From 62fd66ab7917147afebd8327d584ab71572334a1 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 13:56:41 +0200 Subject: [PATCH 02/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index ac558db8e6..6647c62ee3 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -71,7 +71,7 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Exhaustiveness**: The compiler knows all possible cases +- **Exhaustiveness**: The compiler knows all possible cases. See [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md) for more details. - **Type safety**: Each case's data is strongly typed - **Familiar syntax**: Builds on existing enum concepts From 87c74cd14173d5c81228e0f441b894f3db45ee7e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 13:59:48 +0200 Subject: [PATCH 03/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 6647c62ee3..62d287bf21 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -165,6 +165,8 @@ enum MathConstants : double These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. +Enhanced constant enums are similar to classical enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. For example, allowing/disallowing conversions from their underlying type, and treating pattern matching as exhaustive or not depending on if all declared values were explicitly matched. + ### Shape Enums Shape enums are C#'s implementation of algebraic data types, allowing each case to carry different data. From aac7ac6dfa546059a449679de79da23624b4f66c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:05:20 +0200 Subject: [PATCH 04/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 62d287bf21..b2a19a15c4 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -550,10 +550,11 @@ Enhanced enums maintain compatibility with: Several design decisions remain open: -- **Case type accessibility**: Can users reference the generated nested types directly, or should they remain compiler-only? -- **Partial enums**: Should enhanced enums support `partial` for source generators? -- **Default values**: What should `default(EnumType)` produce for shape enums? -- **Serialization**: How should enhanced enums interact with System.Text.Json and other serializers? +1. Can users reference the generated nested types directly, or should they remain compiler-only? +2. Should enhanced enums support `partial` for source generators? +3. What should `default(EnumType)` produce for shape enums? +4. How should enhanced enums interact with System.Text.Json and other serializers? +5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? ## Appendix A: Grammar Changes From 5c2b1aa9bd9f32e240a0a7e07d0412c11c39efe4 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:05:53 +0200 Subject: [PATCH 05/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index b2a19a15c4..530738d793 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -560,12 +560,7 @@ Several design decisions remain open: ```antlr enum_declaration - : attributes? enum_modifier* 'enum' enum_kind? identifier enum_base? enum_body ';'? - ; - -enum_kind - : 'class' - | 'struct' + : attributes? enum_modifier* 'enum' ('class' | 'struct')? identifier enum_base? enum_body ';'? ; enum_base From c6424cc3664ce815dce3db9b1804d2b3b5567c3c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:08:12 +0200 Subject: [PATCH 06/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 530738d793..aa09b4c46d 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -568,12 +568,7 @@ enum_base ; enum_underlying_type - : integral_type - | 'bool' - | 'char' - | 'float' - | 'double' - | 'decimal' + : simple_type // equivalent to all integral types, fp-types, decimal, bool and char | 'string' | type_name // Must resolve to one of the above ; From 90011a6a286cb360ec5f9dfff6283c41f592dc44 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:10:33 +0200 Subject: [PATCH 07/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 1 + 1 file changed, 1 insertion(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index aa09b4c46d..799de79cf7 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -555,6 +555,7 @@ Several design decisions remain open: 3. What should `default(EnumType)` produce for shape enums? 4. How should enhanced enums interact with System.Text.Json and other serializers? 5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? +6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. ## Appendix A: Grammar Changes From 42f616faa797d37d3fd42e13a8b3b1a99e86fe7f Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:12:41 +0200 Subject: [PATCH 08/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 799de79cf7..81a08b394a 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -1,6 +1,6 @@ # Discriminated Unions and Enhanced Enums for C# -This proposal extends C#'s union capabilities by introducing enhanced enums as algebraic data types. While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. +This proposal extends C#'s union capabilities by introducing enhanced enums as algebraic sum types. While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. ## 1. Overview @@ -33,7 +33,7 @@ Type unions and enhanced enums address fundamentally different modeling needs: **Type unions** bring together disparate existing types. You use them when the types already exist and you need to express "this or that" relationships. The focus is on the types themselves. -**Enhanced enums** define a single type with multiple shapes or cases. You use them for algebraic data types where the focus is on the different forms a value can take, not on combining pre-existing types. +**Enhanced enums** define a single type with multiple shapes or cases. You use them for algebraic sum types where the focus is on the different forms a value can take, not on combining pre-existing types. ### Limitations of Current Enums @@ -169,7 +169,7 @@ Enhanced constant enums are similar to classical enums in that they are open by ### Shape Enums -Shape enums are C#'s implementation of algebraic data types, allowing each case to carry different data. +Shape enums are C#'s implementation of algebraic sum types, allowing each case to carry different data. #### Basic Shape Declarations From c9b26ef08ee173fc14c77fc68cb672cec308fc55 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:18:30 +0200 Subject: [PATCH 09/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 81a08b394a..377873eafc 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -1,6 +1,6 @@ # Discriminated Unions and Enhanced Enums for C# -This proposal extends C#'s union capabilities by introducing enhanced enums as algebraic sum types. While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. +This proposal extends C#'s union capabilities by introducing enhanced enums as [algebraic sum types](https://en.wikipedia.org/wiki/Sum_type). While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. ## 1. Overview From 9ff3125ade34fc7b6ffcff6c09f922d9e9e0e18a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:19:19 +0200 Subject: [PATCH 10/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 377873eafc..77bd3c498f 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -6,7 +6,7 @@ This proposal extends C#'s union capabilities by introducing enhanced enums as [ ### Two Complementary Features -C# will gain two separate features for different modeling needs: type unions (already specified) and enhanced enums (this proposal). These features work together but solve distinct problems in the type system. +C# will gain two separate features for different modeling needs: [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) and enhanced enums (this proposal). These features work together but solve distinct problems in the type system. ### At a Glance From a1354defe7cba62bbfb6ee8df49312f1f204dea2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:22:35 +0200 Subject: [PATCH 11/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 77bd3c498f..5245b9b72d 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -72,7 +72,7 @@ Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in - **Exhaustiveness**: The compiler knows all possible cases. See [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md) for more details. -- **Type safety**: Each case's data is strongly typed +- **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. - **Familiar syntax**: Builds on existing enum concepts ### Syntax Extensions From 77967a8e919472c8b704c13e51df21ffb4cdda15 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:25:11 +0200 Subject: [PATCH 12/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 5245b9b72d..e16d97f29a 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -85,8 +85,8 @@ Traditional enums only support integral types. Enhanced enums support any consta ```csharp enum Traditional : int { A = 1, B = 2 } -enum Extended : string { Active = "active", Inactive = "inactive" } -enum Extended : double { Pi = 3.14159, E = 2.71828 } +enum Priority : string { Low = "low", Medium = "medium", High = "high" } +enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } ``` #### Shape Declarations From c36ef2ea6a91ddab32b4f80faa4740b6fe441590 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:26:41 +0200 Subject: [PATCH 13/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index e16d97f29a..ded6991eca 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -155,7 +155,7 @@ enum Priority : string High = "high" } -enum MathConstants : double +enum IrrationalConstants : double { Pi = 3.14159265359, E = 2.71828182846, From 64838ed6238f117d023191f248d5f017a2b3e9c0 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:32:43 +0200 Subject: [PATCH 14/57] Apply suggestions from code review --- meetings/working-groups/discriminated-unions/extended-enums.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index ded6991eca..50ec04bbcb 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -109,6 +109,8 @@ enum Result { Ok(int value), Error } // implicitly 'enum class' enum class Result { Ok(int value), Error } ``` +Shape declaration members have `record`-like semantics around concepts like equality. + #### Data-Carrying Cases Shape enum members can have parameter lists to carry data: @@ -556,6 +558,7 @@ Several design decisions remain open: 4. How should enhanced enums interact with System.Text.Json and other serializers? 5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? 6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. +7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. ## Appendix A: Grammar Changes From e42aa566f277f1be4e98a6746e5499260ea47421 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:50:07 +0200 Subject: [PATCH 15/57] Update extended-enums.md --- .../discriminated-unions/extended-enums.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 50ec04bbcb..29d9bb6570 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -71,7 +71,7 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Exhaustiveness**: The compiler knows all possible cases. See [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md) for more details. +- **Exhaustiveness**: The compiler knows all possible cases. Traditional "open" enums allow any value of the underlying type to be cast to the enum, even values not explicitly declared. "Closed" enums (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) restrict values to only those declared, enabling true exhaustiveness. **Shape enums are always exhaustive** - there's no way to create values outside the declared cases. - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. - **Familiar syntax**: Builds on existing enum concepts @@ -91,7 +91,7 @@ enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } #### Shape Declarations -A shape enum (ADT) is created by EITHER: +A shape enum (C#'s implementation of algebraic sum types) is created by EITHER: - Adding `class` or `struct` after `enum`, OR (inclusive) - Having a parameter list on any enum member @@ -109,7 +109,7 @@ enum Result { Ok(int value), Error } // implicitly 'enum class' enum class Result { Ok(int value), Error } ``` -Shape declaration members have `record`-like semantics around concepts like equality. +Shape declaration members have `record`-like semantics, meaning equality is value-based - two instances are equal if they have the same case and equal data values (like records). #### Data-Carrying Cases @@ -187,7 +187,7 @@ enum FileOperation } ``` -Each case defines a constructor (and corresponding destructor) pattern. Cases without parameter lists are singletons, while cases with parameters create new instances. +Each case defines a constructor (and corresponding deconstructor) pattern. Cases without parameter lists are singletons, while cases with parameters create new instances. #### Reference vs Value Semantics @@ -419,7 +419,7 @@ enum OrderStatus public bool IsComplete => this switch { - Delivered(_) => true, + Delivered => true, _ => false }; } @@ -437,7 +437,7 @@ enum class Result public Result Map(Func mapper) => this switch { - Ok(var value) => new Ok(mapper(value)), + Ok(var value) => new Ok(mapper(value)), // Note: Constructor syntax TBD, see Open Question #7 Error(var err) => new Error(err) }; } @@ -455,6 +455,8 @@ enum struct Option } ``` +*Note: The exact constructor syntax for shape enum cases is still being determined. See [Open Question #7](#10-open-questions) for the options under consideration.* + ### State Machines Enhanced enums excel at modeling state machines with associated state data: @@ -489,7 +491,7 @@ Extending the existing `enum` keyword rather than introducing new syntax provide - **Familiarity**: Developers already understand enums conceptually - **Progressive disclosure**: Simple cases remain simple -- **Cognitive load**: One concept (enums) instead of two (enums + ADTs) +- **Cognitive load**: One concept (enums) instead of two (enums + algebraic sum types) - **Migration path**: Existing enums can be enhanced incrementally ## 9. Performance Characteristics @@ -548,7 +550,7 @@ Enhanced enums maintain compatibility with: - Debugger visualization - Binary serialization (with caveats for shape enums) -### 10. Open Questions +## 10. Open Questions Several design decisions remain open: @@ -559,6 +561,7 @@ Several design decisions remain open: 5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? 6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. 7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. +8. Should enum cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only be feasible for `enum class` implementations, not `enum struct` due to layout constraints. ## Appendix A: Grammar Changes From 37c09ad0f5299633c8b24a4cd1175c3604e0986b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:54:13 +0200 Subject: [PATCH 16/57] Apply suggestions from code review --- .../discriminated-unions/extended-enums.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 29d9bb6570..32d691b90b 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -234,7 +234,7 @@ enum class Result public bool IsSuccess => this switch { - Success(_) => true, + Success(_) => true, // or just: Success => true, _ => false }; @@ -271,7 +271,7 @@ The compiler understands the structure of each case and provides appropriate dec ### Exhaustiveness -Switch expressions over enhanced enums are exhaustive when all cases are handled: +Switch expressions over enhanced shape enums are exhaustive when all cases are handled: ```csharp enum Status { Active, Pending(DateTime since), Inactive } @@ -513,11 +513,11 @@ Extending the existing `enum` keyword rather than introducing new syntax provide ```csharp // Allocation per call enum class Result { Ok(int value), Error(string message) } -var r1 = new Ok(42); // Heap allocation +var r1 = new Result.Ok(42); // Heap allocation // No allocation enum struct Result { Ok(int value), Error(string message) } -var r2 = new Ok(42); // Stack only +var r2 = new Result.Ok(42); // Stack only ``` ### Optimization Opportunities @@ -560,7 +560,7 @@ Several design decisions remain open: 4. How should enhanced enums interact with System.Text.Json and other serializers? 5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? 6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. -7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. +7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. This also likely ties into the investigations into [target-typed-static-member-lookup](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-static-member-lookup.md). 8. Should enum cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only be feasible for `enum class` implementations, not `enum struct` due to layout constraints. ## Appendix A: Grammar Changes From 7cb35cc61185a2f7ed8ba87a4d4b7a5502089ec4 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 14:57:36 +0200 Subject: [PATCH 17/57] Apply suggestions from code review --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 32d691b90b..a5754790e1 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -71,7 +71,7 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Exhaustiveness**: The compiler knows all possible cases. Traditional "open" enums allow any value of the underlying type to be cast to the enum, even values not explicitly declared. "Closed" enums (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) restrict values to only those declared, enabling true exhaustiveness. **Shape enums are always exhaustive** - there's no way to create values outside the declared cases. +- **Exhaustiveness**: The compiler knows all possible cases. Traditional "open" enums allow any value of the underlying type to be cast to the enum, even values not explicitly declared. "Closed" enums (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) restrict values to only those declared, enabling true exhaustiveness. **Shape enums** can be `open` or `closed`, just like constant enums. An open enum means that consumers should expect more shapes will arise in the future, and thus all processing code must be explicitly resilient to that. A `closed shape enum` states that all cases are known and unchanging. Consumption code that handles all declared cases is correct, and will not receive values outside of that set in the future. - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. - **Familiar syntax**: Builds on existing enum concepts From b1c843c47c9d1e477758fc975fa00ea6bc0bbdba Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:00:32 +0200 Subject: [PATCH 18/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index a5754790e1..0c1c3e6f1b 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -71,7 +71,7 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Exhaustiveness**: The compiler knows all possible cases. Traditional "open" enums allow any value of the underlying type to be cast to the enum, even values not explicitly declared. "Closed" enums (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) restrict values to only those declared, enabling true exhaustiveness. **Shape enums** can be `open` or `closed`, just like constant enums. An open enum means that consumers should expect more shapes will arise in the future, and thus all processing code must be explicitly resilient to that. A `closed shape enum` states that all cases are known and unchanging. Consumption code that handles all declared cases is correct, and will not receive values outside of that set in the future. +- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums guarantee their case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. For shape enums, "open" means the author reserves the right to add new shape cases in the future, while "closed" promises the current set of shapes is final. - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. - **Familiar syntax**: Builds on existing enum concepts From 1c0be4393189987411551b81191e29d9856fc135 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:02:06 +0200 Subject: [PATCH 19/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 0c1c3e6f1b..045534093a 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -71,9 +71,9 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums guarantee their case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. For shape enums, "open" means the author reserves the right to add new shape cases in the future, while "closed" promises the current set of shapes is final. - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. -- **Familiar syntax**: Builds on existing enum concepts +- **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts. +- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums guarantee their case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. For shape enums, "open" means the author reserves the right to add new shape cases in the future, while "closed" promises the current set of shapes is final. ### Syntax Extensions From 47b3ba0fff8ea4dcdfc660597aa3960c9a6a085d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:12:18 +0200 Subject: [PATCH 20/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 045534093a..8d8888c236 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -2,6 +2,24 @@ This proposal extends C#'s union capabilities by introducing enhanced enums as [algebraic sum types](https://en.wikipedia.org/wiki/Sum_type). While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. +It also aims to consolidate the majority of the design space and feedback over many years in this repository, especially from: + +- [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings +- [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class +- [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct +- [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations +- [#3760](https://github.com/dotnet/csharplang/discussions/3760) - Community "shopping list" of desired discriminated union features +- [#7544](https://github.com/dotnet/csharplang/issues/7544) - Simple encoding of unions exploring type unions vs tagged unions +- [#8804](https://github.com/dotnet/csharplang/discussions/8804) - String-based enums for cloud services with extensibility needs +- [#1860](https://github.com/dotnet/csharplang/issues/1860) - Long-running request for string enum support citing TypeScript/Java +- [#9010](https://github.com/dotnet/csharplang/discussions/9010) - "Closed" enum types that guarantee exhaustiveness +- [#6927](https://github.com/dotnet/csharplang/discussions/6927) - Constant enums discussion around strict value enforcement +- [#7854](https://github.com/dotnet/csharplang/issues/7854) - Exhaustiveness checking for ADT patterns using private constructors +- [#8942](https://github.com/dotnet/csharplang/discussions/8942) - Track subtype exhaustiveness for closed hierarchies +- [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case +- [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result +- [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors + ## 1. Overview ### Two Complementary Features From 45130f7a6b2d7e0b17905f8e8c037114f59c8621 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:14:00 +0200 Subject: [PATCH 21/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 8d8888c236..0e93a3a039 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -5,6 +5,9 @@ This proposal extends C#'s union capabilities by introducing enhanced enums as [ It also aims to consolidate the majority of the design space and feedback over many years in this repository, especially from: - [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings + +as well as: + - [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct - [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations @@ -19,6 +22,7 @@ It also aims to consolidate the majority of the design space and feedback over m - [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case - [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result - [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors + ## 1. Overview From 64de21e441f7fc846fd2c47cc6021e6c9c51f2ad Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:14:53 +0200 Subject: [PATCH 22/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 0e93a3a039..dcfce8f27c 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -7,7 +7,7 @@ It also aims to consolidate the majority of the design space and feedback over m - [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings as well as: - +
- [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct - [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations @@ -22,7 +22,7 @@ as well as: - [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case - [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result - [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors -
+ ## 1. Overview From 100dfd4ecd8c80c6706f0119288a88d3e563363b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:16:56 +0200 Subject: [PATCH 23/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index dcfce8f27c..3b58a30606 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -8,20 +8,20 @@ It also aims to consolidate the majority of the design space and feedback over m as well as:
-- [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class -- [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct -- [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations -- [#3760](https://github.com/dotnet/csharplang/discussions/3760) - Community "shopping list" of desired discriminated union features -- [#7544](https://github.com/dotnet/csharplang/issues/7544) - Simple encoding of unions exploring type unions vs tagged unions -- [#8804](https://github.com/dotnet/csharplang/discussions/8804) - String-based enums for cloud services with extensibility needs -- [#1860](https://github.com/dotnet/csharplang/issues/1860) - Long-running request for string enum support citing TypeScript/Java -- [#9010](https://github.com/dotnet/csharplang/discussions/9010) - "Closed" enum types that guarantee exhaustiveness -- [#6927](https://github.com/dotnet/csharplang/discussions/6927) - Constant enums discussion around strict value enforcement -- [#7854](https://github.com/dotnet/csharplang/issues/7854) - Exhaustiveness checking for ADT patterns using private constructors -- [#8942](https://github.com/dotnet/csharplang/discussions/8942) - Track subtype exhaustiveness for closed hierarchies -- [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case -- [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result -- [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors + - [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class + - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct + - [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations + - [#3760](https://github.com/dotnet/csharplang/discussions/3760) - Community "shopping list" of desired discriminated union features + - [#7544](https://github.com/dotnet/csharplang/issues/7544) - Simple encoding of unions exploring type unions vs tagged unions + - [#8804](https://github.com/dotnet/csharplang/discussions/8804) - String-based enums for cloud services with extensibility needs + - [#1860](https://github.com/dotnet/csharplang/issues/1860) - Long-running request for string enum support citing TypeScript/Java + - [#9010](https://github.com/dotnet/csharplang/discussions/9010) - "Closed" enum types that guarantee exhaustiveness + - [#6927](https://github.com/dotnet/csharplang/discussions/6927) - Constant enums discussion around strict value enforcement + - [#7854](https://github.com/dotnet/csharplang/issues/7854) - Exhaustiveness checking for ADT patterns using private constructors + - [#8942](https://github.com/dotnet/csharplang/discussions/8942) - Track subtype exhaustiveness for closed hierarchies + - [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case + - [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result + - [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors
## 1. Overview From 806707f82d21d70ef551bf969b0cd29131368dce Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:18:52 +0200 Subject: [PATCH 24/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 3b58a30606..cf7e780555 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -8,6 +8,8 @@ It also aims to consolidate the majority of the design space and feedback over m as well as:
+ any many more... + - [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct - [#7016](https://github.com/dotnet/csharplang/issues/7016) - Fast, efficient unions proposal focusing on struct-based implementations From aec8c501671af30da925fca1f81dbef89be33e08 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:19:48 +0200 Subject: [PATCH 25/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 1 - 1 file changed, 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index cf7e780555..c5738193f8 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -6,7 +6,6 @@ It also aims to consolidate the majority of the design space and feedback over m - [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings -as well as:
any many more... From cb98a73f9cd62f42536a510e1f1af1264b1db153 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:20:06 +0200 Subject: [PATCH 26/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index c5738193f8..e78acbbe4e 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -7,7 +7,7 @@ It also aims to consolidate the majority of the design space and feedback over m - [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings
- any many more... + as well as... - [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct From 6bdfd7adb83ab4ee361f128db7267b4cf258098c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:30:13 +0200 Subject: [PATCH 27/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index e78acbbe4e..2b749417dd 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -294,10 +294,10 @@ The compiler understands the structure of each case and provides appropriate dec ### Exhaustiveness -Switch expressions over enhanced shape enums are exhaustive when all cases are handled: +Enhanced shape enums are similar to classical enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. Closed versus open shape enums treat pattern matching as exhaustive or not depending on if all declared values were explicitly matched. ```csharp -enum Status { Active, Pending(DateTime since), Inactive } +closed enum Status { Active, Pending(DateTime since), Inactive } // Compiler knows this is exhaustive - no default needed var description = status switch From 42a035b2c453d8c1e1c7576fa111f3738fe3a8be Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:32:50 +0200 Subject: [PATCH 28/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 2b749417dd..dc702459cf 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -460,7 +460,7 @@ enum class Result public Result Map(Func mapper) => this switch { - Ok(var value) => new Ok(mapper(value)), // Note: Constructor syntax TBD, see Open Question #7 + Ok(var value) => new Ok(mapper(value)), // Note: Construction syntax TBD, see Open Question #7 Error(var err) => new Error(err) }; } @@ -478,7 +478,7 @@ enum struct Option } ``` -*Note: The exact constructor syntax for shape enum cases is still being determined. See [Open Question #7](#10-open-questions) for the options under consideration.* +*Note: The exact construction syntax for shape enum cases is still being determined. See [Open Question #7](#10-open-questions) for the options under consideration.* ### State Machines From 2cb0f4f1cdb1e0064c9d4dc60f68a9d8ee4ea31e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:40:42 +0200 Subject: [PATCH 29/57] Apply suggestions from code review --- .../discriminated-unions/extended-enums.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index dc702459cf..58bbe3d410 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -136,7 +136,7 @@ Shape declaration members have `record`-like semantics, meaning equality is valu #### Data-Carrying Cases -Shape enum members can have parameter lists to carry data: +Shape enum members can have parameter lists, similar to a record's primary constructor, to carry data: ```csharp enum Result @@ -245,9 +245,11 @@ Benefits: - Better cache locality - Reduced GC pressure +Similar to evolution of `records`, these variations can ship at separate times. + #### Members and Methods -Enhanced enums can contain members just like unions: +Enhanced enums can contain members just like unions. This applies to both constant and shape enums. ```csharp enum class Result @@ -270,8 +272,8 @@ enum class Result ``` Members are restricted to: -- Methods and properties (no additional state) -- Static members +- Methods, operators, properties and indexers (members that add no additional state). Though an open question tracks if we might want to allow additional state in a shape enum. +- Static members. An open question tracks if that could potentially include consructors. - Nested types ## 5. Pattern Matching From 4810fce0a66660a6baea46c2f2d1e183ee78c2c2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:41:20 +0200 Subject: [PATCH 30/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 58bbe3d410..3902bf3142 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -272,8 +272,8 @@ enum class Result ``` Members are restricted to: -- Methods, operators, properties and indexers (members that add no additional state). Though an open question tracks if we might want to allow additional state in a shape enum. -- Static members. An open question tracks if that could potentially include consructors. +- Instance methods, properties and indexers (members that add no additional state). Though an open question tracks if we might want to allow additional state in a shape enum. +- Static members. An open question tracks if that could potentially include constructors. - Nested types ## 5. Pattern Matching From d0b308fdeec757b4b5195ae5bc45ac1a031c5070 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 15:46:58 +0200 Subject: [PATCH 31/57] Apply suggestions from code review --- .../working-groups/discriminated-unions/extended-enums.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 3902bf3142..f09aaea9cd 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -447,6 +447,8 @@ enum OrderStatus Delivered => true, _ => false }; + + // Alternatively: public bool IsComplete => this is Delivered; } ``` @@ -517,7 +519,7 @@ Extending the existing `enum` keyword rather than introducing new syntax provide - **Familiarity**: Developers already understand enums conceptually - **Progressive disclosure**: Simple cases remain simple - **Cognitive load**: One concept (enums) instead of two (enums + algebraic sum types) -- **Migration path**: Existing enums can be enhanced incrementally +- **Migration path**: Existing enums can be enhanced incrementally. Changing from a constant to shape based enum would be a breaking binary change, though ideally not a source break. ## 9. Performance Characteristics From 298e7b6ad4c29f988c1a44b953a582ce5c978004 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 19:05:08 +0200 Subject: [PATCH 32/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index f09aaea9cd..9043a5b1f1 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -23,6 +23,10 @@ It also aims to consolidate the majority of the design space and feedback over m - [#8926](https://github.com/dotnet/csharplang/discussions/8926) - Extensive discussion on Option as canonical DU use case - [#7010](https://github.com/dotnet/csharplang/discussions/7010) - Union types discussion heavily featuring Option and Result - [#274](https://github.com/dotnet/csharplang/discussions/274) - Java-style class-level enums with methods and constructors + - [#8987](https://github.com/dotnet/csharplang/discussions/8987) - Champion "permit methods in enum declarations" + - [#5937](https://github.com/dotnet/csharplang/discussions/5937) - Smart Enums In C# Like Java" (extra state!) + - [#782](https://github.com/dotnet/csharplang/discussions/782) Sealed enums (completeness checking in switch statements) + - [#2669](https://github.com/dotnet/csharplang/discussions/2669) Feature request: Partial enums
## 1. Overview From 8729b99d238ad8da4bba0d6692526fc22402e3ab Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 19:13:02 +0200 Subject: [PATCH 33/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 9043a5b1f1..bdc54cab10 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -118,8 +118,8 @@ enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } #### Shape Declarations -A shape enum (C#'s implementation of algebraic sum types) is created by EITHER: -- Adding `class` or `struct` after `enum`, OR (inclusive) +Any of the following creates a shape enum (C#'s implementation of algebraic sum types): +- Adding `class` or `struct` after `enum` - Having a parameter list on any enum member ```csharp From fc8902de434ad6ce2a619564379d6e26f0b27e74 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 20:56:22 +0200 Subject: [PATCH 34/57] Update meetings/working-groups/discriminated-unions/extended-enums.md Co-authored-by: Joseph Musser --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index bdc54cab10..2cfd4bf928 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -74,7 +74,7 @@ Enhanced enums address all these limitations while preserving the conceptual sim ### Building on Familiar Concepts -By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide progressive disclosure. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. +By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide a grow-up story. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. ## 3. Type Unions (Brief Overview) From 110a6f3987b50f872cfbdb4e5c4463d18caf1578 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 20:56:47 +0200 Subject: [PATCH 35/57] Update meetings/working-groups/discriminated-unions/extended-enums.md Co-authored-by: Joseph Musser --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 2cfd4bf928..83d072689c 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -100,7 +100,7 @@ Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. - **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts. -- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums guarantee their case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. For shape enums, "open" means the author reserves the right to add new shape cases in the future, while "closed" promises the current set of shapes is final. +- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums can be used to signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums signal that there is no need to handle unknown cases, such as when the case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. ### Syntax Extensions From bbfed396a7ff24fc40780ea241937aa4f460e493 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 20:58:49 +0200 Subject: [PATCH 36/57] Apply suggestions from code review Co-authored-by: Joseph Musser --- .../discriminated-unions/extended-enums.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 83d072689c..a06ab6c8d3 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -194,7 +194,7 @@ enum IrrationalConstants : double These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. -Enhanced constant enums are similar to classical enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. For example, allowing/disallowing conversions from their underlying type, and treating pattern matching as exhaustive or not depending on if all declared values were explicitly matched. +Enhanced constant enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. For example, allowing/disallowing conversions from their underlying type, and treating pattern matching as exhaustive or not depending on if all declared values were explicitly matched. ### Shape Enums @@ -216,9 +216,9 @@ enum FileOperation Each case defines a constructor (and corresponding deconstructor) pattern. Cases without parameter lists are singletons, while cases with parameters create new instances. -#### Reference vs Value Semantics +#### Reference Type and Value Type -**`enum class`** creates reference-type enums, stored on the heap: +**`enum class`** creates reference-type enums, stored according to underlying runtime choices (normally the heap): ```csharp enum class WebResponse @@ -234,7 +234,7 @@ Benefits: - No risk of struct tearing - Natural null representation -**`enum struct`** creates value-type enums, optimized for stack storage: +**`enum struct`** creates value-type enums, optimized for inline storage: ```csharp enum struct Option @@ -253,7 +253,7 @@ Similar to evolution of `records`, these variations can ship at separate times. #### Members and Methods -Enhanced enums can contain members just like unions. This applies to both constant and shape enums. +Enums can contain members just like unions. This applies to both constant and shape enums. ```csharp enum class Result @@ -300,7 +300,7 @@ The compiler understands the structure of each case and provides appropriate dec ### Exhaustiveness -Enhanced shape enums are similar to classical enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. Closed versus open shape enums treat pattern matching as exhaustive or not depending on if all declared values were explicitly matched. +Enhanced shape enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. Closed versus open shape enums treat pattern matching as exhaustive or not depending on if all declared values were explicitly matched. ```csharp closed enum Status { Active, Pending(DateTime since), Inactive } @@ -587,12 +587,13 @@ Several design decisions remain open: 1. Can users reference the generated nested types directly, or should they remain compiler-only? 2. Should enhanced enums support `partial` for source generators? -3. What should `default(EnumType)` produce for shape enums? +3. What should `default(EnumType)` produce for struct-based shape enums? 4. How should enhanced enums interact with System.Text.Json and other serializers? 5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? 6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. 7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. This also likely ties into the investigations into [target-typed-static-member-lookup](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-static-member-lookup.md). 8. Should enum cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only be feasible for `enum class` implementations, not `enum struct` due to layout constraints. +9. This could open the door for enums (all enums, or non-classic enums) to automatically implement IEquatable, ISpanFormattable, and other interfaces when appropriate such as IBinaryInteger. This has been requested at , , and for example. ## Appendix A: Grammar Changes From fc46de76638da41ef522e6f789e6b4558c1f9a04 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:02:34 +0200 Subject: [PATCH 37/57] Update extended-enums.md --- .../discriminated-unions/extended-enums.md | 337 ++++++++++-------- 1 file changed, 183 insertions(+), 154 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index a06ab6c8d3..d507612fe0 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -89,7 +89,7 @@ Type unions are fully specified in the [unions proposal](https://raw.githubuserc ### Relationship to This Proposal -This proposal leaves type unions unchanged. Enhanced enums are built independently, though both features share conceptual ground in making C#'s type system more expressive. Where unions excel at "or" relationships between types, enhanced enums excel at modeling variants within a single type. +This proposal builds on type unions for shape enums. Shape enums become a convenient syntax for declaring both the case types and their union in a single declaration, with all behaviors deriving from the underlying union machinery. ## 4. Enhanced Enums @@ -98,9 +98,9 @@ This proposal leaves type unions unchanged. Enhanced enums are built independent Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner. -- **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts. -- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums can be used to signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums signal that there is no need to handle unknown cases, such as when the case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. +- **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner +- **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts +- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) ### Syntax Extensions @@ -136,8 +136,6 @@ enum Result { Ok(int value), Error } // implicitly 'enum class' enum class Result { Ok(int value), Error } ``` -Shape declaration members have `record`-like semantics, meaning equality is value-based - two instances are equal if they have the same case and equal data values (like records). - #### Data-Carrying Cases Shape enum members can have parameter lists, similar to a record's primary constructor, to carry data: @@ -152,7 +150,7 @@ enum Result #### Combination Rules -- **Constant enums**: Can use extended base types but NOT have parameter lists. +- **Constant enums**: Can use extended base types but NOT have parameter lists - **Shape enums**: Can have parameter lists but NOT specify a base type - **Mixing cases**: Cannot mix constant values and parameterized cases in the same enum @@ -194,11 +192,11 @@ enum IrrationalConstants : double These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. -Enhanced constant enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. For example, allowing/disallowing conversions from their underlying type, and treating pattern matching as exhaustive or not depending on if all declared values were explicitly matched. +Enhanced constant enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). ### Shape Enums -Shape enums are C#'s implementation of algebraic sum types, allowing each case to carry different data. +Shape enums are C#'s implementation of algebraic sum types. They provide convenient syntax for declaring a set of case types and their union in a single declaration. #### Basic Shape Declarations @@ -214,11 +212,11 @@ enum FileOperation } ``` -Each case defines a constructor (and corresponding deconstructor) pattern. Cases without parameter lists are singletons, while cases with parameters create new instances. +Each case with parameters generates a corresponding type (typically a record). Cases without parameters generate singleton types. The enum itself becomes a union of these generated types. #### Reference Type and Value Type -**`enum class`** creates reference-type enums, stored according to underlying runtime choices (normally the heap): +**`enum class`** creates a union where the case types are reference types: ```csharp enum class WebResponse @@ -230,11 +228,11 @@ enum class WebResponse ``` Benefits: -- Cheap to pass around (pointer-sized) +- Cheap to pass around (pointer-sized union) - No risk of struct tearing - Natural null representation -**`enum struct`** creates value-type enums, optimized for inline storage: +**`enum struct`** creates a union with optimized value-type storage: ```csharp enum struct Option @@ -253,7 +251,7 @@ Similar to evolution of `records`, these variations can ship at separate times. #### Members and Methods -Enums can contain members just like unions. This applies to both constant and shape enums. +Enums can contain members just like unions. This applies to both constant and shape enums: ```csharp enum class Result @@ -263,7 +261,7 @@ enum class Result public bool IsSuccess => this switch { - Success(_) => true, // or just: Success => true, + Success(_) => true, _ => false }; @@ -276,71 +274,24 @@ enum class Result ``` Members are restricted to: -- Instance methods, properties and indexers (members that add no additional state). Though an open question tracks if we might want to allow additional state in a shape enum. -- Static members. An open question tracks if that could potentially include constructors. +- Instance methods, properties and indexers (members that add no additional state) +- Static members - Nested types -## 5. Pattern Matching +## 5. Translation Strategy -### Enhanced Enum Patterns +### Shape Enum Translation Overview -Enhanced enums support natural pattern matching syntax: +Shape enums are syntactic sugar that generates: +1. Individual case types (as records or similar types) +2. A union type that combines these cases +3. Convenience members for construction and pattern matching -```csharp -var message = operation switch -{ - Open(var path) => $"Opening {path}", - Close => "Closing file", - Read(_, var offset, var count) => $"Reading {count} bytes at {offset}", - Write(var buffer) => $"Writing {buffer.Length} bytes" -}; -``` - -The compiler understands the structure of each case and provides appropriate deconstruction. - -### Exhaustiveness +### `enum class` Translation -Enhanced shape enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open and closed enums with non-integral backing types behave similarly to their integral counterparts. Closed versus open shape enums treat pattern matching as exhaustive or not depending on if all declared values were explicitly matched. - -```csharp -closed enum Status { Active, Pending(DateTime since), Inactive } - -// Compiler knows this is exhaustive - no default needed -var description = status switch -{ - Active => "Currently active", - Pending(var date) => $"Pending since {date}", - Inactive => "Not active" -}; -``` - -### Comparison with Union Patterns - -Enhanced enums and type unions have different pattern matching behaviors: - -```csharp -// Union - patterns apply to the contained type -union Animal { Dog, Cat } -var sound = animal switch -{ - Dog d => d.Bark(), // Matches the Dog inside the union - Cat c => c.Meow() // Matches the Cat inside the union -}; - -// Enhanced enum - patterns match the enum's cases -enum Animal { Dog(string name), Cat(int lives) } -var description = animal switch -{ - Dog(var name) => $"Dog named {name}", // Matches the Dog case - Cat(var lives) => $"Cat with {lives} lives" // Matches the Cat case -}; -``` - -## 6. Translation Strategies - -### `enum class` Implementation - -Shape enums declared with `enum class` translate to abstract base classes with nested record types: +An `enum class` generates: +1. A set of record class types for each case +2. A union declaration combining these types ```csharp enum class Result @@ -349,38 +300,44 @@ enum class Result Failure(int code) } -// Translates to approximately: -abstract class Result : System.Enum +// Conceptually translates to: + +// Generated case types +public sealed record class Result_Success(string value); +public sealed record class Result_Failure(int code); + +// Generated union +public union Result(Result_Success, Result_Failure) { - private Result() { } - - public sealed record Success(string value) : Result; - public sealed record Failure(int code) : Result; + // Convenience constructors/factories + public static Result Success(string value) => new Result(new Result_Success(value)); + public static Result Failure(int code) => new Result(new Result_Failure(code)); } ``` -Singleton cases (those without parameters) use a shared instance: +Singleton cases (those without parameters) generate types with shared instances: ```csharp enum class State { Ready, Processing, Complete } -// Translates to approximately: -abstract class State : System.Enum +// Conceptually translates to: +public sealed class State_Ready { - private State() { } - - public sealed class Ready : State - { - public static readonly State Instance = new Ready(); - private Ready() { } - } - // Similar for Processing and Complete + public static readonly State_Ready Instance = new(); + private State_Ready() { } +} +// Similar for Processing and Complete + +public union State(State_Ready, State_Processing, State_Complete) +{ + public static State Ready => new State(State_Ready.Instance); + // etc. } ``` -### `enum struct` Implementation +### `enum struct` Translation -Shape enums declared with `enum struct` use a layout-optimized struct approach: +An `enum struct` also generates case types and a union, but the union uses an optimized storage layout as permitted by the [non-boxing union access pattern](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#union-patterns): ```csharp enum struct Option @@ -389,36 +346,112 @@ enum struct Option Some(T value) } -// Translates to approximately: -struct Option : System.Enum +// Conceptually translates to: + +// Generated case types +public readonly struct Option_None { } +public readonly record struct Option_Some(T value); + +// Generated union with optimized storage +public struct Option : IUnion { + // Optimized layout: discriminator + space for largest case private byte _discriminant; - private T _value; + private T _value; // Space for Some's data - public bool IsNone => _discriminant == 0; - public bool IsSome => _discriminant == 1; + // Implements IUnion.Value + object? IUnion.Value => _discriminant switch + { + 1 => new Option_None(), + 2 => new Option_Some(_value), + _ => null + }; - public T GetSome() + // Non-boxing access pattern + public bool HasValue => _discriminant != 0; + + public bool TryGetValue(out Option_None value) + { + value = default; + return _discriminant == 1; + } + + public bool TryGetValue(out Option_Some value) { - if (_discriminant != 1) throw new InvalidOperationException(); - return _value; + if (_discriminant == 2) + { + value = new Option_Some(_value); + return true; + } + value = default!; + return false; } + + // Constructors + public Option(Option_None _) => _discriminant = 1; + public Option(Option_Some some) => (_discriminant, _value) = (2, some.value); + + // Convenience factories + public static Option None => new Option(new Option_None()); + public static Option Some(T value) => new Option(new Option_Some(value)); } ``` -For complex cases with multiple fields of different types, the compiler employs union-like storage optimization: +For more complex cases with multiple fields of different types, the compiler would allocate: +- A discriminator field +- Unmanaged memory sufficient for the largest unmanaged data +- Reference fields sufficient for the maximum number of references in any case + +This optimized layout provides the benefits of struct storage while maintaining full union semantics. + +## 6. Pattern Matching and Behaviors + +### Pattern Matching + +Shape enums inherit all pattern matching behavior from their underlying union implementation. The compiler provides convenient syntax that maps to the underlying union patterns: ```csharp -enum struct Message +var message = operation switch { - Text(string content), - Binary(byte[] data, int length), - Error(int code, string message) -} + Open(var path) => $"Opening {path}", + Close => "Closing file", + Read(_, var offset, var count) => $"Reading {count} bytes at {offset}", + Write(var buffer) => $"Writing {buffer.Length} bytes" +}; +``` + +This works because: +- Each case name corresponds to a generated type +- The union's pattern matching unwraps to check these types +- Deconstruction works via the generated records' deconstructors + +### Exhaustiveness + +Shape enums benefit from union exhaustiveness checking. When all case types are handled, the switch is exhaustive: + +```csharp +closed enum Status { Active, Pending(DateTime since), Inactive } -// Uses overlapping storage for fields, minimizing struct size +// Compiler knows this is exhaustive - no default needed +var description = status switch +{ + Active => "Currently active", + Pending(var date) => $"Pending since {date}", + Inactive => "Not active" +}; ``` +Open vs closed shape enums follow the same rules as type unions for exhaustiveness. + +### Other Union Behaviors + +Shape enums automatically inherit from unions: +- **Implicit conversions** from case values to the enum type +- **Nullability tracking** for the union's contents +- **Well-formedness** guarantees about values + +See the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#union-behaviors) for complete details on these behaviors. + ## 7. Examples and Use Cases ### Migrating Traditional Enums @@ -451,8 +484,6 @@ enum OrderStatus Delivered => true, _ => false }; - - // Alternatively: public bool IsComplete => this is Delivered; } ``` @@ -468,8 +499,8 @@ enum class Result public Result Map(Func mapper) => this switch { - Ok(var value) => new Ok(mapper(value)), // Note: Construction syntax TBD, see Open Question #7 - Error(var err) => new Error(err) + Ok(var value) => Result.Ok(mapper(value)), + Error(var err) => Result.Error(err) }; } @@ -486,8 +517,6 @@ enum struct Option } ``` -*Note: The exact construction syntax for shape enum cases is still being determined. See [Open Question #7](#10-open-questions) for the options under consideration.* - ### State Machines Enhanced enums excel at modeling state machines with associated state data: @@ -504,11 +533,11 @@ enum class ConnectionState public ConnectionState HandleTimeout() => this switch { Connecting(var started, var attempts) when attempts < 3 => - new Reconnecting(null, attempts + 1, DateTime.Now.AddSeconds(Math.Pow(2, attempts))), + ConnectionState.Reconnecting(null, attempts + 1, DateTime.Now.AddSeconds(Math.Pow(2, attempts))), Connecting(_, _) => - new Failed("Connection timeout", new TimeoutException()), + ConnectionState.Failed("Connection timeout", new TimeoutException()), Connected(var endpoint, _) => - new Reconnecting(endpoint, 1, DateTime.Now.AddSeconds(1)), + ConnectionState.Reconnecting(endpoint, 1, DateTime.Now.AddSeconds(1)), _ => this }; } @@ -523,32 +552,44 @@ Extending the existing `enum` keyword rather than introducing new syntax provide - **Familiarity**: Developers already understand enums conceptually - **Progressive disclosure**: Simple cases remain simple - **Cognitive load**: One concept (enums) instead of two (enums + algebraic sum types) -- **Migration path**: Existing enums can be enhanced incrementally. Changing from a constant to shape based enum would be a breaking binary change, though ideally not a source break. +- **Migration path**: Existing enums can be enhanced incrementally + +### Building on Unions + +By implementing shape enums as syntactic sugar over type unions, we ensure: +- Consistent semantics between the two features +- All union optimizations and improvements benefit shape enums +- Reduced implementation complexity +- No risk of behavioral divergence + +### Storage Strategy Trade-offs + +The distinction between `enum class` (reference types) and `enum struct` (optimized value types) allows developers to choose the right trade-off for their scenario, similar to the choice between `record class` and `record struct`. ## 9. Performance Characteristics ### Memory Layout **`enum class`**: -- Single pointer per instance (8 bytes on 64-bit) -- Heap allocation for each unique case instance +- Union contains single reference (8 bytes on 64-bit) +- Case instances allocated on heap - Singleton pattern for parameter-less cases **`enum struct`**: -- Size equals discriminant (typically 1-4 bytes) plus largest case data -- Stack allocated or embedded in containing types -- Potential for struct tearing with concurrent access +- Size equals discriminator plus space for largest case +- Inline storage, no heap allocation +- Optimized layout per union's non-boxing pattern ### Allocation Patterns ```csharp -// Allocation per call +// Allocation per construction enum class Result { Ok(int value), Error(string message) } -var r1 = new Result.Ok(42); // Heap allocation +var r1 = Result.Ok(42); // Heap allocation for Ok instance // No allocation enum struct Result { Ok(int value), Error(string message) } -var r2 = new Result.Ok(42); // Stack only +var r2 = Result.Ok(42); // Stack only, value stored inline ``` ### Optimization Opportunities @@ -556,44 +597,32 @@ var r2 = new Result.Ok(42); // Stack only The compiler can optimize: - Singleton cases to shared instances - Small enum structs to fit in registers -- Pattern matching to jump tables +- Pattern matching via union's optimized paths - Exhaustive switches to avoid default branches -## 10. Runtime Representation +## 10. Open Questions + +Several design decisions remain open: -Enhanced enums map to CLR types as follows: +1. **Nested type accessibility**: Should users be able to reference the generated case types directly (e.g., `Result_Success`), or should they remain compiler-only? -### Constant Enums -- Subclass `System.Enum` with appropriate backing field -- Metadata preserves enum semantics for reflection -- Compatible with existing enum APIs +2. **Partial support**: Should enhanced enums support `partial` for source generators? -### Shape Enums -- **`enum class`**: Abstract class hierarchy with sealed nested classes -- **`enum struct`**: Struct with discriminant and union-style storage -- Custom attributes mark these as compiler-generated enhanced enums +3. **Default values**: What should `default(EnumType)` produce for shape enums? The union default (null `Value`)? -### Interop Considerations +4. **Serialization**: How should enhanced enums interact with System.Text.Json and other serializers? -Enhanced enums maintain compatibility with: -- Existing `System.Enum` APIs where applicable -- Reflection-based frameworks -- Debugger visualization -- Binary serialization (with caveats for shape enums) +5. **Additional state**: Should shape enums allow instance fields outside of case data? The union structure could accommodate this. -## 10. Open Questions +6. **Custom constructors**: Should enums allow custom constructors that delegate to cases? Should cases support multiple constructors? -Several design decisions remain open: +7. **Construction syntax**: Should we use `Result.Ok(42)` or `new Result.Ok(42)` or support both? This ties into [target-typed static member lookup](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-static-member-lookup.md). + +8. **Generic cases**: Should cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only work for `enum class`. + +9. **Interface implementation**: Should enhanced enums automatically implement interfaces like `IEquatable` when appropriate? -1. Can users reference the generated nested types directly, or should they remain compiler-only? -2. Should enhanced enums support `partial` for source generators? -3. What should `default(EnumType)` produce for struct-based shape enums? -4. How should enhanced enums interact with System.Text.Json and other serializers? -5. Enums *could* allow for state, outside of the individual shape cases. There is a clear place to store these in both the `enum class` and `enum struct` layouts. Should we allow this? Or could it be too confusing? -6. Enums *could* allow for constructors, though they would likely need to defer to an existing case. Should we allow this? Similarly, should individual cases allow for multiple constructors? Perhaps that is better by allowing cases to have their own record-like bodies. -7. No syntax has been presented for getting instances of data-carrying enum-members. `new OrderStatus.Processing(...)` seems heavyweight, esp. compared to `OrderState.Pending`. Perhaps we keep construction of data-carrying values simple, and just include the argument list, without the need for `new`. This also likely ties into the investigations into [target-typed-static-member-lookup](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-static-member-lookup.md). -8. Should enum cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only be feasible for `enum class` implementations, not `enum struct` due to layout constraints. -9. This could open the door for enums (all enums, or non-classic enums) to automatically implement IEquatable, ISpanFormattable, and other interfaces when appropriate such as IBinaryInteger. This has been requested at , , and for example. +10. **Exact lowering**: Should the spec define the exact names and shapes of generated types, or leave these as implementation details? ## Appendix A: Grammar Changes From 7470dba621cb4bfe6aca3df10c74a0cf1ab5840a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:13:08 +0200 Subject: [PATCH 38/57] Apply suggestions from code review --- .../discriminated-unions/extended-enums.md | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index d507612fe0..f020c7f6e6 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -302,36 +302,34 @@ enum class Result // Conceptually translates to: -// Generated case types -public sealed record class Result_Success(string value); -public sealed record class Result_Failure(int code); // Generated union -public union Result(Result_Success, Result_Failure) +public union Result { - // Convenience constructors/factories - public static Result Success(string value) => new Result(new Result_Success(value)); - public static Result Failure(int code) => new Result(new Result_Failure(code)); + Success, + Failure; + + // Generated case types + public sealed record class Success(string value); + public sealed record class Failure(int code); } -``` Singleton cases (those without parameters) generate types with shared instances: ```csharp enum class State { Ready, Processing, Complete } -// Conceptually translates to: -public sealed class State_Ready -{ - public static readonly State_Ready Instance = new(); - private State_Ready() { } -} -// Similar for Processing and Complete - -public union State(State_Ready, State_Processing, State_Complete) +public union State { - public static State Ready => new State(State_Ready.Instance); - // etc. + Ready, Processing, Complete; + + // Conceptually translates to: + public sealed class Ready + { + public static readonly Ready Instance = new(); + private Ready() { } + } + // Similar for Processing and Complete } ``` From fa8884fe10f3e685654dd516f8752b093d5c52c1 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:15:57 +0200 Subject: [PATCH 39/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index f020c7f6e6..593ef751c3 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -346,13 +346,13 @@ enum struct Option // Conceptually translates to: -// Generated case types -public readonly struct Option_None { } -public readonly record struct Option_Some(T value); - // Generated union with optimized storage public struct Option : IUnion { + // Generated case types + public readonly struct None { } + public readonly record struct Some(T value); + // Optimized layout: discriminator + space for largest case private byte _discriminant; private T _value; // Space for Some's data @@ -360,25 +360,25 @@ public struct Option : IUnion // Implements IUnion.Value object? IUnion.Value => _discriminant switch { - 1 => new Option_None(), - 2 => new Option_Some(_value), + 1 => newNone(), + 2 => new Some(_value), _ => null }; // Non-boxing access pattern public bool HasValue => _discriminant != 0; - public bool TryGetValue(out Option_None value) + public bool TryGetValue(out None value) { value = default; return _discriminant == 1; } - public bool TryGetValue(out Option_Some value) + public bool TryGetValue(out Some value) { if (_discriminant == 2) { - value = new Option_Some(_value); + value = new Some(_value); return true; } value = default!; From 73e4a4cbe649e2e235d40a42f3d66d3160a24e66 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:16:34 +0200 Subject: [PATCH 40/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../working-groups/discriminated-unions/extended-enums.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 593ef751c3..b1425fdc64 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -386,12 +386,12 @@ public struct Option : IUnion } // Constructors - public Option(Option_None _) => _discriminant = 1; - public Option(Option_Some some) => (_discriminant, _value) = (2, some.value); + public Option(None _) => _discriminant = 1; + public Option(Some some) => (_discriminant, _value) = (2, some.value); // Convenience factories - public static Option None => new Option(new Option_None()); - public static Option Some(T value) => new Option(new Option_Some(value)); + public static Option None => new Option(new None()); + public static Option Some(T value) => new Option(new Some(value)); } ``` From 78348aae11d09b15267a1b7be2624acf873be3a8 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:27:23 +0200 Subject: [PATCH 41/57] Update extended-enums.md --- .../discriminated-unions/extended-enums.md | 141 ++++++++---------- 1 file changed, 62 insertions(+), 79 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index b1425fdc64..1e73351a25 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -1,6 +1,6 @@ # Discriminated Unions and Enhanced Enums for C# -This proposal extends C#'s union capabilities by introducing enhanced enums as [algebraic sum types](https://en.wikipedia.org/wiki/Sum_type). While [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) solve the problem of values that can be one of several existing types, enhanced enums provide rich, exhaustive case-based types with associated data, building on the familiar enum keyword. +This proposal introduces enhanced enums as an elegant way to build discriminated unions in C#. Building on the foundational [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) feature, enhanced enums provide familiar, concise syntax for the common pattern of defining algebraic sum types where the cases are known at declaration time. It also aims to consolidate the majority of the design space and feedback over many years in this repository, especially from: @@ -31,17 +31,17 @@ It also aims to consolidate the majority of the design space and feedback over m ## 1. Overview -### Two Complementary Features +### Building on Type Unions -C# will gain two separate features for different modeling needs: [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) and enhanced enums (this proposal). These features work together but solve distinct problems in the type system. +C# gains a layered approach to union types: [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) provide the foundational building block for combining types, while enhanced enums (this proposal) offer elegant syntax for the discriminated union pattern where you define the cases and their union together. ### At a Glance ```csharp -// Type unions - one value, multiple possible types +// Type unions - the foundation for combining existing types union Result { string, ValidationError, NetworkException } -// Enhanced enums - one type, multiple possible shapes +// Shape enums - elegant discriminated unions with integrated case definitions enum PaymentResult { Success(string transactionId), @@ -50,17 +50,19 @@ enum PaymentResult } ``` -Type unions excel when you need to handle values that could be any of several existing types. Enhanced enums shine when modeling a single concept that can take different forms, each potentially carrying different data. +Type unions are the lower-level building block—you use them when the types already exist and you need to express "one of these" relationships. Shape enums build on this foundation to provide the natural way to express discriminated unions, where you want to define the cases and their union as a cohesive unit. ## 2. Motivation and Design Philosophy -### Distinct Problem Spaces +### From Type Unions to Discriminated Unions -Type unions and enhanced enums address fundamentally different modeling needs: +Type unions solve the fundamental problem of representing a value that can be one of several types. However, the most common use case for unions is the discriminated union pattern, where: -**Type unions** bring together disparate existing types. You use them when the types already exist and you need to express "this or that" relationships. The focus is on the types themselves. +- The cases are defined together as a logical unit +- Each case may carry different data +- The set of cases is typically closed and known at design time -**Enhanced enums** define a single type with multiple shapes or cases. You use them for algebraic sum types where the focus is on the different forms a value can take, not on combining pre-existing types. +Shape enums provide elegant syntax for this discriminated union pattern. Rather than manually defining types and then combining them with a union declaration, shape enums let you express the entire discriminated union naturally in a single declaration. ### Limitations of Current Enums @@ -68,28 +70,29 @@ Today's C# enums have served us well but have significant limitations: 1. **No associated data**: Cases are merely integral values, unable to carry additional information 2. **Not truly exhaustive**: Any integer can be cast to an enum type, breaking exhaustiveness guarantees -2. **Limited to integers**: Cannot use other primitive types like strings or doubles +3. **Limited to integers**: Cannot use other primitive types like strings or doubles Enhanced enums address all these limitations while preserving the conceptual simplicity developers expect. ### Building on Familiar Concepts -By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide a grow-up story. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. +By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide a grow-up story. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. Most importantly, shape enums are not a separate feature from unions—they are the idiomatic way to express discriminated unions in C#. -## 3. Type Unions (Brief Overview) +## 3. Type Unions (Foundation) ### Core Concepts -Type unions are fully specified in the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#summary). They provide: +Type unions are fully specified in the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#summary). They provide the foundational machinery: - Implicit conversions from case types to the union type - Pattern matching that unwraps union contents - Exhaustiveness checking in switch expressions - Enhanced nullability tracking +- Flexible storage strategies (boxing or non-boxing) -### Relationship to This Proposal +### The Building Block -This proposal builds on type unions for shape enums. Shape enums become a convenient syntax for declaring both the case types and their union in a single declaration, with all behaviors deriving from the underlying union machinery. +Type unions are the essential building block that makes discriminated unions possible. They handle all the complex mechanics of storing values of different types, pattern matching, and ensuring type safety. Shape enums leverage all this machinery while providing a more convenient and integrated syntax for the common discriminated union pattern. ## 4. Enhanced Enums @@ -100,7 +103,7 @@ Enhanced enums follow these core principles: - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in - **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner - **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts -- **Exhaustiveness**: The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)) +- **Union foundation**: Shape enums are discriminated unions built on the type union machinery ### Syntax Extensions @@ -118,7 +121,7 @@ enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } #### Shape Declarations -Any of the following creates a shape enum (C#'s implementation of algebraic sum types): +Any of the following creates a shape enum (a discriminated union with integrated case definitions): - Adding `class` or `struct` after `enum` - Having a parameter list on any enum member @@ -192,11 +195,9 @@ enum IrrationalConstants : double These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. -Enhanced constant enums are similar to classic enums in that they are open by default, but can be potentially 'closed' (see [Closed Enums](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). +### Shape Enums: Discriminated Unions Made Elegant -### Shape Enums - -Shape enums are C#'s implementation of algebraic sum types. They provide convenient syntax for declaring a set of case types and their union in a single declaration. +Shape enums are discriminated unions expressed through familiar enum syntax. They combine the power of type unions with the convenience of defining cases and their union together. When you write a shape enum, you're creating a complete discriminated union—the compiler generates the case types, creates the union that combines them, and provides convenient access patterns. #### Basic Shape Declarations @@ -212,11 +213,11 @@ enum FileOperation } ``` -Each case with parameters generates a corresponding type (typically a record). Cases without parameters generate singleton types. The enum itself becomes a union of these generated types. +This single declaration creates a complete discriminated union: the case types, the union that combines them, and all the machinery for pattern matching and exhaustiveness checking. #### Reference Type and Value Type -**`enum class`** creates a union where the case types are reference types: +**`enum class`** creates a discriminated union where the case types are reference types: ```csharp enum class WebResponse @@ -232,7 +233,7 @@ Benefits: - No risk of struct tearing - Natural null representation -**`enum struct`** creates a union with optimized value-type storage: +**`enum struct`** creates a discriminated union with optimized value-type storage: ```csharp enum struct Option @@ -280,18 +281,13 @@ Members are restricted to: ## 5. Translation Strategy -### Shape Enum Translation Overview +### Shape Enums as Discriminated Unions -Shape enums are syntactic sugar that generates: -1. Individual case types (as records or similar types) -2. A union type that combines these cases -3. Convenience members for construction and pattern matching +Shape enums translate directly to the union pattern—they generate the case types as nested types and create a union that combines them. This isn't just an implementation detail; it's the fundamental design: shape enums ARE discriminated unions with convenient integrated syntax. ### `enum class` Translation -An `enum class` generates: -1. A set of record class types for each case -2. A union declaration combining these types +An `enum class` generates a union with nested record classes: ```csharp enum class Result @@ -300,10 +296,7 @@ enum class Result Failure(int code) } -// Conceptually translates to: - - -// Generated union +// Translates to: public union Result { Success, @@ -313,17 +306,18 @@ public union Result public sealed record class Success(string value); public sealed record class Failure(int code); } +``` Singleton cases (those without parameters) generate types with shared instances: ```csharp enum class State { Ready, Processing, Complete } +// Translates to: public union State { Ready, Processing, Complete; - // Conceptually translates to: public sealed class Ready { public static readonly Ready Instance = new(); @@ -335,7 +329,7 @@ public union State ### `enum struct` Translation -An `enum struct` also generates case types and a union, but the union uses an optimized storage layout as permitted by the [non-boxing union access pattern](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#union-patterns): +An `enum struct` also generates a union with nested types, but uses the union's non-boxing access pattern for optimized storage: ```csharp enum struct Option @@ -351,7 +345,7 @@ public struct Option : IUnion { // Generated case types public readonly struct None { } - public readonly record struct Some(T value); + public readonly record struct Some(T value); // Optimized layout: discriminator + space for largest case private byte _discriminant; @@ -360,8 +354,8 @@ public struct Option : IUnion // Implements IUnion.Value object? IUnion.Value => _discriminant switch { - 1 => newNone(), - 2 => new Some(_value), + 1 => new None(), + 2 => new Some(_value), _ => null }; @@ -374,11 +368,11 @@ public struct Option : IUnion return _discriminant == 1; } - public bool TryGetValue(out Some value) + public bool TryGetValue(out Some value) { if (_discriminant == 2) { - value = new Some(_value); + value = new Some(_value); return true; } value = default!; @@ -387,26 +381,21 @@ public struct Option : IUnion // Constructors public Option(None _) => _discriminant = 1; - public Option(Some some) => (_discriminant, _value) = (2, some.value); + public Option(Some some) => (_discriminant, _value) = (2, some.value); // Convenience factories public static Option None => new Option(new None()); - public static Option Some(T value) => new Option(new Some(value)); + public static Option Some(T value) => new Option(new Some(value)); } ``` -For more complex cases with multiple fields of different types, the compiler would allocate: -- A discriminator field -- Unmanaged memory sufficient for the largest unmanaged data -- Reference fields sufficient for the maximum number of references in any case - -This optimized layout provides the benefits of struct storage while maintaining full union semantics. +This optimized layout leverages the flexibility of the union pattern while providing the performance characteristics developers expect from value types. ## 6. Pattern Matching and Behaviors -### Pattern Matching +### Unified Pattern Matching -Shape enums inherit all pattern matching behavior from their underlying union implementation. The compiler provides convenient syntax that maps to the underlying union patterns: +Shape enums ARE unions, so they inherit all union pattern matching behavior directly. There's no separate implementation or semantics—the patterns you write against shape enums are handled by the exact same union machinery: ```csharp var message = operation switch @@ -418,14 +407,9 @@ var message = operation switch }; ``` -This works because: -- Each case name corresponds to a generated type -- The union's pattern matching unwraps to check these types -- Deconstruction works via the generated records' deconstructors - ### Exhaustiveness -Shape enums benefit from union exhaustiveness checking. When all case types are handled, the switch is exhaustive: +Because shape enums are unions, they get union exhaustiveness checking for free: ```csharp closed enum Status { Active, Pending(DateTime since), Inactive } @@ -439,28 +423,26 @@ var description = status switch }; ``` -Open vs closed shape enums follow the same rules as type unions for exhaustiveness. +### All Union Behaviors -### Other Union Behaviors - -Shape enums automatically inherit from unions: +Shape enums automatically get all union behaviors: - **Implicit conversions** from case values to the enum type - **Nullability tracking** for the union's contents - **Well-formedness** guarantees about values -See the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#union-behaviors) for complete details on these behaviors. +There's no duplication or risk of divergence—shape enums are unions with convenient syntax. ## 7. Examples and Use Cases ### Migrating Traditional Enums -Traditional enums can be progressively enhanced: +Traditional enums can be progressively enhanced to become discriminated unions: ```csharp // Step 1: Traditional enum enum OrderStatus { Pending = 1, Processing = 2, Shipped = 3, Delivered = 4 } -// Step 2: Add data to specific states +// Step 2: Transform into discriminated union with data enum OrderStatus { Pending, @@ -487,7 +469,7 @@ enum OrderStatus ### Result and Option Types -Enhanced enums make functional patterns natural: +Shape enums provide the natural way to express these fundamental discriminated union patterns: ```csharp enum class Result @@ -517,7 +499,7 @@ enum struct Option ### State Machines -Enhanced enums excel at modeling state machines with associated state data: +Enhanced enums excel at modeling state machines—a classic discriminated union use case: ```csharp enum class ConnectionState @@ -552,13 +534,14 @@ Extending the existing `enum` keyword rather than introducing new syntax provide - **Cognitive load**: One concept (enums) instead of two (enums + algebraic sum types) - **Migration path**: Existing enums can be enhanced incrementally -### Building on Unions +### Shape Enums ARE Discriminated Unions + +This is not just an implementation detail—it's the core design principle. Shape enums are the idiomatic way to express discriminated unions in C#. By building directly on the union machinery: -By implementing shape enums as syntactic sugar over type unions, we ensure: -- Consistent semantics between the two features -- All union optimizations and improvements benefit shape enums -- Reduced implementation complexity -- No risk of behavioral divergence +- All union optimizations automatically benefit shape enums +- There's no risk of semantic divergence between features +- The mental model is simple: shape enums generate types and combine them with a union +- Future union enhancements immediately apply to shape enums ### Storage Strategy Trade-offs @@ -592,17 +575,17 @@ var r2 = Result.Ok(42); // Stack only, value stored inline ### Optimization Opportunities -The compiler can optimize: +Because shape enums are unions, they benefit from all union optimizations: - Singleton cases to shared instances -- Small enum structs to fit in registers +- Small structs fitting in registers - Pattern matching via union's optimized paths -- Exhaustive switches to avoid default branches +- Exhaustive switches avoiding default branches ## 10. Open Questions Several design decisions remain open: -1. **Nested type accessibility**: Should users be able to reference the generated case types directly (e.g., `Result_Success`), or should they remain compiler-only? +1. **Nested type accessibility**: Should users be able to reference the generated case types directly (e.g., `Result.Success`), or should they remain compiler-only? 2. **Partial support**: Should enhanced enums support `partial` for source generators? From ac73d03b6f47c6b5b221d74f7f1269e3e4375555 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:37:48 +0200 Subject: [PATCH 42/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 1e73351a25..6cb7d76d2a 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -56,7 +56,7 @@ Type unions are the lower-level building block—you use them when the types alr ### From Type Unions to Discriminated Unions -Type unions solve the fundamental problem of representing a value that can be one of several types. However, the most common use case for unions is the discriminated union pattern, where: +Type unions solve the fundamental problem of representing a value that can be one of several types. However, a particularly important use case for unions is the discriminated union pattern, where: - The cases are defined together as a logical unit - Each case may carry different data From 80ce1b1dff858ac423a74f69f8d37c8591d0aaa8 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:47:37 +0200 Subject: [PATCH 43/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 6cb7d76d2a..130b18c1c2 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -151,6 +151,9 @@ enum Result } ``` +This syntax isn't just similar to records, .each case with a parameter list is shorthand for defining a nested `record` type with that name and those parameters. For `enum class`, the compiler generates nested `sealed record class` types. For `enum struct`, it generates nested `readonly record struct` types. These are real record types with all the expected record behaviors (equality, deconstruction, etc.). + +Note that while these record types are what the enum declaration synthesizes, the union implementation *itself* is free to optimize its internal storage. For example, an `enum struct` might store case data inline rather than storing references to record instances, as long as it can reconstruct the record values when needed (such as for pattern matching or the IUnion.Value property). #### Combination Rules - **Constant enums**: Can use extended base types but NOT have parameter lists From d7e3804b2ba53fc0a9365ab99c67d80a44f41ee3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:50:23 +0200 Subject: [PATCH 44/57] Apply suggestions from code review --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 130b18c1c2..37e71be662 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -196,7 +196,7 @@ enum IrrationalConstants : double } ``` -These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, non-integral constant enums require explicit values for each member. +These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, which auto-increment from zero, non-integral constant enums require explicit values for each member. ### Shape Enums: Discriminated Unions Made Elegant From 8709653c5dd497cd75ed37f06f823470c139e700 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 22:52:36 +0200 Subject: [PATCH 45/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 37e71be662..7a57cfc3b8 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -278,7 +278,7 @@ enum class Result ``` Members are restricted to: -- Instance methods, properties and indexers (members that add no additional state) +- Instance methods, properties, indexers and events. Note, these must be members that add no additional state. So this does not include auto-properties, or field-backed-events. - Static members - Nested types From 9403ba794388c8e3a0561ef11d010c5a5a85c875 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:05:13 +0200 Subject: [PATCH 46/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 7a57cfc3b8..2aafd95e7f 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -141,7 +141,7 @@ enum class Result { Ok(int value), Error } #### Data-Carrying Cases -Shape enum members can have parameter lists, similar to a record's primary constructor, to carry data: +Shape enum members can have parameter lists, to carry data: ```csharp enum Result From 6f859a2a2947cdaa9505ad7eddc2e02aa82ae77c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:28:27 +0200 Subject: [PATCH 47/57] Update extended-enums.md --- .../discriminated-unions/extended-enums.md | 264 +++++------------- 1 file changed, 70 insertions(+), 194 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 2aafd95e7f..05e4bdcb75 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -1,13 +1,11 @@ # Discriminated Unions and Enhanced Enums for C# -This proposal introduces enhanced enums as an elegant way to build discriminated unions in C#. Building on the foundational [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) feature, enhanced enums provide familiar, concise syntax for the common pattern of defining algebraic sum types where the cases are known at declaration time. +This proposal introduces enhanced enums as an elegant way to build discriminated unions in C#. Building on [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md), enhanced enums provide familiar, concise syntax for algebraic sum types where cases are known at declaration time. -It also aims to consolidate the majority of the design space and feedback over many years in this repository, especially from: - -- [#113](https://github.com/dotnet/csharplang/issues/113) - Main champion issue for discriminated unions, 7+ years of discussion and multiple LDM meetings +It consolidates design feedback from many years of repository discussions, especially [#113](https://github.com/dotnet/csharplang/issues/113) and related issues.
- as well as... + Key discussion threads... - [#75](https://github.com/dotnet/csharplang/issues/75) - Early comprehensive discussion exploring DU syntax options including enum class - [#2962](https://github.com/dotnet/csharplang/discussions/2962) - Major discussion on Andy Gocke's DU proposal, debating enum class vs enum struct @@ -31,17 +29,13 @@ It also aims to consolidate the majority of the design space and feedback over m ## 1. Overview -### Building on Type Unions - -C# gains a layered approach to union types: [type unions](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md) provide the foundational building block for combining types, while enhanced enums (this proposal) offer elegant syntax for the discriminated union pattern where you define the cases and their union together. - -### At a Glance +C# gains a layered approach to union types: type unions provide the foundation for combining types, while enhanced enums offer elegant syntax for discriminated unions where you define cases and their union together. ```csharp -// Type unions - the foundation for combining existing types +// Type unions - combine existing types union Result { string, ValidationError, NetworkException } -// Shape enums - elegant discriminated unions with integrated case definitions +// Shape enums - discriminated unions with integrated case definitions enum PaymentResult { Success(string transactionId), @@ -50,39 +44,35 @@ enum PaymentResult } ``` -Type unions are the lower-level building block—you use them when the types already exist and you need to express "one of these" relationships. Shape enums build on this foundation to provide the natural way to express discriminated unions, where you want to define the cases and their union as a cohesive unit. - ## 2. Motivation and Design Philosophy ### From Type Unions to Discriminated Unions -Type unions solve the fundamental problem of representing a value that can be one of several types. However, a particularly important use case for unions is the discriminated union pattern, where: +Type unions solve the fundamental problem of representing "one of several types". A particularly important pattern is discriminated unions, where: -- The cases are defined together as a logical unit +- Cases are defined together as a logical unit - Each case may carry different data - The set of cases is typically closed and known at design time -Shape enums provide elegant syntax for this discriminated union pattern. Rather than manually defining types and then combining them with a union declaration, shape enums let you express the entire discriminated union naturally in a single declaration. +Shape enums provide natural syntax for this pattern—expressing the entire discriminated union in a single declaration rather than manually defining and combining types. ### Limitations of Current Enums -Today's C# enums have served us well but have significant limitations: +Today's C# enums have significant limitations: -1. **No associated data**: Cases are merely integral values, unable to carry additional information -2. **Not truly exhaustive**: Any integer can be cast to an enum type, breaking exhaustiveness guarantees -3. **Limited to integers**: Cannot use other primitive types like strings or doubles +1. **No associated data**: Cases are merely integral values +2. **Not truly exhaustive**: Any integer can be cast to an enum type +3. **Limited to integers**: Cannot use strings or doubles -Enhanced enums address all these limitations while preserving the conceptual simplicity developers expect. +Enhanced enums address all these limitations while preserving conceptual simplicity. ### Building on Familiar Concepts -By extending the existing `enum` keyword rather than introducing entirely new syntax, enhanced enums provide a grow-up story. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. Most importantly, shape enums are not a separate feature from unions—they are the idiomatic way to express discriminated unions in C#. +By extending the existing `enum` keyword, enhanced enums provide a grow-up story. Simple enums remain simple, while advanced scenarios become possible without abandoning familiar patterns. ## 3. Type Unions (Foundation) -### Core Concepts - -Type unions are fully specified in the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#summary). They provide the foundational machinery: +Type unions are fully specified in the [unions proposal](https://raw.githubusercontent.com/dotnet/csharplang/refs/heads/main/proposals/unions.md#summary). They provide: - Implicit conversions from case types to the union type - Pattern matching that unwraps union contents @@ -90,28 +80,22 @@ Type unions are fully specified in the [unions proposal](https://raw.githubuserc - Enhanced nullability tracking - Flexible storage strategies (boxing or non-boxing) -### The Building Block - -Type unions are the essential building block that makes discriminated unions possible. They handle all the complex mechanics of storing values of different types, pattern matching, and ensuring type safety. Shape enums leverage all this machinery while providing a more convenient and integrated syntax for the common discriminated union pattern. - ## 4. Enhanced Enums ### Design Principles -Enhanced enums follow these core principles: - - **Progressive enhancement**: Simple enums stay simple; complexity is opt-in -- **Data carrying**: Each case can carry along its own constituent data in a safe and strongly typed manner -- **Familiar syntax**: Builds on existing enum and record/primary-constructor concepts -- **Union foundation**: Shape enums are discriminated unions built on the type union machinery +- **Data carrying**: Each case can carry its own constituent data +- **Familiar syntax**: Builds on existing enum and record concepts +- **Union foundation**: Shape enums are discriminated unions ### Syntax Extensions -Enhanced enums extend the traditional enum syntax in three orthogonal ways: +Enhanced enums extend traditional enum syntax in three orthogonal ways: #### Extended Base Types -Traditional enums only support integral types. Enhanced enums support any constant-bearing type: +Support any constant-bearing type: ```csharp enum Traditional : int { A = 1, B = 2 } @@ -121,7 +105,7 @@ enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } #### Shape Declarations -Any of the following creates a shape enum (a discriminated union with integrated case definitions): +Create a shape enum (discriminated union) by: - Adding `class` or `struct` after `enum` - Having a parameter list on any enum member @@ -131,18 +115,14 @@ enum struct Result { Success, Failure } // shape enum via 'struct' keyword enum Result { Success(), Failure() } // shape enum via parameter lists ``` -When created via parameter lists alone, it defaults to `enum class` (reference type): +When created via parameter lists alone, defaults to `enum class`: ```csharp enum Result { Ok(int value), Error } // implicitly 'enum class' -// equivalent to: -enum class Result { Ok(int value), Error } ``` #### Data-Carrying Cases -Shape enum members can have parameter lists, to carry data: - ```csharp enum Result { @@ -151,14 +131,13 @@ enum Result } ``` -This syntax isn't just similar to records, .each case with a parameter list is shorthand for defining a nested `record` type with that name and those parameters. For `enum class`, the compiler generates nested `sealed record class` types. For `enum struct`, it generates nested `readonly record struct` types. These are real record types with all the expected record behaviors (equality, deconstruction, etc.). +Each case with a parameter list generates a nested record type. `enum class` generates `sealed record class` types; `enum struct` generates `readonly record struct` types. While these are the generated types, the union implementation may optimize internal storage. -Note that while these record types are what the enum declaration synthesizes, the union implementation *itself* is free to optimize its internal storage. For example, an `enum struct` might store case data inline rather than storing references to record instances, as long as it can reconstruct the record values when needed (such as for pattern matching or the IUnion.Value property). #### Combination Rules - **Constant enums**: Can use extended base types but NOT have parameter lists - **Shape enums**: Can have parameter lists but NOT specify a base type -- **Mixing cases**: Cannot mix constant values and parameterized cases in the same enum +- **Mixing cases**: Cannot mix constant values and parameterized cases ```csharp // ✓ Valid - constant enum with string base @@ -174,11 +153,11 @@ enum Bad { A = 1, B(string x) } enum struct Bad : int { A, B } ``` -For the complete formal grammar specification, see [Appendix A: Grammar Changes](#appendix-a-grammar-changes). +For the complete formal grammar, see [Appendix A: Grammar Changes](#appendix-a-grammar-changes). ### Constant Value Enums -Enhanced constant enums extend traditional enums to support any primitive type that can have compile-time constants: +Enhanced constant enums support any primitive type with compile-time constants: ```csharp enum Priority : string @@ -187,24 +166,13 @@ enum Priority : string Medium = "medium", High = "high" } - -enum IrrationalConstants : double -{ - Pi = 3.14159265359, - E = 2.71828182846, - GoldenRatio = 1.61803398875 -} ``` -These compile to subclasses of `System.Enum` with the appropriate backing field `value__` with the appropriate underlying type. Unlike integral enums, which auto-increment from zero, non-integral constant enums require explicit values for each member. +These compile to `System.Enum` subclasses with the appropriate `value__` backing field. Non-integral constant enums require explicit values for each member. ### Shape Enums: Discriminated Unions Made Elegant -Shape enums are discriminated unions expressed through familiar enum syntax. They combine the power of type unions with the convenience of defining cases and their union together. When you write a shape enum, you're creating a complete discriminated union—the compiler generates the case types, creates the union that combines them, and provides convenient access patterns. - -#### Basic Shape Declarations - -Shape enums can mix cases with and without data: +Shape enums combine type unions with convenient integrated syntax: ```csharp enum FileOperation @@ -216,46 +184,21 @@ enum FileOperation } ``` -This single declaration creates a complete discriminated union: the case types, the union that combines them, and all the machinery for pattern matching and exhaustiveness checking. - #### Reference Type and Value Type -**`enum class`** creates a discriminated union where the case types are reference types: - -```csharp -enum class WebResponse -{ - Success(string content), - Error(int statusCode, string message), - Timeout -} -``` - -Benefits: -- Cheap to pass around (pointer-sized union) -- No risk of struct tearing +**`enum class`** creates discriminated unions with reference type cases: +- Cheap to pass around (pointer-sized) +- No struct tearing risk - Natural null representation -**`enum struct`** creates a discriminated union with optimized value-type storage: - -```csharp -enum struct Option -{ - None, - Some(T value) -} -``` - -Benefits: +**`enum struct`** creates discriminated unions with optimized value-type storage: - No heap allocation - Better cache locality - Reduced GC pressure -Similar to evolution of `records`, these variations can ship at separate times. - #### Members and Methods -Enums can contain members just like unions. This applies to both constant and shape enums: +Enums can contain members just like unions: ```csharp enum class Result @@ -278,20 +221,16 @@ enum class Result ``` Members are restricted to: -- Instance methods, properties, indexers and events. Note, these must be members that add no additional state. So this does not include auto-properties, or field-backed-events. +- Instance methods, properties, indexers and events (no additional state) - Static members - Nested types ## 5. Translation Strategy -### Shape Enums as Discriminated Unions - -Shape enums translate directly to the union pattern—they generate the case types as nested types and create a union that combines them. This isn't just an implementation detail; it's the fundamental design: shape enums ARE discriminated unions with convenient integrated syntax. +Shape enums translate directly to unions—generating case types as nested types and creating a union that combines them. ### `enum class` Translation -An `enum class` generates a union with nested record classes: - ```csharp enum class Result { @@ -305,13 +244,12 @@ public union Result Success, Failure; - // Generated case types public sealed record class Success(string value); public sealed record class Failure(int code); } ``` -Singleton cases (those without parameters) generate types with shared instances: +Singleton cases generate types with shared instances: ```csharp enum class State { Ready, Processing, Complete } @@ -332,8 +270,6 @@ public union State ### `enum struct` Translation -An `enum struct` also generates a union with nested types, but uses the union's non-boxing access pattern for optimized storage: - ```csharp enum struct Option { @@ -342,19 +278,15 @@ enum struct Option } // Conceptually translates to: - -// Generated union with optimized storage public struct Option : IUnion { - // Generated case types public readonly struct None { } public readonly record struct Some(T value); // Optimized layout: discriminator + space for largest case private byte _discriminant; - private T _value; // Space for Some's data + private T _value; - // Implements IUnion.Value object? IUnion.Value => _discriminant switch { 1 => new None(), @@ -363,42 +295,23 @@ public struct Option : IUnion }; // Non-boxing access pattern - public bool HasValue => _discriminant != 0; + public bool TryGetValue(out None value) { ... } + public bool TryGetValue(out Some value) { ... } - public bool TryGetValue(out None value) - { - value = default; - return _discriminant == 1; - } - - public bool TryGetValue(out Some value) - { - if (_discriminant == 2) - { - value = new Some(_value); - return true; - } - value = default!; - return false; - } - - // Constructors + // Constructors and factories public Option(None _) => _discriminant = 1; public Option(Some some) => (_discriminant, _value) = (2, some.value); - // Convenience factories public static Option None => new Option(new None()); public static Option Some(T value) => new Option(new Some(value)); } ``` -This optimized layout leverages the flexibility of the union pattern while providing the performance characteristics developers expect from value types. - ## 6. Pattern Matching and Behaviors ### Unified Pattern Matching -Shape enums ARE unions, so they inherit all union pattern matching behavior directly. There's no separate implementation or semantics—the patterns you write against shape enums are handled by the exact same union machinery: +Shape enums inherit all union pattern matching behavior: ```csharp var message = operation switch @@ -412,8 +325,6 @@ var message = operation switch ### Exhaustiveness -Because shape enums are unions, they get union exhaustiveness checking for free: - ```csharp closed enum Status { Active, Pending(DateTime since), Inactive } @@ -428,33 +339,20 @@ var description = status switch ### All Union Behaviors -Shape enums automatically get all union behaviors: -- **Implicit conversions** from case values to the enum type -- **Nullability tracking** for the union's contents -- **Well-formedness** guarantees about values - -There's no duplication or risk of divergence—shape enums are unions with convenient syntax. +Shape enums automatically get: +- Implicit conversions from case values +- Nullability tracking +- Well-formedness guarantees ## 7. Examples and Use Cases ### Migrating Traditional Enums -Traditional enums can be progressively enhanced to become discriminated unions: - ```csharp -// Step 1: Traditional enum +// Traditional enum enum OrderStatus { Pending = 1, Processing = 2, Shipped = 3, Delivered = 4 } -// Step 2: Transform into discriminated union with data -enum OrderStatus -{ - Pending, - Processing(DateTime startedAt), - Shipped(string trackingNumber), - Delivered(DateTime deliveredAt) -} - -// Step 3: Add methods for common operations +// Enhanced with data enum OrderStatus { Pending, @@ -462,18 +360,12 @@ enum OrderStatus Shipped(string trackingNumber), Delivered(DateTime deliveredAt); - public bool IsComplete => this switch - { - Delivered => true, - _ => false - }; + public bool IsComplete => this is Delivered; } ``` ### Result and Option Types -Shape enums provide the natural way to express these fundamental discriminated union patterns: - ```csharp enum class Result { @@ -502,8 +394,6 @@ enum struct Option ### State Machines -Enhanced enums excel at modeling state machines—a classic discriminated union use case: - ```csharp enum class ConnectionState { @@ -530,25 +420,22 @@ enum class ConnectionState ### Why Extend `enum` -Extending the existing `enum` keyword rather than introducing new syntax provides several benefits: - - **Familiarity**: Developers already understand enums conceptually - **Progressive disclosure**: Simple cases remain simple -- **Cognitive load**: One concept (enums) instead of two (enums + algebraic sum types) +- **Cognitive load**: One concept instead of two - **Migration path**: Existing enums can be enhanced incrementally -### Shape Enums ARE Discriminated Unions - -This is not just an implementation detail—it's the core design principle. Shape enums are the idiomatic way to express discriminated unions in C#. By building directly on the union machinery: +### Union Foundation +Shape enums are discriminated unions expressed through enum syntax. By building on union machinery: - All union optimizations automatically benefit shape enums -- There's no risk of semantic divergence between features -- The mental model is simple: shape enums generate types and combine them with a union -- Future union enhancements immediately apply to shape enums +- No risk of semantic divergence between features +- Simple mental model: shape enums generate types and combine them with a union +- Future union enhancements immediately apply ### Storage Strategy Trade-offs -The distinction between `enum class` (reference types) and `enum struct` (optimized value types) allows developers to choose the right trade-off for their scenario, similar to the choice between `record class` and `record struct`. +The distinction between `enum class` and `enum struct` allows developers to choose the right trade-off, similar to choosing between `record class` and `record struct`. ## 9. Performance Characteristics @@ -569,44 +456,33 @@ The distinction between `enum class` (reference types) and `enum struct` (optimi ```csharp // Allocation per construction enum class Result { Ok(int value), Error(string message) } -var r1 = Result.Ok(42); // Heap allocation for Ok instance +var r1 = Result.Ok(42); // Heap allocation // No allocation enum struct Result { Ok(int value), Error(string message) } -var r2 = Result.Ok(42); // Stack only, value stored inline +var r2 = Result.Ok(42); // Stack only ``` ### Optimization Opportunities -Because shape enums are unions, they benefit from all union optimizations: +Shape enums benefit from all union optimizations: - Singleton cases to shared instances - Small structs fitting in registers -- Pattern matching via union's optimized paths +- Pattern matching via optimized paths - Exhaustive switches avoiding default branches ## 10. Open Questions -Several design decisions remain open: - -1. **Nested type accessibility**: Should users be able to reference the generated case types directly (e.g., `Result.Success`), or should they remain compiler-only? - +1. **Nested type accessibility**: Should users reference generated case types directly? 2. **Partial support**: Should enhanced enums support `partial` for source generators? - -3. **Default values**: What should `default(EnumType)` produce for shape enums? The union default (null `Value`)? - -4. **Serialization**: How should enhanced enums interact with System.Text.Json and other serializers? - -5. **Additional state**: Should shape enums allow instance fields outside of case data? The union structure could accommodate this. - -6. **Custom constructors**: Should enums allow custom constructors that delegate to cases? Should cases support multiple constructors? - -7. **Construction syntax**: Should we use `Result.Ok(42)` or `new Result.Ok(42)` or support both? This ties into [target-typed static member lookup](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-static-member-lookup.md). - -8. **Generic cases**: Should cases support independent generic parameters? For example: `enum Result { Ok(T value), Error(string message) }`. This would likely only work for `enum class`. - -9. **Interface implementation**: Should enhanced enums automatically implement interfaces like `IEquatable` when appropriate? - -10. **Exact lowering**: Should the spec define the exact names and shapes of generated types, or leave these as implementation details? +3. **Default values**: What should `default(EnumType)` produce for shape enums? +4. **Serialization**: How should enhanced enums interact with System.Text.Json? +5. **Additional state**: Should shape enums allow instance fields outside case data? +6. **Custom constructors**: Should enums allow custom constructors that delegate to cases? +7. **Construction syntax**: `Result.Ok(42)` or `new Result.Ok(42)` or both? +8. **Generic cases**: Should cases support independent generic parameters? +9. **Interface implementation**: Should enhanced enums automatically implement `IEquatable`? +10. **Exact lowering**: Should the spec define exact names and shapes of generated types? ## Appendix A: Grammar Changes @@ -620,7 +496,7 @@ enum_base ; enum_underlying_type - : simple_type // equivalent to all integral types, fp-types, decimal, bool and char + : simple_type // all integral types, fp-types, decimal, bool and char | 'string' | type_name // Must resolve to one of the above ; From 5bfa4ef0ee18dd8be6b89fa250ceaa000ddf0616 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:32:05 +0200 Subject: [PATCH 48/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 05e4bdcb75..d17bddd7b0 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -186,6 +186,19 @@ enum FileOperation #### Reference Type and Value Type +```csharp +enum class WebResponse +{ + Success(string content), + Error(int statusCode, string message), + Timeout +} + +enum struct Option +{ + None, + Some(T value) +} **`enum class`** creates discriminated unions with reference type cases: - Cheap to pass around (pointer-sized) - No struct tearing risk From 71deced6e9f7766f267d93c6f6d5428962d32042 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:33:06 +0200 Subject: [PATCH 49/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- .../discriminated-unions/extended-enums.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index d17bddd7b0..f0c84c372a 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -308,8 +308,22 @@ public struct Option : IUnion }; // Non-boxing access pattern - public bool TryGetValue(out None value) { ... } - public bool TryGetValue(out Some value) { ... } + public bool TryGetValue(out None value) + { + value = default; + return _discriminant == 1; + } + + public bool TryGetValue(out Some value) + { + if (_discriminant == 2) + { + value = new Some(_value); + return true; + } + value = default!; + return false; + } // Constructors and factories public Option(None _) => _discriminant = 1; From e828966f4b49e7c6c8545622fa0d0572eece5ee5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:38:20 +0200 Subject: [PATCH 50/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index f0c84c372a..ff0deef28f 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -199,6 +199,8 @@ enum struct Option None, Some(T value) } +``` + **`enum class`** creates discriminated unions with reference type cases: - Cheap to pass around (pointer-sized) - No struct tearing risk From e0650b78f2d689d9106c05a996c2e0e8664c4c65 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:45:27 +0200 Subject: [PATCH 51/57] Update meetings/working-groups/discriminated-unions/extended-enums.md --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index ff0deef28f..22b43f416e 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -354,6 +354,8 @@ var message = operation switch ### Exhaustiveness +The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums can be used to signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums signal that there is no need to handle unknown cases, such as when the case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. + ```csharp closed enum Status { Active, Pending(DateTime since), Inactive } From 685e85ce9be762fdd82b59d60636bf81946eaa4c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 25 Sep 2025 23:49:20 +0200 Subject: [PATCH 52/57] Apply suggestions from code review --- .../discriminated-unions/extended-enums.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 22b43f416e..98b7c15040 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -354,7 +354,11 @@ var message = operation switch ### Exhaustiveness -The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). Open enums can be used to signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums signal that there is no need to handle unknown cases, such as when the case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. For constant enums, "open" means values outside the declared set can be cast to the enum type. +The compiler tracks all declared cases. Both constant and shape enums can be open or closed (see [Closed Enums proposal](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)). + +Open enums can be used to signal that the enum author may add new cases in future versions—consumers must handle unknown cases defensively (e.g., with a default branch). Closed enums signal that there is no need to handle unknown cases, such as when the case set is complete and will never change—the compiler ensures exhaustive matching without requiring a default case. + +For constant enums, "open" also means values outside the declared set can be safely freely cast to the enum type. ```csharp closed enum Status { Active, Pending(DateTime since), Inactive } @@ -366,6 +370,17 @@ var description = status switch Pending(var date) => $"Pending since {date}", Inactive => "Not active" }; + +enum Priority : string { Low = "low", Medium = "medium", High = "high" } + +// Default case is needed, other string values may be converted to Priority, or new cases may be added in the future: +var value = priority switch +{ + Low => -1, + Medium => 0, + High => 1, + _ => /* fallback to low priority */ -1, +} ``` ### All Union Behaviors From a6e6d24bc736a8361ec63f6165dd8fd74d53c6f3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 29 Sep 2025 20:15:24 +0200 Subject: [PATCH 53/57] Update extended-enums.md --- .../discriminated-unions/extended-enums.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 98b7c15040..4e0acd3507 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -33,7 +33,7 @@ C# gains a layered approach to union types: type unions provide the foundation f ```csharp // Type unions - combine existing types -union Result { string, ValidationError, NetworkException } +union Result(string, ValidationError, NetworkException); // Shape enums - discriminated unions with integrated case definitions enum PaymentResult @@ -254,11 +254,8 @@ enum class Result } // Translates to: -public union Result -{ - Success, - Failure; - +public union Result(Sucess, Failure) +{ public sealed record class Success(string value); public sealed record class Failure(int code); } @@ -270,10 +267,8 @@ Singleton cases generate types with shared instances: enum class State { Ready, Processing, Complete } // Translates to: -public union State -{ - Ready, Processing, Complete; - +public union State(Ready, Processing, Complete) +{ public sealed class Ready { public static readonly Ready Instance = new(); From 04a98a6e19a755988e88d6c957d5c2c22ab3113a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 29 Sep 2025 22:26:48 +0200 Subject: [PATCH 54/57] Refactor enum definitions and clarify shape enum rules Updated enum syntax to include 'class' or 'struct' keywords and clarified rules for shape enums. --- .../discriminated-unions/extended-enums.md | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 4e0acd3507..905a7d535e 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -36,7 +36,7 @@ C# gains a layered approach to union types: type unions provide the foundation f union Result(string, ValidationError, NetworkException); // Shape enums - discriminated unions with integrated case definitions -enum PaymentResult +enum struct PaymentResult // or `enum class` { Success(string transactionId), Declined(string reason), @@ -107,24 +107,16 @@ enum TranscendentalConstants : double { Pi = 3.14159, E = 2.71828 } Create a shape enum (discriminated union) by: - Adding `class` or `struct` after `enum` -- Having a parameter list on any enum member ```csharp enum class Result { Success, Failure } // shape enum via 'class' keyword enum struct Result { Success, Failure } // shape enum via 'struct' keyword -enum Result { Success(), Failure() } // shape enum via parameter lists -``` - -When created via parameter lists alone, defaults to `enum class`: - -```csharp -enum Result { Ok(int value), Error } // implicitly 'enum class' ``` #### Data-Carrying Cases ```csharp -enum Result +enum class Result // or enum struct { Success(string id), Failure(int code, string message) @@ -133,6 +125,8 @@ enum Result Each case with a parameter list generates a nested record type. `enum class` generates `sealed record class` types; `enum struct` generates `readonly record struct` types. While these are the generated types, the union implementation may optimize internal storage. +TODO: Should the structs be readonly? Seems like that goes against normal structs (including `record struct`). Seems like that could be opt in with `readonly enum struct X`. + #### Combination Rules - **Constant enums**: Can use extended base types but NOT have parameter lists @@ -144,13 +138,16 @@ Each case with a parameter list generates a nested record type. `enum class` gen enum Status : string { Active = "A", Inactive = "I" } // ✓ Valid - shape enum with data -enum class Result { Ok(int value), Error(string msg) } +enum class class Result { Ok(int value), Error(string msg) } // ✗ Invalid - cannot mix constants and shapes -enum Bad { A = 1, B(string x) } +enum class Bad { A = 1, B(string x) } // ✗ Invalid - shape enums cannot have base types enum struct Bad : int { A, B } + +// ✗ Invalid - Constant enums cannot have parameter lists +enum Bad { A(), B() } ``` For the complete formal grammar, see [Appendix A: Grammar Changes](#appendix-a-grammar-changes). @@ -175,7 +172,7 @@ These compile to `System.Enum` subclasses with the appropriate `value__` backing Shape enums combine type unions with convenient integrated syntax: ```csharp -enum FileOperation +enum class FileOperation // or enum struct { Open(string path), Close, @@ -394,7 +391,7 @@ Shape enums automatically get: enum OrderStatus { Pending = 1, Processing = 2, Shipped = 3, Delivered = 4 } // Enhanced with data -enum OrderStatus +enum structOrderStatus { Pending, Processing(DateTime startedAt), From a9e32d562261e7a9ea4bbe763b4a151e2fafec9a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 29 Sep 2025 22:39:05 +0200 Subject: [PATCH 55/57] Update meetings/working-groups/discriminated-unions/extended-enums.md Co-authored-by: Rikki Gibson --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 905a7d535e..9f4001c182 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -391,7 +391,7 @@ Shape enums automatically get: enum OrderStatus { Pending = 1, Processing = 2, Shipped = 3, Delivered = 4 } // Enhanced with data -enum structOrderStatus +enum struct OrderStatus { Pending, Processing(DateTime startedAt), From ae4958e4dd301045e2f2152d5f6819874ede5385 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 30 Sep 2025 02:05:37 -0700 Subject: [PATCH 56/57] Update meetings/working-groups/discriminated-unions/extended-enums.md Co-authored-by: Rikki Gibson --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index 9f4001c182..a1366fe07c 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -251,7 +251,7 @@ enum class Result } // Translates to: -public union Result(Sucess, Failure) +public union Result(Success, Failure) { public sealed record class Success(string value); public sealed record class Failure(int code); From de212cb4456ee6979a9573d6624655282e812379 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 9 Oct 2025 20:12:02 +0200 Subject: [PATCH 57/57] Update meetings/working-groups/discriminated-unions/extended-enums.md Co-authored-by: Gerard Smit --- meetings/working-groups/discriminated-unions/extended-enums.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meetings/working-groups/discriminated-unions/extended-enums.md b/meetings/working-groups/discriminated-unions/extended-enums.md index a1366fe07c..07c160aa0c 100644 --- a/meetings/working-groups/discriminated-unions/extended-enums.md +++ b/meetings/working-groups/discriminated-unions/extended-enums.md @@ -138,7 +138,7 @@ TODO: Should the structs be readonly? Seems like that goes against normal struc enum Status : string { Active = "A", Inactive = "I" } // ✓ Valid - shape enum with data -enum class class Result { Ok(int value), Error(string msg) } +enum class Result { Ok(int value), Error(string msg) } // ✗ Invalid - cannot mix constants and shapes enum class Bad { A = 1, B(string x) }