using System.Collections.Generic; using System.Text; namespace AyCode.Core.Serializers.SourceGenerator; /// /// Reader-side emit pass: generates the IGeneratedBinaryReader implementation for each /// [AcBinarySerializable] type. Emits ReadProperties (inline property reads with marker /// dispatch) and ReadObject (entry point with cache-index registration). /// /// Sub-passes: /// /// EmitReadProp — per-property read emit (markerless + markered variants). /// EmitReadString — H2Q6 string-tier marker dispatch (FixStrAscii + tier-tables + /// intern cases gated by EnableInternStringFeature). /// EmitReadComplex — Object / ObjectRef* / FixObj-slot dispatch for IId-typed children. /// EmitReadCollection / EmitReadCollectionInline / EmitReadCollectionElement / /// EmitReadNonComplexCollectionElement — collection-shape inline reading. /// EmitReadDictionary / EmitReadDictElement — dict-shape inline reading. /// EmitReadMarkeredValue / EmitReadMarkeredValueForKind — primitive value-with-marker reads. /// EmitReadMarkerless — markerless primitive reads (FastMode + per-property markerless types). /// /// public partial class AcBinarySourceGenerator { #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(); // ReadProperties — reads all properties into an existing instance (mirrors WriteProperties) // No depth safety net on deserialize: wire format is linear + finite, the serializer-side counter // already prevents pathological depth in well-formed payloads. sb.AppendLine(" public void ReadProperties(object value, AcBinaryDeserializer.BinaryDeserializationContext context)"); sb.AppendLine(" where TInput : struct, IBinaryInputBase"); sb.AppendLine(" {"); sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);"); // Emit property reads — markerless for primitive types, markered for the rest foreach (var p in ci.Properties) { sb.AppendLine(); EmitReadProp(sb, p, " ", ci.EnableMetadata, ci.EnableInternString); } sb.AppendLine(" }"); sb.AppendLine(); // ReadObject — IGeneratedBinaryReader implementation (delegates to ReadProperties) sb.AppendLine(" public object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int cacheIndex)"); sb.AppendLine(" where TInput : struct, IBinaryInputBase"); sb.AppendLine(" {"); sb.AppendLine($" var obj = new {ci.FullTypeName}();"); sb.AppendLine(" if (cacheIndex >= 0)"); sb.AppendLine(" context.RegisterInternedValueAt(cacheIndex, obj);"); sb.AppendLine(" ReadProperties(obj, context);"); 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, bool enableInternString) { 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; } // ACCORE-BIN-T-K9M3 — caller-driven string marker dispatch. SGen-emit reads the marker byte // locally + handles FastWire on a separate branch; BinaryDeserializationContext.TryReadStringProperty // decodes every non-interning marker (FixStrAscii / StringAscii / StringSmall/Medium/Big / Null / // StringEmpty) in one inlinable body. The 3 interning markers go through TryReadStringColdPath // (AggressiveOptimization, Tier-1 direct). enableInternString gates the `|| TryReadStringColdPath` // emit: interning-enabled types get the short-circuit; non-interning types omit the cold call // entirely — the writer never produces interning markers for them, so TryReadStringProperty alone // is total. PropertySkip / unknown → TryReadStringProperty returns false → property left at // default (don't-touch contract preserved). if (p.TypeKind == PropertyTypeKind.String) { sb.AppendLine($"{i}if (context.FastWire)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} {a} = context.ReadStringUtf16Markerless()!;"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var tc_{p.Name} = context.ReadByte();"); sb.AppendLine($"{i} string? v_{p.Name};"); if (enableInternString) sb.AppendLine($"{i} if (context.TryReadStringProperty(tc_{p.Name}, out v_{p.Name}) || context.TryReadStringColdPath(tc_{p.Name}, out v_{p.Name}))"); else sb.AppendLine($"{i} if (context.TryReadStringProperty(tc_{p.Name}, out v_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {a} = v_{p.Name}!;"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{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 + " ", enableInternString); break; case PropertyTypeKind.Complex: EmitReadComplex(sb, p, a, tc, i + " "); break; case PropertyTypeKind.Collection: EmitReadCollection(sb, p, a, tc, i + " ", enableInternString); break; case PropertyTypeKind.Dictionary: EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString); 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}));"); else sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;"); 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 H2Q6 (v3 wire format) string markers: /// FixStr (short-form universal, 135-166), String (long-form universal, 167), /// StringUtf16 (FastWire marker, 91), /// StringInternFirstSmall/Medium (interning tiers, 104/105), /// StringInterned (cache ref, 92), StringEmpty (93), Null. /// /// FixStr is checked first as the hot path for short strings; non-ASCII /// tier markers carry both charLen and utf8Len in fixed-width headers (1-pass decode). /// private static void EmitReadString(StringBuilder sb, string a, string tc, string i, bool enableInternString) { // FixStr is the hot path — short-form universal marker with charLength in the marker. sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStr({tc}))"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} {a} = context.ReadUniversalFixStr({tc});"); sb.AppendLine($"{i}}}"); // Switch gives O(1) dispatch via JIT jump table for the remaining markers. sb.AppendLine($"{i}else switch ({tc})"); sb.AppendLine($"{i}{{"); // Interning case (2nd+ occurrence ref) — only emit when EnableInternStringFeature is enabled // on this type. When disabled, the writer never emits StringInterned markers for this type's // properties, so the reader doesn't need to handle them. ACCORE-BIN-T-K9M3 Phase C. if (enableInternString) { sb.AppendLine($"{i} case BinaryTypeCode.StringInterned:"); sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());"); sb.AppendLine($"{i} break;"); } // StringUtf16 marker + String. Wire-decode body is shared with the runtime path // (TypeReaderTable + cross-type populate) — see context.ReadStringUtf16Marker() // and ReadUniversalLongString. // These markers are feature-independent: writer emits them on any string property regardless of // intern setting (intern is opt-in per-property via [AcStringIntern] + InternBit). sb.AppendLine($"{i} case BinaryTypeCode.StringUtf16:"); sb.AppendLine($"{i} {a} = context.ReadStringUtf16Marker();"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i} case BinaryTypeCode.String:"); sb.AppendLine($"{i} {a} = context.ReadUniversalLongString();"); sb.AppendLine($"{i} break;"); // Interning first-occurrence cases — see comment above. if (enableInternString) { sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstSmall:"); sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringSmall();"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:"); sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringMedium();"); sb.AppendLine($"{i} break;"); } sb.AppendLine($"{i} case BinaryTypeCode.Null:"); sb.AppendLine($"{i} {a} = null;"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i} case BinaryTypeCode.StringEmpty:"); sb.AppendLine($"{i} {a} = string.Empty;"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i}}}"); } /// /// 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}));"); sb.AppendLine($"{i}}}"); } else { sb.AppendLine($"{i}context._position--;"); sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;"); } return; } var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); var cast = $"({p.TypeNameForTypeof})"; // Ref-aware switch decision routed through RefAwareEmitPredicate — single source of truth shared // with the writer-side EmitDirectCollectionWrite + the sibling EmitReadCollectionElement. The // decision depends EXCLUSIVELY on the child compile-time fact `ChildNeedsRefScan` — the parent // EnableRefHandlingFeature flag is NOT a factor here (it governs only the parent's SELF-tracking // emit in the scan pass, not the marker dispatch for child property reads). Asymmetry-bug fix: // see AcBinarySerializerIIdReferenceTests.Serialize_RefMarkerCollectionElement_ParentRefHandlingFeatureOff_DriftReproduction. if (!RefAwareEmitPredicate.ChildEmitsRefMarker(p)) { // Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream // Inline: parent creates instance, calls ReadProperties directly (mirrors EmitDirectObjectWrite) // FixObj slot bytes (0..SlotCount-1) are also valid markers here — populate slot cache // to keep _nextRuntimeSlot in sync with the serializer's _nextTypeSlot counter. if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc}); if ({tc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1; }}"); sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();"); sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);"); sb.AppendLine($"{i} {a} = rc_{p.Name};"); sb.AppendLine($"{i}}}"); } else { // ZERO branches — tc is always Object or FixObj sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc}); if ({tc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1; }}"); sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();"); sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);"); sb.AppendLine($"{i} {a} = rc_{p.Name};"); sb.AppendLine($"{i}}}"); } } else { // Ref tracking possible — switch on tc (Object / ObjectRefFirst / [Null] / ObjectRef / = context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1;"); sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();"); sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);"); sb.AppendLine($"{i} {a} = rc_{p.Name};"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i}}}"); } } /// /// Returns true when collection element reading can be inlined (no runtime ReadValue dispatch needed). /// private static bool CanInlineCollectionRead(PropInfo p) { if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter) return true; if (p.ElementKind == PropertyTypeKind.String) return true; if (p.ElementKind == PropertyTypeKind.Enum) return true; if (IsMarkerless(p.ElementKind)) return true; // all primitives return false; } /// /// Emits inline read for a Collection property. /// Known collection kind + inlineable element → inline Array loop with direct element reads. /// Else → runtime fallback via ReadValueGenerated. /// private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString) { // Check if we can inline: known collection shape + inlineable element type if (p.CollectionKind != null && CanInlineCollectionRead(p)) { EmitReadCollectionInline(sb, p, a, tc, i, enableInternString); 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}));"); sb.AppendLine($"{i}}}"); } else { sb.AppendLine($"{i}context._position--;"); sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;"); } } /// /// Emits inline read for a Dictionary property. /// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...]. /// Keys and values are read inline when their types are known (primitive/string/Complex+SGen). /// private static void EmitReadDictionary(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString) { var s = p.Name; var keyType = p.DictKeyTypeName ?? "object"; var valType = p.DictValueTypeName ?? "object"; // Can we inline key/value reads? var canInlineKey = p.DictKeyKind == PropertyTypeKind.String || IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum; var canInlineValue = p.DictValueKind == PropertyTypeKind.String || IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum || (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter); var canInline = canInlineKey || canInlineValue; // partial inline is still beneficial if (p.IsNullable) { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Dictionary)"); } else { sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Dictionary)"); } sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var dict_{s} = new System.Collections.Generic.Dictionary<{keyType}, {valType}>(cnt_{s});"); sb.AppendLine($"{i} for (var di_{s} = 0; di_{s} < cnt_{s}; di_{s}++)"); sb.AppendLine($"{i} {{"); // Read key if (canInlineKey) EmitReadDictElement(sb, p.DictKeyKind, keyType, $"dk_{s}", s, i + " ", null, false, enableInternString); else sb.AppendLine($"{i} var dk_{s} = ({keyType})AcBinaryDeserializer.ReadValueGenerated(context, typeof({keyType}))!;"); // Read value if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter) { var valReader = p.DictValueWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); var vtc = $"vtc_{s}"; sb.AppendLine($"{i} var {vtc} = context.ReadByte();"); sb.AppendLine($"{i} {valType}? dv_{s} = null;"); sb.AppendLine($"{i} if ({vtc} == BinaryTypeCode.Object)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var rv_{s} = new {valType}();"); sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);"); sb.AppendLine($"{i} dv_{s} = rv_{s};"); sb.AppendLine($"{i} }}"); // ObjectRefFirst / ObjectRef cases — routed through RefAwareEmitPredicate. Single source of // truth shared with EmitReadComplex / EmitReadCollectionElement / EmitDirectCollectionWrite. // The decision depends EXCLUSIVELY on the dict-value compile-time fact `DictValueNeedsRefScan` // — the parent EnableRefHandlingFeature flag is NOT a factor here (it governs only the parent's // SELF-tracking emit in the scan pass, GenWriter.cs:140). Symmetric with the writer-side // dict-value emit. Asymmetry-bug fix: see AcBinarySerializerIIdReferenceTests // .Serialize_RefMarkerCollectionElement_ParentRefHandlingFeatureOff_DriftReproduction. if (RefAwareEmitPredicate.DictValueEmitsRefMarker(p)) { sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var rv_{s} = new {valType}();"); sb.AppendLine($"{i} context.RegisterInternedValueAt(rci_{s}, rv_{s});"); sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);"); sb.AppendLine($"{i} dv_{s} = rv_{s};"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)"); sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;"); } sb.AppendLine($"{i} else if ({vtc} != BinaryTypeCode.Null)"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} context._position--;"); sb.AppendLine($"{i} dv_{s} = ({valType}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({valType}));"); sb.AppendLine($"{i} }}"); } else if (canInlineValue) EmitReadDictElement(sb, p.DictValueKind, valType, $"dv_{s}", s, i + " ", null, true, enableInternString); else sb.AppendLine($"{i} var dv_{s} = ({valType}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({valType}));"); // Add to dictionary sb.AppendLine($"{i} if (dk_{s} != null) dict_{s}[dk_{s}] = dv_{s}!;"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} {a} = dict_{s};"); sb.AppendLine($"{i}}}"); } /// /// Emits inline read for a single dictionary key or value element. /// Reads type code byte, then dispatches based on element kind. /// private static void EmitReadDictElement(StringBuilder sb, PropertyTypeKind kind, string typeName, string varName, string propSuffix, string i, PropInfo? p, bool isRefType, bool enableInternString) { var etc = $"{varName}_tc"; sb.AppendLine($"{i}var {etc} = context.ReadByte();"); if (kind == PropertyTypeKind.String) { sb.AppendLine($"{i}{typeName}? {varName} = null;"); EmitReadString(sb, varName, etc, i, enableInternString); } else if (kind == PropertyTypeKind.Enum) { sb.AppendLine($"{i}{typeName} {varName} = default;"); sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Enum)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var eb = context.ReadByte();"); sb.AppendLine($"{i} int eiv;"); sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) eiv = BinaryTypeCode.DecodeTinyInt(eb);"); sb.AppendLine($"{i} else eiv = context.ReadVarInt();"); sb.AppendLine($"{i} {varName} = ({typeName})(object)eiv;"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({etc})) {varName} = ({typeName})(object)BinaryTypeCode.DecodeTinyInt({etc});"); } else { // Primitive value type — never nullable sb.AppendLine($"{i}{typeName} {varName} = default;"); EmitReadMarkeredValueForKind(sb, kind, varName, etc, i); } } /// /// Emits markered value read by kind only (no PropInfo needed). For dict key/value inline reads. /// private static void EmitReadMarkeredValueForKind(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i) { switch (k) { case PropertyTypeKind.Int32: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();"); break; case PropertyTypeKind.Int64: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {a} = context.ReadVarLong();"); break; case PropertyTypeKind.Boolean: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {a} = true;"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {a} = false;"); break; case PropertyTypeKind.Double: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {a} = context.ReadDoubleUnsafe();"); break; case PropertyTypeKind.Single: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {a} = context.ReadSingleUnsafe();"); break; case PropertyTypeKind.Decimal: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {a} = context.ReadDecimalUnsafe();"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {a} = context.ReadDateTimeUnsafe();"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {a} = context.ReadGuidUnsafe();"); break; case PropertyTypeKind.Byte: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (byte)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {a} = context.ReadByte();"); break; case PropertyTypeKind.Int16: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (short)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {a} = context.ReadInt16Unsafe();"); break; case PropertyTypeKind.UInt16: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ushort)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {a} = context.ReadUInt16Unsafe();"); break; case PropertyTypeKind.UInt32: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (uint)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {a} = context.ReadVarUInt();"); break; case PropertyTypeKind.UInt64: sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ulong)BinaryTypeCode.DecodeTinyInt({tc});"); sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {a} = context.ReadVarULong();"); break; case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {a} = context.ReadTimeSpanUnsafe();"); break; case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {a} = context.ReadDateTimeOffsetUnsafe();"); break; } } /// /// Emits inline collection read: Array marker already consumed as tc. /// Reads count + loops with direct element reads (Complex with SGen, or primitive/string/enum). /// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance. /// private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString) { var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter; var elemType = p.ElementFullTypeName!; 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();"); // Create collection + loop 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} {{"); if (isComplexElement) EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan, enableInternString); else EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null, enableInternString); sb.AppendLine($"{i} }}"); } else if (p.CollectionKind == "Counted" && p.CollectionAddMethod != null) { // Concrete custom collection — use actual type + correct add method if (p.CollectionHasCapacityCtor) sb.AppendLine($"{i} var col_{s} = new {p.TypeNameForTypeof}(cnt_{s});"); else sb.AppendLine($"{i} var col_{s} = new {p.TypeNameForTypeof}();"); sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); sb.AppendLine($"{i} {{"); if (isComplexElement) EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString, p.CollectionAddMethod); else EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod, enableInternString); sb.AppendLine($"{i} }}"); } else // List, IndexedCollection, Counted-interface → 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} {{"); if (isComplexElement) EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString); else EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null, enableInternString); 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 elemTypeName, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan, bool enableInternString, string? addMethod = null) { var etc = $"etc_{propSuffix}"; sb.AppendLine($"{i}var {etc} = context.ReadByte();"); var addCall = addMethod ?? "Add"; var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);"; var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = re_{propSuffix};" : $"col_{propSuffix}.{addCall}(re_{propSuffix});"; // Ref-aware switch decision routed through RefAwareEmitPredicate — single source of truth shared // with the writer-side EmitDirectCollectionWrite + EmitReadComplex. The decision depends // EXCLUSIVELY on the element compile-time fact `needsRefScan` — the parent EnableRefHandlingFeature // flag is NOT a factor here. Asymmetry-bug fix: // see AcBinarySerializerIIdReferenceTests.Serialize_RefMarkerCollectionElement_ParentRefHandlingFeatureOff_DriftReproduction. if (!RefAwareEmitPredicate.ElementEmitsRefMarker(needsRefScan)) { // No ref tracking → only Object, FixObj or Null in stream — inline ReadProperties // FixObj slot: populate slot cache to keep _nextRuntimeSlot in sync. sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} if ({etc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({elemTypeName}), {etc}); if ({etc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {etc} + 1; }}"); sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();"); sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);"); sb.AppendLine($"{i} {assignExpr}"); sb.AppendLine($"{i}}}"); } else { // Switch on etc (Object / ObjectRefFirst / Null / ObjectRef / = context._nextRuntimeSlot) context._nextRuntimeSlot = {etc} + 1;"); sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();"); sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);"); sb.AppendLine($"{i} {assignExpr}"); sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} break;"); sb.AppendLine($"{i}}}"); } } /// /// Emits per-element read for non-Complex collection elements (String, primitive, Enum). /// Reads type code byte, then dispatches based on ElementKind. /// private static void EmitReadNonComplexCollectionElement(StringBuilder sb, PropInfo p, string indexVar, string propSuffix, string i, bool isArray, string? addMethod, bool enableInternString) { var addCall = addMethod ?? "Add"; var elemType = p.ElementFullTypeName!; var colRef = $"col_{propSuffix}"; // String element FastWire markerless fast-path — same wire as property-level (int32 sentinel header). // All FastWire string writes funnel through `WriteStringWithDispatch.FastWire = WriteStringUtf16Markerless`, // so collection elements use the same markerless format. Skips the etc-read entirely in FastWire mode. if (p.ElementKind == PropertyTypeKind.String) { var tempVar = $"sv_{propSuffix}"; sb.AppendLine($"{i}string? {tempVar};"); sb.AppendLine($"{i}if (context.FastWire)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} {tempVar} = context.ReadStringUtf16Markerless();"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var etc_{propSuffix} = context.ReadByte();"); sb.AppendLine($"{i} {tempVar} = null;"); EmitReadString(sb, tempVar, $"etc_{propSuffix}", i + " ", enableInternString); sb.AppendLine($"{i}}}"); if (isArray) sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar}!;"); else sb.AppendLine($"{i}{colRef}.{addCall}({tempVar}!);"); return; } var etc = $"etc_{propSuffix}"; sb.AppendLine($"{i}var {etc} = context.ReadByte();"); if (p.ElementKind == PropertyTypeKind.Enum) { // Enum element: Enum marker or TinyInt var tempVar = $"ev_{propSuffix}"; sb.AppendLine($"{i}{elemType} {tempVar} = default;"); sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Enum)"); sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var eb = context.ReadByte();"); sb.AppendLine($"{i} int eiv;"); sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) eiv = BinaryTypeCode.DecodeTinyInt(eb);"); sb.AppendLine($"{i} else eiv = context.ReadVarInt();"); sb.AppendLine($"{i} {tempVar} = ({elemType})(object)eiv;"); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({etc})) {tempVar} = ({elemType})(object)BinaryTypeCode.DecodeTinyInt({etc});"); if (isArray) sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar};"); else sb.AppendLine($"{i}{colRef}.{addCall}({tempVar});"); } else { // Primitive element: read markered value var tempVar = $"pv_{propSuffix}"; sb.AppendLine($"{i}{elemType} {tempVar} = default;"); // Create a minimal PropInfo-like context for EmitReadMarkeredValue EmitReadMarkeredValue(sb, p.ElementKind, tempVar, etc, i, p, nullable: false); if (isArray) sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar};"); else sb.AppendLine($"{i}{colRef}.{addCall}({tempVar});"); } } /// /// 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 }