916 lines
52 KiB
C#
916 lines
52 KiB
C#
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace AyCode.Core.Serializers.SourceGenerator;
|
|
|
|
/// <summary>
|
|
/// Reader-side emit pass: generates the <c>IGeneratedBinaryReader</c> implementation for each
|
|
/// <c>[AcBinarySerializable]</c> type. Emits <c>ReadProperties</c> (inline property reads with marker
|
|
/// dispatch) and <c>ReadObject</c> (entry point with cache-index registration).
|
|
///
|
|
/// <para>Sub-passes:</para>
|
|
/// <list type="bullet">
|
|
/// <item><c>EmitReadProp</c> — per-property read emit (markerless + markered variants).</item>
|
|
/// <item><c>EmitReadString</c> — H2Q6 string-tier marker dispatch (FixStrAscii + tier-tables +
|
|
/// intern cases gated by <c>EnableInternStringFeature</c>).</item>
|
|
/// <item><c>EmitReadComplex</c> — Object / ObjectRef* / FixObj-slot dispatch for IId-typed children.</item>
|
|
/// <item><c>EmitReadCollection</c> / <c>EmitReadCollectionInline</c> / <c>EmitReadCollectionElement</c> /
|
|
/// <c>EmitReadNonComplexCollectionElement</c> — collection-shape inline reading.</item>
|
|
/// <item><c>EmitReadDictionary</c> / <c>EmitReadDictElement</c> — dict-shape inline reading.</item>
|
|
/// <item><c>EmitReadMarkeredValue</c> / <c>EmitReadMarkeredValueForKind</c> — primitive value-with-marker reads.</item>
|
|
/// <item><c>EmitReadMarkerless</c> — markerless primitive reads (FastMode + per-property markerless types).</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public partial class AcBinarySourceGenerator
|
|
{
|
|
#region Reader Code Generation
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static string GenReader(SerializableClassInfo ci)
|
|
{
|
|
var sb = new StringBuilder(4096);
|
|
sb.AppendLine("// <auto-generated/>");
|
|
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<TInput>(object value, AcBinaryDeserializer.BinaryDeserializationContext<TInput> 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<TInput>(AcBinaryDeserializer.BinaryDeserializationContext<TInput> 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<TInput>(obj, context);");
|
|
sb.AppendLine(" return obj;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<int, {p.TypeNameForTypeof}>(ref ev); }}");
|
|
else
|
|
EmitReadMarkerless(sb, p.TypeKind, a, i);
|
|
return;
|
|
}
|
|
|
|
// ACCORE-BIN-T-K9M3 Ötlet A (refined) — caller-driven hot/cold split. SGen-emit reads the marker
|
|
// byte locally + dispatches FastWire/PropertySkip checks at the call site; the shared
|
|
// BinaryDeserializationContext.TryReadStringProperty handles only the hot marker switch
|
|
// (small body → high inline confidence). Cold markers go through TryReadStringColdPath
|
|
// (AggressiveOptimization, Tier-1 direct). The || short-circuit ensures cold is called only
|
|
// when hot didn't match — common case has zero method-call overhead beyond the inlined Try body.
|
|
// PropertySkip lands in the cold path's "return false" sink, so the property is left at default
|
|
// (don't-touch contract preserved). enableInternString stays a no-op at the emit site (StringInterned
|
|
// sits inside the cold path body now — writer-side feature gating handles non-emission).
|
|
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};");
|
|
sb.AppendLine($"{i} if (context.TryReadStringProperty(tc_{p.Name}, out v_{p.Name}) || context.TryReadStringColdPath(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}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly.
|
|
/// </summary>
|
|
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<long>());"); break;
|
|
case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break;
|
|
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits inline string read from type code. Handles all H2Q6 (v3 wire format) string markers:
|
|
/// FixStrAscii (ASCII short, 135-166), StringAscii (ASCII long, 167),
|
|
/// StringSmall/Medium/Big (non-ASCII tiers, 91/94/103),
|
|
/// StringInternFirstSmall/Medium (interning tiers, 104/105),
|
|
/// StringInterned (cache ref, 92), StringEmpty (93), Null.
|
|
///
|
|
/// FixStrAscii is checked first as the hot path for short ASCII property names; non-ASCII
|
|
/// tier markers carry both <c>charLen</c> and <c>utf8Len</c> in fixed-width headers (1-pass decode).
|
|
/// </summary>
|
|
private static void EmitReadString(StringBuilder sb, string a, string tc, string i, bool enableInternString)
|
|
{
|
|
// FixStrAscii is the hot path — most short strings (property names) are ASCII.
|
|
sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStrAscii({tc}))");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} var falen = BinaryTypeCode.DecodeFixStrAsciiLength({tc});");
|
|
sb.AppendLine($"{i} {a} = falen == 0 ? string.Empty : context.ReadAsciiBytesAsString(falen);");
|
|
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;");
|
|
}
|
|
// H2Q6 string-tier markers + StringAscii. Wire-decode body is shared with the runtime path
|
|
// (TypeReaderTable + cross-type populate) — see context.ReadStringSmall/Medium/Big, ReadPlainStringAscii.
|
|
// 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.StringSmall:");
|
|
// FastWire mode reuses the StringSmall (=91) marker but with a different body — emit
|
|
// inline ternary so call sites that can run in either mode (Dictionary key/value, runtime
|
|
// cross-type populate) dispatch without an extra method-frame.
|
|
sb.AppendLine($"{i} {a} = context.FastWire ? context.ReadStringSmallFastWire() : context.ReadStringSmallCompact();");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.StringMedium:");
|
|
sb.AppendLine($"{i} {a} = context.ReadStringMedium();");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.StringBig:");
|
|
sb.AppendLine($"{i} {a} = context.ReadStringBig();");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.StringAscii:");
|
|
sb.AppendLine($"{i} {a} = context.ReadPlainStringAscii();");
|
|
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}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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 / <Object).
|
|
// The 4 known TypeCode constants are emitted as switch cases — the JIT compiles them as a
|
|
// jump-table for O(1) dispatch (vs the previous if-else chain's sequential ==-compares).
|
|
// The polymorphic FixObj range-check (tc < Object) goes into the default branch — runtime
|
|
// bridge path is rare on a typical SGen graph, so default fall-through is acceptable.
|
|
// Inline: parent creates instance + handles cache registration.
|
|
sb.AppendLine($"{i}switch ({tc})");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.Object:");
|
|
sb.AppendLine($"{i} {{");
|
|
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} break;");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRefFirst:");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var ci_{p.Name} = (int)context.ReadVarUInt();");
|
|
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
|
|
sb.AppendLine($"{i} context.RegisterInternedValueAt(ci_{p.Name}, rc_{p.Name});");
|
|
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
|
|
sb.AppendLine($"{i} {a} = rc_{p.Name};");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} }}");
|
|
if (p.IsNullable)
|
|
sb.AppendLine($"{i} case BinaryTypeCode.Null: break;");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRef:");
|
|
sb.AppendLine($"{i} {a} = {cast}context.GetInternedObject((int)context.ReadVarUInt())!;");
|
|
sb.AppendLine($"{i} break;");
|
|
// FixObj slot (0..SlotCount-1): same type via FixObj marker (non-meta, non-ref mode).
|
|
// Populate slot cache to keep _nextRuntimeSlot in sync with the serializer.
|
|
sb.AppendLine($"{i} default:");
|
|
sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc});");
|
|
sb.AppendLine($"{i} 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} }}");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when collection element reading can be inlined (no runtime ReadValue dispatch needed).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits inline read for a Collection property.
|
|
/// Known collection kind + inlineable element → inline Array loop with direct element reads.
|
|
/// Else → runtime fallback via ReadValueGenerated.
|
|
/// </summary>
|
|
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}))!;");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits inline read for a single dictionary key or value element.
|
|
/// Reads type code byte, then dispatches based on element kind.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits markered value read by kind only (no PropInfo needed). For dict key/value inline reads.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<T> 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}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits per-element read inside collection loop.
|
|
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
|
|
/// !needsRefScan → only Object/Null possible → 1 branch per element.
|
|
/// </summary>
|
|
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 / <Object). The JIT emits the
|
|
// 4 known TypeCode constants as a jump-table (O(1) dispatch); the polymorphic FixObj
|
|
// range-check (etc < Object) goes into the default branch. Object hot-path stays first.
|
|
sb.AppendLine($"{i}switch ({etc})");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.Object:");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
|
|
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
|
|
sb.AppendLine($"{i} {assignExpr}");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRefFirst:");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var ci_{propSuffix} = (int)context.ReadVarUInt();");
|
|
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
|
|
sb.AppendLine($"{i} context.RegisterInternedValueAt(ci_{propSuffix}, re_{propSuffix});");
|
|
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
|
|
sb.AppendLine($"{i} {assignExpr}");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
|
|
sb.AppendLine($"{i} {assignNull}");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRef:");
|
|
if (isArray)
|
|
sb.AppendLine($"{i} col_{propSuffix}[{indexVar}] = {elemCast}context.GetInternedObject((int)context.ReadVarUInt())!;");
|
|
else
|
|
sb.AppendLine($"{i} col_{propSuffix}.{addCall}({elemCast}context.GetInternedObject((int)context.ReadVarUInt())!);");
|
|
sb.AppendLine($"{i} break;");
|
|
// FixObj slot (0..SlotCount-1): same type via FixObj marker.
|
|
// Populate slot cache to keep _nextRuntimeSlot in sync with the serializer.
|
|
sb.AppendLine($"{i} default:");
|
|
sb.AppendLine($"{i} if ({etc} < BinaryTypeCode.Object)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.GetWrapper(typeof({elemTypeName}), {etc});");
|
|
sb.AppendLine($"{i} 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} }}");
|
|
sb.AppendLine($"{i} break;");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits per-element read for non-Complex collection elements (String, primitive, Enum).
|
|
/// Reads type code byte, then dispatches based on ElementKind.
|
|
/// </summary>
|
|
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});");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits markered value read for primitive types (with type code already read).
|
|
/// Handles TinyInt encoding for integer types.
|
|
/// </summary>
|
|
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
|
|
}
|