1426 lines
75 KiB
C#
1426 lines
75 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace AyCode.Core.Serializers.SourceGenerator;
|
|
|
|
/// <summary>
|
|
/// Generates IGeneratedBinaryWriter implementations for [AcBinarySerializable] types.
|
|
/// Also generates a ModuleInitializer that auto-registers all writers at startup.
|
|
/// </summary>
|
|
[Generator]
|
|
public class AcBinarySourceGenerator : IIncrementalGenerator
|
|
{
|
|
private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute";
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
var classDeclarations = context.SyntaxProvider
|
|
.ForAttributeWithMetadataName(
|
|
AttributeName,
|
|
predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax,
|
|
transform: static (ctx, _) => GetClassInfo(ctx))
|
|
.Where(static info => info != null);
|
|
|
|
context.RegisterSourceOutput(classDeclarations.Collect(),
|
|
static (spc, classes) => Execute(classes!, spc));
|
|
}
|
|
|
|
private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context)
|
|
{
|
|
if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol))
|
|
return null;
|
|
|
|
var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty
|
|
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
|
|
var properties = new List<PropInfo>();
|
|
|
|
// Read feature flags from [AcBinarySerializable] — disabled features eliminate
|
|
// corresponding code blocks from generated ScanObject/WriteProperties.
|
|
var enableIdTracking = true;
|
|
var enableRefHandling = true;
|
|
var enableInternString = true;
|
|
var enableMetadata = true;
|
|
var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (binarySerializableAttr != null)
|
|
{
|
|
if (binarySerializableAttr.ConstructorArguments.Length == 1)
|
|
{
|
|
// Single bool ctor: AcBinarySerializable(enableAllFeatures)
|
|
var all = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = all;
|
|
enableRefHandling = all;
|
|
enableInternString = all;
|
|
enableMetadata = all;
|
|
}
|
|
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
|
|
{
|
|
// Four bool ctor: (metadata, idTracking, refHandling, internString)
|
|
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
|
|
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
|
|
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
|
|
}
|
|
}
|
|
|
|
foreach (var member in typeSymbol.GetMembers())
|
|
{
|
|
if (member is IPropertySymbol p &&
|
|
p.DeclaredAccessibility == Accessibility.Public &&
|
|
p.GetMethod != null && p.SetMethod != null &&
|
|
!p.IsIndexer && !p.IsStatic)
|
|
{
|
|
var hasIgnore = p.GetAttributes().Any(a =>
|
|
{
|
|
var name = a.AttributeClass?.Name ?? "";
|
|
return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute";
|
|
});
|
|
if (hasIgnore) continue;
|
|
|
|
// String interning attribútum detektálás (null = no attr, true/false = explicit)
|
|
bool? stringInternAttr = null;
|
|
if (!enableInternString)
|
|
{
|
|
stringInternAttr = false;
|
|
}
|
|
else if (GetKind(p.Type) == PropertyTypeKind.String)
|
|
{
|
|
var attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute");
|
|
if (attr != null && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Kind == TypedConstantKind.Primitive)
|
|
{
|
|
stringInternAttr = (bool)attr.ConstructorArguments[0].Value!;
|
|
}
|
|
}
|
|
|
|
// For typeof(): strip trailing '?' from nullable reference types (typeof(T?) is invalid for ref types)
|
|
// Nullable value types (int?, Guid?) keep '?' because typeof(int?) == typeof(Nullable<int>) is valid
|
|
var typeDisplayName = p.Type.ToDisplayString();
|
|
var typeNameForTypeof = (p.Type.NullableAnnotation == NullableAnnotation.Annotated && !p.Type.IsValueType)
|
|
? typeDisplayName.TrimEnd('?')
|
|
: typeDisplayName;
|
|
|
|
// Direct object write detection for Complex property types:
|
|
// Check if the property type has [AcBinarySerializable] (→ has generated writer)
|
|
// and if it implements IId<T> (→ needs ref tracking in generated code)
|
|
var kind = GetKind(p.Type);
|
|
bool hasGenWriter = false;
|
|
bool propTypeIsIId = false;
|
|
bool propEnableMetadata = true;
|
|
string? writerClassName = null;
|
|
string? propIdTypeName = null;
|
|
int childTypeNameHash = 0;
|
|
int[]? childPropertyHashes = null;
|
|
if (kind == PropertyTypeKind.Complex)
|
|
{
|
|
// Resolve to the actual type symbol (strip nullable annotation for ref types)
|
|
// For SharedTag? → SharedTag. OriginalDefinition handles generic types.
|
|
var resolvedType = p.Type is INamedTypeSymbol namedPropType
|
|
? namedPropType.OriginalDefinition
|
|
: p.Type;
|
|
|
|
hasGenWriter = resolvedType.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
|
|
if (hasGenWriter)
|
|
{
|
|
// Read child type's EnableMetadataFeature
|
|
propEnableMetadata = ReadEnableMetadata(resolvedType);
|
|
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i =>
|
|
i.IsGenericType &&
|
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
propTypeIsIId = iidIface != null;
|
|
if (iidIface != null)
|
|
propIdTypeName = iidIface.TypeArguments[0].ToDisplayString();
|
|
|
|
// Writer class: {Namespace}.{FlatName}_GeneratedWriter
|
|
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
|
|
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty
|
|
: resolvedType.ContainingNamespace.ToDisplayString();
|
|
writerClassName = string.IsNullOrEmpty(ns)
|
|
? $"{flatName}_GeneratedWriter"
|
|
: $"{ns}.{flatName}_GeneratedWriter";
|
|
|
|
// UseMetadata: compute child type hash-es for inline metadata
|
|
childTypeNameHash = ComputeFnvHash(resolvedType.Name);
|
|
childPropertyHashes = ComputeChildPropertyHashes(resolvedType);
|
|
}
|
|
}
|
|
|
|
// Collection element type analysis for inline collection write
|
|
PropertyTypeKind elemKind = PropertyTypeKind.Unknown;
|
|
bool elemHasGenWriter = false;
|
|
bool elemIsIId = false;
|
|
bool elemEnableMetadata = true;
|
|
string? elemWriterClassName = null;
|
|
string? elemIdTypeName = null;
|
|
string? collKind = null;
|
|
string? elemFullTypeName = null;
|
|
int elementTypeNameHash = 0;
|
|
int[]? elementPropertyHashes = null;
|
|
if (kind == PropertyTypeKind.Collection)
|
|
{
|
|
var elemType = GetCollectionElementType(p.Type);
|
|
if (elemType != null)
|
|
{
|
|
elemKind = GetKind(elemType);
|
|
elemFullTypeName = elemType.ToDisplayString();
|
|
|
|
// Detect collection shape for inline write
|
|
if (p.Type is IArrayTypeSymbol)
|
|
collKind = "Array";
|
|
else if (p.Type is INamedTypeSymbol collNamedType)
|
|
{
|
|
var origDef = collNamedType.OriginalDefinition.ToDisplayString();
|
|
collKind = origDef switch
|
|
{
|
|
"System.Collections.Generic.List<T>" => "List",
|
|
"System.Collections.Generic.IList<T>" => "List", // has Count + indexer
|
|
"System.Collections.Generic.IReadOnlyList<T>" => "List", // has Count + indexer
|
|
"System.Collections.Generic.HashSet<T>" => "Counted", // has Count, no indexer
|
|
"System.Collections.Generic.Queue<T>" => "Counted",
|
|
"System.Collections.Generic.ICollection<T>" => "Counted",
|
|
"System.Collections.Generic.IReadOnlyCollection<T>" => "Counted",
|
|
"System.Collections.Generic.SortedSet<T>" => "Counted",
|
|
"System.Collections.Generic.LinkedList<T>" => "Counted",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// Read element type's EnableMetadataFeature
|
|
elemEnableMetadata = ReadEnableMetadata(resolvedElem);
|
|
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc =>
|
|
ifc.IsGenericType &&
|
|
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
elemIsIId = elemIidIface != null;
|
|
if (elemIidIface != null)
|
|
elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString();
|
|
|
|
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
|
|
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
|
elemWriterClassName = string.IsNullOrEmpty(ens)
|
|
? $"{elemFlatName}_GeneratedWriter"
|
|
: $"{ens}.{elemFlatName}_GeneratedWriter";
|
|
|
|
// UseMetadata: compute element type hash-es for inline metadata
|
|
elementTypeNameHash = ComputeFnvHash(resolvedElem.Name);
|
|
elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
properties.Add(new PropInfo(
|
|
p.Name,
|
|
typeDisplayName,
|
|
typeNameForTypeof,
|
|
kind,
|
|
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
|
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
|
|
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
|
|
childTypeNameHash, childPropertyHashes,
|
|
elementTypeNameHash, elementPropertyHashes,
|
|
propEnableMetadata, elemEnableMetadata));
|
|
}
|
|
}
|
|
|
|
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
|
|
// If EnableIdTrackingFeature == false, skip IId detection entirely → isIId = false
|
|
var isIId = false;
|
|
string? idTypeName = null;
|
|
if (enableIdTracking)
|
|
{
|
|
var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i =>
|
|
i.IsGenericType &&
|
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
if (iidInterface != null)
|
|
{
|
|
isIId = true;
|
|
idTypeName = iidInterface.TypeArguments[0].ToDisplayString();
|
|
}
|
|
}
|
|
|
|
properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
|
|
|
var className = BuildFlatName(typeSymbol);
|
|
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
|
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
|
|
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata);
|
|
}
|
|
|
|
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
|
|
{
|
|
if (classes.IsDefaultOrEmpty) return;
|
|
var valid = classes.Where(c => c != null).Cast<SerializableClassInfo>().ToList();
|
|
if (valid.Count == 0) return;
|
|
|
|
foreach (var ci in valid)
|
|
context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8));
|
|
|
|
context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8));
|
|
}
|
|
|
|
private static string GenWriter(SerializableClassInfo ci)
|
|
{
|
|
var sb = new StringBuilder(4096);
|
|
sb.AppendLine("// <auto-generated/>");
|
|
sb.AppendLine("#nullable enable");
|
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
|
|
// ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan
|
|
sb.AppendLine("using AyCode.Core.Serializers;");
|
|
sb.AppendLine();
|
|
if (!string.IsNullOrEmpty(ci.Namespace))
|
|
sb.AppendLine($"namespace {ci.Namespace};");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();");
|
|
sb.AppendLine($" internal static readonly int s_metadataSlot = AcBinarySerializer.AllocateMetadataSlot();");
|
|
if (ci.IsIId || ci.EnableRefHandling)
|
|
sb.AppendLine($" internal static readonly int s_trackingSlot = AcBinarySerializer.AllocateTrackingSlot();");
|
|
sb.AppendLine($" internal static readonly int s_typeNameHash = {ci.TypeNameHash};");
|
|
sb.Append( $" internal static readonly int[] s_propertyHashes = new int[] {{ ");
|
|
sb.Append(string.Join(", ", ci.PropertyNameHashes));
|
|
sb.AppendLine(" };");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" public void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
|
|
|
|
foreach (var p in ci.Properties)
|
|
{
|
|
sb.AppendLine();
|
|
EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata);
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
|
|
// ScanObject — full scan pass (null/depth + self ref tracking + property scan)
|
|
GenScanProperties(sb, ci);
|
|
|
|
sb.AppendLine();
|
|
|
|
// ScanForDuplicates — instance method on IGeneratedBinaryWriter, called from Serialize
|
|
sb.AppendLine(" public void ScanForDuplicates<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context)");
|
|
sb.AppendLine(" where TOutput : struct, IBinaryOutputBase");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" if (!context.HasCaching) return;");
|
|
sb.AppendLine(" ScanObject(value, context, 0);");
|
|
sb.AppendLine(" context.SortWritePlan();");
|
|
sb.AppendLine(" }");
|
|
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the ScanObject method — full scan pass entry point for this type.
|
|
/// Includes: null/depth check, self ref tracking (IId or All mode), property scan.
|
|
/// Only emits code for reference properties (strings + complex types) — primitives are skipped.
|
|
/// </summary>
|
|
private static void GenScanProperties(StringBuilder sb, SerializableClassInfo ci)
|
|
{
|
|
sb.AppendLine(" public void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
|
|
sb.AppendLine(" {");
|
|
|
|
// Null/depth guard — matches runtime ScanValue entry
|
|
sb.AppendLine(" if (value == null || depth > context.MaxDepth) return;");
|
|
sb.AppendLine();
|
|
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
|
|
|
|
// Self ref tracking — matches runtime ScanValue UseTypeReferenceHandling block
|
|
// Only emitted when the corresponding feature flag is enabled.
|
|
if (ci.IsIId)
|
|
{
|
|
// IId type: track when ReferenceHandling != None
|
|
var trackMethod = ci.IdTypeName switch
|
|
{
|
|
"int" => "ScanTrackObjectInt32",
|
|
"long" => "ScanTrackObjectInt64",
|
|
"System.Guid" => "ScanTrackObjectGuid",
|
|
_ => "ScanTrackObjectInt32"
|
|
};
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine($" if (!context.{trackMethod}(s_trackingSlot, obj.Id))");
|
|
sb.AppendLine(" return;");
|
|
sb.AppendLine(" }");
|
|
}
|
|
else if (ci.EnableRefHandling)
|
|
{
|
|
// Non-IId type: track when ReferenceHandling == All
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" if (!context.ScanTrackObjectInt32(s_trackingSlot, System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value)))");
|
|
sb.AppendLine(" return;");
|
|
sb.AppendLine(" }");
|
|
}
|
|
|
|
// Collect scannable properties
|
|
var scanProps = ci.Properties.Where(p =>
|
|
p.TypeKind == PropertyTypeKind.String ||
|
|
p.TypeKind == PropertyTypeKind.Complex ||
|
|
p.TypeKind == PropertyTypeKind.Collection).ToList();
|
|
|
|
// Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed
|
|
var hasStringScan = scanProps.Any(p =>
|
|
(p.TypeKind == PropertyTypeKind.String && p.InterningFlags != 0) ||
|
|
(p.TypeKind == PropertyTypeKind.Collection && p.ElementKind == PropertyTypeKind.String && p.InterningFlags != 0));
|
|
|
|
if (hasStringScan)
|
|
{
|
|
// Hoist the shift once — per-property InterningFlags check uses internBit directly.
|
|
// Cannot combine flags (OR) because different properties may have different flags
|
|
// and Attribute mode must NOT scan All-only properties.
|
|
sb.AppendLine();
|
|
sb.AppendLine(" var internBit = 1 << (int)context.Options.UseStringInterning;");
|
|
sb.AppendLine(" int minIntern = 0, maxIntern = 0;");
|
|
sb.AppendLine(" if (internBit > 1) { minIntern = context.MinStringInternLength; maxIntern = context.MaxStringInternLength; }");
|
|
}
|
|
|
|
var hasAnyScanProp = false;
|
|
foreach (var p in scanProps)
|
|
{
|
|
sb.AppendLine();
|
|
hasAnyScanProp = true;
|
|
EmitScanProp(sb, p, " ", ci.FullTypeName);
|
|
}
|
|
|
|
if (!hasAnyScanProp)
|
|
{
|
|
sb.AppendLine(" // No reference properties to scan");
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
}
|
|
|
|
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata)
|
|
{
|
|
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)
|
|
// When EnableMetadataFeature=false: always markerless (no UseMetadata branch needed)
|
|
// When EnableMetadataFeature=true: UseMetadata=true uses markered path (EmitSkip)
|
|
if (IsMarkerless(p.TypeKind))
|
|
{
|
|
if (!enableMetadata)
|
|
{
|
|
// Per-type metadata disabled — always markerless, no branch
|
|
EmitMarkerless(sb, p.TypeKind, a, i);
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}if (context.UseMetadata)");
|
|
sb.AppendLine($"{i}{{");
|
|
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " ");
|
|
sb.AppendLine($"{i}}}");
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i}{{");
|
|
EmitMarkerless(sb, p.TypeKind, a, i + " ");
|
|
sb.AppendLine($"{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))
|
|
{
|
|
sb.AppendLine($"{i}if ({a}.HasValue)");
|
|
sb.AppendLine($"{i}{{");
|
|
EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + " ");
|
|
sb.AppendLine($"{i}}}");
|
|
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}skip_{p.Name}:;");
|
|
return;
|
|
}
|
|
|
|
// Non-markerless types: write WITH type marker byte (markered path)
|
|
switch (p.TypeKind)
|
|
{
|
|
case PropertyTypeKind.String:
|
|
if (p.InterningFlags == 0)
|
|
sb.AppendLine($"{i}context.StringInternEligible = false;");
|
|
else
|
|
sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;");
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);");
|
|
break;
|
|
case PropertyTypeKind.Complex:
|
|
// Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely
|
|
// when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise.
|
|
if (p.HasGeneratedWriter)
|
|
EmitDirectObjectWrite(sb, p, a, i);
|
|
else if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
}
|
|
else
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
break;
|
|
case PropertyTypeKind.Collection:
|
|
// Direct collection write for List<T>/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);");
|
|
}
|
|
else
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
break;
|
|
default:
|
|
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i);
|
|
break;
|
|
}
|
|
|
|
sb.AppendLine($"{i}skip_{p.Name}:;");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true for property types that use markerless serialization in FastMode.
|
|
/// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults.
|
|
/// </summary>
|
|
private static bool IsMarkerless(PropertyTypeKind k) => k switch
|
|
{
|
|
PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or
|
|
PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or
|
|
PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or
|
|
PropertyTypeKind.DateTime or PropertyTypeKind.Guid or
|
|
PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset or
|
|
PropertyTypeKind.Boolean or PropertyTypeKind.Enum => true,
|
|
_ => false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Emits raw value only — no type marker, no PropertySkip.
|
|
/// Matches runtime WritePropertyMarkerless exactly.
|
|
/// </summary>
|
|
private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i)
|
|
{
|
|
switch (k)
|
|
{
|
|
case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteVarInt({a});"); break;
|
|
case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteVarLong({a});"); break;
|
|
case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break;
|
|
case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break;
|
|
case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteGuidBits({a});"); break;
|
|
case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte({a});"); break;
|
|
case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteVarUInt({a});"); break;
|
|
case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteVarULong({a});"); break;
|
|
case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break;
|
|
case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break;
|
|
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? (byte)1 : (byte)0);"); break;
|
|
case PropertyTypeKind.Enum: sb.AppendLine($"{i}context.WriteVarInt((int){a});"); break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
|
|
#region Scan Pass Code Generation
|
|
|
|
/// <summary>
|
|
/// Emits scan pass code for a single property.
|
|
/// String: interning check + ScanInternString.
|
|
/// Complex (SGen): ref tracking via slot IdentityMap + recursive ScanProperties.
|
|
/// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup).
|
|
/// Collection: iterate elements with same patterns.
|
|
/// </summary>
|
|
private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
|
|
{
|
|
var a = $"obj.{p.Name}";
|
|
|
|
// PropertyFilter: must match write pass — if filter skips property, scan must skip too
|
|
// Only for non-markerless properties (matching EmitProp behavior)
|
|
if (!IsMarkerless(p.TypeKind))
|
|
{
|
|
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} goto scanskip_{p.Name};");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
|
|
switch (p.TypeKind)
|
|
{
|
|
case PropertyTypeKind.String:
|
|
EmitScanString(sb, p, a, i);
|
|
break;
|
|
|
|
case PropertyTypeKind.Complex:
|
|
if (p.HasGeneratedWriter)
|
|
EmitScanComplexSGen(sb, p, a, i);
|
|
else
|
|
EmitScanComplexRuntime(sb, p, a, i);
|
|
break;
|
|
|
|
case PropertyTypeKind.Collection:
|
|
EmitScanCollection(sb, p, a, i);
|
|
break;
|
|
}
|
|
|
|
if (!IsMarkerless(p.TypeKind))
|
|
sb.AppendLine($"{i}scanskip_{p.Name}:;");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits scan pass code for a string property: interning flags check + ScanInternString.
|
|
/// </summary>
|
|
private static void EmitScanString(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
if (p.InterningFlags == 0)
|
|
{
|
|
// Never interned (explicit [AcStringIntern(false)] or no flags) — skip entirely
|
|
return;
|
|
}
|
|
|
|
// Per-property InterningFlags check with hoisted internBit (context.Options read once)
|
|
sb.AppendLine($"{i}if (({p.InterningFlags} & internBit) != 0)");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} var str_{p.Name} = {a};");
|
|
sb.AppendLine($"{i} if (str_{p.Name} != null)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var slen_{p.Name} = str_{p.Name}.Length;");
|
|
sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
|
|
sb.AppendLine($"{i} context.ScanInternString(str_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits scan pass code for a Complex property with SGen writer.
|
|
/// No parent-side ref tracking — child ScanObject handles its own (ScanTrackObjectXxx).
|
|
/// </summary>
|
|
private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.WriterClassName;
|
|
var childVar = $"sc_{p.Name}";
|
|
|
|
// Null check only — ScanObject handles depth + ref tracking internally
|
|
sb.AppendLine($"{i}var {childVar} = {a};");
|
|
sb.AppendLine($"{i}if ({childVar} != null)");
|
|
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
|
|
/// </summary>
|
|
private static void EmitScanComplexRuntime(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var childVar = $"sc_{p.Name}";
|
|
sb.AppendLine($"{i}var {childVar} = {a};");
|
|
sb.AppendLine($"{i}if ({childVar} != null)");
|
|
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context, depth + 1);");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits scan pass code for a Collection property.
|
|
/// Handles string collections (interning) and complex element collections (SGen or runtime fallback).
|
|
/// </summary>
|
|
private static void EmitScanCollection(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
// String element collection
|
|
if (p.ElementKind == PropertyTypeKind.String)
|
|
{
|
|
if (p.InterningFlags == 0) return; // never interned
|
|
|
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
|
sb.AppendLine($"{i}if (scol_{p.Name} != null && ({p.InterningFlags} & internBit) != 0)");
|
|
sb.AppendLine($"{i}{{");
|
|
|
|
if (p.CollectionKind == "Array")
|
|
{
|
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
|
}
|
|
else if (p.CollectionKind == "List")
|
|
{
|
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})");
|
|
sb.AppendLine($"{i} {{");
|
|
}
|
|
|
|
sb.AppendLine($"{i} if (se_{p.Name} != null)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var slen_{p.Name} = se_{p.Name}.Length;");
|
|
sb.AppendLine($"{i} if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
|
|
sb.AppendLine($"{i} context.ScanInternString(se_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
return;
|
|
}
|
|
|
|
// Complex element collection with SGen writer
|
|
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null)
|
|
{
|
|
var writer = p.ElementWriterClassName;
|
|
|
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
|
sb.AppendLine($"{i}if (scol_{p.Name} != null)");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;");
|
|
|
|
if (p.CollectionKind == "Array")
|
|
{
|
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
|
}
|
|
else if (p.CollectionKind == "List")
|
|
{
|
|
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
|
|
}
|
|
else // Counted (foreach)
|
|
{
|
|
sb.AppendLine($"{i} foreach (var se_{p.Name} in scol_{p.Name})");
|
|
sb.AppendLine($"{i} {{");
|
|
}
|
|
|
|
var e = $"se_{p.Name}";
|
|
// Null check only — ScanObject handles depth + ref tracking internally
|
|
sb.AppendLine($"{i} if ({e} == null) continue;");
|
|
sb.AppendLine($"{i} {writer}.Instance.ScanObject({e}, context, snd_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
return;
|
|
}
|
|
|
|
// Complex element collection without SGen writer — runtime fallback
|
|
if (p.ElementKind == PropertyTypeKind.Complex)
|
|
{
|
|
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
|
|
sb.AppendLine($"{i}if (scol_{p.Name} != null)");
|
|
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated(scol_{p.Name}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
return;
|
|
}
|
|
|
|
// Primitive element collection — no scanning needed
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Emits inline object write for a Complex property that has a generated writer.
|
|
/// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties.
|
|
/// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
|
|
/// Non-IId types: guard ReferenceHandling == All (tracked only in All mode).
|
|
/// No fallback to WriteObjectGenerated — handles both UseMetadata=true and false inline.
|
|
/// </summary>
|
|
private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.WriterClassName;
|
|
var nextDepth = "depth + 1";
|
|
|
|
if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i}{{");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}{{");
|
|
}
|
|
|
|
// MaxDepth check — matches WriteObjectGenerated
|
|
sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
|
|
if (!p.ChildEnableMetadata)
|
|
{
|
|
// Child type has EnableMetadataFeature=false — no metadata, always Object marker
|
|
// Inline ref tracking still needed for IId/All mode
|
|
var refGuard = p.IsIId
|
|
? "context.ReferenceHandling != ReferenceHandlingMode.None"
|
|
: "context.ReferenceHandling == ReferenceHandlingMode.All";
|
|
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{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)pe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
|
|
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({a}, context, {nextDepth});");
|
|
sb.AppendLine($"{i} }}");
|
|
}
|
|
else
|
|
{
|
|
// UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed)
|
|
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);");
|
|
|
|
// Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior.
|
|
var refGuard = p.IsIId
|
|
? "context.ReferenceHandling != ReferenceHandlingMode.None"
|
|
: "context.ReferenceHandling == ReferenceHandlingMode.All";
|
|
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
// RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
|
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
|
|
EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " ");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
// No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
|
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
|
EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " ");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
|
|
sb.AppendLine($"{i} }}");
|
|
}
|
|
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits inline metadata write: typeNameHash + (if first) propCount + property hashes.
|
|
/// All values are compile-time constants.
|
|
/// </summary>
|
|
private static void EmitInlineMetadata(StringBuilder sb, int typeNameHash, int[] propertyHashes, string isFirstVar, string i)
|
|
{
|
|
sb.AppendLine($"{i}context.WriteRaw({typeNameHash});");
|
|
sb.AppendLine($"{i}if ({isFirstVar})");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} context.WriteVarUInt({(uint)propertyHashes.Length});");
|
|
foreach (var hash in propertyHashes)
|
|
sb.AppendLine($"{i} context.WriteRaw({hash});");
|
|
sb.AppendLine($"{i}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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]...
|
|
/// Handles both UseMetadata=true and false inline — no fallback to WriteValueGenerated.
|
|
/// </summary>
|
|
private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.ElementWriterClassName;
|
|
|
|
if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i}{{");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}{{");
|
|
}
|
|
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);");
|
|
|
|
// Get count and iteration 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 if (p.CollectionKind == "Counted")
|
|
{
|
|
sb.AppendLine($"{i} var col_{p.Name} = {a};");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
|
|
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
|
|
sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})");
|
|
sb.AppendLine($"{i} {{");
|
|
}
|
|
else // List, IList<T>, IReadOnlyList<T> — Count + indexer
|
|
{
|
|
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
|
|
var e = $"elem_{p.Name}";
|
|
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; }}");
|
|
|
|
// Inline ref tracking
|
|
var elemRefGuard = p.ElementIsIId
|
|
? "context.ReferenceHandling != ReferenceHandlingMode.None"
|
|
: "context.ReferenceHandling == ReferenceHandlingMode.All";
|
|
|
|
if (!p.ElementEnableMetadata)
|
|
{
|
|
// Element type has EnableMetadataFeature=false — no metadata, always Object marker
|
|
sb.AppendLine($"{i} if ({elemRefGuard} && 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
|
|
{
|
|
// UseMetadata: register element type for first/repeated tracking
|
|
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);");
|
|
|
|
sb.AppendLine($"{i} if ({elemRefGuard} && 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} {{");
|
|
// RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
|
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
|
|
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
|
|
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} }}");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
// No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
|
|
sb.AppendLine($"{i} if (context.UseMetadata)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
|
EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
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}}}");
|
|
}
|
|
|
|
private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i)
|
|
{
|
|
switch (k)
|
|
{
|
|
case PropertyTypeKind.Int32:
|
|
{
|
|
// Mirrors runtime WritePropertyOrSkip → WriteInt32 (TinyInt optimization)
|
|
var s32 = a.Replace(".", "_");
|
|
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt({a}, out var ti_{s32})) context.WriteByte(ti_{s32});");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}");
|
|
break;
|
|
}
|
|
case PropertyTypeKind.Int64:
|
|
{
|
|
// Mirrors runtime WritePropertyOrSkip → WriteInt64 → WriteInt32 (int range + TinyInt)
|
|
var s64 = a.Replace(".", "_");
|
|
sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else if ({a} >= int.MinValue && {a} <= int.MaxValue)");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} var iv_{s64} = (int){a};");
|
|
sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(iv_{s64}, out var ti_{s64})) context.WriteByte(ti_{s64});");
|
|
sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{s64}); }}");
|
|
sb.AppendLine($"{i}}}");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}");
|
|
break;
|
|
}
|
|
case PropertyTypeKind.Boolean:
|
|
sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);");
|
|
break;
|
|
case PropertyTypeKind.Double:
|
|
sb.AppendLine($"{i}if ({a} == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.Single:
|
|
sb.AppendLine($"{i}if ({a} == 0f) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.Decimal:
|
|
sb.AppendLine($"{i}if ({a} == 0m) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.DateTime:
|
|
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});");
|
|
break;
|
|
case PropertyTypeKind.Guid:
|
|
sb.AppendLine($"{i}if ({a} == System.Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.Byte:
|
|
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.Int16:
|
|
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.UInt16:
|
|
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.UInt32:
|
|
sb.AppendLine($"{i}if ({a} == 0U) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.UInt64:
|
|
sb.AppendLine($"{i}if ({a} == 0UL) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a}); }}");
|
|
break;
|
|
case PropertyTypeKind.Enum:
|
|
var s = a.Replace(".", "_");
|
|
sb.AppendLine($"{i}var ev_{s} = (int){a};");
|
|
sb.AppendLine($"{i}if (ev_{s} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt(ev_{s}, out var te_{s})) {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(te_{s}); }}");
|
|
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{s}); }}");
|
|
break;
|
|
case PropertyTypeKind.TimeSpan:
|
|
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);");
|
|
break;
|
|
case PropertyTypeKind.DateTimeOffset:
|
|
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});");
|
|
break;
|
|
default:
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({typeName}), context, depth);");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i)
|
|
{
|
|
switch (k)
|
|
{
|
|
case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a});"); break;
|
|
case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a});"); break;
|
|
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);"); break;
|
|
case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});"); break;
|
|
case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break;
|
|
case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break;
|
|
case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break;
|
|
default: EmitSkip(sb, k, a, typeName, i); break;
|
|
}
|
|
}
|
|
|
|
private static string GenInit(List<SerializableClassInfo> classes)
|
|
{
|
|
var sb = new StringBuilder(512);
|
|
sb.AppendLine("// <auto-generated/>");
|
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
|
|
sb.AppendLine();
|
|
sb.AppendLine("namespace AyCode.Core.Serializers.Generated;");
|
|
sb.AppendLine();
|
|
sb.AppendLine("internal static class AcBinaryGeneratedWritersInit");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine(" [ModuleInitializer]");
|
|
sb.AppendLine(" internal static void Register()");
|
|
sb.AppendLine(" {");
|
|
foreach (var ci in classes)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute.
|
|
/// Returns true (default) if no attribute or enableAllFeatures=true.
|
|
/// </summary>
|
|
private static bool ReadEnableMetadata(ITypeSymbol type)
|
|
{
|
|
var attr = type.GetAttributes().FirstOrDefault(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (attr == null) return true;
|
|
if (attr.ConstructorArguments.Length == 1)
|
|
return (bool)attr.ConstructorArguments[0].Value!;
|
|
if (attr.ConstructorArguments.Length == 4)
|
|
return (bool)attr.ConstructorArguments[0].Value!;
|
|
return true;
|
|
}
|
|
|
|
#region FNV-1a Hash (compile-time)
|
|
|
|
private static int ComputeFnvHash(string value)
|
|
{
|
|
uint hash = 2166136261;
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
hash ^= value[i];
|
|
hash *= 16777619;
|
|
}
|
|
return (int)hash;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes FNV-1a hashes for all serializable properties of a child type.
|
|
/// Property filtering and ordering matches runtime TypeMetadataBase exactly:
|
|
/// public get+set, non-indexer, non-static, no ignore attributes, sorted alphabetically.
|
|
/// </summary>
|
|
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
|
|
{
|
|
var propNames = new List<string>();
|
|
foreach (var member in resolvedType.GetMembers())
|
|
{
|
|
if (member is IPropertySymbol cp &&
|
|
cp.DeclaredAccessibility == Accessibility.Public &&
|
|
cp.GetMethod != null && cp.SetMethod != null &&
|
|
!cp.IsIndexer && !cp.IsStatic)
|
|
{
|
|
var hasIgnore = cp.GetAttributes().Any(a =>
|
|
{
|
|
var name = a.AttributeClass?.Name ?? "";
|
|
return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute";
|
|
});
|
|
if (hasIgnore) continue;
|
|
propNames.Add(cp.Name);
|
|
}
|
|
}
|
|
|
|
propNames.Sort(StringComparer.Ordinal);
|
|
|
|
return propNames.Select(ComputeFnvHash).ToArray();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Type analysis
|
|
|
|
private static bool IsNullableVT(ITypeSymbol t) =>
|
|
t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T;
|
|
|
|
private static PropertyTypeKind GetKind(ITypeSymbol type)
|
|
{
|
|
if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
|
|
return GetKindCore(n.TypeArguments[0], true);
|
|
return GetKindCore(type, false);
|
|
}
|
|
|
|
private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable)
|
|
{
|
|
switch (type.SpecialType)
|
|
{
|
|
case SpecialType.System_String: return PropertyTypeKind.String;
|
|
case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32;
|
|
case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64;
|
|
case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16;
|
|
case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte;
|
|
case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16;
|
|
case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32;
|
|
case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64;
|
|
case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean;
|
|
case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single;
|
|
case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double;
|
|
case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal;
|
|
case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime;
|
|
default: break;
|
|
}
|
|
var fn = type.ToDisplayString();
|
|
if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid;
|
|
if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan;
|
|
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;
|
|
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;
|
|
return PropertyTypeKind.Unknown;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the element type T from List<T>, T[], IList<T>, IEnumerable<T>.
|
|
/// Returns null if the element type cannot be determined.
|
|
/// </summary>
|
|
private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type)
|
|
{
|
|
// T[] → element type
|
|
if (type is IArrayTypeSymbol arrayType)
|
|
return arrayType.ElementType;
|
|
|
|
// Generic collections: List<T>, IList<T>, ICollection<T>, IEnumerable<T>
|
|
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
|
|
{
|
|
// Direct: List<T>, HashSet<T>, 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
|
|
{
|
|
PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64,
|
|
PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte,
|
|
PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32,
|
|
PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean,
|
|
PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double,
|
|
PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime,
|
|
PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan,
|
|
PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum,
|
|
_ => PropertyTypeKind.Unknown
|
|
};
|
|
|
|
#endregion
|
|
}
|
|
|
|
internal sealed class SerializableClassInfo
|
|
{
|
|
public string Namespace { get; }
|
|
public string ClassName { get; }
|
|
public string FullTypeName { get; }
|
|
public List<PropInfo> Properties { get; }
|
|
/// <summary>True if this type implements IId<T></summary>
|
|
public bool IsIId { get; }
|
|
/// <summary>The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise</summary>
|
|
public string? IdTypeName { get; }
|
|
/// <summary>True if EnableRefHandlingFeature is enabled — controls non-IId All mode tracking code emission.</summary>
|
|
public bool EnableRefHandling { get; }
|
|
/// <summary>FNV-1a hash of ClassName (matches runtime SourceType.Name hash)</summary>
|
|
public int TypeNameHash { get; }
|
|
/// <summary>FNV-1a hash of each property name, in property order</summary>
|
|
public int[] PropertyNameHashes { get; }
|
|
/// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
|
|
public bool EnableMetadata { get; }
|
|
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata)
|
|
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; }
|
|
}
|
|
|
|
internal sealed class PropInfo
|
|
{
|
|
public string Name { get; }
|
|
public string TypeName { get; }
|
|
/// <summary>
|
|
/// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types).
|
|
/// </summary>
|
|
public string TypeNameForTypeof { get; }
|
|
public PropertyTypeKind TypeKind { get; }
|
|
public bool IsNullable { get; }
|
|
/// <summary>
|
|
/// Pre-computed interning flags matching runtime BinaryPropertyAccessorBase._interningFlags.
|
|
/// Bit layout: bit N = eligible when StringInterningMode == N.
|
|
/// None=0 → bit 0 never set. Attribute=1 → bit 1. All=2 → bit 2.
|
|
/// No attr: 0b100 (4), [AcStringIntern(true)]: 0b110 (6), [AcStringIntern(false)]: 0b000 (0).
|
|
/// </summary>
|
|
public int InterningFlags { get; }
|
|
|
|
/// <summary>True if the Complex property type has [AcBinarySerializable] → has a generated writer.</summary>
|
|
public bool HasGeneratedWriter { get; }
|
|
/// <summary>True if the Complex property type implements IId<T> → needs ref tracking in write pass.</summary>
|
|
public bool IsIId { get; }
|
|
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
|
public string? WriterClassName { get; }
|
|
/// <summary>Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId.</summary>
|
|
public string? IdTypeName { get; }
|
|
|
|
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
|
|
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
|
|
public PropertyTypeKind ElementKind { get; }
|
|
/// <summary>True if collection element type has [AcBinarySerializable].</summary>
|
|
public bool ElementHasGeneratedWriter { get; }
|
|
/// <summary>True if collection element type implements IId<T>.</summary>
|
|
public bool ElementIsIId { get; }
|
|
/// <summary>Generated writer class name for collection element type.</summary>
|
|
public string? ElementWriterClassName { get; }
|
|
/// <summary>Id type name for collection element IId types. Null if not IId.</summary>
|
|
public string? ElementIdTypeName { get; }
|
|
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary>
|
|
public string? CollectionKind { get; }
|
|
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
|
|
public string? ElementFullTypeName { get; }
|
|
|
|
// UseMetadata inline hash-ek (Complex/Collection child típushoz)
|
|
/// <summary>FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter.</summary>
|
|
public int ChildTypeNameHash { get; }
|
|
/// <summary>FNV-1a hashes of child type's properties. Only set when HasGeneratedWriter.</summary>
|
|
public int[]? ChildPropertyHashes { get; }
|
|
/// <summary>FNV-1a hash of collection element type name. Only set when ElementHasGeneratedWriter.</summary>
|
|
public int ElementTypeNameHash { get; }
|
|
/// <summary>FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter.</summary>
|
|
public int[]? ElementPropertyHashes { get; }
|
|
/// <summary>When false, child Complex type skips inline metadata in generated code.</summary>
|
|
public bool ChildEnableMetadata { get; }
|
|
/// <summary>When false, collection element type skips inline metadata in generated code.</summary>
|
|
public bool ElementEnableMetadata { 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, 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,
|
|
int childTypeNameHash = 0, int[]? childPropertyHashes = null,
|
|
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null,
|
|
bool childEnableMetadata = true, bool elementEnableMetadata = true)
|
|
{
|
|
Name = n;
|
|
TypeName = tn;
|
|
TypeNameForTypeof = tnForTypeof;
|
|
TypeKind = tk;
|
|
IsNullable = nullable;
|
|
HasGeneratedWriter = hasGeneratedWriter;
|
|
IsIId = isIId;
|
|
WriterClassName = writerClassName;
|
|
IdTypeName = idTypeName;
|
|
ElementKind = elementKind;
|
|
ElementHasGeneratedWriter = elementHasGenWriter;
|
|
ElementIsIId = elementIsIId;
|
|
ElementWriterClassName = elementWriterClassName;
|
|
ElementIdTypeName = elementIdTypeName;
|
|
CollectionKind = collectionKind;
|
|
ElementFullTypeName = elementFullTypeName;
|
|
ChildTypeNameHash = childTypeNameHash;
|
|
ChildPropertyHashes = childPropertyHashes;
|
|
ElementTypeNameHash = elementTypeNameHash;
|
|
ElementPropertyHashes = elementPropertyHashes;
|
|
ChildEnableMetadata = childEnableMetadata;
|
|
ElementEnableMetadata = elementEnableMetadata;
|
|
// Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase
|
|
int flags = 0;
|
|
if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit
|
|
if (stringInternAttr != false) flags |= (1 << 2); // All bit
|
|
InterningFlags = flags;
|
|
}
|
|
}
|
|
|
|
internal enum PropertyTypeKind
|
|
{
|
|
Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64,
|
|
Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum,
|
|
Collection, Complex,
|
|
NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64,
|
|
NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime,
|
|
NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum
|
|
}
|