Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 65 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,34 +69,33 @@ The generator is purely standalone; the generated union source does not depend o

## Declaring a Non-Boxing Custom Union Type

Declare a partial struct type with a partial void `Cases` method, whose parameters denote the case types for the union.
The names of the parameters are not used, so any name will do.
Declare a partial struct type with `@union` in its leading comments and a partial constructor for each case type for the generator to implement.

The generator will layout the contents of the custom union so that the fields storing the different case types are overlapped with each other using the same memory space, if possible, or at least consume less space than simply having separate fields for each case type. By default, no case value is boxed.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
int case1,
float case2,
string case3,
IManifest case4,
Coordinate case5,
Address case6
);
public partial MyUnion(int value);
public partial MyUnion(float value);
public partial MyUnion(string value);
public partial MyUnion(IManifest value);
public partial MyUnion(Coordinate value);
public partial MyUnion(Address value);
}

record struct Coordinate(float Longitude, float Latitude);
record struct Address(int Id, string Name);
interface IManifest { ... }
```

The generator will layout the contents of the custom union so that the fields storing the different case types are overlapped with each other using the same memory space, if possible, or at least consume less space than simply having separate fields for each case type. By default, no case value is boxed.

In this example, there will be a single field storing a struct that contains enough space to store either an int, float, Coordinate or the address ID and a separate object field used to store either a string, IManifest or the address name.


## Further Customization

You can override the layout algorithm per case by using annotations in the comments preceding each case type parameter declaration.
You can override the layout algorithm per case by using annotations in the comments preceding each case type's constructor.

**@box** - store a boxed value-type in an object field.
**@isolate** - use a separate strongly typed field.
Expand All @@ -108,18 +107,19 @@ For example, if a struct cannot be overlapped with other cases because it contai
If you don't want this to happen, you can use the @isolate annotation to keep the value whole by using an extra field to store it, increasing the memory footprint of the union, or use the @box annotation to box it instead and store it in the same field used to store the string or IManifest cases.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
int case1,
float case2,
string case3,
IManifest case4,
// @box
Coordinate case5, // store this as a boxed object
// @isolate
Address case6 // use a separate field for this one
);
public partial MyUnion(int value);
public partial MyUnion(flaot value);
public partial MyUnion(string value);
public partial MyUnion(IManifest value);

// @box
public partial MyUnion(Coordinate value); // store this as a boxed object

// @isolate
public partial MyUnion(Address value); // use a separate field for this one
}

record struct Coordinate(float Longitude, float Latitude);
Expand All @@ -136,17 +136,19 @@ Typically, specifying the `@overlap` annotation does nothing since the case woul
However, if you specify `@overlap` on a type known to not be overlappable, the generator will produce a warning and not overlap the type.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
int case1,
// @overlap
string case2, // warning! not overlappable
// @overlap
GoodStruct case3, // overlappable, so this is a no-op
// @overlap
BadStruct case4 // warning! not overlappable
);
public partial MyUnion(int value);

// @overlap
public partial MyUnion(string value); // warning! not overlappable, is a reference type

// @overlap
public partial MyUnion(GoodStruct value); // okay, was going to do this anyway

// @overlap
public partial MyUnion(BadStruct value); // // warning! not overlappable, contains references types
}

public record struct GoodStruct(int x, float y);
Expand All @@ -162,18 +164,21 @@ The generator will choose to decompose a struct if it is not overlappable and de
If the generator does not choose to decompose your struct case, but you are confident it can be trusted you can specify the `@decompose` annotation to request it do so.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
GoodStructA structA, // automatically decomposed
GoodStructB structB, // ...
GoodStructC structC, // ...
GoodStructD structD, // ...
// @decompose
BadStructE structE, // okay, if you say so, but Z will be lost
// @decompose
BadStructF structF // Warning! will not decompose
);
public partial MyUnion(GoodStructA value); // automatically decomposed
public partial MyUnion(GoodStructB value); // automatically decomposed
public partial MyUnion(GoodStructC value); // automatically decomposed

// @decompose
public partial MyUnion(GoodStructD value); // okay, but was already going to decompose

// @decompose
public partial MyUnion(BadStructE value); // okay, if you say so, but Z will be lost

// @decompose
public partial MyUnion(BadStructF value); // Warning! will not decompose
}

public record struct GoodStructA(int X, string Y);
Expand All @@ -191,36 +196,38 @@ Some structs can still not be decomposed even if you request it. This may happen
You can choose to have any case isolated into its own field. This happens automatically, if the case cannot be overlapped or decomposed. If neither overlapping or decomposition is suitable, you can specify the `@isolate` annotation to force it to be isolated as whole.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
int case1,
float case2,
// @isolate
SketchyStruct case3 // good idea
);
public partial MyUnion(int value);

// @isolate
public partial MyUnion(float value); // okay, but unnecessary

// @isolate
public partial MyUnion(SketchyStruct value); // okay, good idea, this struct may contain references or inaccessible members

// @isolate
public partial MyUnion(string value); // ignored: will still use shared object field
}
```

This can be a good idea if you have reasons to believe that metadata may not be accurate and the type may contain either non-overlappable members or contain data you don't wish to lose from decomposition.

> Note: If you attempt to isolate a reference type, it will still be stored using the boxed technique in an object field instead of a strongly-typed field. This is to improve field sharing across cases.

### @box

The source generator will never choose to box a struct value, but you can request it using the `@box` annotation.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
// @box
int case1,
string case2
);
// @box
public partial MyUnion(int value); // okay

// @box
public partial MyUnion(string value); // okay, was already going to store in object field anyway
}
```

*You can choose this an alternative to using @isolate when you believe the metadata for a case type is incomplete and should not be overlapped or decomposed.*


*You can choose this an alternative to using @isolate when you believe the metadata for a case type is incomplete and should not be overlapped or decomposed.*
64 changes: 40 additions & 24 deletions src/Generators/CustomUnionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,25 +166,27 @@ private void WriteCaseConstructors(UnionLayout layout)
void WriteConstructor(CaseLayout caseLayout)
{
var caseType = caseLayout.Case.Type;
_writer.WriteLine($"public {layout.Union.SimpleName}({caseType.TypeName} value)");
var parameterName = caseLayout.Case.ConstructorParameterName ?? "value";
var partialMod = caseLayout.Case.HasPartialConstructorDefinition ? "partial " : "";
_writer.WriteLine($"{caseLayout.Case.ConstructorAccessibility} {partialMod}{layout.Union.SimpleName}({caseType.TypeName} {parameterName})");
_writer.WriteBraceNested(() =>
{
if (caseType.IsReference
|| caseType.MightBeNullable)
{
// null values become equivalent of default for the struct
_writer.WriteLine("if (value is {} v)");
_writer.WriteLine($"if ({parameterName} is {{}} v)");
_writer.WriteBraceNested(() =>
{
WriteBody("v");
});
}
else
{
WriteBody("value");
WriteBody(parameterName);
}

void WriteBody(string valueName)
void WriteBody(string parameterName)
{
if (layout.TagField != null)
{
Expand All @@ -193,12 +195,12 @@ void WriteBody(string valueName)

if (caseLayout.IsDecomposed)
{
Decompose(caseLayout, valueName);
Decompose(caseLayout, parameterName);
}
else if (caseLayout.Field != null)
{
WriteFieldReference(caseLayout.Field);
_writer.WriteLine($" = {valueName};");
_writer.WriteLine($" = {parameterName};");
}
else
{
Expand Down Expand Up @@ -568,7 +570,7 @@ private void WriteTryGetValueMethods(UnionLayout layout)

void WriteCaseTypeGetValue(CaseLayout caseLayout)
{
_writer.WriteLine($"{caseLayout.Case.Accessibility} bool TryGetValue([NotNullWhen(true)] out {caseLayout.Case.Type.TypeName} value)");
_writer.WriteLine($"{caseLayout.Case.MemberAccessibility} bool TryGetValue([NotNullWhen(true)] out {caseLayout.Case.Type.TypeName} value)");
_writer.WriteBraceNested(() =>
{
if (caseLayout.Case.NonDisjointCases.Count > 0)
Expand Down Expand Up @@ -1242,40 +1244,54 @@ public class CaseDesc : IEquatable<CaseDesc>
/// </summary>
public IReadOnlyList<int> NonDisjointCases { get; }

public string Accessibility { get; }
/// <summary>
/// The accessibility for members generated for this type
/// </summary>
public string MemberAccessibility { get; }

/// <summary>
/// If true, a record struct type for the case will be generated from the members as a nested type within the union type.
/// </summary>
public bool GenerateType { get; }

private CaseDesc(
/// <summary>
/// The accessibility of the constructor (may be different than the general member accessibility)
/// </summary>
public string ConstructorAccessibility { get; }

/// <summary>
/// The name of the constructor parameter associated with this case
/// </summary>
public string ConstructorParameterName { get; }

/// <summary>
/// True if the constructor had a partial definition
/// </summary>
public bool HasPartialConstructorDefinition { get; }

public CaseDesc(
TypeDesc type,
IReadOnlyList<int>? nonDisjointCases,
bool generateType,
string accessibility)
IReadOnlyList<int>? nonDisjointCases = null,
string memberAccessibility = "public",
bool generateType = false,
string constructorAccessibility = "public",
string constructorParameterName = "value",
bool hasPartialConstructorDefinition = false)
{
this.Type = type;
this.NonDisjointCases = nonDisjointCases ?? Array.Empty<int>();
this.MemberAccessibility = memberAccessibility;
this.GenerateType = generateType;
this.Accessibility = accessibility;
}

public CaseDesc(TypeDesc type, bool generateType, string accessibility = "public")
: this(type, null, generateType, accessibility)
{
}

public CaseDesc(TypeDesc type, IReadOnlyList<int>? nonDisjointCases = null, string accessibility = "public")
: this(type, nonDisjointCases, false, accessibility)
{
this.ConstructorAccessibility = constructorAccessibility;
this.ConstructorParameterName = constructorParameterName;
this.HasPartialConstructorDefinition = hasPartialConstructorDefinition;
}

public bool Equals(CaseDesc other)
{
if (this.Type.Equals(other.Type)
&& this.GenerateType == other.GenerateType
&& this.Accessibility == other.Accessibility)
&& this.MemberAccessibility == other.MemberAccessibility)
{
if (this.NonDisjointCases.Count != other.NonDisjointCases.Count)
return false;
Expand Down
22 changes: 10 additions & 12 deletions src/SourceGenerators.Package/ReadMe.Nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,27 @@ It may include additional generators in the future.

## Declaring a Non-Boxing Custom Union Type

Declare a partial struct type with a partial void `Cases` method, whose parameters denote the case types for the union.
The names of the parameters are not used, so any name will do.
Declare a partial struct type with `@Union` in its leading comments and a partial constructor for each case type for the generator to implement.

The generator will layout the contents of the custom union so that the fields storing the different case types are overlapped with each other using the same memory space, if possible, or at least consume less space than simply having separate fields for each case type. No case value is boxed.

```csharp
// @union
public partial struct MyUnion
{
partial void Cases(
int case1,
float case2,
string case3,
IManifest case4,
Coordinate case5,
Address case6
);
public partial MyUnion(int value);
public partial MyUnion(float value);
public partial MyUnion(string value);
public partial MyUnion(IManifest value);
public partial MyUnion(Coordinate value);
public partial MyUnion(Address value);
}

record struct Coordinate(float Longitude, float Latitude);
record struct Address(int Id, string Name);
interface IManifest { ... }
```

The generator will layout the contents of the custom union so that the fields storing the different case types are overlapped with each other using the same memory space, if possible, or at least consume less space than simply having separate fields for each case type. No case value is boxed.

In this example, there will be a single field storing a struct that contains enough space to store either an int, float, Coordinate or the address Id and a sparate object field used to store either a string, IManifest or the address Name.

[Learn how to customize the union generation further](https://github.com/mattwar/UnionTypes.Toolkit)
Loading
Loading