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 TestIterations = 10;
#else
private static int WarmupIterations = 3000;
private static int WarmupIterations = 5000;
private static int TestIterations = 1000;
//private static int WarmupIterations = 5000;

View File

@ -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<T>");
// 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<T>");
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<SerializableClassInfo?> 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}:;");
}
/// <summary>
@ -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();
}
/// <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
private static bool IsNullableVT(ITypeSymbol t) =>

View File

@ -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;

View File

@ -223,10 +223,11 @@ public static partial class AcBinarySerializer
/// <summary>
/// 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.
/// </summary>
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);
}

View File

@ -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;

View File

@ -13,7 +13,7 @@ public readonly struct BinaryPropertyFilterContext
private readonly object? _instance;
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;
DeclaringType = declaringType;