using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace AyCode.Core.Serializers.SourceGenerator; /// /// Generates IGeneratedBinaryWriter implementations for [AcBinarySerializable] types. /// Also generates a ModuleInitializer that auto-registers all writers at startup. /// [Generator] public class AcBinarySourceGenerator : IIncrementalGenerator { private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { var classDeclarations = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeName, predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax, transform: static (ctx, _) => GetClassInfo(ctx)) .Where(static info => info != null); context.RegisterSourceOutput(classDeclarations.Collect(), static (spc, classes) => Execute(classes!, spc)); } private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context) { if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) return null; // Skip nested types — generated writer class can't be placed inside containing type if (typeSymbol.ContainingType != null) return null; var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : typeSymbol.ContainingNamespace.ToDisplayString(); var properties = new List(); foreach (var member in typeSymbol.GetMembers()) { if (member is IPropertySymbol p && p.DeclaredAccessibility == Accessibility.Public && p.GetMethod != null && p.SetMethod != null && !p.IsIndexer && !p.IsStatic) { var hasIgnore = p.GetAttributes().Any(a => { var name = a.AttributeClass?.Name ?? ""; return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; }); if (hasIgnore) continue; properties.Add(new PropInfo( p.Name, p.Type.ToDisplayString(), GetKind(p.Type), p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type))); } } // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering var isIId = typeSymbol.AllInterfaces.Any(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); if (isIId) properties.Sort((a, b) => { var aIsId = a.Name == "Id" ? 0 : 1; var bIsId = b.Name == "Id" ? 0 : 1; if (aIsId != bIsId) return aIsId.CompareTo(bIsId); return string.Compare(a.Name, b.Name, StringComparison.Ordinal); }); else properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); return new SerializableClassInfo(namespaceName, typeSymbol.Name, typeSymbol.ToDisplayString(), properties); } private static void Execute(ImmutableArray classes, SourceProductionContext context) { if (classes.IsDefaultOrEmpty) return; var valid = classes.Where(c => c != null).Cast().ToList(); if (valid.Count == 0) return; foreach (var ci in valid) context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8)); context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8)); } private static string GenWriter(SerializableClassInfo ci) { var sb = new StringBuilder(2048); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); sb.AppendLine(); if (!string.IsNullOrEmpty(ci.Namespace)) sb.AppendLine($"namespace {ci.Namespace};"); sb.AppendLine(); sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter"); sb.AppendLine("{"); sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();"); sb.AppendLine(); sb.AppendLine(" public void WriteProperties(object value, AcBinarySerializer.BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); foreach (var p in ci.Properties) { sb.AppendLine(); EmitProp(sb, p, " "); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static void EmitProp(StringBuilder sb, PropInfo p, string i) { var a = $"obj.{p.Name}"; // Nullable value types always use markered path (need Null marker) if (IsNullableVTKind(p.TypeKind)) { sb.AppendLine($"{i}if ({a}.HasValue)"); sb.AppendLine($"{i}{{"); EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", i + " "); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);"); return; } // Markerless types: write raw value only, no type marker, no PropertySkip // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode if (IsMarkerless(p.TypeKind)) { EmitMarkerless(sb, p.TypeKind, a, i); return; } // Non-markerless types: write WITH type marker byte (markered path) switch (p.TypeKind) { case PropertyTypeKind.String: sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); break; case PropertyTypeKind.Complex: case PropertyTypeKind.Collection: if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); } else sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); break; default: EmitSkip(sb, p.TypeKind, a, i); break; } } /// /// Returns true for property types that use markerless serialization in FastMode. /// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults. /// private static bool IsMarkerless(PropertyTypeKind k) => k switch { PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or PropertyTypeKind.DateTime or PropertyTypeKind.Guid or PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset or PropertyTypeKind.Boolean or PropertyTypeKind.Enum => true, _ => false }; /// /// Emits raw value only — no type marker, no PropertySkip. /// Matches runtime WritePropertyMarkerless exactly. /// private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i) { switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteVarInt({a});"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteVarLong({a});"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteRaw({a});"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteRaw({a});"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteGuidBits({a});"); break; case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte({a});"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteVarUInt({a});"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteVarULong({a});"); break; case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break; case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? (byte)1 : (byte)0);"); break; case PropertyTypeKind.Enum: sb.AppendLine($"{i}context.WriteVarInt((int){a});"); break; } } private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string i) { switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}if ({a} == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a}); }}"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}if ({a} == 0f) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a}); }}"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}if ({a} == 0m) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a}); }}"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}if ({a} == System.Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a}); }}"); break; case PropertyTypeKind.Byte: sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a}); }}"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a}); }}"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a}); }}"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}if ({a} == 0U) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a}); }}"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}if ({a} == 0UL) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a}); }}"); break; case PropertyTypeKind.Enum: var s = a.Replace(".", "_"); sb.AppendLine($"{i}var ev_{s} = (int){a};"); sb.AppendLine($"{i}if (ev_{s} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt(ev_{s}, out var te_{s})) {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(te_{s}); }}"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{s}); }}"); break; case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);"); break; case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});"); break; default: sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); break; } } private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string i) { switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a});"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a});"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break; default: EmitSkip(sb, k, a, i); break; } } private static string GenInit(List classes) { var sb = new StringBuilder(512); sb.AppendLine("// "); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); sb.AppendLine(); sb.AppendLine("namespace AyCode.Core.Serializers.Generated;"); sb.AppendLine(); sb.AppendLine("internal static class AcBinaryGeneratedWritersInit"); sb.AppendLine("{"); sb.AppendLine(" [ModuleInitializer]"); sb.AppendLine(" internal static void Register()"); sb.AppendLine(" {"); foreach (var ci in classes) sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {ci.FullTypeName}_GeneratedWriter.Instance);"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } #region Type analysis private static bool IsNullableVT(ITypeSymbol t) => t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; private static PropertyTypeKind GetKind(ITypeSymbol type) { if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) return GetKindCore(n.TypeArguments[0], true); return GetKindCore(type, false); } private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable) { switch (type.SpecialType) { case SpecialType.System_String: return PropertyTypeKind.String; case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; default: break; } var fn = type.ToDisplayString(); if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection; if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)) return PropertyTypeKind.Collection; if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; return PropertyTypeKind.Unknown; } private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch { PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64, PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte, PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32, PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean, PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double, PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime, PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan, PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum, _ => PropertyTypeKind.Unknown }; #endregion } internal sealed class SerializableClassInfo { public string Namespace { get; } public string ClassName { get; } public string FullTypeName { get; } public List Properties { get; } public SerializableClassInfo(string ns, string cn, string ftn, List p) { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; } } internal sealed class PropInfo { public string Name { get; } public string TypeName { get; } public PropertyTypeKind TypeKind { get; } public bool IsNullable { get; } public PropInfo(string n, string tn, PropertyTypeKind tk, bool nullable) { Name = n; TypeName = tn; TypeKind = tk; IsNullable = nullable; } } internal enum PropertyTypeKind { Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, Collection, Complex, NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum }