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.
This commit is contained in:
Loretta 2026-02-17 21:07:19 +01:00
parent 418d9f839a
commit d40e40a45a
6 changed files with 69 additions and 32 deletions

View File

@ -49,7 +49,7 @@ public static class Program
private static int WarmupIterations = 5; private static int WarmupIterations = 5;
private static int TestIterations = 10; private static int TestIterations = 10;
#else #else
private static int WarmupIterations = 3000; private static int WarmupIterations = 5000;
private static int TestIterations = 1000; private static int TestIterations = 1000;
//private static int WarmupIterations = 5000; //private static int WarmupIterations = 5000;

View File

@ -36,10 +36,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol)) if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol))
return null; 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 var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
? string.Empty ? string.Empty
: typeSymbol.ContainingNamespace.ToDisplayString(); : typeSymbol.ContainingNamespace.ToDisplayString();
@ -94,22 +90,20 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
hasGenWriter = resolvedType.GetAttributes().Any(a => hasGenWriter = resolvedType.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName); 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) if (hasGenWriter)
{ {
propTypeIsIId = resolvedType.AllInterfaces.Any(i => propTypeIsIId = resolvedType.AllInterfaces.Any(i =>
i.IsGenericType && i.IsGenericType &&
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>"); i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
// Writer class: {Namespace}.{TypeName}_GeneratedWriter // Writer class: {Namespace}.{FlatName}_GeneratedWriter
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
? string.Empty ? string.Empty
: resolvedType.ContainingNamespace.ToDisplayString(); : resolvedType.ContainingNamespace.ToDisplayString();
writerClassName = string.IsNullOrEmpty(ns) writerClassName = string.IsNullOrEmpty(ns)
? $"{resolvedType.Name}_GeneratedWriter" ? $"{flatName}_GeneratedWriter"
: $"{ns}.{resolvedType.Name}_GeneratedWriter"; : $"{ns}.{flatName}_GeneratedWriter";
} }
} }
@ -156,18 +150,17 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
? namedElem.OriginalDefinition : elemType; ? namedElem.OriginalDefinition : elemType;
elemHasGenWriter = resolvedElem.GetAttributes().Any(a => elemHasGenWriter = resolvedElem.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName); a.AttributeClass?.ToDisplayString() == AttributeName);
if (elemHasGenWriter && resolvedElem is INamedTypeSymbol nte && nte.ContainingType != null)
elemHasGenWriter = false;
if (elemHasGenWriter) if (elemHasGenWriter)
{ {
elemIsIId = resolvedElem.AllInterfaces.Any(ifc => elemIsIId = resolvedElem.AllInterfaces.Any(ifc =>
ifc.IsGenericType && ifc.IsGenericType &&
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>"); ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString(); ? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
elemWriterClassName = string.IsNullOrEmpty(ens) elemWriterClassName = string.IsNullOrEmpty(ens)
? $"{resolvedElem.Name}_GeneratedWriter" ? $"{elemFlatName}_GeneratedWriter"
: $"{ens}.{resolvedElem.Name}_GeneratedWriter"; : $"{ens}.{elemFlatName}_GeneratedWriter";
} }
} }
} }
@ -200,7 +193,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); 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<SerializableClassInfo?> classes, SourceProductionContext context) private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
@ -240,7 +233,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
foreach (var p in ci.Properties) foreach (var p in ci.Properties)
{ {
sb.AppendLine(); sb.AppendLine();
EmitProp(sb, p, " "); EmitProp(sb, p, " ", ci.FullTypeName);
} }
sb.AppendLine(" }"); sb.AppendLine(" }");
@ -248,10 +241,31 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
return sb.ToString(); 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}"; 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) // Nullable value types always use markered path (need Null marker)
if (IsNullableVTKind(p.TypeKind)) if (IsNullableVTKind(p.TypeKind))
{ {
@ -260,14 +274,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + " "); EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + " ");
sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);");
return; sb.AppendLine($"{i}skip_{p.Name}:;");
}
// 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);
return; return;
} }
@ -310,6 +317,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i);
break; break;
} }
sb.AppendLine($"{i}skip_{p.Name}:;");
} }
/// <summary> /// <summary>
@ -653,12 +662,37 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" internal static void Register()"); sb.AppendLine(" internal static void Register()");
sb.AppendLine(" {"); sb.AppendLine(" {");
foreach (var ci in classes) 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(" }");
sb.AppendLine("}"); sb.AppendLine("}");
return sb.ToString(); return sb.ToString();
} }
/// <summary>
/// Builds a flat class name for nested types: Outer_Inner_Leaf.
/// For top-level types returns the simple name unchanged.
/// </summary>
private static string BuildFlatName(INamedTypeSymbol typeSymbol)
{
if (typeSymbol.ContainingType == null)
return typeSymbol.Name;
var parts = new List<string>();
var current = typeSymbol;
while (current != null)
{
parts.Add(current.Name);
current = current.ContainingType;
}
parts.Reverse();
return string.Join("_", parts);
}
#region Type analysis #region Type analysis
private static bool IsNullableVT(ITypeSymbol t) => private static bool IsNullableVT(ITypeSymbol t) =>

View File

@ -19,7 +19,8 @@ public abstract class AcSerializerOptions
set => _referenceHandling = value; set => _referenceHandling = value;
} }
private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.OnlyId; private ReferenceHandlingMode _referenceHandling = ReferenceHandlingMode.All;
private readonly byte _maxDepth = byte.MaxValue; private readonly byte _maxDepth = byte.MaxValue;
private readonly bool _throwOnCircularReference = true; private readonly bool _throwOnCircularReference = true;
private readonly PropertyMapperDelegate? _propertyMapper; private readonly PropertyMapperDelegate? _propertyMapper;

View File

@ -223,10 +223,11 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// True when generated writers can bypass WriteObject entirely and write markers + properties inline. /// 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. /// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
/// </summary> /// </summary>
public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter; public bool IsDirectObjectWrite => !UseMetadata;
//public bool FastWire { get; private set; } //public bool FastWire { get; private set; }
public byte MinStringInternLength => Options.MinStringInternLength; public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength;
@ -847,6 +848,7 @@ public static partial class AcBinarySerializer
property.Name, property.Name,
property.PropertyType, property.PropertyType,
property.DynamicGetter); property.DynamicGetter);
return PropertyFilter(context); return PropertyFilter(context);
} }

View File

@ -1089,7 +1089,7 @@ public static partial class AcBinarySerializer
if (context.UseGeneratedCode) if (context.UseGeneratedCode)
{ {
var generatedWriter = wrapper.GeneratedWriter; var generatedWriter = wrapper.GeneratedWriter;
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata) if (generatedWriter != null && !context.UseMetadata)
{ {
generatedWriter.WriteProperties(value, context, nextDepth); generatedWriter.WriteProperties(value, context, nextDepth);
return; return;

View File

@ -13,7 +13,7 @@ public readonly struct BinaryPropertyFilterContext
private readonly object? _instance; private readonly object? _instance;
private readonly Func<object, object?>? _valueGetter; private readonly Func<object, object?>? _valueGetter;
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter) public BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
{ {
_instance = instance; _instance = instance;
DeclaringType = declaringType; DeclaringType = declaringType;