diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 36e80b5..1ea9f08 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -181,6 +181,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator string? elemWriterClassName = null; string? elemIdTypeName = null; string? collKind = null; + string? collAddMethod = null; + bool collHasCapacityCtor = false; string? elemFullTypeName = null; int elementTypeNameHash = 0; int[]? elementPropertyHashes = null; @@ -211,6 +213,22 @@ public class AcBinarySourceGenerator : IIncrementalGenerator "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 @@ -250,6 +268,43 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + // 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; + 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.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"; + } + } + } + } + properties.Add(new PropInfo( p.Name, typeDisplayName, @@ -259,6 +314,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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, childTypeNameHash, childPropertyHashes, elementTypeNameHash, elementPropertyHashes, propEnableMetadata, elemEnableMetadata, @@ -342,6 +399,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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; } @@ -542,7 +604,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var scanProps = ci.Properties.Where(p => p.TypeKind == PropertyTypeKind.String || p.TypeKind == PropertyTypeKind.Complex || - p.TypeKind == PropertyTypeKind.Collection).ToList(); + p.TypeKind == PropertyTypeKind.Collection || + p.TypeKind == PropertyTypeKind.Dictionary).ToList(); // Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed var hasStringScan = scanProps.Any(p => @@ -681,6 +744,16 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; + case PropertyTypeKind.Dictionary: + // Dictionary: always runtime fallback via WriteValueGenerated (which calls WriteDictionary) + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + } + else + sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + break; default: EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); break; @@ -774,6 +847,17 @@ public class AcBinarySourceGenerator : IIncrementalGenerator case PropertyTypeKind.Collection: EmitScanCollection(sb, p, a, i); break; + + case PropertyTypeKind.Dictionary: + // Dictionary scan: runtime fallback via ScanValueGenerated + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} != null)"); + sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + } + else + sb.AppendLine($"{i}AcBinarySerializer.ScanValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + break; } if (!IsMarkerless(p.TypeKind)) @@ -1500,6 +1584,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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--;"); @@ -1631,15 +1719,27 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + /// + /// 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. - /// Complex element with SGen + known collection kind → inline Array loop with direct element reader calls. + /// 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: need SGen element reader + known collection shape + Array marker - if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) + // Check if we can inline: known collection shape + inlineable element type + if (p.CollectionKind != null && CanInlineCollectionRead(p)) { EmitReadCollectionInline(sb, p, a, tc, i); return; @@ -1662,16 +1762,186 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + /// + /// 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} var nd_{s} = depth + 1;"); + 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}), nd_{s})!;"); + + // 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} dv_{s} = ({valType}){valReader}.Instance.ReadObject(context, nd_{s}, -1)!;"); + sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)"); + sb.AppendLine($"{i} dv_{s} = ({valType}){valReader}.Instance.ReadObject(context, nd_{s}, (int)context.ReadVarUInt())!;"); + 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}), nd_{s});"); + 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}), nd_{s});"); + + // 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 reader calls. + /// 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 reader = p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); + var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter; var elemType = p.ElementFullTypeName!; - var elemCast = $"({elemType})"; var s = p.Name; // Null check @@ -1687,23 +1957,45 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();"); - sb.AppendLine($"{i} var nd_{s} = depth + 1;"); + if (isComplexElement) + sb.AppendLine($"{i} var nd_{s} = depth + 1;"); - // Create collection based on kind + // 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} {{"); - EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); + if (isComplexElement) + EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); + else + EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null); sb.AppendLine($"{i} }}"); } - else // List, IndexedCollection, Counted — all use List with Add + 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})", $"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} {{"); - EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan); + if (isComplexElement) + EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan); + else + EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null); sb.AppendLine($"{i} }}"); } @@ -1716,13 +2008,14 @@ public class AcBinarySourceGenerator : IIncrementalGenerator /// SGen reader = non-metadata mode → no ObjectWithMetadata fallback. /// !needsRefScan → only Object/Null possible → 1 branch per element. /// - private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan) + private static void EmitReadCollectionElement(StringBuilder sb, string reader, 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 assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.Add(null!);"; - var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.Add("; + var addCall = addMethod ?? "Add"; + var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);"; + var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.{addCall}("; var assignEnd = isArray ? ";" : ");"; if (!needsRefScan) @@ -1744,6 +2037,63 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + /// + /// 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 etc = $"etc_{propSuffix}"; + sb.AppendLine($"{i}var {etc} = context.ReadByte();"); + + var addCall = addMethod ?? "Add"; + var elemType = p.ElementFullTypeName!; + var colRef = $"col_{propSuffix}"; + + if (p.ElementKind == PropertyTypeKind.String) + { + // String element: FixStr / String / StringInternFirst / StringInterned / Null / StringEmpty + var tempVar = $"sv_{propSuffix}"; + sb.AppendLine($"{i}string? {tempVar} = null;"); + EmitReadString(sb, tempVar, etc, i); + if (isArray) + sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar}!;"); + else + sb.AppendLine($"{i}{colRef}.{addCall}({tempVar}!);"); + } + else 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. @@ -1994,6 +2344,28 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } } + + // 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); @@ -2081,6 +2453,15 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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; @@ -2110,6 +2491,26 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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 @@ -2204,6 +2605,24 @@ internal sealed class PropInfo 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; } // UseMetadata inline hash-ek (Complex/Collection child típushoz) /// FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter. @@ -2244,6 +2663,10 @@ internal sealed class PropInfo 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, int childTypeNameHash = 0, int[]? childPropertyHashes = null, int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, bool childEnableMetadata = true, bool elementEnableMetadata = true, @@ -2267,6 +2690,14 @@ internal sealed class PropInfo ElementIdTypeName = elementIdTypeName; CollectionKind = collectionKind; ElementFullTypeName = elementFullTypeName; + CollectionAddMethod = collectionAddMethod; + CollectionHasCapacityCtor = collectionHasCapacityCtor; + DictKeyKind = dictKeyKind; + DictValueKind = dictValueKind; + DictKeyTypeName = dictKeyTypeName; + DictValueTypeName = dictValueTypeName; + DictValueHasGeneratedWriter = dictValueHasGeneratedWriter; + DictValueWriterClassName = dictValueWriterClassName; ChildTypeNameHash = childTypeNameHash; ChildPropertyHashes = childPropertyHashes; ElementTypeNameHash = elementTypeNameHash; @@ -2291,7 +2722,7 @@ internal enum PropertyTypeKind { Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, - Collection, Complex, + Collection, Complex, Dictionary, NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum