Skip to content

[NO-MERGE] Prototype: C# union types, closed enums, and closed hierarchies#404

Draft
eiriktsarpalis wants to merge 1 commit intomainfrom
csharp-unions
Draft

[NO-MERGE] Prototype: C# union types, closed enums, and closed hierarchies#404
eiriktsarpalis wants to merge 1 commit intomainfrom
csharp-unions

Conversation

@eiriktsarpalis
Copy link
Owner

⚠️ NO-MERGE — Prototype / Design Exploration

This PR is a design prototype for mapping upcoming C# language features to PolyType's type shape abstractions. It is not intended to be merged in its current form — it exists to validate API feasibility and gather feedback.

Background

C# is introducing three related language features:

See also the System.Text.Json design for the same features.

API Changes

New types

Type Description
UnionKind enum Distinguishes ClassHierarchy, FSharpUnion, and CSharpUnion union origins
DelegateMarshaler<TSource, TTarget> Constraint-free IMarshaler using Func delegates, enabling marshal/unmarshal between unrelated types

Extended interfaces

Interface New member Description
IUnionTypeShape UnionKind UnionKind Identifies the kind of union (class hierarchy, F# DU, or C# union)
IEnumTypeShape bool IsClosed Whether the enum is marked [Closed]

Extended attributes

Attribute New property Description
TypeShapeAttribute bool InferDerivedTypes Opt-in for auto-discovering derived types from [ClosedSubtype] attributes or assembly scanning
GenerateShapeAttribute bool InferDerivedTypes Same, for source-generated shapes
GenerateShapeForAttribute bool InferDerivedTypes Same, for external type shapes

How C# Union Types Map to IUnionTypeShape

The key insight is that IUnionCaseShape<TUnionCase, TUnion> has no TUnionCase : TUnion constraint at the interface level. This means PolyType already supports the case where union case types are unrelated to the union type — exactly what C# unions require.

C# union Pet                    PolyType mapping
─────────────────               ──────────────────────────────
union Pet {                     IUnionTypeShape<Pet>
  Dog(string Name, int Age);      UnionKind = CSharpUnion
  Cat(string Name, bool Indoor);  UnionCases[0] = IUnionCaseShape<Dog, Pet>
  int;                            UnionCases[1] = IUnionCaseShape<Cat, Pet>
}                                 UnionCases[2] = IUnionCaseShape<int, Pet>

Marshaling: DelegateMarshaler<Dog, Pet> wraps:

  • Marshal: calls new Pet(dog) (union constructor)
  • Unmarshal: calls ((IUnion)pet).Value and casts to Dog

Case index: determined by checking the runtime type of IUnion.Value.

Detection: by metadata name (System.Runtime.CompilerServices.UnionAttribute + System.IUnion), not by PolyType-defined types. Mock attributes are provided in test cases since no compiler supports these yet.

Closed Hierarchy Support

[ClosedSubtype(typeof(Circle))] attributes on a base type are always honored for union detection (similar to [DerivedTypeShapeAttribute]). Assembly scanning (finding all subtypes in the same assembly) requires explicit InferDerivedTypes = true opt-in.

What's included

  • Reflection provider: full C# union, closed enum, and closed hierarchy support
  • Source generator: closed enum IsClosed and [ClosedSubtype]-based union detection
  • Example JSON serializer: structural matching converter for C# unions (no discriminator — matches by JSON token type and property names)
  • 21 new tests, 273,600 total tests pass across net10.0/net9.0/net8.0/net472

@eiriktsarpalis eiriktsarpalis force-pushed the csharp-unions branch 2 times, most recently from 8826560 to 7f27772 Compare March 12, 2026 18:06
…erarchies

This commit introduces experimental support for three upcoming C# language features:

1. **C# Union Types** - Maps to existing IUnionTypeShape abstraction via new
   UnionKind enum (ClassHierarchy, FSharpUnion, CSharpUnion). Uses
   DelegateMarshaler<TSource, TTarget> for marshal/unmarshal between
   unrelated case types and union types. Detection via [Union] attribute +
   IUnion interface by metadata name.

2. **Closed Enums** - Adds IsClosed property to IEnumTypeShape, detected via
   [Closed] attribute by metadata name. Supported in both reflection and
   source generator providers.

3. **Closed Hierarchies** - [ClosedSubtype] attributes are always honored for
   union detection. Assembly scanning fallback requires InferDerivedTypes
   opt-in via TypeShapeAttribute property.

Key design decisions:
- No breaking changes to public interfaces (InternalImplementationsOnly)
- Detection by metadata name avoids shipping BCL-conflicting types
- IUnionCaseShape has no TUnionCase:TUnion constraint at interface level,
  enabling C# union support without new abstractions
- Source gen uses string UnionKindName to avoid accessibility issues
- Example JSON serializer extended with structural matching for C# unions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant