From d9ab3940eb1919ee97bb528c6d33b6229c932e5e Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 15 May 2026 18:54:22 +0200 Subject: [PATCH] Refactor AcBinarySourceGenerator into partial classes Split AcBinarySourceGenerator.cs into multiple partial class files for improved maintainability and clarity. Each major concern (models, type analysis, class info extraction, writer/reader emit, diagnostics, and module init) now resides in its own file. Updated .gitignore and settings.local.json to support the new structure. No functional changes to generator output; this is a pure organizational refactor. --- .claude/settings.local.json | 20 +- .refactor-snapshot/.gitignore | 2 + .../AcBinarySourceGenerator.Diagnostics.cs | 182 + .../AcBinarySourceGenerator.GenInit.cs | 43 + .../AcBinarySourceGenerator.GenReader.cs | 884 +++++ .../AcBinarySourceGenerator.GenWriter.cs | 1211 +++++++ .../AcBinarySourceGenerator.GetClassInfo.cs | 363 ++ .../AcBinarySourceGenerator.Models.cs | 250 ++ .../AcBinarySourceGenerator.TypeAnalysis.cs | 368 ++ .../AcBinarySourceGenerator.cs | 3198 +---------------- 10 files changed, 3349 insertions(+), 3172 deletions(-) create mode 100644 .refactor-snapshot/.gitignore create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Diagnostics.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenInit.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenReader.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenWriter.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GetClassInfo.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Models.cs create mode 100644 AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.TypeAnalysis.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 88779da..f06fff6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -85,7 +85,25 @@ "Bash(git -C H:/Applications/Aycode/Source/AyCode.Core log -p --all -S \"MaxDepth\" -- AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs)", "Bash(git -C H:/Applications/Aycode/Source/AyCode.Core show ac6e66f^:AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs)", "Bash(ls -la \"H:\\\\Applications\\\\Aycode\\\\Source\\\\\" 2>&1 && echo \"---\" && ls -la \"H:\\\\Applications\\\\Aycode\\\\\" 2>&1)", - "Bash(dotnet publish *)" + "Bash(dotnet publish *)", + "Bash(head -n 2998 AcBinarySourceGenerator.cs)", + "Bash(echo \"\")", + "Bash(mv AcBinarySourceGenerator.cs.tmp AcBinarySourceGenerator.cs)", + "Read(//h/h/Applications/Aycode/Source/AyCode.Core/AyCode.Core.Serializers.SourceGenerator/**)", + "Bash(head -n 2644 AcBinarySourceGenerator.cs)", + "Bash(cat)", + "Bash(mv tmp.cs AcBinarySourceGenerator.cs)", + "Bash(awk 'NR < 407 || NR > 533 { print }' AcBinarySourceGenerator.cs)", + "Bash(awk 'NR == 406 { print; print \"\"; print \" // DetectAndReportCycles + DetectAndReportPolymorphicMisuse + ShortTypeName moved to\"; print \" // AcBinarySourceGenerator.Diagnostics.cs.\"; next } { print }' tmp.cs)", + "Bash(rm tmp.cs)", + "Bash(cat /tmp/getclassinfo-body.txt)", + "Bash(echo \"}\")", + "Bash(awk 'NR < 44 || NR > 387 { print }' AcBinarySourceGenerator.cs)", + "Bash(awk 'NR == 43 { print; print \"\"; print \" // GetClassInfo extraction pass moved to AcBinarySourceGenerator.GetClassInfo.cs.\"; next } { print }' tmp.cs)", + "Bash(awk 'NR < 69 || NR > 1257 { print }' AcBinarySourceGenerator.cs)", + "Bash(awk 'NR == 68 { print; print \"\"; print \" // Writer-side emit pass \\(GenWriter + GenScanProperties + EmitProp + EmitScan* + EmitDirect*Write +\"; print \" // EmitSkip + EmitVal + EmitMarkerless + helpers\\) moved to AcBinarySourceGenerator.GenWriter.cs.\"; next } { print }' tmp.cs)", + "Bash(awk 'NR < 73 || NR > 930 { print }' AcBinarySourceGenerator.cs)", + "Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)" ] } } diff --git a/.refactor-snapshot/.gitignore b/.refactor-snapshot/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.refactor-snapshot/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Diagnostics.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Diagnostics.cs new file mode 100644 index 0000000..c61b0f2 --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Diagnostics.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Build-time diagnostics for the AcBinary source generator. +/// +/// Registered diagnostics: +/// +/// ACBIN001: detects circular type references +/// among [AcBinarySerializable] types and warns the developer to consider ref-handling mode. +/// ACBIN002: ACCORE-BIN-I-T7K3 +/// compile-time guard. Fires when a type opts out of EnablePolymorphDetectFeature AND still +/// declares an object property — the SGen-emitted writer would silently corrupt the wire. +/// +/// +public partial class AcBinarySourceGenerator +{ + private static readonly DiagnosticDescriptor CircularReferenceWarning = new( + id: "ACBIN001", + title: "Circular reference detected", + messageFormat: "Type '{0}' participates in a circular reference chain: {1}. Consider using ReferenceHandling.OnlyId or .All to avoid exponential serialization size.", + category: "AcBinarySerializer", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as System.Object requires + /// polymorphic-prefix emit (ObjectWithTypeName) so the deserializer can resolve the + /// concrete runtime type. When the type opts out of the feature via + /// [AcBinarySerializable(enablePolymorphDetectFeature: false)], the prefix is suppressed + /// and the wire silently corrupts on round-trip (FixObj slot byte against typeof(object) + /// at read-time → 0-byte object wrapper → reader position drifts → downstream + /// DECIMAL_DRIFT / IndexOutOfRangeException). + /// + /// Surface the misconfiguration at build time so the silent corruption is structurally + /// impossible. Three escape hatches for the developer: + /// 1. Enable the polymorph-detect feature on the type + /// ([AcBinarySerializable(...enablePolymorphDetectFeature: true)] — default true). + /// 2. Change the property type to a concrete type (no polymorphism needed). + /// 3. Mark the property with [AcBinaryIgnore] — ignored properties are filtered out + /// at property enumeration, so this diagnostic does not fire for them. + /// + private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new( + id: "ACBIN002", + title: "Polymorphic property requires EnablePolymorphDetectFeature", + messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but EnablePolymorphDetectFeature is disabled on the type. " + + "The generated writer would silently corrupt the wire on round-trip. " + + "To fix: (1) enable EnablePolymorphDetectFeature on [AcBinarySerializable], (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].", + category: "AcBinarySerializer", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// ACCORE-BIN-I-T7K3 guard: emits + /// (ACBIN002) for every System.Object-declared property on any + /// [AcBinarySerializable] type whose EnablePolymorphDetectFeature is false. + /// Per-class gating: types with the feature enabled (default) skip the check entirely; only + /// opt-out types are scanned for misuse. + /// + private static void DetectAndReportPolymorphicMisuse(List classes, SourceProductionContext spc) + { + foreach (var ci in classes) + { + if (ci.EnablePolymorphDetect) continue; // Feature enabled → polymorphic prefix is emitted, no misuse possible. + + foreach (var p in ci.Properties) + { + if (p.IsObjectDeclaredType) + { + spc.ReportDiagnostic(Diagnostic.Create( + PolymorphicPropertyWithFeatureDisabledError, Location.None, + ci.ClassName, p.Name)); + } + } + } + } + + /// + /// Detects circular reference chains among [AcBinarySerializable] types at compile time + /// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges. + /// + private static void DetectAndReportCycles(List classes, SourceProductionContext spc) + { + // Build lookup: WriterClassName → FullTypeName + var writerToFull = new Dictionary(classes.Count); + foreach (var ci in classes) + { + var writerName = string.IsNullOrEmpty(ci.Namespace) + ? $"{ci.ClassName}_GeneratedWriter" + : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; + writerToFull[writerName] = ci.FullTypeName; + } + + // Build adjacency list: FullTypeName → set of referenced FullTypeNames + var adjacency = new Dictionary>(classes.Count); + foreach (var ci in classes) + { + var edges = new HashSet(); + foreach (var p in ci.Properties) + { + if (p.TypeKind == PropertyTypeKind.Complex && p.HasGeneratedWriter && p.WriterClassName != null) + { + if (writerToFull.TryGetValue(p.WriterClassName, out var target)) + edges.Add(target); + } + if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.ElementWriterClassName != null) + { + if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target)) + edges.Add(target); + } + if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter && p.DictValueWriterClassName != null) + { + if (writerToFull.TryGetValue(p.DictValueWriterClassName, out var target)) + edges.Add(target); + } + } + adjacency[ci.FullTypeName] = edges; + } + + // DFS with 3-color marking: White=0, Gray=1, Black=2 + var color = new Dictionary(classes.Count); + foreach (var ci in classes) + color[ci.FullTypeName] = 0; + + var stack = new List(); + var reported = new HashSet(); + + void Dfs(string node) + { + color[node] = 1; // Gray + stack.Add(node); + + if (adjacency.TryGetValue(node, out var neighbors)) + { + foreach (var next in neighbors) + { + if (!color.TryGetValue(next, out var c)) continue; + if (c == 1) // Gray → back-edge = cycle + { + var cycleStart = stack.IndexOf(next); + var parts = new List(); + for (var i = cycleStart; i < stack.Count; i++) + parts.Add(ShortTypeName(stack[i])); + parts.Add(ShortTypeName(next)); // close the cycle + + var cycleDesc = string.Join(" → ", parts); + for (var i = cycleStart; i < stack.Count; i++) + { + if (reported.Add(stack[i])) + { + spc.ReportDiagnostic(Diagnostic.Create( + CircularReferenceWarning, Location.None, + ShortTypeName(stack[i]), cycleDesc)); + } + } + } + else if (c == 0) // White → unvisited + { + Dfs(next); + } + } + } + + stack.RemoveAt(stack.Count - 1); + color[node] = 2; // Black + } + + foreach (var ci in classes) + { + if (color[ci.FullTypeName] == 0) + Dfs(ci.FullTypeName); + } + } + + private static string ShortTypeName(string fullTypeName) + { + var dot = fullTypeName.LastIndexOf('.'); + return dot >= 0 ? fullTypeName.Substring(dot + 1) : fullTypeName; + } +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenInit.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenInit.cs new file mode 100644 index 0000000..a4a76d0 --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenInit.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Module-init emit pass: generates the static class with a [ModuleInitializer] method that +/// auto-registers every generated writer / reader instance into the runtime registries +/// (AcBinarySerializer.RegisterGeneratedWriter / AcBinaryDeserializer.RegisterGeneratedReader). +/// Emitted once per compilation as AcBinaryGeneratedWriters_Init.g.cs. +/// +public partial class AcBinarySourceGenerator +{ + private static string GenInit(List classes) + { + var sb = new StringBuilder(512); + sb.AppendLine("// "); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + sb.AppendLine(); + sb.AppendLine("namespace AyCode.Core.Serializers.Generated;"); + sb.AppendLine(); + sb.AppendLine("internal static class AcBinaryGeneratedWritersInit"); + sb.AppendLine("{"); + sb.AppendLine(" [ModuleInitializer]"); + sb.AppendLine(" internal static void Register()"); + sb.AppendLine(" {"); + foreach (var ci in classes) + { + var writerRef = string.IsNullOrEmpty(ci.Namespace) + ? $"{ci.ClassName}_GeneratedWriter" + : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; + var readerRef = string.IsNullOrEmpty(ci.Namespace) + ? $"{ci.ClassName}_GeneratedReader" + : $"{ci.Namespace}.{ci.ClassName}_GeneratedReader"; + sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);"); + sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.Instance);"); + } + sb.AppendLine(" }"); + sb.AppendLine("}"); + return sb.ToString(); + } +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenReader.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenReader.cs new file mode 100644 index 0000000..cf9df98 --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenReader.cs @@ -0,0 +1,884 @@ +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; + } + + // String FastWire markerless fast-path: int32 sentinel header (-1 = null, 0 = empty, N > 0 = content). + // Wire-symmetric with `WriteStringGenerated` (SGen) and `WriteStringUtf16Markerless` (Runtime). + // Skips the typeCode-read entirely in FastWire mode; falls through to markered dispatch in Compact. + 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} if (tc_{p.Name} != BinaryTypeCode.PropertySkip)"); + sb.AppendLine($"{i} {{"); + EmitReadString(sb, a, $"tc_{p.Name}", i + " ", enableInternString); + 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: + /// 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 charLen and utf8Len in fixed-width headers (1-pass decode). + /// + 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:"); + sb.AppendLine($"{i} {a} = context.ReadStringSmall();"); + 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}}}"); + } + + /// + /// 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})"; + + if (!p.ChildNeedsRefScan) + { + // 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} }}"); + 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});"; + + if (!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 +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenWriter.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenWriter.cs new file mode 100644 index 0000000..290b191 --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GenWriter.cs @@ -0,0 +1,1211 @@ +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: + if (p.InterningFlags == 0) + sb.AppendLine($"{i}context.StringInternEligible = false;"); + else + sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); + sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); + break; + case PropertyTypeKind.Complex: + // Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely + // when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise. + if (p.HasGeneratedWriter) + EmitDirectObjectWrite(sb, p, a, i); + else if (p.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 if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); + } + else + sb.AppendLine($"{i}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 if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); + } + else + sb.AppendLine($"{i}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"; + + if (!p.ElementNeedsRefScan && !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 (p.ElementNeedsRefScan && !p.ElementEnableMetadata) + { + sb.AppendLine($"{i} if (context.WriteObjectRefMarker{elemRefSuffix}()) {writer}.Instance.WriteProperties({e}, context);"); + } + else if (!p.ElementNeedsRefScan && 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; + } + } +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GetClassInfo.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GetClassInfo.cs new file mode 100644 index 0000000..a23168e --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.GetClassInfo.cs @@ -0,0 +1,363 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Class-info extraction pass — transforms a Roslyn +/// (a class/struct annotated with [AcBinarySerializable]) into the +/// model consumed by the emit passes (writer / reader / scan / init). +/// +/// Reads the attribute's feature flags (1-, 4-, 5-, 6-bool ctor variants), walks the inheritance +/// hierarchy via GetAllSerializablePropertySymbols, and computes per-property metadata: kind, +/// nullability, intern eligibility, complex / collection / dictionary element types, generated-writer +/// pointers, FNV hashes for inline-metadata, and recursive scan-need flags. +/// +public partial class AcBinarySourceGenerator +{ + private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context) + { + if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) + return null; + + var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : typeSymbol.ContainingNamespace.ToDisplayString(); + + var properties = new List(); + + // Read feature flags from [AcBinarySerializable] — disabled features eliminate + // corresponding code blocks from generated ScanObject/WriteProperties. + var enableIdTracking = true; + var enableRefHandling = true; + var enableInternString = true; + var enableMetadata = true; + var enablePropertyFilter = true; + var enablePolymorphDetect = true; + var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (binarySerializableAttr != null) + { + if (binarySerializableAttr.ConstructorArguments.Length == 1) + { + // Single bool ctor: AcBinarySerializable(enableAllFeatures) + var all = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; + enableIdTracking = all; + enableRefHandling = all; + enableInternString = all; + enableMetadata = all; + enablePropertyFilter = all; + enablePolymorphDetect = all; + } + else if (binarySerializableAttr.ConstructorArguments.Length == 4) + { + // Four bool ctor: (metadata, idTracking, refHandling, internString) — filter + polymorph default to true + enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; + enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; + enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; + enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; + } + else if (binarySerializableAttr.ConstructorArguments.Length == 5) + { + // Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter) — polymorph defaults to true + enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; + enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; + enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; + enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; + enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!; + } + else if (binarySerializableAttr.ConstructorArguments.Length == 6) + { + // Six bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter, polymorphDetect) + enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; + enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; + enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; + enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; + enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!; + enablePolymorphDetect = (bool)binarySerializableAttr.ConstructorArguments[5].Value!; + } + } + + foreach (var p in GetAllSerializablePropertySymbols(typeSymbol)) + { + // String interning attribútum detektálás (null = no attr, true/false = explicit) + bool? stringInternAttr = null; + if (!enableInternString) + { + stringInternAttr = false; + } + else if (GetKind(p.Type) == PropertyTypeKind.String) + { + var attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); + if (attr != null && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Kind == TypedConstantKind.Primitive) + { + stringInternAttr = (bool)attr.ConstructorArguments[0].Value!; + } + } + + // For typeof(): strip trailing '?' from nullable reference types (typeof(T?) is invalid for ref types) + // Nullable value types (int?, Guid?) keep '?' because typeof(int?) == typeof(Nullable) is valid + var typeDisplayName = p.Type.ToDisplayString(); + var typeNameForTypeof = (p.Type.NullableAnnotation == NullableAnnotation.Annotated && !p.Type.IsValueType) + ? typeDisplayName.TrimEnd('?') + : typeDisplayName; + + // Direct object write detection for Complex property types: + // Check if the property type has [AcBinarySerializable] (→ has generated writer) + // and if it implements IId (→ needs ref tracking in generated code) + var kind = GetKind(p.Type); + bool hasGenWriter = false; + bool propTypeIsIId = false; + bool propEnableMetadata = true; + bool childNeedsIdScan = true; + bool childNeedsAllRefScan = true; + bool childNeedsInternScan = true; + string? writerClassName = null; + string? propIdTypeName = null; + int childTypeNameHash = 0; + int[]? childPropertyHashes = null; + if (kind == PropertyTypeKind.Complex) + { + // Resolve to the actual type symbol (strip nullable annotation for ref types) + // For SharedTag? → SharedTag. OriginalDefinition handles generic types. + var resolvedType = p.Type is INamedTypeSymbol namedPropType + ? namedPropType.OriginalDefinition + : p.Type; + + hasGenWriter = resolvedType.Locations.Any(l => l.IsInSource) + && resolvedType.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + + if (hasGenWriter) + { + // Read child type's EnableMetadataFeature + propEnableMetadata = ReadEnableMetadata(resolvedType); + var childScanFlags = ComputeNeedsScan(resolvedType); + childNeedsIdScan = childScanFlags.needsIdScan; + childNeedsAllRefScan = childScanFlags.needsAllRefScan; + childNeedsInternScan = childScanFlags.needsInternScan; + var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i => + i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + propTypeIsIId = iidIface != null; + if (iidIface != null) + propIdTypeName = iidIface.TypeArguments[0].ToDisplayString(); + + // Writer class: {Namespace}.{FlatName}_GeneratedWriter + var flatName = BuildFlatName((INamedTypeSymbol)resolvedType); + var ns = resolvedType.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : resolvedType.ContainingNamespace.ToDisplayString(); + writerClassName = string.IsNullOrEmpty(ns) + ? $"{flatName}_GeneratedWriter" + : $"{ns}.{flatName}_GeneratedWriter"; + + // UseMetadata: compute child type hash-es for inline metadata + childTypeNameHash = ComputeFnvHash(resolvedType.Name); + childPropertyHashes = ComputeChildPropertyHashes(resolvedType); + } + } + + // Collection element type analysis for inline collection write + PropertyTypeKind elemKind = PropertyTypeKind.Unknown; + bool elemHasGenWriter = false; + bool elemIsIId = false; + bool elemEnableMetadata = true; + bool elemNeedsIdScan = true; + bool elemNeedsAllRefScan = true; + bool elemNeedsInternScan = true; + string? elemWriterClassName = null; + string? elemIdTypeName = null; + string? collKind = null; + string? collAddMethod = null; + bool collHasCapacityCtor = false; + string? elemFullTypeName = null; + int elementTypeNameHash = 0; + int[]? elementPropertyHashes = null; + if (kind == PropertyTypeKind.Collection) + { + var elemType = GetCollectionElementType(p.Type); + if (elemType != null) + { + elemKind = GetKind(elemType); + elemFullTypeName = elemType.ToDisplayString(); + + // Detect collection shape for inline write + if (p.Type is IArrayTypeSymbol) + collKind = "Array"; + else if (p.Type is INamedTypeSymbol collNamedType) + { + var origDef = collNamedType.OriginalDefinition.ToDisplayString(); + collKind = origDef switch + { + "System.Collections.Generic.List" => "List", + "System.Collections.Generic.IList" => "IndexedCollection", + "System.Collections.Generic.IReadOnlyList" => "IndexedCollection", + "System.Collections.Generic.HashSet" => "Counted", // has Count, no indexer + "System.Collections.Generic.Queue" => "Counted", + "System.Collections.Generic.ICollection" => "Counted", + "System.Collections.Generic.IReadOnlyCollection" => "Counted", + "System.Collections.Generic.SortedSet" => "Counted", + "System.Collections.Generic.LinkedList" => "Counted", + _ => null + }; + + // Determine add method + capacity ctor for Counted concrete types + if (collKind == "Counted") + { + collAddMethod = origDef switch + { + "System.Collections.Generic.HashSet" => "Add", + "System.Collections.Generic.SortedSet" => "Add", + "System.Collections.Generic.Queue" => "Enqueue", + "System.Collections.Generic.LinkedList" => "AddLast", + _ => null // ICollection, IReadOnlyCollection → backed by List + }; + collHasCapacityCtor = origDef is + "System.Collections.Generic.HashSet" or + "System.Collections.Generic.Queue"; + } + } + + // For Complex element types, check for generated writer + if (elemKind == PropertyTypeKind.Complex) + { + var resolvedElem = elemType is INamedTypeSymbol namedElem + ? namedElem.OriginalDefinition : elemType; + elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource) + && resolvedElem.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (elemHasGenWriter) + { + // Read element type's EnableMetadataFeature + elemEnableMetadata = ReadEnableMetadata(resolvedElem); + var elemScanFlags = ComputeNeedsScan(resolvedElem); + elemNeedsIdScan = elemScanFlags.needsIdScan; + elemNeedsAllRefScan = elemScanFlags.needsAllRefScan; + elemNeedsInternScan = elemScanFlags.needsInternScan; + var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc => + ifc.IsGenericType && + ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + elemIsIId = elemIidIface != null; + if (elemIidIface != null) + elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString(); + + var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem); + var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace + ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); + elemWriterClassName = string.IsNullOrEmpty(ens) + ? $"{elemFlatName}_GeneratedWriter" + : $"{ens}.{elemFlatName}_GeneratedWriter"; + + // UseMetadata: compute element type hash-es for inline metadata + elementTypeNameHash = ComputeFnvHash(resolvedElem.Name); + elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem); + } + } + } + } + + // Dictionary key/value type analysis for inline dictionary read + PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown; + PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown; + string? dictKeyTypeName = null; + string? dictValueTypeName = null; + bool dictValueHasGenWriter = false; + string? dictValueWriterClassName = null; + bool dictValueIsIId = false; + bool dictValueEnableMetadata = true; + bool dictValueNeedsIdScan = true; + bool dictValueNeedsAllRefScan = true; + bool dictValueNeedsInternScan = true; + int dictValueTypeNameHash = 0; + int[]? dictValuePropertyHashes = null; + if (kind == PropertyTypeKind.Dictionary) + { + var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type); + if (keyType != null) + { + dictKeyKind = GetKind(keyType); + dictKeyTypeName = keyType.ToDisplayString(); + } + if (valueType != null) + { + dictValueKind = GetKind(valueType); + dictValueTypeName = valueType.ToDisplayString(); + if (dictValueKind == PropertyTypeKind.Complex) + { + var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType; + dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource) + && resolvedValue.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (dictValueHasGenWriter) + { + var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue); + var vns = resolvedValue.ContainingNamespace.IsGlobalNamespace + ? string.Empty : resolvedValue.ContainingNamespace.ToDisplayString(); + dictValueWriterClassName = string.IsNullOrEmpty(vns) + ? $"{vfn}_GeneratedWriter" + : $"{vns}.{vfn}_GeneratedWriter"; + + dictValueEnableMetadata = ReadEnableMetadata(resolvedValue); + var dvScanFlags = ComputeNeedsScan(resolvedValue); + dictValueNeedsIdScan = dvScanFlags.needsIdScan; + dictValueNeedsAllRefScan = dvScanFlags.needsAllRefScan; + dictValueNeedsInternScan = dvScanFlags.needsInternScan; + var dvIidIface = resolvedValue.AllInterfaces.FirstOrDefault(ifc => + ifc.IsGenericType && + ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + dictValueIsIId = dvIidIface != null; + dictValueTypeNameHash = ComputeFnvHash(resolvedValue.Name); + dictValuePropertyHashes = ComputeChildPropertyHashes(resolvedValue); + } + } + } + } + + properties.Add(new PropInfo( + p.Name, + typeDisplayName, + typeNameForTypeof, + kind, + p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), + p.Type.SpecialType == SpecialType.System_Object, + stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, + elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, + collAddMethod, collHasCapacityCtor, + dictKeyKind, dictValueKind, dictKeyTypeName, dictValueTypeName, dictValueHasGenWriter, dictValueWriterClassName, + dictValueIsIId, dictValueEnableMetadata, dictValueTypeNameHash, dictValuePropertyHashes, + dictValueNeedsIdScan, dictValueNeedsAllRefScan, dictValueNeedsInternScan, + childTypeNameHash, childPropertyHashes, + elementTypeNameHash, elementPropertyHashes, + propEnableMetadata, elemEnableMetadata, + childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan, + elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan)); + } + + // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering + // If EnableIdTrackingFeature == false, skip IId detection entirely → isIId = false + var isIId = false; + string? idTypeName = null; + if (enableIdTracking) + { + var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i => + i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + if (iidInterface != null) + { + isIId = true; + idTypeName = iidInterface.TypeArguments[0].ToDisplayString(); + } + } + + // Properties are already in runtime-matching order from GetAllSerializablePropertySymbols: + // derived → base, each level sorted alphabetically (matches TypeMetadataBase.GetUnfilteredProperties). + + var className = BuildFlatName(typeSymbol); + var typeNameHash = ComputeFnvHash(typeSymbol.Name); + var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); + var selfScanFlags = ComputeNeedsScan(typeSymbol); + return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, enablePolymorphDetect, enableInternString, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan); + } +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Models.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Models.cs new file mode 100644 index 0000000..e7b347d --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.Models.cs @@ -0,0 +1,250 @@ +using System.Collections.Generic; + +namespace AyCode.Core.Serializers.SourceGenerator; + +// Source-generator model types — pure POCO data carriers describing a `[AcBinarySerializable]` type +// and its serializable properties. Consumed by all emit / diagnostics / analysis passes in the partial +// `AcBinarySourceGenerator` class (see siblings `*.GenWriter.cs`, `*.GenReader.cs`, etc.). + +internal sealed class SerializableClassInfo +{ + public string Namespace { get; } + public string ClassName { get; } + public string FullTypeName { get; } + public List Properties { get; } + /// True if this type implements IId<T> + public bool IsIId { get; } + /// The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise + public string? IdTypeName { get; } + /// True if EnableRefHandlingFeature is enabled — controls non-IId All mode tracking code emission. + public bool EnableRefHandling { get; } + /// FNV-1a hash of ClassName (matches runtime SourceType.Name hash) + public int TypeNameHash { get; } + /// FNV-1a hash of each property name, in property order + public int[] PropertyNameHashes { get; } + /// When false, skip inline metadata and use markerless property write for this type. + public bool EnableMetadata { get; } + /// True if EnablePropertyFilterFeature is enabled — controls per-property HasPropertyFilter + /// guard emission in WriteProperties / ScanObject. When false, the filter check is omitted entirely + /// → leaner generated code on the hot path (typical for high-throughput types that never use a filter). + public bool EnablePropertyFilter { get; } + /// True if EnablePolymorphDetectFeature is enabled — controls ObjectWithTypeName + AQN + /// prefix emit on System.Object-declared properties. When false, the prefix is suppressed + /// AND ACBIN002 fires at build time if such a property exists on this type (guarding against silent + /// wire corruption). Opt-out is intentional: dev guarantees no polymorphic object property + /// will be serialized on this type, or all such properties are excluded via [AcBinaryIgnore]. + public bool EnablePolymorphDetect { get; } + /// True if EnableInternStringFeature is enabled — controls whether the SGen-emitted reader + /// contains StringInterned, StringInternFirstSmall, StringInternFirstMedium case-ágakat. + /// When false, those cases are omitted (the writer doesn't emit those markers when intern is off, + /// so the reader doesn't need to handle them). Leaner switch dispatch (~30% fewer string cases) + + /// smaller IL → faster cold-start JIT + smaller AOT publish. + public bool EnableInternString { get; } + /// When true, type subtree has IId types needing scan (active in OnlyId + All). + public bool NeedsIdScan { get; } + /// When true, type subtree has non-IId ref tracking (active only in All mode). + public bool NeedsAllRefScan { get; } + /// When true, type subtree needs string interning scan. + public bool NeedsInternScan { get; } + /// Derived: NeedsIdScan || NeedsAllRefScan. + public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan; + /// Derived: any scan axis active. + public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan; + public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect, bool enableInternString, bool needsIdScan, bool needsAllRefScan, bool needsInternScan) + { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; EnablePolymorphDetect = enablePolymorphDetect; EnableInternString = enableInternString; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; } +} + +internal sealed class PropInfo +{ + public string Name { get; } + public string TypeName { get; } + /// + /// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types). + /// + public string TypeNameForTypeof { get; } + public PropertyTypeKind TypeKind { get; } + public bool IsNullable { get; } + /// + /// Pre-computed interning flags matching runtime BinaryPropertyAccessorBase._interningFlags. + /// Bit layout: bit N = eligible when StringInterningMode == N. + /// None=0 → bit 0 never set. Attribute=1 → bit 1. All=2 → bit 2. + /// No attr: 0b100 (4), [AcStringIntern(true)]: 0b110 (6), [AcStringIntern(false)]: 0b000 (0). + /// + public int InterningFlags { get; } + + /// True when declared property type is System.Object. Runtime type dispatch needed. + public bool IsObjectDeclaredType { get; } + /// True if the Complex property type has [AcBinarySerializable] → has a generated writer. + public bool HasGeneratedWriter { get; } + /// True if the Complex property type implements IId<T> → needs ref tracking in write pass. + public bool IsIId { get; } + /// Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter. + public string? WriterClassName { get; } + /// Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId. + public string? IdTypeName { get; } + + // Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer + /// Element type kind for collection properties. Only meaningful when TypeKind == Collection. + public PropertyTypeKind ElementKind { get; } + /// True if collection element type has [AcBinarySerializable]. + public bool ElementHasGeneratedWriter { get; } + /// True if collection element type implements IId<T>. + public bool ElementIsIId { get; } + /// Generated writer class name for collection element type. + public string? ElementWriterClassName { get; } + /// Id type name for collection element IId types. Null if not IId. + public string? ElementIdTypeName { get; } + /// Collection type: "List", "Array", "IndexedCollection", "Counted", or null (unknown — fallback to runtime). + public string? CollectionKind { get; } + /// Full element type name for generated code (e.g. "SharedTag"). + public string? ElementFullTypeName { get; } + /// Add method for Counted concrete collections. null → List<T>.Add(), "Add" → HashSet/SortedSet, "Enqueue" → Queue, "AddLast" → LinkedList. + public string? CollectionAddMethod { get; } + /// True if the concrete Counted collection has a capacity constructor (HashSet, Queue). + public bool CollectionHasCapacityCtor { get; } + + // Dictionary metadata — set when TypeKind == Dictionary + /// Key type kind for dictionary properties. + public PropertyTypeKind DictKeyKind { get; } + /// Value type kind for dictionary properties. + public PropertyTypeKind DictValueKind { get; } + /// Key type name for generated code. + public string? DictKeyTypeName { get; } + /// Value type name for generated code. + public string? DictValueTypeName { get; } + /// True if dictionary value type has [AcBinarySerializable]. + public bool DictValueHasGeneratedWriter { get; } + /// Generated writer class name for dictionary value type. + public string? DictValueWriterClassName { get; } + /// True if dictionary value type implements IId<T>. + public bool DictValueIsIId { get; } + /// When false, dict value type skips inline metadata. + public bool DictValueEnableMetadata { get; } + /// FNV-1a hash of dict value type name. + public int DictValueTypeNameHash { get; } + /// FNV-1a hashes of dict value type's properties. + public int[]? DictValuePropertyHashes { get; } + /// When true, dict value subtree has IId types needing scan. + public bool DictValueNeedsIdScan { get; } + /// When true, dict value subtree has non-IId ref tracking. + public bool DictValueNeedsAllRefScan { get; } + /// When true, dict value subtree needs string interning scan. + public bool DictValueNeedsInternScan { get; } + /// Derived: DictValueNeedsIdScan || DictValueNeedsAllRefScan. + public bool DictValueNeedsRefScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan; + /// Derived: any dict value scan axis active. + public bool DictValueNeedsScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan || DictValueNeedsInternScan; + + // UseMetadata inline hash-ek (Complex/Collection child típushoz) + /// FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter. + public int ChildTypeNameHash { get; } + /// FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter. + public int[]? ChildPropertyHashes { get; } + /// FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter. + public int ElementTypeNameHash { get; } + /// FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter. + public int[]? ElementPropertyHashes { get; } + /// When false, child Complex type skips inline metadata in generated code. + public bool ChildEnableMetadata { get; } + /// When false, collection element type skips inline metadata in generated code. + public bool ElementEnableMetadata { get; } + /// When true, child subtree has IId types needing scan (active in OnlyId + All). + public bool ChildNeedsIdScan { get; } + /// When true, child subtree has non-IId ref tracking (active only in All mode). + public bool ChildNeedsAllRefScan { get; } + /// When true, child subtree needs string interning scan. + public bool ChildNeedsInternScan { get; } + /// Derived: ChildNeedsIdScan || ChildNeedsAllRefScan. + public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan; + /// Derived: any child scan axis active. + public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan; + /// When true, element subtree has IId types needing scan (active in OnlyId + All). + public bool ElementNeedsIdScan { get; } + /// When true, element subtree has non-IId ref tracking (active only in All mode). + public bool ElementNeedsAllRefScan { get; } + /// When true, element subtree needs string interning scan. + public bool ElementNeedsInternScan { get; } + /// Derived: ElementNeedsIdScan || ElementNeedsAllRefScan. + public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan; + /// Derived: any element scan axis active. + public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan; + + public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, + bool isObjectDeclaredType = false, + bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, + PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, + string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, + string? collectionAddMethod = null, bool collectionHasCapacityCtor = false, + PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown, PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown, + string? dictKeyTypeName = null, string? dictValueTypeName = null, + bool dictValueHasGeneratedWriter = false, string? dictValueWriterClassName = null, + bool dictValueIsIId = false, bool dictValueEnableMetadata = true, + int dictValueTypeNameHash = 0, int[]? dictValuePropertyHashes = null, + bool dictValueNeedsIdScan = true, bool dictValueNeedsAllRefScan = true, bool dictValueNeedsInternScan = true, + int childTypeNameHash = 0, int[]? childPropertyHashes = null, + int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, + bool childEnableMetadata = true, bool elementEnableMetadata = true, + bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true, + bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true) + { + Name = n; + TypeName = tn; + TypeNameForTypeof = tnForTypeof; + TypeKind = tk; + IsNullable = nullable; + IsObjectDeclaredType = isObjectDeclaredType; + HasGeneratedWriter = hasGeneratedWriter; + IsIId = isIId; + WriterClassName = writerClassName; + IdTypeName = idTypeName; + ElementKind = elementKind; + ElementHasGeneratedWriter = elementHasGenWriter; + ElementIsIId = elementIsIId; + ElementWriterClassName = elementWriterClassName; + ElementIdTypeName = elementIdTypeName; + CollectionKind = collectionKind; + ElementFullTypeName = elementFullTypeName; + CollectionAddMethod = collectionAddMethod; + CollectionHasCapacityCtor = collectionHasCapacityCtor; + DictKeyKind = dictKeyKind; + DictValueKind = dictValueKind; + DictKeyTypeName = dictKeyTypeName; + DictValueTypeName = dictValueTypeName; + DictValueHasGeneratedWriter = dictValueHasGeneratedWriter; + DictValueWriterClassName = dictValueWriterClassName; + DictValueIsIId = dictValueIsIId; + DictValueEnableMetadata = dictValueEnableMetadata; + DictValueTypeNameHash = dictValueTypeNameHash; + DictValuePropertyHashes = dictValuePropertyHashes; + DictValueNeedsIdScan = dictValueNeedsIdScan; + DictValueNeedsAllRefScan = dictValueNeedsAllRefScan; + DictValueNeedsInternScan = dictValueNeedsInternScan; + ChildTypeNameHash = childTypeNameHash; + ChildPropertyHashes = childPropertyHashes; + ElementTypeNameHash = elementTypeNameHash; + ElementPropertyHashes = elementPropertyHashes; + ChildEnableMetadata = childEnableMetadata; + ElementEnableMetadata = elementEnableMetadata; + ChildNeedsIdScan = childNeedsIdScan; + ChildNeedsAllRefScan = childNeedsAllRefScan; + ChildNeedsInternScan = childNeedsInternScan; + ElementNeedsIdScan = elementNeedsIdScan; + ElementNeedsAllRefScan = elementNeedsAllRefScan; + ElementNeedsInternScan = elementNeedsInternScan; + // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase + int flags = 0; + if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit + if (stringInternAttr != false) flags |= (1 << 2); // All bit + InterningFlags = flags; + } +} + +internal enum PropertyTypeKind +{ + Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, + Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, + Collection, Complex, Dictionary, + NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, + NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, + NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.TypeAnalysis.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.TypeAnalysis.cs new file mode 100644 index 0000000..515acbf --- /dev/null +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.TypeAnalysis.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace AyCode.Core.Serializers.SourceGenerator; + +/// +/// Type-analysis utilities for the AcBinary source generator: kind detection, FNV-1a hashing, +/// symbol enumeration, name flattening, and recursive scan-need computation. All methods are +/// pure functions over Roslyn symbols (no mutable state, safe to call from any emit pass). +/// +public partial class AcBinarySourceGenerator +{ + /// + /// Returns true for property types that use markerless serialization in FastMode. + /// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults. + /// + private static bool IsMarkerless(PropertyTypeKind k) => k switch + { + PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or + PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or + PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or + PropertyTypeKind.DateTime or PropertyTypeKind.Guid or + PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset or + PropertyTypeKind.Boolean or PropertyTypeKind.Enum => true, + _ => false + }; + + /// + /// Builds a flat class name for nested types: Outer_Inner_Leaf. + /// For top-level types returns the simple name unchanged. + /// + private static string BuildFlatName(INamedTypeSymbol typeSymbol) + { + if (typeSymbol.ContainingType == null) + return typeSymbol.Name; + + var parts = new List(); + var current = typeSymbol; + while (current != null) + { + parts.Add(current.Name); + current = current.ContainingType; + } + parts.Reverse(); + return string.Join("_", parts); + } + + /// + /// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute. + /// Returns true (default) if no attribute or enableAllFeatures=true. + /// + private static bool ReadEnableMetadata(ITypeSymbol type) + { + var attr = type.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (attr == null) return true; + if (attr.ConstructorArguments.Length == 1) + return (bool)attr.ConstructorArguments[0].Value!; + if (attr.ConstructorArguments.Length == 4) + return (bool)attr.ConstructorArguments[0].Value!; + return true; + } + + /// + /// Computes whether a type needs scan pass work, split into ref tracking and string interning. + /// Uses a per-call HashSet to guard against circular references (no static cache — + /// static state is unsafe in incremental generators as it persists across builds). + /// Returns (needsRefScan, needsInternScan) — these are independent axes. + /// + private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type) + { + return ComputeNeedsScanCore(type, new HashSet()); + } + + private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet visiting) + { + // Circular reference guard: if already visiting this type, assume true (safe fallback) + var key = type.ToDisplayString(); + if (!visiting.Add(key)) + return (true, true, true); + + // Read [AcBinarySerializable] flags + var attr = type.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + + bool enableIdTracking = true, enableRefHandling = true, enableInternString = true; + if (attr != null) + { + if (attr.ConstructorArguments.Length == 1) + { + var all = (bool)attr.ConstructorArguments[0].Value!; + enableIdTracking = enableRefHandling = enableInternString = all; + } + else if (attr.ConstructorArguments.Length == 4) + { + enableIdTracking = (bool)attr.ConstructorArguments[1].Value!; + enableRefHandling = (bool)attr.ConstructorArguments[2].Value!; + enableInternString = (bool)attr.ConstructorArguments[3].Value!; + } + } + + // IId tracking: active in OnlyId + All modes + var isIId = enableIdTracking && type.AllInterfaces.Any(i => + i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + var needsIdScan = isIId; + // Non-IId ref tracking: active only in All mode + var needsAllRefScan = !isIId && enableRefHandling; + var needsInternScan = false; + + // Check properties for string interning or complex children + foreach (var p in GetAllSerializablePropertySymbols(type)) + { + // Early exit: if all flags are already true, no need to check more properties + if (needsIdScan && needsAllRefScan && needsInternScan) break; + + var kind = GetKind(p.Type); + + // String with interning? + if (enableInternString && kind == PropertyTypeKind.String) + { + var internAttr = p.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); + if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!)) + needsInternScan = true; + } + + // Complex child → recurse + if (kind == PropertyTypeKind.Complex) + { + var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type; + var childFlags = ComputeNeedsScanCore(resolved, visiting); + needsIdScan |= childFlags.needsIdScan; + needsAllRefScan |= childFlags.needsAllRefScan; + needsInternScan |= childFlags.needsInternScan; + } + + // Collection → check element type + if (kind == PropertyTypeKind.Collection) + { + var elemType = GetCollectionElementType(p.Type); + if (elemType != null) + { + var elemKind = GetKind(elemType); + if (enableInternString && elemKind == PropertyTypeKind.String) + needsInternScan = true; + if (elemKind == PropertyTypeKind.Complex) + { + var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType; + var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting); + needsIdScan |= elemFlags.needsIdScan; + needsAllRefScan |= elemFlags.needsAllRefScan; + needsInternScan |= elemFlags.needsInternScan; + } + } + } + + // Dictionary → check key and value types + if (kind == PropertyTypeKind.Dictionary) + { + var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type); + if (keyType != null && enableInternString && GetKind(keyType) == PropertyTypeKind.String) + needsInternScan = true; + if (valueType != null) + { + var valKind = GetKind(valueType); + if (enableInternString && valKind == PropertyTypeKind.String) + needsInternScan = true; + if (valKind == PropertyTypeKind.Complex) + { + var resolvedVal = valueType is INamedTypeSymbol nv ? nv.OriginalDefinition : valueType; + var valFlags = ComputeNeedsScanCore(resolvedVal, visiting); + needsIdScan |= valFlags.needsIdScan; + needsAllRefScan |= valFlags.needsAllRefScan; + needsInternScan |= valFlags.needsInternScan; + } + } + } + } + + return (needsIdScan, needsAllRefScan, needsInternScan); + } + + #region FNV-1a Hash (compile-time) + + private static int ComputeFnvHash(string value) + { + uint hash = 2166136261; + for (int i = 0; i < value.Length; i++) + { + hash ^= value[i]; + hash *= 16777619; + } + return (int)hash; + } + + /// + /// Computes FNV-1a hashes for all serializable properties of a child type. + /// Property filtering and ordering matches runtime TypeMetadataBase exactly: + /// derived → base, each level sorted alphabetically, with ignore attribute filtering. + /// + private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType) + { + // Use hierarchy-walking helper — order matches runtime TypeMetadataBase + var props = GetAllSerializablePropertySymbols(resolvedType); + return props.Select(p => ComputeFnvHash(p.Name)).ToArray(); + } + + #endregion + + /// + /// Collects all serializable property symbols from the full inheritance hierarchy. + /// Order matches runtime TypeMetadataBase.GetUnfilteredProperties exactly: + /// derived → base, each level sorted alphabetically by name. + /// Filters: public, get+set, non-indexer, non-static, no ignore attributes. + /// Deduplicates by name (most-derived override wins). + /// + private static List GetAllSerializablePropertySymbols(ITypeSymbol typeSymbol) + { + var result = new List(); + var seen = new HashSet(); + + for (var currentType = typeSymbol as INamedTypeSymbol; + currentType != null && currentType.SpecialType != SpecialType.System_Object; + currentType = currentType.BaseType) + { + var levelProps = new List(); + + foreach (var member in currentType.GetMembers()) + { + if (member is IPropertySymbol p && + p.DeclaredAccessibility == Accessibility.Public && + p.GetMethod != null && p.SetMethod != null && + !p.IsIndexer && !p.IsStatic && + seen.Add(p.Name)) // dedup: most-derived wins + { + var hasIgnore = p.GetAttributes().Any(a => + { + var name = a.AttributeClass?.Name ?? ""; + return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; + }); + if (hasIgnore) continue; + + levelProps.Add(p); + } + } + + // Sort each level alphabetically — matches runtime OrderBy(p => p.Name, Ordinal) + levelProps.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + result.AddRange(levelProps); + } + + return result; + } + + #region Type analysis + + private static bool IsNullableVT(ITypeSymbol t) => + t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + + private static PropertyTypeKind GetKind(ITypeSymbol type) + { + if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + return GetKindCore(n.TypeArguments[0], true); + return GetKindCore(type, false); + } + + private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable) + { + switch (type.SpecialType) + { + case SpecialType.System_String: return PropertyTypeKind.String; + case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; + case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; + case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; + case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; + case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; + case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; + case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; + case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; + case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; + case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; + case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; + case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; + default: break; + } + var fn = type.ToDisplayString(); + if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; + if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; + if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; + if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; + if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection; + // Dictionary detection: must come before IEnumerable (Dictionary implements both) + if (type is INamedTypeSymbol dictNt && dictNt.IsGenericType) + { + var orig = dictNt.OriginalDefinition.ToDisplayString(); + if (orig == "System.Collections.Generic.IDictionary" || + orig == "System.Collections.Generic.Dictionary" || + dictNt.AllInterfaces.Any(ifc => ifc.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary")) + return PropertyTypeKind.Dictionary; + } + if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)) + return PropertyTypeKind.Collection; + if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; + return PropertyTypeKind.Unknown; + } + + /// + /// Extracts the element type T from List<T>, T[], IList<T>, IEnumerable<T>. + /// Returns null if the element type cannot be determined. + /// + private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type) + { + // T[] → element type + if (type is IArrayTypeSymbol arrayType) + return arrayType.ElementType; + + // Generic collections: List, IList, ICollection, IEnumerable + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + // Direct: List, HashSet, etc. — first type argument + var iface = namedType.AllInterfaces + .FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); + if (iface != null) + return iface.TypeArguments[0]; + } + + return null; + } + + /// + /// Extracts key and value types from Dictionary<K,V> or IDictionary<K,V>. + /// + private static (ITypeSymbol? keyType, ITypeSymbol? valueType) GetDictionaryKeyValueTypes(ITypeSymbol type) + { + if (type is INamedTypeSymbol nt && nt.IsGenericType) + { + var orig = nt.OriginalDefinition.ToDisplayString(); + if (orig == "System.Collections.Generic.Dictionary" || + orig == "System.Collections.Generic.IDictionary") + return (nt.TypeArguments[0], nt.TypeArguments[1]); + + var iface = nt.AllInterfaces.FirstOrDefault(i => + i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary"); + if (iface != null) + return (iface.TypeArguments[0], iface.TypeArguments[1]); + } + return (null, null); + } + + private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; + + private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch + { + PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64, + PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte, + PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32, + PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean, + PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double, + PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime, + PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan, + PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum, + _ => PropertyTypeKind.Unknown + }; + + #endregion +} diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 78c77a2..8cbc4a0 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; @@ -10,11 +8,33 @@ using Microsoft.CodeAnalysis.Text; namespace AyCode.Core.Serializers.SourceGenerator; /// -/// Generates IGeneratedBinaryWriter implementations for [AcBinarySerializable] types. -/// Also generates a ModuleInitializer that auto-registers all writers at startup. +/// Incremental source generator for [AcBinarySerializable] types. Emits an +/// IGeneratedBinaryWriter + IGeneratedBinaryReader implementation for every annotated +/// type, plus a [ModuleInitializer]-based registry hook that wires the generated instances +/// into the runtime serializer at startup. +/// +/// Source organization — this generator class is split across multiple partial files +/// for navigational clarity: +/// +/// AcBinarySourceGenerator.cs (this file) — entry point: [Generator] attribute, +/// Initialize + Execute orchestration. +/// AcBinarySourceGenerator.Models.cs — non-partial model types +/// (SerializableClassInfo, PropInfo, PropertyTypeKind). +/// AcBinarySourceGenerator.TypeAnalysis.cs — Roslyn-symbol utility passes +/// (kind detection, FNV hashing, name flattening, scan-need recursion). +/// AcBinarySourceGenerator.Diagnostics.cs — ACBIN001 (cycle warning) + ACBIN002 +/// (polymorph misuse error) descriptors and detection methods. +/// AcBinarySourceGenerator.GetClassInfo.cs — class-info extraction pass (attribute +/// flags + property metadata building). +/// AcBinarySourceGenerator.GenWriter.cs — writer-side emit (WriteProperties, ScanObject, +/// ScanForDuplicates, per-property emit helpers). +/// AcBinarySourceGenerator.GenReader.cs — reader-side emit (ReadProperties, ReadObject, +/// per-property read helpers). +/// AcBinarySourceGenerator.GenInit.cs — ModuleInitializer-based registry hook emit. +/// /// [Generator] -public class AcBinarySourceGenerator : IIncrementalGenerator +public partial class AcBinarySourceGenerator : IIncrementalGenerator { private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute"; @@ -23,42 +43,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator // • EnablePropertyFilter → omits the per-property `HasPropertyFilter` branch when false. // • EnablePolymorphDetect → omits the `ObjectWithTypeName + AQN` prefix on `System.Object`- // declared properties when false (then ACBIN002 guards misuse). - // Both default `true`; opt-out is per type via the attribute ctor parameters. - - private static readonly DiagnosticDescriptor CircularReferenceWarning = new( - id: "ACBIN001", - title: "Circular reference detected", - messageFormat: "Type '{0}' participates in a circular reference chain: {1}. Consider using ReferenceHandling.OnlyId or .All to avoid exponential serialization size.", - category: "AcBinarySerializer", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - /// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as System.Object requires - /// polymorphic-prefix emit (ObjectWithTypeName) so the deserializer can resolve the - /// concrete runtime type. When the type opts out of the feature via - /// [AcBinarySerializable(enablePolymorphDetectFeature: false)], the prefix is suppressed - /// and the wire silently corrupts on round-trip (FixObj slot byte against typeof(object) - /// at read-time → 0-byte object wrapper → reader position drifts → downstream - /// DECIMAL_DRIFT / IndexOutOfRangeException). - /// - /// Surface the misconfiguration at build time so the silent corruption is structurally - /// impossible. Three escape hatches for the developer: - /// 1. Enable the polymorph-detect feature on the type - /// ([AcBinarySerializable(...enablePolymorphDetectFeature: true)] — default true). - /// 2. Change the property type to a concrete type (no polymorphism needed). - /// 3. Mark the property with [AcBinaryIgnore] — ignored properties are filtered out - /// at property enumeration, so this diagnostic does not fire for them. - /// - private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new( - id: "ACBIN002", - title: "Polymorphic property requires EnablePolymorphDetectFeature", - messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but EnablePolymorphDetectFeature is disabled on the type. " + - "The generated writer would silently corrupt the wire on round-trip. " + - "To fix: (1) enable EnablePolymorphDetectFeature on [AcBinarySerializable], (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].", - category: "AcBinarySerializer", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + // • EnableInternString → omits StringInterned* case-emit in the reader switch when false. + // All default `true`; opt-out is per type via the attribute ctor parameters. public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -73,351 +59,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator static (spc, classes) => Execute(classes!, spc)); } - private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context) - { - if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) - return null; - - var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : typeSymbol.ContainingNamespace.ToDisplayString(); - - var properties = new List(); - - // Read feature flags from [AcBinarySerializable] — disabled features eliminate - // corresponding code blocks from generated ScanObject/WriteProperties. - var enableIdTracking = true; - var enableRefHandling = true; - var enableInternString = true; - var enableMetadata = true; - var enablePropertyFilter = true; - var enablePolymorphDetect = true; - var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - if (binarySerializableAttr != null) - { - if (binarySerializableAttr.ConstructorArguments.Length == 1) - { - // Single bool ctor: AcBinarySerializable(enableAllFeatures) - var all = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; - enableIdTracking = all; - enableRefHandling = all; - enableInternString = all; - enableMetadata = all; - enablePropertyFilter = all; - enablePolymorphDetect = all; - } - else if (binarySerializableAttr.ConstructorArguments.Length == 4) - { - // Four bool ctor: (metadata, idTracking, refHandling, internString) — filter + polymorph default to true - enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; - enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; - enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; - enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; - } - else if (binarySerializableAttr.ConstructorArguments.Length == 5) - { - // Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter) — polymorph defaults to true - enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; - enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; - enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; - enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; - enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!; - } - else if (binarySerializableAttr.ConstructorArguments.Length == 6) - { - // Six bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter, polymorphDetect) - enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; - enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; - enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; - enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; - enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!; - enablePolymorphDetect = (bool)binarySerializableAttr.ConstructorArguments[5].Value!; - } - } - - foreach (var p in GetAllSerializablePropertySymbols(typeSymbol)) - { - // String interning attribútum detektálás (null = no attr, true/false = explicit) - bool? stringInternAttr = null; - if (!enableInternString) - { - stringInternAttr = false; - } - else if (GetKind(p.Type) == PropertyTypeKind.String) - { - var attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); - if (attr != null && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Kind == TypedConstantKind.Primitive) - { - stringInternAttr = (bool)attr.ConstructorArguments[0].Value!; - } - } - - // For typeof(): strip trailing '?' from nullable reference types (typeof(T?) is invalid for ref types) - // Nullable value types (int?, Guid?) keep '?' because typeof(int?) == typeof(Nullable) is valid - var typeDisplayName = p.Type.ToDisplayString(); - var typeNameForTypeof = (p.Type.NullableAnnotation == NullableAnnotation.Annotated && !p.Type.IsValueType) - ? typeDisplayName.TrimEnd('?') - : typeDisplayName; - - // Direct object write detection for Complex property types: - // Check if the property type has [AcBinarySerializable] (→ has generated writer) - // and if it implements IId (→ needs ref tracking in generated code) - var kind = GetKind(p.Type); - bool hasGenWriter = false; - bool propTypeIsIId = false; - bool propEnableMetadata = true; - bool childNeedsIdScan = true; - bool childNeedsAllRefScan = true; - bool childNeedsInternScan = true; - string? writerClassName = null; - string? propIdTypeName = null; - int childTypeNameHash = 0; - int[]? childPropertyHashes = null; - if (kind == PropertyTypeKind.Complex) - { - // Resolve to the actual type symbol (strip nullable annotation for ref types) - // For SharedTag? → SharedTag. OriginalDefinition handles generic types. - var resolvedType = p.Type is INamedTypeSymbol namedPropType - ? namedPropType.OriginalDefinition - : p.Type; - - hasGenWriter = resolvedType.Locations.Any(l => l.IsInSource) - && resolvedType.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - - if (hasGenWriter) - { - // Read child type's EnableMetadataFeature - propEnableMetadata = ReadEnableMetadata(resolvedType); - var childScanFlags = ComputeNeedsScan(resolvedType); - childNeedsIdScan = childScanFlags.needsIdScan; - childNeedsAllRefScan = childScanFlags.needsAllRefScan; - childNeedsInternScan = childScanFlags.needsInternScan; - var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i => - i.IsGenericType && - i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - propTypeIsIId = iidIface != null; - if (iidIface != null) - propIdTypeName = iidIface.TypeArguments[0].ToDisplayString(); - - // Writer class: {Namespace}.{FlatName}_GeneratedWriter - var flatName = BuildFlatName((INamedTypeSymbol)resolvedType); - var ns = resolvedType.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : resolvedType.ContainingNamespace.ToDisplayString(); - writerClassName = string.IsNullOrEmpty(ns) - ? $"{flatName}_GeneratedWriter" - : $"{ns}.{flatName}_GeneratedWriter"; - - // UseMetadata: compute child type hash-es for inline metadata - childTypeNameHash = ComputeFnvHash(resolvedType.Name); - childPropertyHashes = ComputeChildPropertyHashes(resolvedType); - } - } - - // Collection element type analysis for inline collection write - PropertyTypeKind elemKind = PropertyTypeKind.Unknown; - bool elemHasGenWriter = false; - bool elemIsIId = false; - bool elemEnableMetadata = true; - bool elemNeedsIdScan = true; - bool elemNeedsAllRefScan = true; - bool elemNeedsInternScan = true; - string? elemWriterClassName = null; - string? elemIdTypeName = null; - string? collKind = null; - string? collAddMethod = null; - bool collHasCapacityCtor = false; - string? elemFullTypeName = null; - int elementTypeNameHash = 0; - int[]? elementPropertyHashes = null; - if (kind == PropertyTypeKind.Collection) - { - var elemType = GetCollectionElementType(p.Type); - if (elemType != null) - { - elemKind = GetKind(elemType); - elemFullTypeName = elemType.ToDisplayString(); - - // Detect collection shape for inline write - if (p.Type is IArrayTypeSymbol) - collKind = "Array"; - else if (p.Type is INamedTypeSymbol collNamedType) - { - var origDef = collNamedType.OriginalDefinition.ToDisplayString(); - collKind = origDef switch - { - "System.Collections.Generic.List" => "List", - "System.Collections.Generic.IList" => "IndexedCollection", - "System.Collections.Generic.IReadOnlyList" => "IndexedCollection", - "System.Collections.Generic.HashSet" => "Counted", // has Count, no indexer - "System.Collections.Generic.Queue" => "Counted", - "System.Collections.Generic.ICollection" => "Counted", - "System.Collections.Generic.IReadOnlyCollection" => "Counted", - "System.Collections.Generic.SortedSet" => "Counted", - "System.Collections.Generic.LinkedList" => "Counted", - _ => null - }; - - // Determine add method + capacity ctor for Counted concrete types - if (collKind == "Counted") - { - collAddMethod = origDef switch - { - "System.Collections.Generic.HashSet" => "Add", - "System.Collections.Generic.SortedSet" => "Add", - "System.Collections.Generic.Queue" => "Enqueue", - "System.Collections.Generic.LinkedList" => "AddLast", - _ => null // ICollection, IReadOnlyCollection → backed by List - }; - collHasCapacityCtor = origDef is - "System.Collections.Generic.HashSet" or - "System.Collections.Generic.Queue"; - } - } - - // For Complex element types, check for generated writer - if (elemKind == PropertyTypeKind.Complex) - { - var resolvedElem = elemType is INamedTypeSymbol namedElem - ? namedElem.OriginalDefinition : elemType; - elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource) - && resolvedElem.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - if (elemHasGenWriter) - { - // Read element type's EnableMetadataFeature - elemEnableMetadata = ReadEnableMetadata(resolvedElem); - var elemScanFlags = ComputeNeedsScan(resolvedElem); - elemNeedsIdScan = elemScanFlags.needsIdScan; - elemNeedsAllRefScan = elemScanFlags.needsAllRefScan; - elemNeedsInternScan = elemScanFlags.needsInternScan; - var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc => - ifc.IsGenericType && - ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - elemIsIId = elemIidIface != null; - if (elemIidIface != null) - elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString(); - - var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem); - var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace - ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); - elemWriterClassName = string.IsNullOrEmpty(ens) - ? $"{elemFlatName}_GeneratedWriter" - : $"{ens}.{elemFlatName}_GeneratedWriter"; - - // UseMetadata: compute element type hash-es for inline metadata - elementTypeNameHash = ComputeFnvHash(resolvedElem.Name); - elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem); - } - } - } - } - - // Dictionary key/value type analysis for inline dictionary read - PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown; - PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown; - string? dictKeyTypeName = null; - string? dictValueTypeName = null; - bool dictValueHasGenWriter = false; - string? dictValueWriterClassName = null; - bool dictValueIsIId = false; - bool dictValueEnableMetadata = true; - bool dictValueNeedsIdScan = true; - bool dictValueNeedsAllRefScan = true; - bool dictValueNeedsInternScan = true; - int dictValueTypeNameHash = 0; - int[]? dictValuePropertyHashes = null; - if (kind == PropertyTypeKind.Dictionary) - { - var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type); - if (keyType != null) - { - dictKeyKind = GetKind(keyType); - dictKeyTypeName = keyType.ToDisplayString(); - } - if (valueType != null) - { - dictValueKind = GetKind(valueType); - dictValueTypeName = valueType.ToDisplayString(); - if (dictValueKind == PropertyTypeKind.Complex) - { - var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType; - dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource) - && resolvedValue.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - if (dictValueHasGenWriter) - { - var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue); - var vns = resolvedValue.ContainingNamespace.IsGlobalNamespace - ? string.Empty : resolvedValue.ContainingNamespace.ToDisplayString(); - dictValueWriterClassName = string.IsNullOrEmpty(vns) - ? $"{vfn}_GeneratedWriter" - : $"{vns}.{vfn}_GeneratedWriter"; - - dictValueEnableMetadata = ReadEnableMetadata(resolvedValue); - var dvScanFlags = ComputeNeedsScan(resolvedValue); - dictValueNeedsIdScan = dvScanFlags.needsIdScan; - dictValueNeedsAllRefScan = dvScanFlags.needsAllRefScan; - dictValueNeedsInternScan = dvScanFlags.needsInternScan; - var dvIidIface = resolvedValue.AllInterfaces.FirstOrDefault(ifc => - ifc.IsGenericType && - ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - dictValueIsIId = dvIidIface != null; - dictValueTypeNameHash = ComputeFnvHash(resolvedValue.Name); - dictValuePropertyHashes = ComputeChildPropertyHashes(resolvedValue); - } - } - } - } - - properties.Add(new PropInfo( - p.Name, - typeDisplayName, - typeNameForTypeof, - kind, - p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), - p.Type.SpecialType == SpecialType.System_Object, - stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, - elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, - collAddMethod, collHasCapacityCtor, - dictKeyKind, dictValueKind, dictKeyTypeName, dictValueTypeName, dictValueHasGenWriter, dictValueWriterClassName, - dictValueIsIId, dictValueEnableMetadata, dictValueTypeNameHash, dictValuePropertyHashes, - dictValueNeedsIdScan, dictValueNeedsAllRefScan, dictValueNeedsInternScan, - childTypeNameHash, childPropertyHashes, - elementTypeNameHash, elementPropertyHashes, - propEnableMetadata, elemEnableMetadata, - childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan, - elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan)); - } - - // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering - // If EnableIdTrackingFeature == false, skip IId detection entirely → isIId = false - var isIId = false; - string? idTypeName = null; - if (enableIdTracking) - { - var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i => - i.IsGenericType && - i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - if (iidInterface != null) - { - isIId = true; - idTypeName = iidInterface.TypeArguments[0].ToDisplayString(); - } - } - - // Properties are already in runtime-matching order from GetAllSerializablePropertySymbols: - // derived → base, each level sorted alphabetically (matches TypeMetadataBase.GetUnfilteredProperties). - - var className = BuildFlatName(typeSymbol); - var typeNameHash = ComputeFnvHash(typeSymbol.Name); - var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); - var selfScanFlags = ComputeNeedsScan(typeSymbol); - return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, enablePolymorphDetect, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan); - } - private static void Execute(ImmutableArray classes, SourceProductionContext context) { if (classes.IsDefaultOrEmpty) return; @@ -435,2789 +76,4 @@ public class AcBinarySourceGenerator : IIncrementalGenerator context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8)); } - - /// - /// ACCORE-BIN-I-T7K3 guard: emits - /// (ACBIN002) for every System.Object-declared property on any - /// [AcBinarySerializable] type whose EnablePolymorphDetectFeature is false. - /// Per-class gating: types with the feature enabled (default) skip the check entirely; only - /// opt-out types are scanned for misuse. - /// - private static void DetectAndReportPolymorphicMisuse(List classes, SourceProductionContext spc) - { - foreach (var ci in classes) - { - if (ci.EnablePolymorphDetect) continue; // Feature enabled → polymorphic prefix is emitted, no misuse possible. - - foreach (var p in ci.Properties) - { - if (p.IsObjectDeclaredType) - { - spc.ReportDiagnostic(Diagnostic.Create( - PolymorphicPropertyWithFeatureDisabledError, Location.None, - ci.ClassName, p.Name)); - } - } - } - } - - /// - /// Detects circular reference chains among [AcBinarySerializable] types at compile time - /// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges. - /// - private static void DetectAndReportCycles(List classes, SourceProductionContext spc) - { - // Build lookup: WriterClassName → FullTypeName - var writerToFull = new Dictionary(classes.Count); - foreach (var ci in classes) - { - var writerName = string.IsNullOrEmpty(ci.Namespace) - ? $"{ci.ClassName}_GeneratedWriter" - : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; - writerToFull[writerName] = ci.FullTypeName; - } - - // Build adjacency list: FullTypeName → set of referenced FullTypeNames - var adjacency = new Dictionary>(classes.Count); - foreach (var ci in classes) - { - var edges = new HashSet(); - foreach (var p in ci.Properties) - { - if (p.TypeKind == PropertyTypeKind.Complex && p.HasGeneratedWriter && p.WriterClassName != null) - { - if (writerToFull.TryGetValue(p.WriterClassName, out var target)) - edges.Add(target); - } - if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.ElementWriterClassName != null) - { - if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target)) - edges.Add(target); - } - if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter && p.DictValueWriterClassName != null) - { - if (writerToFull.TryGetValue(p.DictValueWriterClassName, out var target)) - edges.Add(target); - } - } - adjacency[ci.FullTypeName] = edges; - } - - // DFS with 3-color marking: White=0, Gray=1, Black=2 - var color = new Dictionary(classes.Count); - foreach (var ci in classes) - color[ci.FullTypeName] = 0; - - var stack = new List(); - var reported = new HashSet(); - - void Dfs(string node) - { - color[node] = 1; // Gray - stack.Add(node); - - if (adjacency.TryGetValue(node, out var neighbors)) - { - foreach (var next in neighbors) - { - if (!color.TryGetValue(next, out var c)) continue; - if (c == 1) // Gray → back-edge = cycle - { - var cycleStart = stack.IndexOf(next); - var parts = new List(); - for (var i = cycleStart; i < stack.Count; i++) - parts.Add(ShortTypeName(stack[i])); - parts.Add(ShortTypeName(next)); // close the cycle - - var cycleDesc = string.Join(" \u2192 ", parts); - for (var i = cycleStart; i < stack.Count; i++) - { - if (reported.Add(stack[i])) - { - spc.ReportDiagnostic(Diagnostic.Create( - CircularReferenceWarning, Location.None, - ShortTypeName(stack[i]), cycleDesc)); - } - } - } - else if (c == 0) // White → unvisited - { - Dfs(next); - } - } - } - - stack.RemoveAt(stack.Count - 1); - color[node] = 2; // Black - } - - foreach (var ci in classes) - { - if (color[ci.FullTypeName] == 0) - Dfs(ci.FullTypeName); - } - } - - private static string ShortTypeName(string fullTypeName) - { - var dot = fullTypeName.LastIndexOf('.'); - return dot >= 0 ? fullTypeName.Substring(dot + 1) : fullTypeName; - } - - 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: - if (p.InterningFlags == 0) - sb.AppendLine($"{i}context.StringInternEligible = false;"); - else - sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); - sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); - break; - case PropertyTypeKind.Complex: - // Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely - // when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise. - if (p.HasGeneratedWriter) - EmitDirectObjectWrite(sb, p, a, i); - else if (p.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 if (p.IsNullable) - { - sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); - } - else - sb.AppendLine($"{i}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 if (p.IsNullable) - { - sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context);"); - } - else - sb.AppendLine($"{i}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}:;"); - } - - /// - /// Returns true for property types that use markerless serialization in FastMode. - /// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults. - /// - private static bool IsMarkerless(PropertyTypeKind k) => k switch - { - PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or - PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or - PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or - PropertyTypeKind.DateTime or PropertyTypeKind.Guid or - PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset or - PropertyTypeKind.Boolean or PropertyTypeKind.Enum => true, - _ => false - }; - - /// - /// Emits raw value only — no type marker, no PropertySkip. - /// Matches runtime WritePropertyMarkerless exactly. - /// - private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i) - { - switch (k) - { - case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteVarInt({a});"); break; - case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteVarLong({a});"); break; - case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteRaw({a});"); break; - case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteRaw({a});"); break; - case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break; - case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break; - case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteGuidBits({a});"); break; - case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte({a});"); break; - case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; - case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteRaw({a});"); break; - case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteVarUInt({a});"); break; - case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteVarULong({a});"); break; - case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break; - case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break; - case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? (byte)1 : (byte)0);"); break; - case PropertyTypeKind.Enum: sb.AppendLine($"{i}context.WriteVarInt((int){a});"); break; - } - } - - /// - /// Emits 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"; - - if (!p.ElementNeedsRefScan && !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 (p.ElementNeedsRefScan && !p.ElementEnableMetadata) - { - sb.AppendLine($"{i} if (context.WriteObjectRefMarker{elemRefSuffix}()) {writer}.Instance.WriteProperties({e}, context);"); - } - else if (!p.ElementNeedsRefScan && 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; - } - } - - #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); - } - - 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) - { - 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; - } - - // String FastWire markerless fast-path: int32 sentinel header (-1 = null, 0 = empty, N > 0 = content). - // Wire-symmetric with `WriteStringGenerated` (SGen) and `WriteStringUtf16Markerless` (Runtime). - // Skips the typeCode-read entirely in FastWire mode; falls through to markered dispatch in Compact. - 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} if (tc_{p.Name} != BinaryTypeCode.PropertySkip)"); - sb.AppendLine($"{i} {{"); - EmitReadString(sb, a, $"tc_{p.Name}", i + " "); - 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 + " "); - break; - - case PropertyTypeKind.Complex: - EmitReadComplex(sb, p, a, tc, i + " "); - break; - - case PropertyTypeKind.Collection: - EmitReadCollection(sb, p, a, tc, i + " "); - break; - - case PropertyTypeKind.Dictionary: - EmitReadDictionary(sb, p, a, tc, i + " "); - break; - - default: - // Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback - sb.AppendLine($"{i} context._position--;"); - if (p.IsNullable) - sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}));"); - 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: - /// 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 charLen and utf8Len in fixed-width headers (1-pass decode). - /// - private static void EmitReadString(StringBuilder sb, string a, string tc, string i) - { - // 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}{{"); - sb.AppendLine($"{i} case BinaryTypeCode.StringInterned:"); - sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());"); - sb.AppendLine($"{i} break;"); - // H2Q6 string-tier markers + StringAscii + interning tiers. Wire-decode body is shared with - // the runtime path (TypeReaderTable + cross-type populate) — see context.ReadStringSmall/Medium/Big, - // ReadPlainStringAscii, ReadAndRegisterInternedStringSmall/Medium. - sb.AppendLine($"{i} case BinaryTypeCode.StringSmall:"); - sb.AppendLine($"{i} {a} = context.ReadStringSmall();"); - 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;"); - 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})"; - - if (!p.ChildNeedsRefScan) - { - // 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) - { - // Check if we can inline: known collection shape + inlineable element type - if (p.CollectionKind != null && CanInlineCollectionRead(p)) - { - EmitReadCollectionInline(sb, p, a, tc, i); - return; - } - - // Runtime fallback - if (p.IsNullable) - { - sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - sb.AppendLine($"{i} context._position--;"); - sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}));"); - 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) - { - 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); - 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} }}"); - 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); - 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) - { - 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); - } - 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) - { - 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); - else - EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null); - 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, p.CollectionAddMethod); - else - EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod); - 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); - else - EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null); - 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, 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});"; - - if (!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) - { - 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 + " "); - 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 - - private static string GenInit(List classes) - { - var sb = new StringBuilder(512); - sb.AppendLine("// "); - sb.AppendLine("using System.Runtime.CompilerServices;"); - sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); - sb.AppendLine(); - sb.AppendLine("namespace AyCode.Core.Serializers.Generated;"); - sb.AppendLine(); - sb.AppendLine("internal static class AcBinaryGeneratedWritersInit"); - sb.AppendLine("{"); - sb.AppendLine(" [ModuleInitializer]"); - sb.AppendLine(" internal static void Register()"); - sb.AppendLine(" {"); - foreach (var ci in classes) - { - var writerRef = string.IsNullOrEmpty(ci.Namespace) - ? $"{ci.ClassName}_GeneratedWriter" - : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; - var readerRef = string.IsNullOrEmpty(ci.Namespace) - ? $"{ci.ClassName}_GeneratedReader" - : $"{ci.Namespace}.{ci.ClassName}_GeneratedReader"; - sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);"); - sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.Instance);"); - } - sb.AppendLine(" }"); - sb.AppendLine("}"); - return sb.ToString(); - } - - /// - /// Builds a flat class name for nested types: Outer_Inner_Leaf. - /// For top-level types returns the simple name unchanged. - /// - private static string BuildFlatName(INamedTypeSymbol typeSymbol) - { - if (typeSymbol.ContainingType == null) - return typeSymbol.Name; - - var parts = new List(); - var current = typeSymbol; - while (current != null) - { - parts.Add(current.Name); - current = current.ContainingType; - } - parts.Reverse(); - return string.Join("_", parts); - } - - /// - /// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute. - /// Returns true (default) if no attribute or enableAllFeatures=true. - /// - private static bool ReadEnableMetadata(ITypeSymbol type) - { - var attr = type.GetAttributes().FirstOrDefault(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - if (attr == null) return true; - if (attr.ConstructorArguments.Length == 1) - return (bool)attr.ConstructorArguments[0].Value!; - if (attr.ConstructorArguments.Length == 4) - return (bool)attr.ConstructorArguments[0].Value!; - return true; - } - - /// - /// Computes whether a type needs scan pass work, split into ref tracking and string interning. - /// Uses a per-call HashSet to guard against circular references (no static cache — - /// static state is unsafe in incremental generators as it persists across builds). - /// Returns (needsRefScan, needsInternScan) — these are independent axes. - /// - private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type) - { - return ComputeNeedsScanCore(type, new HashSet()); - } - - private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet visiting) - { - // Circular reference guard: if already visiting this type, assume true (safe fallback) - var key = type.ToDisplayString(); - if (!visiting.Add(key)) - return (true, true, true); - - // Read [AcBinarySerializable] flags - var attr = type.GetAttributes().FirstOrDefault(a => - a.AttributeClass?.ToDisplayString() == AttributeName); - - bool enableIdTracking = true, enableRefHandling = true, enableInternString = true; - if (attr != null) - { - if (attr.ConstructorArguments.Length == 1) - { - var all = (bool)attr.ConstructorArguments[0].Value!; - enableIdTracking = enableRefHandling = enableInternString = all; - } - else if (attr.ConstructorArguments.Length == 4) - { - enableIdTracking = (bool)attr.ConstructorArguments[1].Value!; - enableRefHandling = (bool)attr.ConstructorArguments[2].Value!; - enableInternString = (bool)attr.ConstructorArguments[3].Value!; - } - } - - // IId tracking: active in OnlyId + All modes - var isIId = enableIdTracking && type.AllInterfaces.Any(i => - i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - var needsIdScan = isIId; - // Non-IId ref tracking: active only in All mode - var needsAllRefScan = !isIId && enableRefHandling; - var needsInternScan = false; - - // Check properties for string interning or complex children - foreach (var p in GetAllSerializablePropertySymbols(type)) - { - // Early exit: if all flags are already true, no need to check more properties - if (needsIdScan && needsAllRefScan && needsInternScan) break; - - var kind = GetKind(p.Type); - - // String with interning? - if (enableInternString && kind == PropertyTypeKind.String) - { - var internAttr = p.GetAttributes().FirstOrDefault(a => - a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute"); - if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!)) - needsInternScan = true; - } - - // Complex child → recurse - if (kind == PropertyTypeKind.Complex) - { - var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type; - var childFlags = ComputeNeedsScanCore(resolved, visiting); - needsIdScan |= childFlags.needsIdScan; - needsAllRefScan |= childFlags.needsAllRefScan; - needsInternScan |= childFlags.needsInternScan; - } - - // Collection → check element type - if (kind == PropertyTypeKind.Collection) - { - var elemType = GetCollectionElementType(p.Type); - if (elemType != null) - { - var elemKind = GetKind(elemType); - if (enableInternString && elemKind == PropertyTypeKind.String) - needsInternScan = true; - if (elemKind == PropertyTypeKind.Complex) - { - var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType; - var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting); - needsIdScan |= elemFlags.needsIdScan; - needsAllRefScan |= elemFlags.needsAllRefScan; - needsInternScan |= elemFlags.needsInternScan; - } - } - } - - // Dictionary → check key and value types - if (kind == PropertyTypeKind.Dictionary) - { - var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type); - if (keyType != null && enableInternString && GetKind(keyType) == PropertyTypeKind.String) - needsInternScan = true; - if (valueType != null) - { - var valKind = GetKind(valueType); - if (enableInternString && valKind == PropertyTypeKind.String) - needsInternScan = true; - if (valKind == PropertyTypeKind.Complex) - { - var resolvedVal = valueType is INamedTypeSymbol nv ? nv.OriginalDefinition : valueType; - var valFlags = ComputeNeedsScanCore(resolvedVal, visiting); - needsIdScan |= valFlags.needsIdScan; - needsAllRefScan |= valFlags.needsAllRefScan; - needsInternScan |= valFlags.needsInternScan; - } - } - } - } - - return (needsIdScan, needsAllRefScan, needsInternScan); - } - - #region FNV-1a Hash (compile-time) - - private static int ComputeFnvHash(string value) - { - uint hash = 2166136261; - for (int i = 0; i < value.Length; i++) - { - hash ^= value[i]; - hash *= 16777619; - } - return (int)hash; - } - - /// - /// Computes FNV-1a hashes for all serializable properties of a child type. - /// Property filtering and ordering matches runtime TypeMetadataBase exactly: - /// derived → base, each level sorted alphabetically, with ignore attribute filtering. - /// - private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType) - { - // Use hierarchy-walking helper — order matches runtime TypeMetadataBase - var props = GetAllSerializablePropertySymbols(resolvedType); - return props.Select(p => ComputeFnvHash(p.Name)).ToArray(); - } - - #endregion - - /// - /// Collects all serializable property symbols from the full inheritance hierarchy. - /// Order matches runtime TypeMetadataBase.GetUnfilteredProperties exactly: - /// derived → base, each level sorted alphabetically by name. - /// Filters: public, get+set, non-indexer, non-static, no ignore attributes. - /// Deduplicates by name (most-derived override wins). - /// - private static List GetAllSerializablePropertySymbols(ITypeSymbol typeSymbol) - { - var result = new List(); - var seen = new HashSet(); - - for (var currentType = typeSymbol as INamedTypeSymbol; - currentType != null && currentType.SpecialType != SpecialType.System_Object; - currentType = currentType.BaseType) - { - var levelProps = new List(); - - foreach (var member in currentType.GetMembers()) - { - if (member is IPropertySymbol p && - p.DeclaredAccessibility == Accessibility.Public && - p.GetMethod != null && p.SetMethod != null && - !p.IsIndexer && !p.IsStatic && - seen.Add(p.Name)) // dedup: most-derived wins - { - var hasIgnore = p.GetAttributes().Any(a => - { - var name = a.AttributeClass?.Name ?? ""; - return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; - }); - if (hasIgnore) continue; - - levelProps.Add(p); - } - } - - // Sort each level alphabetically — matches runtime OrderBy(p => p.Name, Ordinal) - levelProps.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - result.AddRange(levelProps); - } - - return result; - } - - #region Type analysis - - private static bool IsNullableVT(ITypeSymbol t) => - t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; - - private static PropertyTypeKind GetKind(ITypeSymbol type) - { - if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) - return GetKindCore(n.TypeArguments[0], true); - return GetKindCore(type, false); - } - - private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable) - { - switch (type.SpecialType) - { - case SpecialType.System_String: return PropertyTypeKind.String; - case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32; - case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64; - case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16; - case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte; - case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16; - case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32; - case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64; - case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean; - case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single; - case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double; - case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal; - case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime; - default: break; - } - var fn = type.ToDisplayString(); - if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid; - if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan; - if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; - if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; - if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection; - // Dictionary detection: must come before IEnumerable (Dictionary implements both) - if (type is INamedTypeSymbol dictNt && dictNt.IsGenericType) - { - var orig = dictNt.OriginalDefinition.ToDisplayString(); - if (orig == "System.Collections.Generic.IDictionary" || - orig == "System.Collections.Generic.Dictionary" || - dictNt.AllInterfaces.Any(ifc => ifc.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary")) - return PropertyTypeKind.Dictionary; - } - if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)) - return PropertyTypeKind.Collection; - if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; - return PropertyTypeKind.Unknown; - } - - /// - /// Extracts the element type T from List<T>, T[], IList<T>, IEnumerable<T>. - /// Returns null if the element type cannot be determined. - /// - private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type) - { - // T[] → element type - if (type is IArrayTypeSymbol arrayType) - return arrayType.ElementType; - - // Generic collections: List, IList, ICollection, IEnumerable - if (type is INamedTypeSymbol namedType && namedType.IsGenericType) - { - // Direct: List, HashSet, etc. — first type argument - var iface = namedType.AllInterfaces - .FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T); - if (iface != null) - return iface.TypeArguments[0]; - } - - return null; - } - - /// - /// Extracts key and value types from Dictionary<K,V> or IDictionary<K,V>. - /// - private static (ITypeSymbol? keyType, ITypeSymbol? valueType) GetDictionaryKeyValueTypes(ITypeSymbol type) - { - if (type is INamedTypeSymbol nt && nt.IsGenericType) - { - var orig = nt.OriginalDefinition.ToDisplayString(); - if (orig == "System.Collections.Generic.Dictionary" || - orig == "System.Collections.Generic.IDictionary") - return (nt.TypeArguments[0], nt.TypeArguments[1]); - - var iface = nt.AllInterfaces.FirstOrDefault(i => - i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary"); - if (iface != null) - return (iface.TypeArguments[0], iface.TypeArguments[1]); - } - return (null, null); - } - - private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; - - private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch - { - PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64, - PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte, - PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32, - PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean, - PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double, - PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime, - PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan, - PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum, - _ => PropertyTypeKind.Unknown - }; - - #endregion -} - -internal sealed class SerializableClassInfo -{ - public string Namespace { get; } - public string ClassName { get; } - public string FullTypeName { get; } - public List Properties { get; } - /// True if this type implements IId<T> - public bool IsIId { get; } - /// The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise - public string? IdTypeName { get; } - /// True if EnableRefHandlingFeature is enabled — controls non-IId All mode tracking code emission. - public bool EnableRefHandling { get; } - /// FNV-1a hash of ClassName (matches runtime SourceType.Name hash) - public int TypeNameHash { get; } - /// FNV-1a hash of each property name, in property order - public int[] PropertyNameHashes { get; } - /// When false, skip inline metadata and use markerless property write for this type. - public bool EnableMetadata { get; } - /// True if EnablePropertyFilterFeature is enabled — controls per-property HasPropertyFilter - /// guard emission in WriteProperties / ScanObject. When false, the filter check is omitted entirely - /// → leaner generated code on the hot path (typical for high-throughput types that never use a filter). - public bool EnablePropertyFilter { get; } - /// True if EnablePolymorphDetectFeature is enabled — controls ObjectWithTypeName + AQN - /// prefix emit on System.Object-declared properties. When false, the prefix is suppressed - /// AND ACBIN002 fires at build time if such a property exists on this type (guarding against silent - /// wire corruption). Opt-out is intentional: dev guarantees no polymorphic object property - /// will be serialized on this type, or all such properties are excluded via [AcBinaryIgnore]. - public bool EnablePolymorphDetect { get; } - /// When true, type subtree has IId types needing scan (active in OnlyId + All). - public bool NeedsIdScan { get; } - /// When true, type subtree has non-IId ref tracking (active only in All mode). - public bool NeedsAllRefScan { get; } - /// When true, type subtree needs string interning scan. - public bool NeedsInternScan { get; } - /// Derived: NeedsIdScan || NeedsAllRefScan. - public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan; - /// Derived: any scan axis active. - public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan; - public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect, bool needsIdScan, bool needsAllRefScan, bool needsInternScan) - { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; EnablePolymorphDetect = enablePolymorphDetect; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; } -} - -internal sealed class PropInfo -{ - public string Name { get; } - public string TypeName { get; } - /// - /// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types). - /// - public string TypeNameForTypeof { get; } - public PropertyTypeKind TypeKind { get; } - public bool IsNullable { get; } - /// - /// Pre-computed interning flags matching runtime BinaryPropertyAccessorBase._interningFlags. - /// Bit layout: bit N = eligible when StringInterningMode == N. - /// None=0 → bit 0 never set. Attribute=1 → bit 1. All=2 → bit 2. - /// No attr: 0b100 (4), [AcStringIntern(true)]: 0b110 (6), [AcStringIntern(false)]: 0b000 (0). - /// - public int InterningFlags { get; } - - /// True when declared property type is System.Object. Runtime type dispatch needed. - public bool IsObjectDeclaredType { get; } - /// True if the Complex property type has [AcBinarySerializable] → has a generated writer. - public bool HasGeneratedWriter { get; } - /// True if the Complex property type implements IId<T> → needs ref tracking in write pass. - public bool IsIId { get; } - /// Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter. - public string? WriterClassName { get; } - /// Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId. - public string? IdTypeName { get; } - - // Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer - /// Element type kind for collection properties. Only meaningful when TypeKind == Collection. - public PropertyTypeKind ElementKind { get; } - /// True if collection element type has [AcBinarySerializable]. - public bool ElementHasGeneratedWriter { get; } - /// True if collection element type implements IId<T>. - public bool ElementIsIId { get; } - /// Generated writer class name for collection element type. - public string? ElementWriterClassName { get; } - /// Id type name for collection element IId types. Null if not IId. - public string? ElementIdTypeName { get; } - /// Collection type: "List", "Array", "IndexedCollection", "Counted", or null (unknown — fallback to runtime). - public string? CollectionKind { get; } - /// Full element type name for generated code (e.g. "SharedTag"). - public string? ElementFullTypeName { get; } - /// Add method for Counted concrete collections. null → List<T>.Add(), "Add" → HashSet/SortedSet, "Enqueue" → Queue, "AddLast" → LinkedList. - public string? CollectionAddMethod { get; } - /// True if the concrete Counted collection has a capacity constructor (HashSet, Queue). - public bool CollectionHasCapacityCtor { get; } - - // Dictionary metadata — set when TypeKind == Dictionary - /// Key type kind for dictionary properties. - public PropertyTypeKind DictKeyKind { get; } - /// Value type kind for dictionary properties. - public PropertyTypeKind DictValueKind { get; } - /// Key type name for generated code. - public string? DictKeyTypeName { get; } - /// Value type name for generated code. - public string? DictValueTypeName { get; } - /// True if dictionary value type has [AcBinarySerializable]. - public bool DictValueHasGeneratedWriter { get; } - /// Generated writer class name for dictionary value type. - public string? DictValueWriterClassName { get; } - /// True if dictionary value type implements IId<T>. - public bool DictValueIsIId { get; } - /// When false, dict value type skips inline metadata. - public bool DictValueEnableMetadata { get; } - /// FNV-1a hash of dict value type name. - public int DictValueTypeNameHash { get; } - /// FNV-1a hashes of dict value type's properties. - public int[]? DictValuePropertyHashes { get; } - /// When true, dict value subtree has IId types needing scan. - public bool DictValueNeedsIdScan { get; } - /// When true, dict value subtree has non-IId ref tracking. - public bool DictValueNeedsAllRefScan { get; } - /// When true, dict value subtree needs string interning scan. - public bool DictValueNeedsInternScan { get; } - /// Derived: DictValueNeedsIdScan || DictValueNeedsAllRefScan. - public bool DictValueNeedsRefScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan; - /// Derived: any dict value scan axis active. - public bool DictValueNeedsScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan || DictValueNeedsInternScan; - - // UseMetadata inline hash-ek (Complex/Collection child típushoz) - /// FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter. - public int ChildTypeNameHash { get; } - /// FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter. - public int[]? ChildPropertyHashes { get; } - /// FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter. - public int ElementTypeNameHash { get; } - /// FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter. - public int[]? ElementPropertyHashes { get; } - /// When false, child Complex type skips inline metadata in generated code. - public bool ChildEnableMetadata { get; } - /// When false, collection element type skips inline metadata in generated code. - public bool ElementEnableMetadata { get; } - /// When true, child subtree has IId types needing scan (active in OnlyId + All). - public bool ChildNeedsIdScan { get; } - /// When true, child subtree has non-IId ref tracking (active only in All mode). - public bool ChildNeedsAllRefScan { get; } - /// When true, child subtree needs string interning scan. - public bool ChildNeedsInternScan { get; } - /// Derived: ChildNeedsIdScan || ChildNeedsAllRefScan. - public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan; - /// Derived: any child scan axis active. - public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan; - /// When true, element subtree has IId types needing scan (active in OnlyId + All). - public bool ElementNeedsIdScan { get; } - /// When true, element subtree has non-IId ref tracking (active only in All mode). - public bool ElementNeedsAllRefScan { get; } - /// When true, element subtree needs string interning scan. - public bool ElementNeedsInternScan { get; } - /// Derived: ElementNeedsIdScan || ElementNeedsAllRefScan. - public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan; - /// Derived: any element scan axis active. - public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan; - - public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, - bool isObjectDeclaredType = false, - bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, - PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, - string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, - string? collectionAddMethod = null, bool collectionHasCapacityCtor = false, - PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown, PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown, - string? dictKeyTypeName = null, string? dictValueTypeName = null, - bool dictValueHasGeneratedWriter = false, string? dictValueWriterClassName = null, - bool dictValueIsIId = false, bool dictValueEnableMetadata = true, - int dictValueTypeNameHash = 0, int[]? dictValuePropertyHashes = null, - bool dictValueNeedsIdScan = true, bool dictValueNeedsAllRefScan = true, bool dictValueNeedsInternScan = true, - int childTypeNameHash = 0, int[]? childPropertyHashes = null, - int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, - bool childEnableMetadata = true, bool elementEnableMetadata = true, - bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true, - bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true) - { - Name = n; - TypeName = tn; - TypeNameForTypeof = tnForTypeof; - TypeKind = tk; - IsNullable = nullable; - IsObjectDeclaredType = isObjectDeclaredType; - HasGeneratedWriter = hasGeneratedWriter; - IsIId = isIId; - WriterClassName = writerClassName; - IdTypeName = idTypeName; - ElementKind = elementKind; - ElementHasGeneratedWriter = elementHasGenWriter; - ElementIsIId = elementIsIId; - ElementWriterClassName = elementWriterClassName; - ElementIdTypeName = elementIdTypeName; - CollectionKind = collectionKind; - ElementFullTypeName = elementFullTypeName; - CollectionAddMethod = collectionAddMethod; - CollectionHasCapacityCtor = collectionHasCapacityCtor; - DictKeyKind = dictKeyKind; - DictValueKind = dictValueKind; - DictKeyTypeName = dictKeyTypeName; - DictValueTypeName = dictValueTypeName; - DictValueHasGeneratedWriter = dictValueHasGeneratedWriter; - DictValueWriterClassName = dictValueWriterClassName; - DictValueIsIId = dictValueIsIId; - DictValueEnableMetadata = dictValueEnableMetadata; - DictValueTypeNameHash = dictValueTypeNameHash; - DictValuePropertyHashes = dictValuePropertyHashes; - DictValueNeedsIdScan = dictValueNeedsIdScan; - DictValueNeedsAllRefScan = dictValueNeedsAllRefScan; - DictValueNeedsInternScan = dictValueNeedsInternScan; - ChildTypeNameHash = childTypeNameHash; - ChildPropertyHashes = childPropertyHashes; - ElementTypeNameHash = elementTypeNameHash; - ElementPropertyHashes = elementPropertyHashes; - ChildEnableMetadata = childEnableMetadata; - ElementEnableMetadata = elementEnableMetadata; - ChildNeedsIdScan = childNeedsIdScan; - ChildNeedsAllRefScan = childNeedsAllRefScan; - ChildNeedsInternScan = childNeedsInternScan; - ElementNeedsIdScan = elementNeedsIdScan; - ElementNeedsAllRefScan = elementNeedsAllRefScan; - ElementNeedsInternScan = elementNeedsInternScan; - // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase - int flags = 0; - if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit - if (stringInternAttr != false) flags |= (1 << 2); // All bit - InterningFlags = flags; - } -} - -internal enum PropertyTypeKind -{ - Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, - Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, - Collection, Complex, Dictionary, - NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, - NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, - NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum }