diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index b89c018..e4debc0 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -113,13 +113,60 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + // Collection element type analysis for inline collection write + PropertyTypeKind elemKind = PropertyTypeKind.Unknown; + bool elemHasGenWriter = false; + bool elemIsIId = false; + string? elemWriterClassName = null; + string? collKind = null; + string? elemFullTypeName = null; + if (kind == PropertyTypeKind.Collection) + { + var elemType = GetCollectionElementType(p.Type); + if (elemType != null) + { + elemKind = GetKind(elemType); + elemFullTypeName = elemType.ToDisplayString(); + + // Detect collection shape: List or T[] + if (p.Type is IArrayTypeSymbol) + collKind = "Array"; + else if (p.Type is INamedTypeSymbol collNamedType && + collNamedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.List") + collKind = "List"; + + // For Complex element types, check for generated writer + if (elemKind == PropertyTypeKind.Complex) + { + var resolvedElem = elemType is INamedTypeSymbol namedElem + ? namedElem.OriginalDefinition : elemType; + elemHasGenWriter = resolvedElem.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (elemHasGenWriter && resolvedElem is INamedTypeSymbol nte && nte.ContainingType != null) + elemHasGenWriter = false; + if (elemHasGenWriter) + { + elemIsIId = resolvedElem.AllInterfaces.Any(ifc => + ifc.IsGenericType && + ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace + ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); + elemWriterClassName = string.IsNullOrEmpty(ens) + ? $"{resolvedElem.Name}_GeneratedWriter" + : $"{ens}.{resolvedElem.Name}_GeneratedWriter"; + } + } + } + } + properties.Add(new PropInfo( p.Name, typeDisplayName, typeNameForTypeof, kind, p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), - stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName)); + stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, + elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName)); } } @@ -161,8 +208,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); - // ReferenceHandlingMode is needed when any Complex property has direct object write - if (ci.Properties.Any(p => p.HasGeneratedWriter)) + // ReferenceHandlingMode is needed when any Complex/Collection property has direct object write + if (ci.Properties.Any(p => p.HasGeneratedWriter || p.ElementHasGeneratedWriter)) sb.AppendLine("using AyCode.Core.Serializers;"); sb.AppendLine(); if (!string.IsNullOrEmpty(ci.Namespace)) @@ -234,8 +281,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; case PropertyTypeKind.Collection: - // typeof() instead of GetType() — avoids virtual dispatch - if (p.IsNullable) + // 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, depth);"); @@ -377,6 +426,105 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } + /// + /// 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]... + /// Falls back to WriteValueGenerated when context.IsDirectObjectWrite is false. + /// + private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i) + { + var writer = p.ElementWriterClassName; + var elemType = p.ElementFullTypeName; + + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)"); + } + else + { + sb.AppendLine($"{i}if (context.IsDirectObjectWrite)"); + } + + sb.AppendLine($"{i}{{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);"); + + // Get count and span/indexer 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} var nextDepth_{p.Name} = depth + 1;"); + 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 // List + { + sb.AppendLine($"{i} var list_{p.Name} = {a};"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);"); + sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); + sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < list_{p.Name}.Count; i_{p.Name}++)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];"); + } + + // Per-element write — same logic as EmitDirectObjectWrite but for each element + var e = $"elem_{p.Name}"; + // Elements in a collection can be null (runtime writes Null marker) + sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); + sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); + + if (p.ElementIsIId) + { + sb.AppendLine($"{i} if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + } + else + { + // Non-IId element: guard against ReferenceHandling.All (cursor alignment) + sb.AppendLine($"{i} if (context.ReferenceHandling == ReferenceHandlingMode.All)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({e}, typeof({elemType}), context, nextDepth_{p.Name} - 1);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});"); + sb.AppendLine($"{i} }}"); + } + + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i}}}"); + + // Fallback for non-direct mode + if (p.IsNullable) + sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + else + { + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + } + } + private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i) { switch (k) @@ -531,6 +679,29 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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; + } + private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch @@ -584,8 +755,24 @@ internal sealed class PropInfo /// Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter. public string? WriterClassName { 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; } + /// Collection type: "List", "Array", or null (unknown — fallback to runtime). + public string? CollectionKind { get; } + /// Full element type name for generated code (e.g. "SharedTag"). + public string? ElementFullTypeName { get; } + public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, - bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null) + bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, + PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, + string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null) { Name = n; TypeName = tn; @@ -595,6 +782,12 @@ internal sealed class PropInfo HasGeneratedWriter = hasGeneratedWriter; IsIId = isIId; WriterClassName = writerClassName; + ElementKind = elementKind; + ElementHasGeneratedWriter = elementHasGenWriter; + ElementIsIId = elementIsIId; + ElementWriterClassName = elementWriterClassName; + CollectionKind = collectionKind; + ElementFullTypeName = elementFullTypeName; // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase int flags = 0; if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit