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; var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : typeSymbol.ContainingNamespace.ToDisplayString(); var properties = new List(); // Read feature flags from [AcBinarySerializable] — disabled features eliminate // corresponding code blocks from generated ScanObject/WriteProperties. var enableIdTracking = true; var enableRefHandling = true; var enableInternString = true; var enableMetadata = true; var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == AttributeName); if (binarySerializableAttr != null) { if (binarySerializableAttr.ConstructorArguments.Length == 1) { // Single bool ctor: AcBinarySerializable(enableAllFeatures) var all = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; enableIdTracking = all; enableRefHandling = all; enableInternString = all; enableMetadata = all; } else if (binarySerializableAttr.ConstructorArguments.Length == 4) { // Four bool ctor: (metadata, idTracking, refHandling, internString) enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; } } 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 (!enableInternString) { stringInternAttr = false; } else 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; bool propEnableMetadata = true; bool childNeedsIdScan = true; bool childNeedsAllRefScan = true; bool childNeedsInternScan = true; string? writerClassName = null; string? propIdTypeName = null; int childTypeNameHash = 0; int[]? childPropertyHashes = 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); if (hasGenWriter) { // Read child type's EnableMetadataFeature propEnableMetadata = ReadEnableMetadata(resolvedType); var childScanFlags = ComputeNeedsScan(resolvedType); childNeedsIdScan = childScanFlags.needsIdScan; childNeedsAllRefScan = childScanFlags.needsAllRefScan; childNeedsInternScan = childScanFlags.needsInternScan; var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); propTypeIsIId = iidIface != null; if (iidIface != null) propIdTypeName = iidIface.TypeArguments[0].ToDisplayString(); // Writer class: {Namespace}.{FlatName}_GeneratedWriter var flatName = BuildFlatName((INamedTypeSymbol)resolvedType); var ns = resolvedType.ContainingNamespace.IsGlobalNamespace ? string.Empty : resolvedType.ContainingNamespace.ToDisplayString(); writerClassName = string.IsNullOrEmpty(ns) ? $"{flatName}_GeneratedWriter" : $"{ns}.{flatName}_GeneratedWriter"; // UseMetadata: compute child type hash-es for inline metadata childTypeNameHash = ComputeFnvHash(resolvedType.Name); childPropertyHashes = ComputeChildPropertyHashes(resolvedType); } } // Collection element type analysis for inline collection write PropertyTypeKind elemKind = PropertyTypeKind.Unknown; bool elemHasGenWriter = false; bool elemIsIId = false; bool elemEnableMetadata = true; bool elemNeedsIdScan = true; bool elemNeedsAllRefScan = true; bool elemNeedsInternScan = true; string? elemWriterClassName = null; string? elemIdTypeName = null; string? collKind = null; string? elemFullTypeName = null; int elementTypeNameHash = 0; int[]? elementPropertyHashes = null; if (kind == PropertyTypeKind.Collection) { var elemType = GetCollectionElementType(p.Type); if (elemType != null) { elemKind = GetKind(elemType); elemFullTypeName = elemType.ToDisplayString(); // Detect collection shape for inline write if (p.Type is IArrayTypeSymbol) collKind = "Array"; else if (p.Type is INamedTypeSymbol collNamedType) { var origDef = collNamedType.OriginalDefinition.ToDisplayString(); collKind = origDef switch { "System.Collections.Generic.List" => "List", "System.Collections.Generic.IList" => "List", // has Count + indexer "System.Collections.Generic.IReadOnlyList" => "List", // has Count + indexer "System.Collections.Generic.HashSet" => "Counted", // has Count, no indexer "System.Collections.Generic.Queue" => "Counted", "System.Collections.Generic.ICollection" => "Counted", "System.Collections.Generic.IReadOnlyCollection" => "Counted", "System.Collections.Generic.SortedSet" => "Counted", "System.Collections.Generic.LinkedList" => "Counted", _ => null }; } // For Complex element types, check for generated writer if (elemKind == PropertyTypeKind.Complex) { var resolvedElem = elemType is INamedTypeSymbol namedElem ? namedElem.OriginalDefinition : elemType; elemHasGenWriter = resolvedElem.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == AttributeName); if (elemHasGenWriter) { // Read element type's EnableMetadataFeature elemEnableMetadata = ReadEnableMetadata(resolvedElem); var elemScanFlags = ComputeNeedsScan(resolvedElem); elemNeedsIdScan = elemScanFlags.needsIdScan; elemNeedsAllRefScan = elemScanFlags.needsAllRefScan; elemNeedsInternScan = elemScanFlags.needsInternScan; var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc => ifc.IsGenericType && ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); elemIsIId = elemIidIface != null; if (elemIidIface != null) elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString(); var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem); var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); elemWriterClassName = string.IsNullOrEmpty(ens) ? $"{elemFlatName}_GeneratedWriter" : $"{ens}.{elemFlatName}_GeneratedWriter"; // UseMetadata: compute element type hash-es for inline metadata elementTypeNameHash = ComputeFnvHash(resolvedElem.Name); elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem); } } } } properties.Add(new PropInfo( p.Name, typeDisplayName, typeNameForTypeof, kind, p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, childTypeNameHash, childPropertyHashes, elementTypeNameHash, elementPropertyHashes, propEnableMetadata, elemEnableMetadata, childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan, elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan)); } } // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering // If EnableIdTrackingFeature == false, skip IId detection entirely → isIId = false var isIId = false; string? idTypeName = null; if (enableIdTracking) { var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); if (iidInterface != null) { isIId = true; idTypeName = iidInterface.TypeArguments[0].ToDisplayString(); } } properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); var className = BuildFlatName(typeSymbol); var typeNameHash = ComputeFnvHash(typeSymbol.Name); var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); var selfScanFlags = ComputeNeedsScan(typeSymbol); return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan); } 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($"{ci.ClassName}_GeneratedReader.g.cs", SourceText.From(GenReader(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(4096); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); // ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan 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($" internal static readonly int s_wrapperSlot = AcBinarySerializer.AllocateWrapperSlot();"); sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};"); sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ "); sb.Append(string.Join(", ", ci.PropertyNameHashes)); sb.AppendLine(" };"); 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, " ", ci.FullTypeName, ci.EnableMetadata); } sb.AppendLine(" }"); sb.AppendLine(); // ScanObject — full scan pass (null/depth + self ref tracking + property scan) GenScanProperties(sb, ci); sb.AppendLine(); // ScanForDuplicates — instance method on IGeneratedBinaryWriter, called from Serialize sb.AppendLine(" public void ScanForDuplicates(object value, AcBinarySerializer.BinarySerializationContext context)"); sb.AppendLine(" where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); sb.AppendLine(" if (!context.HasCaching) return;"); sb.AppendLine(" ScanObject(value, context, 0);"); sb.AppendLine(" context.SortWritePlan();"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } /// /// Generates the ScanObject method — full scan pass entry point for this type. /// Includes: null/depth check, self ref tracking (IId or All mode), property scan. /// Only emits code for reference properties (strings + complex types) — primitives are skipped. /// private static void GenScanProperties(StringBuilder sb, SerializableClassInfo ci) { sb.AppendLine(" public void ScanObject(object value, AcBinarySerializer.BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); // Compile-time proven: no scan work needed for this type if (!ci.NeedsScan) { sb.AppendLine(" // NeedsScan=false: no ref tracking, no string interning, no scannable children"); sb.AppendLine(" }"); return; } // Early return: skip scan when no active runtime feature matches this type's needs if (!ci.NeedsIdScan) { if (ci.NeedsAllRefScan && ci.NeedsInternScan) sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All && !context.UseStringInterning) return;"); else if (ci.NeedsAllRefScan) sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All) return;"); else if (ci.NeedsInternScan) sb.AppendLine(" if (!context.UseStringInterning) return;"); } // Null/depth guard — matches runtime ScanValue entry sb.AppendLine(" if (value == null || depth > context.MaxDepth) return;"); sb.AppendLine(); sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); // Self ref tracking — inline TryTrack via wrapper (no bridge method overhead) // Only emitted when the corresponding feature flag is enabled. if (ci.IsIId) { var tryTrackMethod = ci.IdTypeName switch { "int" => "TryTrackInt32", "long" => "TryTrackInt64", "System.Guid" => "TryTrackGuid", _ => "TryTrackInt32" }; sb.AppendLine(); sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine(" {"); sb.AppendLine(" if (firstVisitIndex >= 0)"); sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);"); sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);"); sb.AppendLine(" return;"); sb.AppendLine(" }"); sb.AppendLine(" }"); } else if (ci.EnableRefHandling) { // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode sb.AppendLine(); sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine(" {"); sb.AppendLine(" if (firstVisitIndex >= 0)"); sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);"); sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);"); sb.AppendLine(" return;"); sb.AppendLine(" }"); sb.AppendLine(" }"); } // Collect scannable properties var scanProps = ci.Properties.Where(p => p.TypeKind == PropertyTypeKind.String || p.TypeKind == PropertyTypeKind.Complex || p.TypeKind == PropertyTypeKind.Collection).ToList(); // Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed var hasStringScan = scanProps.Any(p => (p.TypeKind == PropertyTypeKind.String && p.InterningFlags != 0) || (p.TypeKind == PropertyTypeKind.Collection && p.ElementKind == PropertyTypeKind.String && p.InterningFlags != 0)); if (hasStringScan) { // Hoist the shift once — per-property InterningFlags check uses internBit directly. // Cannot combine flags (OR) because different properties may have different flags // and Attribute mode must NOT scan All-only properties. sb.AppendLine(); sb.AppendLine(" var internBit = 1 << (int)context.Options.UseStringInterning;"); sb.AppendLine(" int minIntern = 0, maxIntern = 0;"); sb.AppendLine(" if (internBit > 1) { minIntern = context.MinStringInternLength; maxIntern = context.MaxStringInternLength; }"); } var hasAnyScanProp = false; foreach (var p in scanProps) { sb.AppendLine(); hasAnyScanProp = true; EmitScanProp(sb, p, " ", ci.FullTypeName); } if (!hasAnyScanProp) { sb.AppendLine(" // No reference properties to scan"); } sb.AppendLine(" }"); } private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata) { var a = $"obj.{p.Name}"; // Markerless types: write raw value only, no type marker, no PropertySkip // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode // NEVER filtered (runtime doesn't filter markerless properties either) // When EnableMetadataFeature=false: always markerless (no UseMetadata branch needed) // When EnableMetadataFeature=true: UseMetadata=true uses markered path (EmitSkip) if (IsMarkerless(p.TypeKind)) { if (!enableMetadata) { // Per-type metadata disabled — always markerless, no branch EmitMarkerless(sb, p.TypeKind, a, i); } else { sb.AppendLine($"{i}if (context.UseMetadata)"); sb.AppendLine($"{i}{{"); EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " "); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); EmitMarkerless(sb, p.TypeKind, a, i + " "); sb.AppendLine($"{i}}}"); } return; } // All non-markerless properties: emit PropertyFilter guard // When filter returns false, write PropertySkip and skip the property write sb.AppendLine($"{i}if (context.HasPropertyFilter)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});"); sb.AppendLine($"{i} if (!context.PropertyFilter!(in fc_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i} goto skip_{p.Name};"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); // 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);"); sb.AppendLine($"{i}skip_{p.Name}:;"); 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: // Direct collection write for List/T[] with Complex element types that have generated writers if (p.ElementHasGeneratedWriter && p.CollectionKind != null) EmitDirectCollectionWrite(sb, p, a, i); else 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; } sb.AppendLine($"{i}skip_{p.Name}:;"); } /// /// 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. #region Scan Pass Code Generation /// /// Emits scan pass code for a single property. /// String: interning check + ScanInternString. /// Complex (SGen): ref tracking via slot IdentityMap + recursive ScanProperties. /// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup). /// Collection: iterate elements with same patterns. /// private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName) { var a = $"obj.{p.Name}"; // PropertyFilter: must match write pass — if filter skips property, scan must skip too // Only for non-markerless properties (matching EmitProp behavior) if (!IsMarkerless(p.TypeKind)) { sb.AppendLine($"{i}if (context.HasPropertyFilter)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});"); sb.AppendLine($"{i} if (!context.PropertyFilter!(in fc_{p.Name}))"); sb.AppendLine($"{i} goto scanskip_{p.Name};"); sb.AppendLine($"{i}}}"); } switch (p.TypeKind) { case PropertyTypeKind.String: EmitScanString(sb, p, a, i); break; case PropertyTypeKind.Complex: if (p.HasGeneratedWriter) EmitScanComplexSGen(sb, p, a, i); else EmitScanComplexRuntime(sb, p, a, i); break; case PropertyTypeKind.Collection: EmitScanCollection(sb, p, a, i); break; } if (!IsMarkerless(p.TypeKind)) sb.AppendLine($"{i}scanskip_{p.Name}:;"); } /// /// Emits scan pass code for a string property: interning flags check + ScanInternString. /// private static void EmitScanString(StringBuilder sb, PropInfo p, string a, string i) { if (p.InterningFlags == 0) { // Never interned (explicit [AcStringIntern(false)] or no flags) — skip entirely return; } // Per-property InterningFlags check with hoisted internBit (context.Options read once) sb.AppendLine($"{i}if (({p.InterningFlags} & internBit) != 0)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var str_{p.Name} = {a};"); sb.AppendLine($"{i} if (str_{p.Name} != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var slen_{p.Name} = str_{p.Name}.Length;"); sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(str_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); } /// /// Emits scan pass code for a Complex property with SGen writer. /// No parent-side ref tracking — child ScanObject handles its own (ScanTrackObjectXxx). /// private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i) { // Compile-time proven: child scan is no-op — skip entirely if (!p.ChildNeedsScan) return; var writer = p.WriterClassName; var childVar = $"sc_{p.Name}"; // 3-axis guard: IId → always call, AllRef → guard All mode, Intern → guard UseStringInterning string? guard = null; if (!p.ChildNeedsIdScan) { if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan) guard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; else if (p.ChildNeedsAllRefScan) guard = "context.ReferenceHandling == ReferenceHandlingMode.All"; else if (p.ChildNeedsInternScan) guard = "context.UseStringInterning"; } if (guard != null) { sb.AppendLine($"{i}if ({guard})"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var {childVar} = {a};"); sb.AppendLine($"{i} if ({childVar} != null)"); sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);"); sb.AppendLine($"{i}}}"); } else { // IId in subtree — always call (active in OnlyId + All) sb.AppendLine($"{i}var {childVar} = {a};"); sb.AppendLine($"{i}if ({childVar} != null)"); sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);"); } } /// /// Emits scan pass code for a Complex property without SGen writer (runtime fallback). /// private static void EmitScanComplexRuntime(StringBuilder sb, PropInfo p, string a, string i) { var childVar = $"sc_{p.Name}"; sb.AppendLine($"{i}var {childVar} = {a};"); sb.AppendLine($"{i}if ({childVar} != null)"); sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context, depth + 1);"); } /// /// Emits scan pass code for a Collection property. /// Handles string collections (interning) and complex element collections (SGen or runtime fallback). /// private static void EmitScanCollection(StringBuilder sb, PropInfo p, string a, string i) { // String element collection if (p.ElementKind == PropertyTypeKind.String) { if (p.InterningFlags == 0) return; // never interned sb.AppendLine($"{i}var scol_{p.Name} = {a};"); sb.AppendLine($"{i}if (scol_{p.Name} != null && ({p.InterningFlags} & internBit) != 0)"); sb.AppendLine($"{i}{{"); if (p.CollectionKind == "Array") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "List") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else { sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})"); sb.AppendLine($"{i} {{"); } sb.AppendLine($"{i} if (se_{p.Name} != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var slen_{p.Name} = se_{p.Name}.Length;"); sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(se_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); return; } // Complex element collection with SGen writer if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) { // Compile-time proven: element scan is no-op — skip entirely if (!p.ElementNeedsScan) return; var writer = p.ElementWriterClassName; // 3-axis guard: IId → always scan, AllRef → guard All mode, Intern → guard UseStringInterning string? elemGuard = null; if (!p.ElementNeedsIdScan) { if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan) elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; else if (p.ElementNeedsAllRefScan) elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; else if (p.ElementNeedsInternScan) elemGuard = "context.UseStringInterning"; } // Guard entire collection scan with runtime check when no IId in element subtree if (elemGuard != null) sb.AppendLine($"{i}if ({elemGuard})"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var scol_{p.Name} = {a};"); sb.AppendLine($"{i} if (scol_{p.Name} != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;"); if (p.CollectionKind == "Array") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "List") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else // Counted (foreach) { sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})"); sb.AppendLine($"{i} {{"); } var e = $"se_{p.Name}"; // Null check only — ScanObject handles depth + ref tracking internally sb.AppendLine($"{i} if ({e} == null) continue;"); sb.AppendLine($"{i} {writer}.Instance.ScanObject({e}, context, snd_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); return; } // Complex element collection without SGen writer — runtime fallback if (p.ElementKind == PropertyTypeKind.Complex) { sb.AppendLine($"{i}var scol_{p.Name} = {a};"); sb.AppendLine($"{i}if (scol_{p.Name} != null)"); sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated(scol_{p.Name}, typeof({p.TypeNameForTypeof}), context, depth);"); return; } // Primitive element collection — no scanning needed } #endregion /// /// Emits inline object write for a Complex property that has a generated writer. /// Compile-time ChildNeedsRefScan eliminates TryConsumeWritePlanEntry when scan never tracks child. /// !ChildNeedsRefScan + !ChildEnableMetadata → ZERO branches: just Object + WriteProperties. /// 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"); sb.AppendLine($"{i}{{"); } else { 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.ChildNeedsRefScan) { // Compile-time proven: scan never tracks child → TryConsumeWritePlanEntry always false if (!p.ChildEnableMetadata) { // No ref, no metadata → ZERO branches: always Object sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); } else { // No ref, but metadata possible → UseMetadata branch only sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); } } else if (!p.ChildEnableMetadata) { // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef var refGuard = p.IsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} {{"); 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} {{"); 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} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); } else { // Full path: ref tracking + metadata sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); var refGuard = p.IsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} {{"); 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} {{"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); 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}}}"); } /// /// Emits inline metadata write: typeNameHash + (if first) propCount + property hashes. /// All values are compile-time constants. /// private static void EmitInlineMetadata(StringBuilder sb, int typeNameHash, int[] propertyHashes, string isFirstVar, string i) { sb.AppendLine($"{i}context.WriteRaw({typeNameHash});"); sb.AppendLine($"{i}if ({isFirstVar})"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteVarUInt({(uint)propertyHashes.Length});"); foreach (var hash in propertyHashes) sb.AppendLine($"{i} context.WriteRaw({hash});"); sb.AppendLine($"{i}}}"); } /// /// Emits inline collection write for List<T> / T[] where T is a Complex type with generated writer. /// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely. /// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]... /// Handles both UseMetadata=true and false inline — no fallback to WriteValueGenerated. /// private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i) { var writer = p.ElementWriterClassName; if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); } else { sb.AppendLine($"{i}{{"); } sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);"); // Get count and iteration based on collection kind if (p.CollectionKind == "Array") { sb.AppendLine($"{i} var arr_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];"); } else if (p.CollectionKind == "Counted") { sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})"); sb.AppendLine($"{i} {{"); } else // List, IList, IReadOnlyList — Count + indexer { sb.AppendLine($"{i} var list_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < list_{p.Name}.Count; i_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];"); } // Per-element write var e = $"elem_{p.Name}"; sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); if (!p.ElementNeedsRefScan) { // Compile-time proven: scan never tracks element → TryConsumeWritePlanEntry always false if (!p.ElementEnableMetadata) { // No ref, no metadata → ZERO branches per element: always Object sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); } else { // No ref, but metadata possible → UseMetadata branch only sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); } } else { // Inline ref tracking var elemRefGuard = p.ElementIsIId ? "context.ReferenceHandling != ReferenceHandlingMode.None" : "context.ReferenceHandling == ReferenceHandlingMode.All"; if (!p.ElementEnableMetadata) { // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); } else { // Full path: ref tracking + metadata sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " "); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); sb.AppendLine($"{i} }}"); } } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); } private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i) { switch (k) { case PropertyTypeKind.Int32: { // Mirrors runtime WritePropertyOrSkip → WriteInt32 (TinyInt optimization) var s32 = a.Replace(".", "_"); sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt({a}, out var ti_{s32})) context.WriteByte(ti_{s32});"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}"); break; } case PropertyTypeKind.Int64: { // Mirrors runtime WritePropertyOrSkip → WriteInt64 → WriteInt32 (int range + TinyInt) var s64 = a.Replace(".", "_"); sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else if ({a} >= int.MinValue && {a} <= int.MaxValue)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var iv_{s64} = (int){a};"); sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(iv_{s64}, out var ti_{s64})) context.WriteByte(ti_{s64});"); sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{s64}); }}"); sb.AppendLine($"{i}}}"); 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; } } #region Reader Code Generation /// /// Generates the IGeneratedBinaryReader implementation for a type. /// Phase 1: handles markerless path (no UseMetadata). UseMetadata/ChainMode → runtime fallback. /// Eliminates: GetWrapper dictionary lookup, CreateInstance delegate, property setter delegates, /// AccessorType switch dispatch, ReadValue dispatch table. /// private static string GenReader(SerializableClassInfo ci) { var sb = new StringBuilder(4096); 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}_GeneratedReader : IGeneratedBinaryReader"); sb.AppendLine("{"); sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedReader Instance = new();"); sb.AppendLine(); // ReadObject — IGeneratedBinaryReader implementation sb.AppendLine(" public object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int depth, int cacheIndex)"); sb.AppendLine(" where TInput : struct, IBinaryInputBase"); sb.AppendLine(" {"); sb.AppendLine($" var obj = new {ci.FullTypeName}();"); sb.AppendLine(); sb.AppendLine(" if (cacheIndex >= 0)"); sb.AppendLine(" context.RegisterInternedValueAt(cacheIndex, obj);"); sb.AppendLine(); // Emit property reads — markerless for primitive types, markered for the rest foreach (var p in ci.Properties) { sb.AppendLine(); EmitReadProp(sb, p, " ", ci.EnableMetadata); } sb.AppendLine(); sb.AppendLine(" return obj;"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } /// /// Emits inline read code for a single property. /// Markerless types: read raw value directly (no type code in stream). /// Markered types: read type code byte, then dispatch. /// Mirrors the serializer's EmitProp symmetry. /// private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata) { var a = $"obj.{p.Name}"; // Markerless types: read raw value directly — mirrors EmitMarkerless in writer if (IsMarkerless(p.TypeKind)) { if (p.TypeKind == PropertyTypeKind.Enum) sb.AppendLine($"{i}{{ var ev = context.ReadVarInt(); {a} = Unsafe.As(ref ev); }}"); else EmitReadMarkerless(sb, p.TypeKind, a, i); return; } // Markered types: read type code, then dispatch var tc = $"tc_{p.Name}"; sb.AppendLine($"{i}var {tc} = context.ReadByte();"); // PropertySkip → leave default sb.AppendLine($"{i}if ({tc} != BinaryTypeCode.PropertySkip)"); sb.AppendLine($"{i}{{"); // Nullable value types if (IsNullableVTKind(p.TypeKind)) { sb.AppendLine($"{i} if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); EmitReadMarkeredValue(sb, Underlying(p.TypeKind), a, tc, i + " ", p, nullable: true); sb.AppendLine($"{i} }}"); } else { switch (p.TypeKind) { case PropertyTypeKind.String: EmitReadString(sb, a, tc, i + " "); break; case PropertyTypeKind.Complex: EmitReadComplex(sb, p, a, tc, i + " "); break; case PropertyTypeKind.Collection: EmitReadCollection(sb, p, a, tc, i + " "); break; default: // Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback sb.AppendLine($"{i} context._position--;"); if (p.IsNullable) sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); else sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); break; } } sb.AppendLine($"{i}}}"); } /// /// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly. /// private static void EmitReadMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i) { switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}{a} = context.ReadVarInt();"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}{a} = context.ReadVarLong();"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}{a} = context.ReadDoubleUnsafe();"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}{a} = context.ReadSingleUnsafe();"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}{a} = context.ReadDecimalUnsafe();"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}{a} = context.ReadDateTimeUnsafe();"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}{a} = context.ReadGuidUnsafe();"); break; case PropertyTypeKind.Byte: sb.AppendLine($"{i}{a} = context.ReadByte();"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}{a} = context.ReadInt16Unsafe();"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}{a} = context.ReadUInt16Unsafe();"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}{a} = context.ReadVarUInt();"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}{a} = context.ReadVarULong();"); break; case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}{a} = new System.TimeSpan(context.ReadRaw());"); break; case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break; } } /// /// Emits inline string read from type code. Handles all string wire formats. /// private static void EmitReadString(StringBuilder sb, string a, string tc, string i) { // FixStr is the hot path — most strings are short (1-31 bytes, encoded in the type code itself) sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStr({tc}))"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var flen = BinaryTypeCode.DecodeFixStrLength({tc});"); sb.AppendLine($"{i} {a} = flen == 0 ? string.Empty : context.ReadStringUtf8(flen);"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.String)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var slen = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} {a} = slen == 0 ? string.Empty : context.ReadStringUtf8(slen);"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInternFirst)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.DisableStringCaching();"); sb.AppendLine($"{i} var sci = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var slen2 = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var sv = slen2 == 0 ? string.Empty : context.ReadStringUtf8(slen2);"); sb.AppendLine($"{i} context.RegisterInternedValueAt(sci, sv);"); sb.AppendLine($"{i} {a} = sv;"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInterned)"); sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {a} = null;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringEmpty) {a} = string.Empty;"); } /// /// Emits inline read for a Complex property. /// SGen reader only runs in non-metadata mode → ObjectWithMetadata never appears. /// Compile-time ChildNeedsRefScan eliminates ObjectRefFirst/ObjectRef when provably unused. /// Non-nullable + no ref → ZERO branches (tc consumed but ignored). /// No SGen → runtime fallback via ReadValueGenerated. /// private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i) { if (!p.HasGeneratedWriter) { // No SGen reader — runtime fallback (rewind + ReadValueGenerated) if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context._position--;"); sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); sb.AppendLine($"{i}}}"); } else { sb.AppendLine($"{i}context._position--;"); sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); } return; } var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); var cast = $"({p.TypeNameForTypeof})"; var nd = "depth + 1"; if (!p.ChildNeedsRefScan) { // Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); sb.AppendLine($"{i}else {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); } else { // ZERO branches — tc is always Object, just call ReadObject sb.AppendLine($"{i}{a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); } } else { // Ref tracking possible — Object/ObjectRefFirst/ObjectRef dispatch sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Object)"); sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRefFirst)"); sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, (int)context.ReadVarUInt())!;"); if (p.IsNullable) sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRef)"); sb.AppendLine($"{i} {a} = {cast}context.GetInternedObject((int)context.ReadVarUInt())!;"); } } /// /// Emits inline read for a Collection property. /// Complex element with SGen + known collection kind → inline Array loop with direct element reader calls. /// Else → runtime fallback via ReadValueGenerated. /// private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i) { // Check if we can inline: need SGen element reader + known collection shape + Array marker if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) { EmitReadCollectionInline(sb, p, a, tc, i); return; } // Runtime fallback if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context._position--;"); sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);"); sb.AppendLine($"{i}}}"); } else { sb.AppendLine($"{i}context._position--;"); sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;"); } } /// /// Emits inline collection read: Array marker already consumed as tc. /// Reads count + loops with direct element reader calls. /// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance. /// private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i) { var reader = p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); var elemType = p.ElementFullTypeName!; var elemCast = $"({elemType})"; var s = p.Name; // Null check if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Array)"); } else { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Array)"); } sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var nd_{s} = depth + 1;"); // Create collection based on kind if (p.CollectionKind == "Array") { sb.AppendLine($"{i} var col_{s} = new {elemType}[cnt_{s}];"); sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); sb.AppendLine($"{i} {{"); EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); sb.AppendLine($"{i} }}"); } else // List, Counted — all use List with Add { sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});"); sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); sb.AppendLine($"{i} {{"); EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan); sb.AppendLine($"{i} }}"); } sb.AppendLine($"{i} {a} = col_{s};"); sb.AppendLine($"{i}}}"); } /// /// Emits per-element read inside collection loop. /// SGen reader = non-metadata mode → no ObjectWithMetadata fallback. /// !needsRefScan → only Object/Null possible → 1 branch per element. /// private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan) { var etc = $"etc_{propSuffix}"; sb.AppendLine($"{i}var {etc} = context.ReadByte();"); var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.Add(null!);"; var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.Add("; var assignEnd = isArray ? ";" : ");"; if (!needsRefScan) { // No ref tracking → only Object or Null in stream — 1 branch sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}"); sb.AppendLine($"{i}else {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}"); } else { // Object hot path first, then ref markers sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Object)"); sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}"); sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRefFirst)"); sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, (int)context.ReadVarUInt())!{assignEnd}"); sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}"); sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRef)"); sb.AppendLine($"{i} {assignExpr}{elemCast}context.GetInternedObject((int)context.ReadVarUInt())!{assignEnd}"); } } /// /// Emits markered value read for primitive types (with type code already read). /// Handles TinyInt encoding for integer types. /// private static void EmitReadMarkeredValue(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i, PropInfo p, bool nullable) { var assign = nullable ? $"{a} = " : $"{a} = "; switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {assign}context.ReadVarLong();"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {assign}true;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {assign}false;"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {assign}context.ReadDoubleUnsafe();"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {assign}context.ReadSingleUnsafe();"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {assign}context.ReadDecimalUnsafe();"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {assign}context.ReadDateTimeUnsafe();"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {assign}context.ReadGuidUnsafe();"); break; case PropertyTypeKind.Byte: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(byte)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {assign}context.ReadByte();"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(short)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {assign}context.ReadInt16Unsafe();"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ushort)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {assign}context.ReadUInt16Unsafe();"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(uint)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {assign}context.ReadVarUInt();"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ulong)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {assign}context.ReadVarULong();"); break; case PropertyTypeKind.Enum: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Enum)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var eb = context.ReadByte();"); sb.AppendLine($"{i} int ev;"); sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) ev = BinaryTypeCode.DecodeTinyInt(eb);"); sb.AppendLine($"{i} else ev = context.ReadVarInt();"); sb.AppendLine($"{i} {assign}({p.TypeNameForTypeof})(object)ev;"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({tc})) {assign}({p.TypeNameForTypeof})(object)BinaryTypeCode.DecodeTinyInt({tc});"); break; case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {assign}context.ReadTimeSpanUnsafe();"); break; case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {assign}context.ReadDateTimeOffsetUnsafe();"); break; } } #endregion 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) { var writerRef = string.IsNullOrEmpty(ci.Namespace) ? $"{ci.ClassName}_GeneratedWriter" : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; var readerRef = string.IsNullOrEmpty(ci.Namespace) ? $"{ci.ClassName}_GeneratedReader" : $"{ci.Namespace}.{ci.ClassName}_GeneratedReader"; sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);"); sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.Instance);"); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } /// /// Builds a flat class name for nested types: Outer_Inner_Leaf. /// For top-level types returns the simple name unchanged. /// private static string BuildFlatName(INamedTypeSymbol typeSymbol) { if (typeSymbol.ContainingType == null) return typeSymbol.Name; var parts = new List(); var current = typeSymbol; while (current != null) { parts.Add(current.Name); current = current.ContainingType; } parts.Reverse(); return string.Join("_", parts); } /// /// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute. /// Returns true (default) if no attribute or enableAllFeatures=true. /// private static bool ReadEnableMetadata(ITypeSymbol type) { var attr = type.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == AttributeName); if (attr == null) return true; if (attr.ConstructorArguments.Length == 1) return (bool)attr.ConstructorArguments[0].Value!; if (attr.ConstructorArguments.Length == 4) return (bool)attr.ConstructorArguments[0].Value!; return true; } /// /// Computes whether a type needs scan pass work, split into ref tracking and string interning. /// Uses a per-call HashSet to guard against circular references (no static cache — /// static state is unsafe in incremental generators as it persists across builds). /// Returns (needsRefScan, needsInternScan) — these are independent axes. /// private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type) { return ComputeNeedsScanCore(type, new HashSet()); } private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet visiting) { // Circular reference guard: if already visiting this type, assume true (safe fallback) var key = type.ToDisplayString(); if (!visiting.Add(key)) return (true, true, true); // Read [AcBinarySerializable] flags var attr = type.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == AttributeName); bool enableIdTracking = true, enableRefHandling = true, enableInternString = true; if (attr != null) { if (attr.ConstructorArguments.Length == 1) { var all = (bool)attr.ConstructorArguments[0].Value!; enableIdTracking = enableRefHandling = enableInternString = all; } else if (attr.ConstructorArguments.Length == 4) { enableIdTracking = (bool)attr.ConstructorArguments[1].Value!; enableRefHandling = (bool)attr.ConstructorArguments[2].Value!; enableInternString = (bool)attr.ConstructorArguments[3].Value!; } } // IId tracking: active in OnlyId + All modes var isIId = enableIdTracking && type.AllInterfaces.Any(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); var needsIdScan = isIId; // Non-IId ref tracking: active only in All mode var needsAllRefScan = !isIId && enableRefHandling; var needsInternScan = false; // Check properties for string interning or complex children foreach (var member in type.GetMembers()) { if (member is not IPropertySymbol p || p.DeclaredAccessibility != Accessibility.Public || p.GetMethod == null || p.SetMethod == null || p.IsIndexer || p.IsStatic) continue; var hasIgnore = p.GetAttributes().Any(a => { var name = a.AttributeClass?.Name ?? ""; return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; }); if (hasIgnore) continue; // Early exit: if all flags are already true, no need to check more properties if (needsIdScan && needsAllRefScan && needsInternScan) break; var kind = GetKind(p.Type); // String with interning? if (enableInternString && kind == PropertyTypeKind.String) { var internAttr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!)) needsInternScan = true; } // Complex child → recurse if (kind == PropertyTypeKind.Complex) { var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type; var childFlags = ComputeNeedsScanCore(resolved, visiting); needsIdScan |= childFlags.needsIdScan; needsAllRefScan |= childFlags.needsAllRefScan; needsInternScan |= childFlags.needsInternScan; } // Collection → check element type if (kind == PropertyTypeKind.Collection) { var elemType = GetCollectionElementType(p.Type); if (elemType != null) { var elemKind = GetKind(elemType); if (enableInternString && elemKind == PropertyTypeKind.String) needsInternScan = true; if (elemKind == PropertyTypeKind.Complex) { var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType; var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting); needsIdScan |= elemFlags.needsIdScan; needsAllRefScan |= elemFlags.needsAllRefScan; needsInternScan |= elemFlags.needsInternScan; } } } } return (needsIdScan, needsAllRefScan, needsInternScan); } #region FNV-1a Hash (compile-time) private static int ComputeFnvHash(string value) { uint hash = 2166136261; for (int i = 0; i < value.Length; i++) { hash ^= value[i]; hash *= 16777619; } return (int)hash; } /// /// Computes FNV-1a hashes for all serializable properties of a child type. /// Property filtering and ordering matches runtime TypeMetadataBase exactly: /// public get+set, non-indexer, non-static, no ignore attributes, sorted alphabetically. /// private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType) { var propNames = new List(); foreach (var member in resolvedType.GetMembers()) { if (member is IPropertySymbol cp && cp.DeclaredAccessibility == Accessibility.Public && cp.GetMethod != null && cp.SetMethod != null && !cp.IsIndexer && !cp.IsStatic) { var hasIgnore = cp.GetAttributes().Any(a => { var name = a.AttributeClass?.Name ?? ""; return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; }); if (hasIgnore) continue; propNames.Add(cp.Name); } } propNames.Sort(StringComparer.Ordinal); return propNames.Select(ComputeFnvHash).ToArray(); } #endregion #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; } /// /// Extracts the element type T from List<T>, T[], IList<T>, IEnumerable<T>. /// Returns null if the element type cannot be determined. /// private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type) { // T[] → element type if (type is IArrayTypeSymbol arrayType) return arrayType.ElementType; // Generic collections: List, IList, ICollection, IEnumerable if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { // Direct: List, HashSet, etc. — first type argument var iface = namedType.AllInterfaces .FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); if (iface != null) return iface.TypeArguments[0]; } return null; } 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; } /// True if this type implements IId<T> public bool IsIId { get; } /// The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise public string? IdTypeName { get; } /// True if EnableRefHandlingFeature is enabled — controls non-IId All mode tracking code emission. public bool EnableRefHandling { get; } /// FNV-1a hash of ClassName (matches runtime SourceType.Name hash) public int TypeNameHash { get; } /// FNV-1a hash of each property name, in property order public int[] PropertyNameHashes { get; } /// When false, skip inline metadata and use markerless property write for this type. public bool EnableMetadata { get; } /// When true, type subtree has IId types needing scan (active in OnlyId + All). public bool NeedsIdScan { get; } /// When true, type subtree has non-IId ref tracking (active only in All mode). public bool NeedsAllRefScan { get; } /// When true, type subtree needs string interning scan. public bool NeedsInternScan { get; } /// Derived: NeedsIdScan || NeedsAllRefScan. public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan; /// Derived: any scan axis active. public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan; public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool needsIdScan, bool needsAllRefScan, bool needsInternScan) { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; } } 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; } /// Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId. public string? IdTypeName { get; } // Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer /// Element type kind for collection properties. Only meaningful when TypeKind == Collection. public PropertyTypeKind ElementKind { get; } /// True if collection element type has [AcBinarySerializable]. public bool ElementHasGeneratedWriter { get; } /// True if collection element type implements IId<T>. public bool ElementIsIId { get; } /// Generated writer class name for collection element type. public string? ElementWriterClassName { get; } /// Id type name for collection element IId types. Null if not IId. public string? ElementIdTypeName { get; } /// Collection type: "List", "Array", or null (unknown — fallback to runtime). public string? CollectionKind { get; } /// Full element type name for generated code (e.g. "SharedTag"). public string? ElementFullTypeName { get; } // UseMetadata inline hash-ek (Complex/Collection child típushoz) /// FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter. public int ChildTypeNameHash { get; } /// FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter. public int[]? ChildPropertyHashes { get; } /// FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter. public int ElementTypeNameHash { get; } /// FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter. public int[]? ElementPropertyHashes { get; } /// When false, child Complex type skips inline metadata in generated code. public bool ChildEnableMetadata { get; } /// When false, collection element type skips inline metadata in generated code. public bool ElementEnableMetadata { get; } /// When true, child subtree has IId types needing scan (active in OnlyId + All). public bool ChildNeedsIdScan { get; } /// When true, child subtree has non-IId ref tracking (active only in All mode). public bool ChildNeedsAllRefScan { get; } /// When true, child subtree needs string interning scan. public bool ChildNeedsInternScan { get; } /// Derived: ChildNeedsIdScan || ChildNeedsAllRefScan. public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan; /// Derived: any child scan axis active. public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan; /// When true, element subtree has IId types needing scan (active in OnlyId + All). public bool ElementNeedsIdScan { get; } /// When true, element subtree has non-IId ref tracking (active only in All mode). public bool ElementNeedsAllRefScan { get; } /// When true, element subtree needs string interning scan. public bool ElementNeedsInternScan { get; } /// Derived: ElementNeedsIdScan || ElementNeedsAllRefScan. public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan; /// Derived: any element scan axis active. public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan; public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, int childTypeNameHash = 0, int[]? childPropertyHashes = null, int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, bool childEnableMetadata = true, bool elementEnableMetadata = true, bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true, bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true) { Name = n; TypeName = tn; TypeNameForTypeof = tnForTypeof; TypeKind = tk; IsNullable = nullable; HasGeneratedWriter = hasGeneratedWriter; IsIId = isIId; WriterClassName = writerClassName; IdTypeName = idTypeName; ElementKind = elementKind; ElementHasGeneratedWriter = elementHasGenWriter; ElementIsIId = elementIsIId; ElementWriterClassName = elementWriterClassName; ElementIdTypeName = elementIdTypeName; CollectionKind = collectionKind; ElementFullTypeName = elementFullTypeName; ChildTypeNameHash = childTypeNameHash; ChildPropertyHashes = childPropertyHashes; ElementTypeNameHash = elementTypeNameHash; ElementPropertyHashes = elementPropertyHashes; ChildEnableMetadata = childEnableMetadata; ElementEnableMetadata = elementEnableMetadata; ChildNeedsIdScan = childNeedsIdScan; ChildNeedsAllRefScan = childNeedsAllRefScan; ChildNeedsInternScan = childNeedsInternScan; ElementNeedsIdScan = elementNeedsIdScan; ElementNeedsAllRefScan = elementNeedsAllRefScan; ElementNeedsInternScan = elementNeedsInternScan; // 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 }