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; // String interning attribútum detektálás (null = no attr, true/false = explicit) bool? stringInternAttr = null; if (GetKind(p.Type) == PropertyTypeKind.String) { var attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); if (attr != null && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Kind == TypedConstantKind.Primitive) { stringInternAttr = (bool)attr.ConstructorArguments[0].Value!; } } // For typeof(): strip trailing '?' from nullable reference types (typeof(T?) is invalid for ref types) // Nullable value types (int?, Guid?) keep '?' because typeof(int?) == typeof(Nullable) is valid var typeDisplayName = p.Type.ToDisplayString(); var typeNameForTypeof = (p.Type.NullableAnnotation == NullableAnnotation.Annotated && !p.Type.IsValueType) ? typeDisplayName.TrimEnd('?') : typeDisplayName; // Direct object write detection for Complex property types: // Check if the property type has [AcBinarySerializable] (→ has generated writer) // and if it implements IId (→ needs ref tracking in generated code) var kind = GetKind(p.Type); bool hasGenWriter = false; bool propTypeIsIId = false; string? writerClassName = null; if (kind == PropertyTypeKind.Complex) { // Resolve to the actual type symbol (strip nullable annotation for ref types) // For SharedTag? → SharedTag. OriginalDefinition handles generic types. var resolvedType = p.Type is INamedTypeSymbol namedPropType ? namedPropType.OriginalDefinition : p.Type; hasGenWriter = resolvedType.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == AttributeName); // Skip nested types (we don't generate writers for them) if (hasGenWriter && resolvedType is INamedTypeSymbol nt && nt.ContainingType != null) hasGenWriter = false; if (hasGenWriter) { propTypeIsIId = resolvedType.AllInterfaces.Any(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); // Writer class: {Namespace}.{TypeName}_GeneratedWriter var ns = resolvedType.ContainingNamespace.IsGlobalNamespace ? string.Empty : resolvedType.ContainingNamespace.ToDisplayString(); writerClassName = string.IsNullOrEmpty(ns) ? $"{resolvedType.Name}_GeneratedWriter" : $"{ns}.{resolvedType.Name}_GeneratedWriter"; } } properties.Add(new PropInfo( p.Name, typeDisplayName, typeNameForTypeof, kind, p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName)); } } // 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;"); // ReferenceHandlingMode is needed when any Complex property has direct object write if (ci.Properties.Any(p => p.HasGeneratedWriter)) sb.AppendLine("using AyCode.Core.Serializers;"); 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", p.TypeNameForTypeof, 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: if (p.InterningFlags == 0) sb.AppendLine($"{i}context.StringInternEligible = false;"); else sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;"); sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); break; case PropertyTypeKind.Complex: // Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely // when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise. if (p.HasGeneratedWriter) EmitDirectObjectWrite(sb, p, a, i); else if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); } else sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; case PropertyTypeKind.Collection: // typeof() instead of GetType() — avoids virtual dispatch if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); } else sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; default: EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, 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; } } /// /// Emits direct object write — bypasses GetWrapper + WriteObject entirely. /// Writes marker bytes + calls child GeneratedWriter.WriteProperties inline. /// For IId types: inlines TryConsumeWritePlanEntry for ref tracking cursor alignment. /// Falls back to WriteObjectGenerated when context.IsDirectObjectWrite is false (UseMetadata/PropertyFilter). /// private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i) { var writer = p.WriterClassName; var nextDepth = "depth + 1"; if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)"); } else { sb.AppendLine($"{i}if (context.IsDirectObjectWrite)"); } sb.AppendLine($"{i}{{"); // MaxDepth check — matches WriteObjectGenerated sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); if (p.IsIId) { // IId type: inline ref tracking from WriteObject // context.UseTypeReferenceHandling gate: ReferenceHandling != None && IsIId // For IId types, IsIId is always true, so: ReferenceHandling != None sb.AppendLine($"{i} if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} {{"); // 2+ occurrence → ObjectRef + cacheIndex (no properties written) sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); // First occurrence → ObjectRefFirst + cacheIndex + properties sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); // No ref tracking entry at this visit → plain Object marker sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); } else { // Non-IId type: usually no ref tracking needed. // Exception: ReferenceHandling == All tracks ALL objects → scan pass increments ScanVisitIndex, // so write pass must also consume from WritePlan to keep cursor aligned. // Guard: fall back to WriteObjectGenerated for All mode (rare). OnlyId mode is safe (non-IId skipped). sb.AppendLine($"{i} if (context.ReferenceHandling == ReferenceHandlingMode.All)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); // Fallback for non-direct mode (UseMetadata=true or HasPropertyFilter=true) if (p.IsNullable) sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); else { sb.AppendLine($"{i}else"); sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); } } private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, 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}, typeof({typeName}), context, depth);"); break; } } private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string typeName, 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, typeName, 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; } /// /// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types). /// public string TypeNameForTypeof { get; } public PropertyTypeKind TypeKind { get; } public bool IsNullable { get; } /// /// Pre-computed interning flags matching runtime BinaryPropertyAccessorBase._interningFlags. /// Bit layout: bit N = eligible when StringInterningMode == N. /// None=0 → bit 0 never set. Attribute=1 → bit 1. All=2 → bit 2. /// No attr: 0b100 (4), [AcStringIntern(true)]: 0b110 (6), [AcStringIntern(false)]: 0b000 (0). /// public int InterningFlags { get; } /// True if the Complex property type has [AcBinarySerializable] → has a generated writer. public bool HasGeneratedWriter { get; } /// True if the Complex property type implements IId<T> → needs ref tracking in write pass. public bool IsIId { get; } /// Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter. public string? WriterClassName { get; } public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null) { Name = n; TypeName = tn; TypeNameForTypeof = tnForTypeof; TypeKind = tk; IsNullable = nullable; HasGeneratedWriter = hasGeneratedWriter; IsIId = isIId; WriterClassName = writerClassName; // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase int flags = 0; if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit if (stringInternAttr != false) flags |= (1 << 2); // All bit InterningFlags = flags; } } 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 }