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
125 changes: 125 additions & 0 deletions src/Orleans.CodeGenerator/EnumSerializerGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Orleans.CodeGenerator.Model;
using Orleans.CodeGenerator.SyntaxGeneration;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Orleans.CodeGenerator;

internal static class EnumSerializerGenerator
{
private const string CodecFieldTypeFieldName = "_codecFieldType";

internal static EnumSerializerPlan CreatePlan(ISerializableTypeDescription type, LibraryTypes libraryTypes)
{
Debug.Assert(type.IsEnumType);
var codec = libraryTypes.StaticCodecs.FindByUnderlyingType(type.BaseType);
Debug.Assert(codec is not null);
return new(
type.Name,
type.Accessibility == Accessibility.Public,
new(type.TypeSyntax.ToString()),
new(type.BaseTypeSyntax.ToString()),
new(codec!.CodecType.ToNameSyntax().ToString()),
new(libraryTypes.FieldCodec_1.ToTypeSyntax(type.TypeSyntax).ToString()),
new(libraryTypes.Type.ToTypeSyntax().ToString()),
new(libraryTypes.Writer.ToTypeSyntax().ToString()),
new(libraryTypes.Reader.ToTypeSyntax(ParseTypeName("TReaderInput")).ToString()),
new(libraryTypes.Field.ToTypeSyntax().ToString()),
new(libraryTypes.IBufferWriter.ToTypeSyntax(PredefinedType(Token(SyntaxKind.ByteKeyword))).ToString()));
}

internal static EnumSerializerOutputPlan? CreateOutputPlan(
ISerializableTypeDescription type,
LibraryTypes libraryTypes,
string assemblyName,
TypeMetadataIdentity metadataIdentity)
{
// Ordinary non-generic enums have one codec field and no copier or activator.
// More general shapes continue through the compilation-aware serializer pipeline.
if (!type.IsEnumType || type.IsGenericType || type.Members.Count > 0
|| type.SerializationHooks.Count > 0 || type.UseActivator)
{
return null;
}

return new(CreatePlan(type, libraryTypes), assemblyName, metadataIdentity, type.GeneratedNamespace);
}

internal static SourceOutputResult GenerateSource(EnumSerializerOutputPlan output)
{
var plan = output.Serializer;
var serializer = ClassDeclaration(SerializerGenerator.GetSimpleClassName(plan.Name))
.AddBaseListTypes(SimpleBaseType(ParseTypeName(plan.CodecInterface.SyntaxString)))
.AddModifiers(Token(plan.IsPublic ? SyntaxKind.PublicKeyword : SyntaxKind.InternalKeyword), Token(SyntaxKind.SealedKeyword))
.AddAttributeLists(GeneratedCodeUtilities.GetGeneratedCodeAttributes())
.AddMembers(
FieldDeclaration(VariableDeclaration(
ParseTypeName(plan.SystemType.SyntaxString),
SingletonSeparatedList(VariableDeclarator(CodecFieldTypeFieldName)
.WithInitializer(EqualsValueClause(TypeOfExpression(ParseTypeName(plan.Type.SyntaxString)))))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ReadOnlyKeyword)),
GenerateWriteMethod(plan),
GenerateReadMethod(plan));

return SourceOutputResult.FromSource(GeneratedSourceOutput.CreateSerializableSourceEntry(
output.AssemblyName, plan.Type.SyntaxString, output.MetadataIdentity,
output.GeneratedNamespace, 0, serializer, null, null, output.GeneratedNamespace));
}

internal static MemberDeclarationSyntax GenerateWriteMethod(EnumSerializerPlan plan)
{
var writerParam = "writer".ToIdentifierName();
var fieldIdDeltaParam = "fieldIdDelta".ToIdentifierName();
var expectedTypeParam = "expectedType".ToIdentifierName();
var valueParam = "value".ToIdentifierName();
var statement = ExpressionStatement(
InvocationExpression(
ParseName(plan.StaticCodec.SyntaxString).Member("WriteField"),
ArgumentList(SeparatedList(
[
Argument(writerParam).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)),
Argument(fieldIdDeltaParam),
Argument(expectedTypeParam),
Argument(CastExpression(ParseTypeName(plan.UnderlyingType.SyntaxString), valueParam)),
Argument(IdentifierName(CodecFieldTypeFieldName))
]))));

return MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), "WriteField")
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(
Parameter("writer".ToIdentifier()).WithType(ParseTypeName(plan.Writer.SyntaxString)).WithModifiers(TokenList(Token(SyntaxKind.RefKeyword))),
Parameter("fieldIdDelta".ToIdentifier()).WithType(PredefinedType(Token(SyntaxKind.UIntKeyword))),
Parameter("expectedType".ToIdentifier()).WithType(ParseTypeName(plan.SystemType.SyntaxString)),
Parameter("value".ToIdentifier()).WithType(ParseTypeName(plan.Type.SyntaxString)))
.AddTypeParameterListParameters(TypeParameter("TBufferWriter"))
.AddConstraintClauses(TypeParameterConstraintClause("TBufferWriter")
.AddConstraints(TypeConstraint(ParseTypeName(plan.BufferWriter.SyntaxString))))
.AddAttributeLists(AttributeList(SingletonSeparatedList(GeneratedCodeUtilities.GetMethodImplAttributeSyntax())))
.AddBodyStatements(statement);
}

internal static MemberDeclarationSyntax GenerateReadMethod(EnumSerializerPlan plan)
{
var readerParam = "reader".ToIdentifierName();
var fieldParam = "field".ToIdentifierName();
var readValue = InvocationExpression(
ParseName(plan.StaticCodec.SyntaxString).Member("ReadValue"),
ArgumentList(SeparatedList(
[
Argument(readerParam).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)),
Argument(fieldParam)
])));

return MethodDeclaration(ParseTypeName(plan.Type.SyntaxString), "ReadValue")
.AddTypeParameterListParameters(TypeParameter("TReaderInput"))
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(
Parameter(readerParam.Identifier).WithType(ParseTypeName(plan.Reader.SyntaxString)).WithModifiers(TokenList(Token(SyntaxKind.RefKeyword))),
Parameter(fieldParam.Identifier).WithType(ParseTypeName(plan.Field.SyntaxString)))
.AddAttributeLists(AttributeList(SingletonSeparatedList(GeneratedCodeUtilities.GetMethodImplAttributeSyntax())))
.AddBodyStatements(ReturnStatement(CastExpression(ParseTypeName(plan.Type.SyntaxString), readValue)));
}
}
21 changes: 21 additions & 0 deletions src/Orleans.CodeGenerator/Model/EnumSerializerPlan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Orleans.CodeGenerator.Model;

// Each type syntax is resolved against the current compilation before this symbol-free cache boundary.
internal sealed record EnumSerializerPlan(
string Name,
bool IsPublic,
TypeRef Type,
TypeRef UnderlyingType,
TypeRef StaticCodec,
TypeRef CodecInterface,
TypeRef SystemType,
TypeRef Writer,
TypeRef Reader,
TypeRef Field,
TypeRef BufferWriter);

internal sealed record EnumSerializerOutputPlan(
EnumSerializerPlan Serializer,
string AssemblyName,
TypeMetadataIdentity MetadataIdentity,
string GeneratedNamespace);
4 changes: 2 additions & 2 deletions src/Orleans.CodeGenerator/Model/SerializableTypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Orleans.CodeGenerator.Model;

/// <summary>
/// Describes a <c>[GenerateSerializer]</c>-annotated type for incremental pipeline caching and generation.
/// Contains all data needed to generate a serializer, copier, and activator without holding <c>ISymbol</c> references.
/// Describes a <c>[GenerateSerializer]</c>-annotated type for incremental discovery and metadata generation.
/// Serializer, copier, and activator emission resolves additional semantic dependencies against the current compilation.
/// </summary>
internal sealed record class SerializableTypeModel(
Accessibility Accessibility,
Expand Down
43 changes: 39 additions & 4 deletions src/Orleans.CodeGenerator/OrleansSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public sealed class OrleansSerializationSourceGenerator : IIncrementalGenerator
internal const string ReferenceAssemblyDataTrackingName = "Orleans.ReferenceAssemblyData";
internal const string MetadataAggregateTrackingName = "Orleans.MetadataAggregate";
internal const string SerializerOutputsTrackingName = "Orleans.SerializerOutputs";
internal const string EnumSerializerPlansTrackingName = "Orleans.EnumSerializerPlans";
internal const string EnumSerializerOutputsTrackingName = "Orleans.EnumSerializerOutputs";
internal const string ReferencedSerializerOutputsTrackingName = "Orleans.ReferencedSerializerOutputs";
internal const string ProxyOutputsTrackingName = "Orleans.ProxyOutputs";
internal const string MetadataOutputsTrackingName = "Orleans.MetadataOutputs";
Expand Down Expand Up @@ -204,20 +206,53 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
input.Right))
.WithTrackingName(MetadataAggregateTrackingName);

var serializerOutputs = collectedTypes
// Enums have one declaration, so their emission plans can retain the discovery provider's per-owner identity.
// Preserve null slots when a type becomes invalid: filtering here can corrupt Roslyn 4.5 tracked state on reparse.
var enumSerializerPlans = serializableTypeResults
.Select(static (result, _) => result.EnumSerializerPlan)
.WithTrackingName(EnumSerializerPlansTrackingName);
var enumSerializerOutputs = enumSerializerPlans
.Select(static (plan, _) => plan is null ? default : EnumSerializerGenerator.GenerateSource(plan))
.WithTrackingName(EnumSerializerOutputsTrackingName);

var compilationAwareTypes = collectedSerializableTypeResults
.Select(static (results, _) => results
.Where(static result => result.Model is not null && result.EnumSerializerPlan is null)
.Select(static result => result.Model!)
.ToImmutableArray())
.WithComparer(ImmutableArrayComparer<SerializableTypeModel>.Instance);
var compilationAwareSerializerOutputs = compilationAwareTypes
.Combine(compilationProvider)
.Combine(generatorOptions)
.Select(static (input, ct) => SerializableSourceOutputGenerator.CreateSerializableSourceOutputs(
input.Left.Right,
input.Left.Left,
input.Right,
ct))
.WithComparer(ImmutableArrayComparer<SourceOutputResult>.Instance);

var serializerOutputs = compilationAwareSerializerOutputs
.Combine(enumSerializerOutputs.Collect())
.Select(static (input, _) => GeneratedSourceOutput.DeduplicateSourceOutputs(
input.Left.AddRange(input.Right.Where(static output => output.SourceEntry is not null))))
.WithComparer(ImmutableArrayComparer<SourceOutputResult>.Instance)
.WithTrackingName(SerializerOutputsTrackingName);

GeneratedSourceOutput.RegisterImplementationSourceOutputs(
context,
serializerOutputs.SelectMany(static (input, _) => input));
// Keep the deduplicated batch intact: Roslyn 4.5 can lose re-added entries when flattening an equal prior batch.
context.RegisterSourceOutput(serializerOutputs, static (productionContext, outputs) =>
{
foreach (var output in outputs)
{
GeneratedSourceOutput.ReportDiagnostic(productionContext, output);
}
});
context.RegisterImplementationSourceOutput(serializerOutputs, static (productionContext, outputs) =>
{
foreach (var output in outputs)
{
GeneratedSourceOutput.EmitSource(productionContext, output);
}
});

var referencedSerializerOutputs = refAssemblyData
.Combine(compilationProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ internal static SerializableTypeResult CreateSerializableTypeResult(
}

var model = ModelExtractor.ExtractSerializableTypeModel(typeDescription, sourceLocation);
return SerializableTypeResult.FromModel(model);
var enumPlan = EnumSerializerGenerator.CreateOutputPlan(
typeDescription, libraryTypes, compilation.AssemblyName ?? "assembly", metadataIdentity);
return SerializableTypeResult.FromModel(model, enumPlan);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Expand Down Expand Up @@ -469,4 +471,3 @@ internal static bool IsCurrentCompilationAssembly(TypeMetadataIdentity metadataI
}
}


88 changes: 3 additions & 85 deletions src/Orleans.CodeGenerator/SerializerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ public ClassDeclarationSyntax Generate(ISerializableTypeDescription type)

if (type.IsEnumType)
{
var writeMethod = GenerateEnumWriteMethod(type);
var readMethod = GenerateEnumReadMethod(type);
var plan = EnumSerializerGenerator.CreatePlan(type, LibraryTypes);
var writeMethod = EnumSerializerGenerator.GenerateWriteMethod(plan);
var readMethod = EnumSerializerGenerator.GenerateReadMethod(plan);
classDeclaration = classDeclaration.AddMembers(writeMethod, readMethod);
}
else
Expand Down Expand Up @@ -1015,89 +1016,6 @@ private MemberDeclarationSyntax GenerateCompoundTypeReadValueMethod(
.AddBodyStatements([.. body]);
}

