using System.Collections.Generic; using System.Linq; using System.Text; namespace AyCode.Core.Serializers.SourceGenerator; /// /// Writer-side emit pass: generates the IGeneratedBinaryWriter implementation for each /// [AcBinarySerializable] type. Emits WriteProperties (write pass), ScanObject /// (scan pass for ref-handling + interning), and ScanForDuplicates (write-plan builder). /// /// Sub-passes: /// /// EmitProp — per-property write emit (markered + markerless variants). /// EmitScan* — per-property scan emit (intern + ref tracking). /// EmitDirect*Write — inline collection / dictionary write loops. /// EmitSkip / EmitVal / EmitMarkerless — primitive write fragments. /// /// public partial class AcBinarySourceGenerator { 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 System.Runtime.InteropServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); // IGeneratedBinaryWriter and other serializer types 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) where TOutput : struct, IBinaryOutputBase"); sb.AppendLine(" {"); sb.AppendLine(" // Depth check + EnterRecursion happens at the CALLER (before marker write)."); sb.AppendLine(" // Body just runs property writes; ExitRecursion at the end balances the caller's EnterRecursion."); sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); foreach (var p in ci.Properties) { sb.AppendLine(); EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter, ci.EnablePolymorphDetect); } sb.AppendLine(); sb.AppendLine(" context.ExitRecursion();"); 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);"); 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) 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.HasAllRefHandling && !context.HasStringInterning) return;"); else if (ci.NeedsAllRefScan) sb.AppendLine(" if (!context.HasAllRefHandling) return;"); else if (ci.NeedsInternScan) sb.AppendLine(" if (!context.HasStringInterning) return;"); } // Null guard — MaxDepth option removed (was: cycle protection via runtime depth check). // Cycle safety now comes from IId-tracking; future [AcBinaryCircular] attr will mark non-IId circular refs. sb.AppendLine(" if (value == null) 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.HasRefHandling)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);"); 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(" }"); } if (ci.EnableRefHandling && !ci.IsIId) { // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode sb.AppendLine(); sb.AppendLine(" if (context.HasAllRefHandling)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);"); 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 || p.TypeKind == PropertyTypeKind.Dictionary).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) || (p.TypeKind == PropertyTypeKind.Dictionary && (p.DictKeyKind == PropertyTypeKind.String || p.DictValueKind == PropertyTypeKind.String) && p.InterningFlags != 0)); // Combined check+inc — gated inside TryEnterRecursion (checks NeedsDepthCheck). // Emitted AFTER all early returns (NeedsScan=false, feature-flag, null guard, IId 2nd-occurrence) // and BEFORE the property scan loop that recurses into children. // On limit hit: helper method (cold path, NoInlining) dispatches Throw or Truncate (return). sb.AppendLine(); sb.AppendLine(" if (context.TryEnterRecursion(hasTruncatePath: false)) return; // scan: skip children"); if (hasStringScan) { // Use pre-computed InternBit from context (avoids Options.UseStringInterning field chain + shift per object). // 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 = context.InternBit;"); 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, ci.EnablePropertyFilter); } if (!hasAnyScanProp) { sb.AppendLine(" // No reference properties to scan"); } sb.AppendLine(); sb.AppendLine(" context.ExitRecursion();"); sb.AppendLine(" }"); } private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect) { 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) EmitMarkerless(sb, p.TypeKind, a, i); else EmitPropertyBridge(sb, p.TypeKind, a, i); return; } // All non-markerless properties: emit PropertyFilter guard // When filter returns false, write PropertySkip and skip the property write. // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — when false, the entire // filter-check block is omitted from emit (no `HasPropertyFilter` test, no filter-context allocation, // no lambda-call) → leaner generated code on hot-path types that never use a property-filter. if (enablePropertyFilter) { 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: // Inlined string-property write -- streamlined chain (bypasses WriteStringGenerated / // WriteString): FastWire -> markerless UTF-16; else null -> PropertySkip, empty -> // StringEmpty, content -> interning attempt (eligible props) + WriteStringWithDispatch. // A local pins the single getter evaluation; the chain is small + branch-friendly so // the JIT folds it into WriteProperties (no per-string call frame). sb.AppendLine($"{i}var str_{p.Name} = {a};"); sb.AppendLine($"{i}if (context.FastWire) context.WriteStringUtf16Markerless(str_{p.Name});"); sb.AppendLine($"{i}else if (str_{p.Name} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else if (str_{p.Name}.Length == 0) context.WriteByte(BinaryTypeCode.StringEmpty);"); if (p.InterningFlags == 0) { sb.AppendLine($"{i}else context.WriteStringWithDispatch(str_{p.Name});"); } else { sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{i} if (!context.TryWriteInternedString(str_{p.Name})) context.WriteStringWithDispatch(str_{p.Name});"); sb.AppendLine($"{i}}}"); } 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.IsObjectDeclaredType) { // System.Object property: runtime type unknown at compile time. // Write ObjectWithTypeName prefix so deserializer can resolve the concrete type. // Use value.GetType() for runtime type dispatch (not typeof(object)). // Gated by `[AcBinarySerializable(enablePolymorphDetectFeature: ...)]` — `false` // skips the type-name emit entirely. Misuse (false + non-null object prop at runtime) // is caught at build time by ACBIN002 in DetectAndReportPolymorphicMisuse. sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); if (enablePolymorphDetect) { sb.AppendLine($"{i} if (!context.UseMetadata)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithTypeName);"); sb.AppendLine($"{i} context.WriteStringUtf8({a}.GetType().AssemblyQualifiedName!);"); sb.AppendLine($"{i} }}"); } sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context);"); sb.AppendLine($"{i}}}"); } else { // Reference type properties can always be null at runtime regardless of nullable // annotation — runtime can violate the nullable-disabled contract via EF lazy-load, // projection gaps, detached navigation properties, etc. Mirrors the EmitDirectObjectWrite // (line ~828) and EmitDirectCollectionWrite (line ~877) defensive null-check. // Reader-side compat: every markered property is wrapped in `if (tc != PropertySkip)` // by EmitReadProperty (GenReader.cs line ~137), so the marker is uniformly handled. sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); } 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 { // Reference type collections (with non-SGen elements, falling onto the // WriteValueGenerated bridge) can always be null at runtime regardless of nullable // annotation — runtime can violate the nullable-disabled contract via EF lazy-load, // projection gaps, missing initializers, etc. Mirrors EmitDirectCollectionWrite // (line ~877) and EmitDirectObjectWrite (line ~828) defensive null-check. // Reader-side compat: every markered property is wrapped in `if (tc != PropertySkip)` // by EmitReadProperty (GenReader.cs line ~137). sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); } break; case PropertyTypeKind.Dictionary: EmitDirectDictionaryWrite(sb, p, a, i); break; default: EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); break; } sb.AppendLine($"{i}skip_{p.Name}:;"); } // IsMarkerless moved to AcBinarySourceGenerator.TypeAnalysis.cs (shared writer/reader utility). /// /// 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 a single bridge method call for markerless property types with enableMetadata=true. /// The bridge method on BinarySerializationContext handles both UseMetadata=true (markered+skip) /// and UseMetadata=false (markerless) paths internally. Replaces 7-11 lines of generated code with 1 line. /// private static void EmitPropertyBridge(StringBuilder sb, PropertyTypeKind k, string a, string i) { var call = k switch { PropertyTypeKind.Int32 => $"context.WriteInt32Property({a});", PropertyTypeKind.Int64 => $"context.WriteInt64Property({a});", PropertyTypeKind.Boolean => $"context.WriteBoolProperty({a});", PropertyTypeKind.Double => $"context.WriteFloat64Property({a});", PropertyTypeKind.Single => $"context.WriteFloat32Property({a});", PropertyTypeKind.Decimal => $"context.WriteDecimalProperty({a});", PropertyTypeKind.DateTime => $"context.WriteDateTimeProperty({a});", PropertyTypeKind.Guid => $"context.WriteGuidProperty({a});", PropertyTypeKind.Byte => $"context.WriteByteProperty({a});", PropertyTypeKind.Int16 => $"context.WriteInt16Property({a});", PropertyTypeKind.UInt16 => $"context.WriteUInt16Property({a});", PropertyTypeKind.UInt32 => $"context.WriteUInt32Property({a});", PropertyTypeKind.UInt64 => $"context.WriteUInt64Property({a});", PropertyTypeKind.Enum => $"context.WriteEnumInt32Property((int){a});", PropertyTypeKind.TimeSpan => $"context.WriteTimeSpanProperty({a});", PropertyTypeKind.DateTimeOffset => $"context.WriteDateTimeOffsetProperty({a});", _ => null }; if (call != null) sb.AppendLine($"{i}{call}"); } /// /// Emits direct object write — bypasses GetWrapper + WriteObject entirely. #region Scan Pass Code Generation /// /// Compile-time check: will EmitScanProp produce any scan work for this property? /// When false, the entire block (including PropertyFilter guard) is skipped. /// private static bool HasScanWork(PropInfo p) => p.TypeKind switch { PropertyTypeKind.String => p.InterningFlags != 0, PropertyTypeKind.Complex when p.HasGeneratedWriter => p.ChildNeedsScan, PropertyTypeKind.Complex => true, PropertyTypeKind.Collection => HasCollectionScanWork(p), PropertyTypeKind.Dictionary => HasDictionaryScanWork(p), _ => false }; private static bool HasCollectionScanWork(PropInfo p) => p.ElementKind switch { PropertyTypeKind.String => p.InterningFlags != 0, PropertyTypeKind.Complex when p.ElementHasGeneratedWriter && p.CollectionKind != null => p.ElementNeedsScan, PropertyTypeKind.Complex => true, _ => false }; private static bool HasDictionaryScanWork(PropInfo p) { if (p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0) return true; if (p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0) return true; if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter) return p.DictValueNeedsScan; if (p.DictValueKind == PropertyTypeKind.Complex) return true; return false; } /// /// 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, bool enablePropertyFilter) { // Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard) if (!HasScanWork(p)) return; 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). // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — same gate as the writer pass. if (enablePropertyFilter && !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; case PropertyTypeKind.Dictionary: EmitScanDictionary(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.HasAllRefHandling || context.HasStringInterning"; else if (p.ChildNeedsAllRefScan) guard = "context.HasAllRefHandling"; else if (p.ChildNeedsInternScan) guard = "context.HasStringInterning"; } 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);"); 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);"); } } /// /// Emits scan pass code for a Complex property without SGen writer (runtime fallback). /// System.Object properties use value.GetType() for runtime type dispatch. /// 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)"); if (p.IsObjectDeclaredType) sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, {childVar}.GetType(), context);"); else sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context);"); } /// /// 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} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});"); sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "IndexedCollection") { 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.HasAllRefHandling || context.HasStringInterning"; else if (p.ElementNeedsAllRefScan) elemGuard = "context.HasAllRefHandling"; else if (p.ElementNeedsInternScan) elemGuard = "context.HasStringInterning"; } // 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} {{"); 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} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});"); sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "IndexedCollection") { 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);"); 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);"); return; } // Primitive element collection — no scanning needed } /// /// Emits inline dictionary scan. Iterates entries and: /// - String keys: ScanInternString if interning flags match /// - String values: ScanInternString if interning flags match /// - Complex+SGen values: ScanObject on each value (handles ref tracking internally) /// Eliminates GetWrapper dictionary lookup for all inlineable dictionary types. /// private static void EmitScanDictionary(StringBuilder sb, PropInfo p, string a, string i) { var s = p.Name; var hasStringKeys = p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0; var hasStringValues = p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0; var hasComplexValues = p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter; // No scanning needed for primitive-only dictionaries without internable strings or complex values if (!hasStringKeys && !hasStringValues && !hasComplexValues) return; // Complex+SGen values: compile-time proven scan is no-op → skip entirely if (hasComplexValues && !p.DictValueNeedsScan && !hasStringKeys && !hasStringValues) return; // Build guard expression for Complex+SGen values (3-axis: IId/AllRef/Intern) string? complexGuard = null; if (hasComplexValues && p.DictValueNeedsScan && !p.DictValueNeedsIdScan) { if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan) complexGuard = "context.HasAllRefHandling || context.HasStringInterning"; else if (p.DictValueNeedsAllRefScan) complexGuard = "context.HasAllRefHandling"; else if (p.DictValueNeedsInternScan) complexGuard = "context.HasStringInterning"; } // For string-only scan (no complex values), use simple interning loop if (!hasComplexValues) { sb.AppendLine($"{i}var sd_{s} = {a};"); sb.AppendLine($"{i}if (sd_{s} != null && ({p.InterningFlags} & internBit) != 0)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} foreach (var sde_{s} in sd_{s})"); sb.AppendLine($"{i} {{"); if (hasStringKeys) { sb.AppendLine($"{i} if (sde_{s}.Key != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var sklen_{s} = sde_{s}.Key.Length;"); sb.AppendLine($"{i} if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Key);"); sb.AppendLine($"{i} }}"); } if (hasStringValues) { sb.AppendLine($"{i} if (sde_{s}.Value != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var svlen_{s} = sde_{s}.Value.Length;"); sb.AppendLine($"{i} if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Value);"); sb.AppendLine($"{i} }}"); } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); return; } // Complex+SGen values (with optional string key/value interning) var writer = p.DictValueWriterClassName!; // Guard entire scan block when no IId in value subtree if (complexGuard != null && !hasStringKeys && !hasStringValues) sb.AppendLine($"{i}if ({complexGuard})"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var sd_{s} = {a};"); sb.AppendLine($"{i} if (sd_{s} != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} foreach (var sde_{s} in sd_{s})"); sb.AppendLine($"{i} {{"); // String key interning if (hasStringKeys) { sb.AppendLine($"{i} if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Key != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var sklen_{s} = sde_{s}.Key.Length;"); sb.AppendLine($"{i} if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Key);"); sb.AppendLine($"{i} }}"); } // String value interning if (hasStringValues) { sb.AppendLine($"{i} if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Value != null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var svlen_{s} = sde_{s}.Value.Length;"); sb.AppendLine($"{i} if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))"); sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Value);"); sb.AppendLine($"{i} }}"); } // Complex value ScanObject if (hasComplexValues) { sb.AppendLine($"{i} if (sde_{s}.Value != null)"); if (complexGuard != null) { sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if ({complexGuard})"); sb.AppendLine($"{i} {writer}.Instance.ScanObject(sde_{s}.Value, context);"); sb.AppendLine($"{i} }}"); } else sb.AppendLine($"{i} {writer}.Instance.ScanObject(sde_{s}.Value, context);"); } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); } #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 refSuffix = p.IsIId ? "IId" : "All"; // Reference type properties can always be null at runtime regardless of nullable annotation sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); if (!p.ChildNeedsRefScan && !p.ChildEnableMetadata) { // Compile-time proven: no ref, no metadata. Combined check+inc BEFORE marker write so Truncate writes // Null wire-correctly. TryEnterRecursion inc'd on success; ExitRecursion at WriteProperties end. sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({a}, context); }}"); } else if (p.ChildNeedsRefScan && !p.ChildEnableMetadata) { sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{refSuffix}()) {writer}.Instance.WriteProperties({a}, context);"); } else if (!p.ChildNeedsRefScan && p.ChildEnableMetadata) { sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);"); } else { sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{refSuffix}({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);"); } } /// /// 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; // Reference type collections can always be null at runtime regardless of nullable annotation sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); 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} 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} foreach (var elem_{p.Name} in col_{p.Name})"); sb.AppendLine($"{i} {{"); } else if (p.CollectionKind == "IndexedCollection") { sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];"); } else // List — CollectionsMarshal.AsSpan for zero-overhead iteration { sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});"); sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];"); } // Per-element write var e = $"elem_{p.Name}"; sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); var elemRefSuffix = p.ElementIsIId ? "IId" : "All"; // Ref-aware emit decision routed through RefAwareEmitPredicate — symmetric counterpart to the // reader-side EmitReadCollectionElement / EmitReadComplex guards. Single source of truth so the // writer-emit (which marker variants may appear on the wire) and the reader-emit (which marker // variants are decoded) NEVER drift apart on the same PropInfo. var elementEmitsRefMarker = RefAwareEmitPredicate.ElementEmitsRefMarker(p); if (!elementEmitsRefMarker && !p.ElementEnableMetadata) { // Compile-time proven: no ref, no metadata. Combined check+inc before marker write. sb.AppendLine($"{i} if (context.TryEnterRecursion(hasTruncatePath: true)) continue;"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context);"); } else if (elementEmitsRefMarker && !p.ElementEnableMetadata) { sb.AppendLine($"{i} if (context.WriteObjectRefMarker{elemRefSuffix}()) {writer}.Instance.WriteProperties({e}, context);"); } else if (!elementEmitsRefMarker && p.ElementEnableMetadata) { sb.AppendLine($"{i} if (context.WriteObjectMetaMarker({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);"); } else { sb.AppendLine($"{i} if (context.WriteObjectFullMarker{elemRefSuffix}({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);"); } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); } /// /// Emits inline write of a primitive/string/enum value in non-property context (no PropertySkip). /// Matches runtime TryWritePrimitive wire format: TinyInt for small int, type code + value otherwise. /// Used for dictionary key/value writes. /// private static void EmitWritePrimitiveValue(StringBuilder sb, PropertyTypeKind kind, string a, string suffix, string i) { switch (kind) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}if (BinaryTypeCode.TryEncodeTinyInt({a}, out var tk_{suffix})) context.WriteByte(tk_{suffix});"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}if ({a} >= int.MinValue && {a} <= int.MaxValue)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var iv_{suffix} = (int){a};"); sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(iv_{suffix}, out var tk_{suffix})) context.WriteByte(tk_{suffix});"); sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{suffix}); }}"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else {{ 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; 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; case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a});"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a});"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a});"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a});"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a});"); break; case PropertyTypeKind.Enum: sb.AppendLine($"{i}{{ var ev_{suffix} = (int){a}; context.WriteByte(BinaryTypeCode.Enum);"); sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(ev_{suffix}, out var te_{suffix})) context.WriteByte(te_{suffix});"); sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{suffix}); }} }}"); break; } } /// /// Emits inline dictionary write. Wire format: [Dictionary][count][key₁ value₁ key₂ value₂ ...]. /// Keys/values are written with type codes matching runtime TryWritePrimitive/WriteValue. /// Eliminates GetWrapper dictionary lookup for all inlineable key/value types. /// private static void EmitDirectDictionaryWrite(StringBuilder sb, PropInfo p, string a, string i) { var s = p.Name; var keyType = p.DictKeyTypeName ?? "object"; var valType = p.DictValueTypeName ?? "object"; // Reference type dictionaries can always be null at runtime regardless of nullable annotation sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Dictionary);"); sb.AppendLine($"{i} context.WriteVarUInt((uint){a}.Count);"); sb.AppendLine($"{i} foreach (var kvp_{s} in {a})"); sb.AppendLine($"{i} {{"); var k = $"kvp_{s}.Key"; var v = $"kvp_{s}.Value"; var ii = i + " "; // Write key if (p.DictKeyKind == PropertyTypeKind.String) { if (p.InterningFlags == 0) sb.AppendLine($"{ii}context.StringInternEligible = false;"); else sb.AppendLine($"{ii}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{ii}AcBinarySerializer.WriteStringGenerated({k}, context);"); } else if (IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum) { EmitWritePrimitiveValue(sb, p.DictKeyKind, k, $"dk_{s}", ii); } else { sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({k}, typeof({keyType}), context);"); } // Write value if (p.DictValueKind == PropertyTypeKind.String) { // String value: null → Null, non-null → WriteStringGenerated sb.AppendLine($"{ii}if ({v} == null) context.WriteByte(BinaryTypeCode.Null);"); sb.AppendLine($"{ii}else"); sb.AppendLine($"{ii}{{"); if (p.InterningFlags == 0) sb.AppendLine($"{ii} context.StringInternEligible = false;"); else sb.AppendLine($"{ii} context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{ii} AcBinarySerializer.WriteStringGenerated({v}, context);"); sb.AppendLine($"{ii}}}"); } else if (IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum) { EmitWritePrimitiveValue(sb, p.DictValueKind, v, $"dv_{s}", ii); } else if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter) { EmitDictValueComplexWrite(sb, p, v, s, ii); } else { // Fallback for non-inlineable value types sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({v}, typeof({valType}), context);"); } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); } /// /// Emits inline write for a Complex+SGen dictionary value with ref tracking and metadata support. /// Delegates marker logic to runtime WriteObjectRefMarker/MetaMarker/FullMarker bridge. /// private static void EmitDictValueComplexWrite(StringBuilder sb, PropInfo p, string v, string s, string i) { var writer = p.DictValueWriterClassName!; sb.AppendLine($"{i}if ({v} == null) {{ context.WriteByte(BinaryTypeCode.Null); }}"); var dvRefSuffix = p.DictValueIsIId ? "IId" : "All"; if (!p.DictValueNeedsRefScan && !p.DictValueEnableMetadata) { // No ref, no metadata. Combined check+inc before marker write. sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}"); sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({v}, context); }}"); } else if (p.DictValueNeedsRefScan && !p.DictValueEnableMetadata) { sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{dvRefSuffix}()) {writer}.Instance.WriteProperties({v}, context);"); } else if (!p.DictValueNeedsRefScan && p.DictValueEnableMetadata) { sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);"); } else { sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{dvRefSuffix}({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);"); } } 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);"); 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; } } }