From d40e40a45a2c677321c385aea08abd634f6e1df3 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 17 Feb 2026 21:07:19 +0100 Subject: [PATCH] Support nested types in source gen; improve prop filtering - Generate writers for nested types using flat class names (Outer_Inner_Leaf) to ensure uniqueness and validity. - Apply property filters in generated code for all non-markerless properties, matching runtime behavior. - Emit skip labels for each property in generated code for correct control flow. - Remove PropertyFilter check from IsDirectObjectWrite; generated code now handles filtering. - Change default ReferenceHandlingMode to All. - Make BinaryPropertyFilterContext constructor public. - Increase release warmup iterations in Program.cs from 3000 to 5000. --- AyCode.Core.Serializers.Console/Program.cs | 2 +- .../AcBinarySourceGenerator.cs | 86 +++++++++++++------ .../Serializers/AcSerializerOptions.cs | 3 +- ...rySerializer.BinarySerializationContext.cs | 6 +- .../Binaries/AcBinarySerializer.cs | 2 +- .../Binaries/BinaryPropertyFilterContext.cs | 2 +- 6 files changed, 69 insertions(+), 32 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 92129ad..b45d6a9 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -49,7 +49,7 @@ public static class Program private static int WarmupIterations = 5; private static int TestIterations = 10; #else - private static int WarmupIterations = 3000; + private static int WarmupIterations = 5000; private static int TestIterations = 1000; //private static int WarmupIterations = 5000; diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 188dd2c..dcae6fe 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -36,10 +36,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) return null; - // Skip nested types — generated writer class can't be placed inside containing type - if (typeSymbol.ContainingType != null) - return null; - var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : typeSymbol.ContainingNamespace.ToDisplayString(); @@ -94,22 +90,20 @@ public class AcBinarySourceGenerator : IIncrementalGenerator hasGenWriter = resolvedType.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == AttributeName); - // Skip nested types (we don't generate writers for them) - if (hasGenWriter && resolvedType is INamedTypeSymbol nt && nt.ContainingType != null) - hasGenWriter = false; if (hasGenWriter) { propTypeIsIId = resolvedType.AllInterfaces.Any(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); - // Writer class: {Namespace}.{TypeName}_GeneratedWriter + // Writer class: {Namespace}.{FlatName}_GeneratedWriter + var flatName = BuildFlatName((INamedTypeSymbol)resolvedType); var ns = resolvedType.ContainingNamespace.IsGlobalNamespace ? string.Empty : resolvedType.ContainingNamespace.ToDisplayString(); writerClassName = string.IsNullOrEmpty(ns) - ? $"{resolvedType.Name}_GeneratedWriter" - : $"{ns}.{resolvedType.Name}_GeneratedWriter"; + ? $"{flatName}_GeneratedWriter" + : $"{ns}.{flatName}_GeneratedWriter"; } } @@ -156,18 +150,17 @@ public class AcBinarySourceGenerator : IIncrementalGenerator ? 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 elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem); var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); elemWriterClassName = string.IsNullOrEmpty(ens) - ? $"{resolvedElem.Name}_GeneratedWriter" - : $"{ens}.{resolvedElem.Name}_GeneratedWriter"; + ? $"{elemFlatName}_GeneratedWriter" + : $"{ens}.{elemFlatName}_GeneratedWriter"; } } } @@ -200,7 +193,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); - return new SerializableClassInfo(namespaceName, typeSymbol.Name, typeSymbol.ToDisplayString(), properties); + return new SerializableClassInfo(namespaceName, BuildFlatName(typeSymbol), typeSymbol.ToDisplayString(), properties); } private static void Execute(ImmutableArray classes, SourceProductionContext context) @@ -240,7 +233,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator foreach (var p in ci.Properties) { sb.AppendLine(); - EmitProp(sb, p, " "); + EmitProp(sb, p, " ", ci.FullTypeName); } sb.AppendLine(" }"); @@ -248,10 +241,31 @@ public class AcBinarySourceGenerator : IIncrementalGenerator return sb.ToString(); } - private static void EmitProp(StringBuilder sb, PropInfo p, string i) + private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName) { 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) + if (IsMarkerless(p.TypeKind)) + { + EmitMarkerless(sb, p.TypeKind, a, i); + return; + } + + // All non-markerless properties: emit PropertyFilter guard + // When filter returns false, write PropertySkip and skip the property write + 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)) { @@ -260,14 +274,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + " "); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);"); - return; - } - - // Markerless types: write raw value only, no type marker, no PropertySkip - // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode - if (IsMarkerless(p.TypeKind)) - { - EmitMarkerless(sb, p.TypeKind, a, i); + sb.AppendLine($"{i}skip_{p.Name}:;"); return; } @@ -310,6 +317,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); break; } + + sb.AppendLine($"{i}skip_{p.Name}:;"); } /// @@ -653,12 +662,37 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(" internal static void Register()"); sb.AppendLine(" {"); foreach (var ci in classes) - sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {ci.FullTypeName}_GeneratedWriter.Instance);"); + { + var writerRef = string.IsNullOrEmpty(ci.Namespace) + ? $"{ci.ClassName}_GeneratedWriter" + : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter"; + sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.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); + } + #region Type analysis private static bool IsNullableVT(ITypeSymbol t) => diff --git a/AyCode.Core/Serializers/AcSerializerOptions.cs b/AyCode.Core/Serializers/AcSerializerOptions.cs index 16adedb..e03a52a 100644 --- a/AyCode.Core/Serializers/AcSerializerOptions.cs +++ b/AyCode.Core/Serializers/AcSerializerOptions.cs @@ -19,7 +19,8 @@ public abstract class AcSerializerOptions set => _referenceHandling = value; } - private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId; + private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.All; + private readonly byte _maxDepth = byte.MaxValue; private readonly bool _throwOnCircularReference = true; private readonly PropertyMapperDelegate? _propertyMapper; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 422a2a6..fb79237 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -223,10 +223,11 @@ public static partial class AcBinarySerializer /// /// True when generated writers can bypass WriteObject entirely and write markers + properties inline. - /// Requires: no UseMetadata (no inline metadata tracking), no PropertyFilter (no per-prop filtering). + /// Requires: no UseMetadata (no inline metadata tracking). + /// PropertyFilter is handled by generated code's per-property filter checks. /// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types. /// - public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter; + public bool IsDirectObjectWrite => !UseMetadata; //public bool FastWire { get; private set; } public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; @@ -847,6 +848,7 @@ public static partial class AcBinarySerializer property.Name, property.PropertyType, property.DynamicGetter); + return PropertyFilter(context); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 6f7d895..223ea11 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1089,7 +1089,7 @@ public static partial class AcBinarySerializer if (context.UseGeneratedCode) { var generatedWriter = wrapper.GeneratedWriter; - if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata) + if (generatedWriter != null && !context.UseMetadata) { generatedWriter.WriteProperties(value, context, nextDepth); return; diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs index 338a306..84d5414 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs @@ -13,7 +13,7 @@ public readonly struct BinaryPropertyFilterContext private readonly object? _instance; private readonly Func? _valueGetter; - internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func? valueGetter) + public BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func? valueGetter) { _instance = instance; DeclaringType = declaringType;