private MemberDeclarationSyntax GenerateEnumWriteMethod(
ISerializableTypeDescription type)
{
var returnType = PredefinedType(Token(SyntaxKind.VoidKeyword));

var writerParam = "writer".ToIdentifierName();
var fieldIdDeltaParam = "fieldIdDelta".ToIdentifierName();
var expectedTypeParam = "expectedType".ToIdentifierName();
var valueParam = "value".ToIdentifierName();

var body = new List<StatementSyntax>();

// Codecs can either be static classes or injected into the constructor.
// Either way, the member signatures are the same.
var staticCodec = LibraryTypes.StaticCodecs.FindByUnderlyingType(type.BaseType);
Debug.Assert(staticCodec is not null);
var codecExpression = staticCodec!.CodecType.ToNameSyntax();

body.Add(
ExpressionStatement(
InvocationExpression(
codecExpression.Member("WriteField"),
ArgumentList(
SeparatedList(
[
Argument(writerParam).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)),
Argument(fieldIdDeltaParam),
Argument(expectedTypeParam),
Argument(CastExpression(type.BaseTypeSyntax, valueParam)),
Argument(IdentifierName(CodecFieldTypeFieldName))
])))));

var parameters = new[]
{
Parameter("writer".ToIdentifier()).WithType(LibraryTypes.Writer.ToTypeSyntax()).WithModifiers(TokenList(Token(SyntaxKind.RefKeyword))),
Parameter("fieldIdDelta".ToIdentifier()).WithType(PredefinedType(Token(SyntaxKind.UIntKeyword))),
Parameter("expectedType".ToIdentifier()).WithType(LibraryTypes.Type.ToTypeSyntax()),
Parameter("value".ToIdentifier()).WithType(type.TypeSyntax)
};

return MethodDeclaration(returnType, WriteFieldMethodName)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(parameters)
.AddTypeParameterListParameters(TypeParameter("TBufferWriter"))
.AddConstraintClauses(TypeParameterConstraintClause("TBufferWriter").AddConstraints(TypeConstraint(LibraryTypes.IBufferWriter.ToTypeSyntax(PredefinedType(Token(SyntaxKind.ByteKeyword))))))
.AddAttributeLists(AttributeList(SingletonSeparatedList(GeneratedCodeUtilities.GetMethodImplAttributeSyntax())))
.AddBodyStatements([.. body]);
}

private MemberDeclarationSyntax GenerateEnumReadMethod(
ISerializableTypeDescription type)
{
var readerParam = "reader".ToIdentifierName();
var fieldParam = "field".ToIdentifierName();

var staticCodec = LibraryTypes.StaticCodecs.FindByUnderlyingType(type.BaseType);
Debug.Assert(staticCodec is not null);
ExpressionSyntax codecExpression = staticCodec!.CodecType.ToNameSyntax();
ExpressionSyntax readValueExpression = InvocationExpression(
codecExpression.Member("ReadValue"),
ArgumentList(SeparatedList([Argument(readerParam).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), Argument(fieldParam)])));

readValueExpression = CastExpression(type.TypeSyntax, readValueExpression);
var body = new List<StatementSyntax>
{
ReturnStatement(readValueExpression)
};

var genericParam = ParseTypeName("TReaderInput");
var parameters = new[]
{
Parameter(readerParam.Identifier).WithType(LibraryTypes.Reader.ToTypeSyntax(genericParam)).WithModifiers(TokenList(Token(SyntaxKind.RefKeyword))),
Parameter(fieldParam.Identifier).WithType(LibraryTypes.Field.ToTypeSyntax())
};

return MethodDeclaration(type.TypeSyntax, ReadValueMethodName)
.AddTypeParameterListParameters(TypeParameter("TReaderInput"))
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(parameters)
.AddAttributeLists(AttributeList(SingletonSeparatedList(GeneratedCodeUtilities.GetMethodImplAttributeSyntax())))
.AddBodyStatements([.. body]);
}

internal abstract class GeneratedFieldDescription(TypeSyntax fieldType, string fieldName)
{
public readonly TypeSyntax FieldType = fieldType;
Expand Down
Loading
Loading