AyCode.Core/AyCode.Core.Serializers.Sou.../AcBinarySourceGenerator.cs

3276 lines
176 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";
// ────────────────────────────────────────────────────────────────────────────────────────────
// TEMPORARY (2026-05-08) — A/B test feature gates for hot-path overhead measurement.
//
// The generated SGen `WriteProperties` / `ScanObject` methods emit two kinds of overhead-blocks
// that are unconditionally present today but rarely exercised in typical workloads:
//
// 1. PropertyFilter guard (`UsePropertyFilter`) — every non-markerless property emit-site
// checks `context.HasPropertyFilter` + filter-context allocation + lambda-call.
// The benchmark workload never sets a property-filter → branch is always false →
// pure overhead (CPU cycles + i-cache pressure on the hot path).
//
// 2. Polymorphic object-with-type-name emit (`UsePolymorphType`) — `System.Object` declared
// properties emit `ObjectWithTypeName` marker + `WriteStringUtf8(AssemblyQualifiedName)`
// under `!context.UseMetadata`. Same: rarely used in typical DTO graphs.
//
// Setting either to `false` skips the corresponding emit at compile time → leaner generated
// code. The bench measures the actual delta vs MemPack apples-to-apples (which has neither
// of these features).
//
// Long-term: these flags will move to `[AcBinarySerializable(UsePropertyFilter = false, ...)]`
// attribute properties so consumers can opt out per type. Until then, keep both `false` for
// benchmark-vs-MemPack measurements; flip to `true` for production where the features are needed.
// ────────────────────────────────────────────────────────────────────────────────────────────
// UsePropertyFilter const removed — replaced by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]`
// attribute flag, propagated through SerializableClassInfo.EnablePropertyFilter to EmitProp/EmitScanProp.
private const bool UsePolymorphType = false;
private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
id: "ACBIN001",
title: "Circular reference detected",
messageFormat: "Type '{0}' participates in a circular reference chain: {1}. Consider using ReferenceHandling.OnlyId or .All to avoid exponential serialization size.",
category: "AcBinarySerializer",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
/// <summary>
/// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as <c>System.Object</c> requires
/// polymorphic-prefix emit (<c>ObjectWithTypeName</c>) so the deserializer can resolve the
/// concrete runtime type. When <see cref="UsePolymorphType"/> is <c>false</c>, the prefix is
/// suppressed and the wire silently corrupts on round-trip (FixObj slot byte against
/// <c>typeof(object)</c> at read-time → 0-byte object wrapper → reader position drifts →
/// downstream <c>DECIMAL_DRIFT</c> / <c>IndexOutOfRangeException</c>).
///
/// Surface the misconfiguration at build time so the silent corruption is structurally
/// impossible. Three escape hatches for the developer:
/// 1. Enable the polymorphic feature (<see cref="UsePolymorphType"/> = true, or — once the
/// planned <c>[AcBinarySerializable(EnablePolymorphicFeature = true)]</c> flag lands — set
/// it on the type).
/// 2. Change the property type to a concrete type (no polymorphism needed).
/// 3. Mark the property with <c>[AcBinaryIgnore]</c> — ignored properties are filtered
/// out at property enumeration, so this diagnostic does not fire for them.
/// </summary>
private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new(
id: "ACBIN002",
title: "Polymorphic property requires polymorphic feature enabled",
messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but polymorphic serialization in the source generator is disabled (UsePolymorphType=false). " +
"The generated writer would silently corrupt the wire on round-trip. " +
"To fix: (1) enable polymorphic serialization, (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].",
category: "AcBinarySerializer",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
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 enablePropertyFilter = 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;
enablePropertyFilter = all;
}
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
{
// Four bool ctor: (metadata, idTracking, refHandling, internString) — filter defaults to true
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
}
else if (binarySerializableAttr.ConstructorArguments.Length == 5)
{
// Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter)
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
}
}
foreach (var p in GetAllSerializablePropertySymbols(typeSymbol))
{
// 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;
bool childNeedsIdScan = true;
bool childNeedsAllRefScan = true;
bool childNeedsInternScan = 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.Locations.Any(l => l.IsInSource)
&& resolvedType.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (hasGenWriter)
{
// Read child type's EnableMetadataFeature
propEnableMetadata = ReadEnableMetadata(resolvedType);
var childScanFlags = ComputeNeedsScan(resolvedType);
childNeedsIdScan = childScanFlags.needsIdScan;
childNeedsAllRefScan = childScanFlags.needsAllRefScan;
childNeedsInternScan = childScanFlags.needsInternScan;
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;
bool elemNeedsIdScan = true;
bool elemNeedsAllRefScan = true;
bool elemNeedsInternScan = true;
string? elemWriterClassName = null;
string? elemIdTypeName = null;
string? collKind = null;
string? collAddMethod = null;
bool collHasCapacityCtor = false;
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>" => "IndexedCollection",
"System.Collections.Generic.IReadOnlyList<T>" => "IndexedCollection",
"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
};
// Determine add method + capacity ctor for Counted concrete types
if (collKind == "Counted")
{
collAddMethod = origDef switch
{
"System.Collections.Generic.HashSet<T>" => "Add",
"System.Collections.Generic.SortedSet<T>" => "Add",
"System.Collections.Generic.Queue<T>" => "Enqueue",
"System.Collections.Generic.LinkedList<T>" => "AddLast",
_ => null // ICollection<T>, IReadOnlyCollection<T> → backed by List<T>
};
collHasCapacityCtor = origDef is
"System.Collections.Generic.HashSet<T>" or
"System.Collections.Generic.Queue<T>";
}
}
// For Complex element types, check for generated writer
if (elemKind == PropertyTypeKind.Complex)
{
var resolvedElem = elemType is INamedTypeSymbol namedElem
? namedElem.OriginalDefinition : elemType;
elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource)
&& resolvedElem.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (elemHasGenWriter)
{
// Read element type's EnableMetadataFeature
elemEnableMetadata = ReadEnableMetadata(resolvedElem);
var elemScanFlags = ComputeNeedsScan(resolvedElem);
elemNeedsIdScan = elemScanFlags.needsIdScan;
elemNeedsAllRefScan = elemScanFlags.needsAllRefScan;
elemNeedsInternScan = elemScanFlags.needsInternScan;
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);
}
}
}
}
// Dictionary key/value type analysis for inline dictionary read
PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown;
PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown;
string? dictKeyTypeName = null;
string? dictValueTypeName = null;
bool dictValueHasGenWriter = false;
string? dictValueWriterClassName = null;
bool dictValueIsIId = false;
bool dictValueEnableMetadata = true;
bool dictValueNeedsIdScan = true;
bool dictValueNeedsAllRefScan = true;
bool dictValueNeedsInternScan = true;
int dictValueTypeNameHash = 0;
int[]? dictValuePropertyHashes = null;
if (kind == PropertyTypeKind.Dictionary)
{
var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type);
if (keyType != null)
{
dictKeyKind = GetKind(keyType);
dictKeyTypeName = keyType.ToDisplayString();
}
if (valueType != null)
{
dictValueKind = GetKind(valueType);
dictValueTypeName = valueType.ToDisplayString();
if (dictValueKind == PropertyTypeKind.Complex)
{
var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType;
dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource)
&& resolvedValue.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (dictValueHasGenWriter)
{
var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue);
var vns = resolvedValue.ContainingNamespace.IsGlobalNamespace
? string.Empty : resolvedValue.ContainingNamespace.ToDisplayString();
dictValueWriterClassName = string.IsNullOrEmpty(vns)
? $"{vfn}_GeneratedWriter"
: $"{vns}.{vfn}_GeneratedWriter";
dictValueEnableMetadata = ReadEnableMetadata(resolvedValue);
var dvScanFlags = ComputeNeedsScan(resolvedValue);
dictValueNeedsIdScan = dvScanFlags.needsIdScan;
dictValueNeedsAllRefScan = dvScanFlags.needsAllRefScan;
dictValueNeedsInternScan = dvScanFlags.needsInternScan;
var dvIidIface = resolvedValue.AllInterfaces.FirstOrDefault(ifc =>
ifc.IsGenericType &&
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
dictValueIsIId = dvIidIface != null;
dictValueTypeNameHash = ComputeFnvHash(resolvedValue.Name);
dictValuePropertyHashes = ComputeChildPropertyHashes(resolvedValue);
}
}
}
}
properties.Add(new PropInfo(
p.Name,
typeDisplayName,
typeNameForTypeof,
kind,
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
p.Type.SpecialType == SpecialType.System_Object,
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
collAddMethod, collHasCapacityCtor,
dictKeyKind, dictValueKind, dictKeyTypeName, dictValueTypeName, dictValueHasGenWriter, dictValueWriterClassName,
dictValueIsIId, dictValueEnableMetadata, dictValueTypeNameHash, dictValuePropertyHashes,
dictValueNeedsIdScan, dictValueNeedsAllRefScan, dictValueNeedsInternScan,
childTypeNameHash, childPropertyHashes,
elementTypeNameHash, elementPropertyHashes,
propEnableMetadata, elemEnableMetadata,
childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan,
elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan));
}
// 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 are already in runtime-matching order from GetAllSerializablePropertySymbols:
// derived → base, each level sorted alphabetically (matches TypeMetadataBase.GetUnfilteredProperties).
var className = BuildFlatName(typeSymbol);
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
var selfScanFlags = ComputeNeedsScan(typeSymbol);
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
}
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;
DetectAndReportCycles(valid, context);
DetectAndReportPolymorphicMisuse(valid, context);
foreach (var ci in valid)
{
context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8));
context.AddSource($"{ci.ClassName}_GeneratedReader.g.cs", SourceText.From(GenReader(ci), Encoding.UTF8));
}
context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8));
}
/// <summary>
/// ACCORE-BIN-I-T7K3 guard: emits <see cref="PolymorphicPropertyWithFeatureDisabledError"/>
/// (ACBIN002) for every <c>System.Object</c>-declared property on any
/// <c>[AcBinarySerializable]</c> type while <see cref="UsePolymorphType"/> is <c>false</c>.
/// Short-circuits when the feature is enabled — no per-property work needed.
/// </summary>
private static void DetectAndReportPolymorphicMisuse(List<SerializableClassInfo> classes, SourceProductionContext spc)
{
if (UsePolymorphType) return; // Feature enabled → polymorphic prefix is emitted, no misuse possible.
foreach (var ci in classes)
{
foreach (var p in ci.Properties)
{
if (p.IsObjectDeclaredType)
{
spc.ReportDiagnostic(Diagnostic.Create(
PolymorphicPropertyWithFeatureDisabledError, Location.None,
ci.ClassName, p.Name));
}
}
}
}
/// <summary>
/// Detects circular reference chains among [AcBinarySerializable] types at compile time
/// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges.
/// </summary>
private static void DetectAndReportCycles(List<SerializableClassInfo> classes, SourceProductionContext spc)
{
// Build lookup: WriterClassName → FullTypeName
var writerToFull = new Dictionary<string, string>(classes.Count);
foreach (var ci in classes)
{
var writerName = string.IsNullOrEmpty(ci.Namespace)
? $"{ci.ClassName}_GeneratedWriter"
: $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter";
writerToFull[writerName] = ci.FullTypeName;
}
// Build adjacency list: FullTypeName → set of referenced FullTypeNames
var adjacency = new Dictionary<string, HashSet<string>>(classes.Count);
foreach (var ci in classes)
{
var edges = new HashSet<string>();
foreach (var p in ci.Properties)
{
if (p.TypeKind == PropertyTypeKind.Complex && p.HasGeneratedWriter && p.WriterClassName != null)
{
if (writerToFull.TryGetValue(p.WriterClassName, out var target))
edges.Add(target);
}
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.ElementWriterClassName != null)
{
if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target))
edges.Add(target);
}
if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter && p.DictValueWriterClassName != null)
{
if (writerToFull.TryGetValue(p.DictValueWriterClassName, out var target))
edges.Add(target);
}
}
adjacency[ci.FullTypeName] = edges;
}
// DFS with 3-color marking: White=0, Gray=1, Black=2
var color = new Dictionary<string, int>(classes.Count);
foreach (var ci in classes)
color[ci.FullTypeName] = 0;
var stack = new List<string>();
var reported = new HashSet<string>();
void Dfs(string node)
{
color[node] = 1; // Gray
stack.Add(node);
if (adjacency.TryGetValue(node, out var neighbors))
{
foreach (var next in neighbors)
{
if (!color.TryGetValue(next, out var c)) continue;
if (c == 1) // Gray → back-edge = cycle
{
var cycleStart = stack.IndexOf(next);
var parts = new List<string>();
for (var i = cycleStart; i < stack.Count; i++)
parts.Add(ShortTypeName(stack[i]));
parts.Add(ShortTypeName(next)); // close the cycle
var cycleDesc = string.Join(" \u2192 ", parts);
for (var i = cycleStart; i < stack.Count; i++)
{
if (reported.Add(stack[i]))
{
spc.ReportDiagnostic(Diagnostic.Create(
CircularReferenceWarning, Location.None,
ShortTypeName(stack[i]), cycleDesc));
}
}
}
else if (c == 0) // White → unvisited
{
Dfs(next);
}
}
}
stack.RemoveAt(stack.Count - 1);
color[node] = 2; // Black
}
foreach (var ci in classes)
{
if (color[ci.FullTypeName] == 0)
Dfs(ci.FullTypeName);
}
}
private static string ShortTypeName(string fullTypeName)
{
var dot = fullTypeName.LastIndexOf('.');
return dot >= 0 ? fullTypeName.Substring(dot + 1) : fullTypeName;
}
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 System.Runtime.InteropServices;");
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
// IGeneratedBinaryWriter and other serializer types
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_wrapperSlot = AcBinarySerializer.AllocateWrapperSlot();");
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) where TOutput : struct, IBinaryOutputBase");
sb.AppendLine(" {");
sb.AppendLine(" // Depth check + EnterRecursion happens at the CALLER (before marker write).");
sb.AppendLine(" // Body just runs property writes; ExitRecursion at the end balances the caller's EnterRecursion.");
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
foreach (var p in ci.Properties)
{
sb.AppendLine();
EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter);
}
sb.AppendLine();
sb.AppendLine(" context.ExitRecursion();");
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);");
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) where TOutput : struct, IBinaryOutputBase");
sb.AppendLine(" {");
// Compile-time proven: no scan work needed for this type
if (!ci.NeedsScan)
{
sb.AppendLine(" // NeedsScan=false: no ref tracking, no string interning, no scannable children");
sb.AppendLine(" }");
return;
}
// Early return: skip scan when no active runtime feature matches this type's needs
if (!ci.NeedsIdScan)
{
if (ci.NeedsAllRefScan && ci.NeedsInternScan)
sb.AppendLine(" if (!context.HasAllRefHandling && !context.HasStringInterning) return;");
else if (ci.NeedsAllRefScan)
sb.AppendLine(" if (!context.HasAllRefHandling) return;");
else if (ci.NeedsInternScan)
sb.AppendLine(" if (!context.HasStringInterning) return;");
}
// Null guard — MaxDepth option removed (was: cycle protection via runtime depth check).
// Cycle safety now comes from IId-tracking; future [AcBinaryCircular] attr will mark non-IId circular refs.
sb.AppendLine(" if (value == null) return;");
sb.AppendLine();
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
// Self ref tracking — inline TryTrack via wrapper (no bridge method overhead)
// Only emitted when the corresponding feature flag is enabled.
if (ci.IsIId)
{
var tryTrackMethod = ci.IdTypeName switch
{
"int" => "TryTrackInt32",
"long" => "TryTrackInt64",
"System.Guid" => "TryTrackGuid",
_ => "TryTrackInt32"
};
sb.AppendLine();
sb.AppendLine(" if (context.HasRefHandling)");
sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
sb.AppendLine(" {");
sb.AppendLine(" if (firstVisitIndex >= 0)");
sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);");
sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);");
sb.AppendLine(" return;");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
if (ci.EnableRefHandling && !ci.IsIId)
{
// Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode
sb.AppendLine();
sb.AppendLine(" if (context.HasAllRefHandling)");
sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
sb.AppendLine(" {");
sb.AppendLine(" if (firstVisitIndex >= 0)");
sb.AppendLine(" context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);");
sb.AppendLine(" context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);");
sb.AppendLine(" return;");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
// Collect scannable properties
var scanProps = ci.Properties.Where(p =>
p.TypeKind == PropertyTypeKind.String ||
p.TypeKind == PropertyTypeKind.Complex ||
p.TypeKind == PropertyTypeKind.Collection ||
p.TypeKind == PropertyTypeKind.Dictionary).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) ||
(p.TypeKind == PropertyTypeKind.Dictionary && (p.DictKeyKind == PropertyTypeKind.String || p.DictValueKind == PropertyTypeKind.String) && p.InterningFlags != 0));
// Combined check+inc — gated inside TryEnterRecursion (checks NeedsDepthCheck).
// Emitted AFTER all early returns (NeedsScan=false, feature-flag, null guard, IId 2nd-occurrence)
// and BEFORE the property scan loop that recurses into children.
// On limit hit: helper method (cold path, NoInlining) dispatches Throw or Truncate (return).
sb.AppendLine();
sb.AppendLine(" if (context.TryEnterRecursion(hasTruncatePath: false)) return; // scan: skip children");
if (hasStringScan)
{
// Use pre-computed InternBit from context (avoids Options.UseStringInterning field chain + shift per object).
// 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 = context.InternBit;");
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, ci.EnablePropertyFilter);
}
if (!hasAnyScanProp)
{
sb.AppendLine(" // No reference properties to scan");
}
sb.AppendLine();
sb.AppendLine(" context.ExitRecursion();");
sb.AppendLine(" }");
}
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter)
{
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)
EmitMarkerless(sb, p.TypeKind, a, i);
else
EmitPropertyBridge(sb, p.TypeKind, a, i);
return;
}
// All non-markerless properties: emit PropertyFilter guard
// When filter returns false, write PropertySkip and skip the property write.
// Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — when false, the entire
// filter-check block is omitted from emit (no `HasPropertyFilter` test, no filter-context allocation,
// no lambda-call) → leaner generated code on hot-path types that never use a property-filter.
if (enablePropertyFilter)
{
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} & context.InternBit) != 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.IsObjectDeclaredType)
{
// System.Object property: runtime type unknown at compile time.
// Write ObjectWithTypeName prefix so deserializer can resolve the concrete type.
// Use value.GetType() for runtime type dispatch (not typeof(object)).
// Gated by `UsePolymorphType` (TEMPORARY const) — `false` skips the type-name emit
// entirely (deser will use the property's declared type, which is `object` so the
// round-trip would fail on polymorphic instances; safe ONLY when the workload is
// known not to use polymorphic object-typed properties — true for the benchmark).
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
if (UsePolymorphType)
{
sb.AppendLine($"{i} if (!context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithTypeName);");
sb.AppendLine($"{i} context.WriteStringUtf8({a}.GetType().AssemblyQualifiedName!);");
sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context);");
sb.AppendLine($"{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);");
}
else
sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);");
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);");
}
else
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context);");
break;
case PropertyTypeKind.Dictionary:
EmitDirectDictionaryWrite(sb, p, a, i);
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 a single bridge method call for markerless property types with enableMetadata=true.
/// The bridge method on BinarySerializationContext handles both UseMetadata=true (markered+skip)
/// and UseMetadata=false (markerless) paths internally. Replaces 7-11 lines of generated code with 1 line.
/// </summary>
private static void EmitPropertyBridge(StringBuilder sb, PropertyTypeKind k, string a, string i)
{
var call = k switch
{
PropertyTypeKind.Int32 => $"context.WriteInt32Property({a});",
PropertyTypeKind.Int64 => $"context.WriteInt64Property({a});",
PropertyTypeKind.Boolean => $"context.WriteBoolProperty({a});",
PropertyTypeKind.Double => $"context.WriteFloat64Property({a});",
PropertyTypeKind.Single => $"context.WriteFloat32Property({a});",
PropertyTypeKind.Decimal => $"context.WriteDecimalProperty({a});",
PropertyTypeKind.DateTime => $"context.WriteDateTimeProperty({a});",
PropertyTypeKind.Guid => $"context.WriteGuidProperty({a});",
PropertyTypeKind.Byte => $"context.WriteByteProperty({a});",
PropertyTypeKind.Int16 => $"context.WriteInt16Property({a});",
PropertyTypeKind.UInt16 => $"context.WriteUInt16Property({a});",
PropertyTypeKind.UInt32 => $"context.WriteUInt32Property({a});",
PropertyTypeKind.UInt64 => $"context.WriteUInt64Property({a});",
PropertyTypeKind.Enum => $"context.WriteEnumInt32Property((int){a});",
PropertyTypeKind.TimeSpan => $"context.WriteTimeSpanProperty({a});",
PropertyTypeKind.DateTimeOffset => $"context.WriteDateTimeOffsetProperty({a});",
_ => null
};
if (call != null)
sb.AppendLine($"{i}{call}");
}
/// <summary>
/// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
#region Scan Pass Code Generation
/// <summary>
/// Compile-time check: will EmitScanProp produce any scan work for this property?
/// When false, the entire block (including PropertyFilter guard) is skipped.
/// </summary>
private static bool HasScanWork(PropInfo p) => p.TypeKind switch
{
PropertyTypeKind.String => p.InterningFlags != 0,
PropertyTypeKind.Complex when p.HasGeneratedWriter => p.ChildNeedsScan,
PropertyTypeKind.Complex => true,
PropertyTypeKind.Collection => HasCollectionScanWork(p),
PropertyTypeKind.Dictionary => HasDictionaryScanWork(p),
_ => false
};
private static bool HasCollectionScanWork(PropInfo p) => p.ElementKind switch
{
PropertyTypeKind.String => p.InterningFlags != 0,
PropertyTypeKind.Complex when p.ElementHasGeneratedWriter && p.CollectionKind != null => p.ElementNeedsScan,
PropertyTypeKind.Complex => true,
_ => false
};
private static bool HasDictionaryScanWork(PropInfo p)
{
if (p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0) return true;
if (p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0) return true;
if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter) return p.DictValueNeedsScan;
if (p.DictValueKind == PropertyTypeKind.Complex) return true;
return false;
}
/// <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, bool enablePropertyFilter)
{
// Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard)
if (!HasScanWork(p)) return;
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).
// Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — same gate as the writer pass.
if (enablePropertyFilter && !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;
case PropertyTypeKind.Dictionary:
EmitScanDictionary(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)
{
// Compile-time proven: child scan is no-op — skip entirely
if (!p.ChildNeedsScan) return;
var writer = p.WriterClassName;
var childVar = $"sc_{p.Name}";
// 3-axis guard: IId → always call, AllRef → guard All mode, Intern → guard UseStringInterning
string? guard = null;
if (!p.ChildNeedsIdScan)
{
if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan)
guard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.ChildNeedsAllRefScan)
guard = "context.HasAllRefHandling";
else if (p.ChildNeedsInternScan)
guard = "context.HasStringInterning";
}
if (guard != null)
{
sb.AppendLine($"{i}if ({guard})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var {childVar} = {a};");
sb.AppendLine($"{i} if ({childVar} != null)");
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context);");
sb.AppendLine($"{i}}}");
}
else
{
// IId in subtree — always call (active in OnlyId + All)
sb.AppendLine($"{i}var {childVar} = {a};");
sb.AppendLine($"{i}if ({childVar} != null)");
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context);");
}
}
/// <summary>
/// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
/// System.Object properties use value.GetType() for runtime type dispatch.
/// </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)");
if (p.IsObjectDeclaredType)
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, {childVar}.GetType(), context);");
else
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context);");
}
/// <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} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
}
else if (p.CollectionKind == "IndexedCollection")
{
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)
{
// Compile-time proven: element scan is no-op — skip entirely
if (!p.ElementNeedsScan) return;
var writer = p.ElementWriterClassName;
// 3-axis guard: IId → always scan, AllRef → guard All mode, Intern → guard UseStringInterning
string? elemGuard = null;
if (!p.ElementNeedsIdScan)
{
if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan)
elemGuard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.ElementNeedsAllRefScan)
elemGuard = "context.HasAllRefHandling";
else if (p.ElementNeedsInternScan)
elemGuard = "context.HasStringInterning";
}
// Guard entire collection scan with runtime check when no IId in element subtree
if (elemGuard != null)
sb.AppendLine($"{i}if ({elemGuard})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var scol_{p.Name} = {a};");
sb.AppendLine($"{i} if (scol_{p.Name} != null)");
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} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
}
else if (p.CollectionKind == "IndexedCollection")
{
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);");
sb.AppendLine($"{i} }}");
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);");
return;
}
// Primitive element collection — no scanning needed
}
/// <summary>
/// Emits inline dictionary scan. Iterates entries and:
/// - String keys: ScanInternString if interning flags match
/// - String values: ScanInternString if interning flags match
/// - Complex+SGen values: ScanObject on each value (handles ref tracking internally)
/// Eliminates GetWrapper dictionary lookup for all inlineable dictionary types.
/// </summary>
private static void EmitScanDictionary(StringBuilder sb, PropInfo p, string a, string i)
{
var s = p.Name;
var hasStringKeys = p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0;
var hasStringValues = p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0;
var hasComplexValues = p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter;
// No scanning needed for primitive-only dictionaries without internable strings or complex values
if (!hasStringKeys && !hasStringValues && !hasComplexValues) return;
// Complex+SGen values: compile-time proven scan is no-op → skip entirely
if (hasComplexValues && !p.DictValueNeedsScan && !hasStringKeys && !hasStringValues) return;
// Build guard expression for Complex+SGen values (3-axis: IId/AllRef/Intern)
string? complexGuard = null;
if (hasComplexValues && p.DictValueNeedsScan && !p.DictValueNeedsIdScan)
{
if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan)
complexGuard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.DictValueNeedsAllRefScan)
complexGuard = "context.HasAllRefHandling";
else if (p.DictValueNeedsInternScan)
complexGuard = "context.HasStringInterning";
}
// For string-only scan (no complex values), use simple interning loop
if (!hasComplexValues)
{
sb.AppendLine($"{i}var sd_{s} = {a};");
sb.AppendLine($"{i}if (sd_{s} != null && ({p.InterningFlags} & internBit) != 0)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} foreach (var sde_{s} in sd_{s})");
sb.AppendLine($"{i} {{");
if (hasStringKeys)
{
sb.AppendLine($"{i} if (sde_{s}.Key != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var sklen_{s} = sde_{s}.Key.Length;");
sb.AppendLine($"{i} if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))");
sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Key);");
sb.AppendLine($"{i} }}");
}
if (hasStringValues)
{
sb.AppendLine($"{i} if (sde_{s}.Value != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var svlen_{s} = sde_{s}.Value.Length;");
sb.AppendLine($"{i} if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))");
sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Value);");
sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
return;
}
// Complex+SGen values (with optional string key/value interning)
var writer = p.DictValueWriterClassName!;
// Guard entire scan block when no IId in value subtree
if (complexGuard != null && !hasStringKeys && !hasStringValues)
sb.AppendLine($"{i}if ({complexGuard})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var sd_{s} = {a};");
sb.AppendLine($"{i} if (sd_{s} != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} foreach (var sde_{s} in sd_{s})");
sb.AppendLine($"{i} {{");
// String key interning
if (hasStringKeys)
{
sb.AppendLine($"{i} if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Key != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var sklen_{s} = sde_{s}.Key.Length;");
sb.AppendLine($"{i} if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))");
sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Key);");
sb.AppendLine($"{i} }}");
}
// String value interning
if (hasStringValues)
{
sb.AppendLine($"{i} if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Value != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var svlen_{s} = sde_{s}.Value.Length;");
sb.AppendLine($"{i} if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))");
sb.AppendLine($"{i} context.ScanInternString(sde_{s}.Value);");
sb.AppendLine($"{i} }}");
}
// Complex value ScanObject
if (hasComplexValues)
{
sb.AppendLine($"{i} if (sde_{s}.Value != null)");
if (complexGuard != null)
{
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if ({complexGuard})");
sb.AppendLine($"{i} {writer}.Instance.ScanObject(sde_{s}.Value, context);");
sb.AppendLine($"{i} }}");
}
else
sb.AppendLine($"{i} {writer}.Instance.ScanObject(sde_{s}.Value, context);");
}
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
}
#endregion
/// <summary>
/// Emits inline object write for a Complex property that has a generated writer.
/// Compile-time ChildNeedsRefScan eliminates TryConsumeWritePlanEntry when scan never tracks child.
/// !ChildNeedsRefScan + !ChildEnableMetadata → ZERO branches: just Object + WriteProperties.
/// </summary>
private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i)
{
var writer = p.WriterClassName;
var refSuffix = p.IsIId ? "IId" : "All";
// Reference type properties can always be null at runtime regardless of nullable annotation
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
if (!p.ChildNeedsRefScan && !p.ChildEnableMetadata)
{
// Compile-time proven: no ref, no metadata. Combined check+inc BEFORE marker write so Truncate writes
// Null wire-correctly. TryEnterRecursion inc'd on success; ExitRecursion at WriteProperties end.
sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({a}, context); }}");
}
else if (p.ChildNeedsRefScan && !p.ChildEnableMetadata)
{
sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{refSuffix}()) {writer}.Instance.WriteProperties({a}, context);");
}
else if (!p.ChildNeedsRefScan && p.ChildEnableMetadata)
{
sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);");
}
else
{
sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{refSuffix}({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);");
}
}
/// <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&lt;T&gt; / 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;
// Reference type collections can always be null at runtime regardless of nullable annotation
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
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} 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} foreach (var elem_{p.Name} in col_{p.Name})");
sb.AppendLine($"{i} {{");
}
else if (p.CollectionKind == "IndexedCollection")
{
sb.AppendLine($"{i} var col_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];");
}
else // List — CollectionsMarshal.AsSpan for zero-overhead iteration
{
sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});");
sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];");
}
// Per-element write
var e = $"elem_{p.Name}";
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
var elemRefSuffix = p.ElementIsIId ? "IId" : "All";
if (!p.ElementNeedsRefScan && !p.ElementEnableMetadata)
{
// Compile-time proven: no ref, no metadata. Combined check+inc before marker write.
sb.AppendLine($"{i} if (context.TryEnterRecursion(hasTruncatePath: true)) continue;");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context);");
}
else if (p.ElementNeedsRefScan && !p.ElementEnableMetadata)
{
sb.AppendLine($"{i} if (context.WriteObjectRefMarker{elemRefSuffix}()) {writer}.Instance.WriteProperties({e}, context);");
}
else if (!p.ElementNeedsRefScan && p.ElementEnableMetadata)
{
sb.AppendLine($"{i} if (context.WriteObjectMetaMarker({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);");
}
else
{
sb.AppendLine($"{i} if (context.WriteObjectFullMarker{elemRefSuffix}({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);");
}
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits inline write of a primitive/string/enum value in non-property context (no PropertySkip).
/// Matches runtime TryWritePrimitive wire format: TinyInt for small int, type code + value otherwise.
/// Used for dictionary key/value writes.
/// </summary>
private static void EmitWritePrimitiveValue(StringBuilder sb, PropertyTypeKind kind, string a, string suffix, string i)
{
switch (kind)
{
case PropertyTypeKind.Int32:
sb.AppendLine($"{i}if (BinaryTypeCode.TryEncodeTinyInt({a}, out var tk_{suffix})) context.WriteByte(tk_{suffix});");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}");
break;
case PropertyTypeKind.Int64:
sb.AppendLine($"{i}if ({a} >= int.MinValue && {a} <= int.MaxValue)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var iv_{suffix} = (int){a};");
sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(iv_{suffix}, out var tk_{suffix})) context.WriteByte(tk_{suffix});");
sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{suffix}); }}");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else {{ 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;
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;
case PropertyTypeKind.Byte:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a});");
break;
case PropertyTypeKind.Int16:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a});");
break;
case PropertyTypeKind.UInt16:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a});");
break;
case PropertyTypeKind.UInt32:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a});");
break;
case PropertyTypeKind.UInt64:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a});");
break;
case PropertyTypeKind.Enum:
sb.AppendLine($"{i}{{ var ev_{suffix} = (int){a}; context.WriteByte(BinaryTypeCode.Enum);");
sb.AppendLine($"{i} if (BinaryTypeCode.TryEncodeTinyInt(ev_{suffix}, out var te_{suffix})) context.WriteByte(te_{suffix});");
sb.AppendLine($"{i} else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{suffix}); }} }}");
break;
}
}
/// <summary>
/// Emits inline dictionary write. Wire format: [Dictionary][count][key₁ value₁ key₂ value₂ ...].
/// Keys/values are written with type codes matching runtime TryWritePrimitive/WriteValue.
/// Eliminates GetWrapper dictionary lookup for all inlineable key/value types.
/// </summary>
private static void EmitDirectDictionaryWrite(StringBuilder sb, PropInfo p, string a, string i)
{
var s = p.Name;
var keyType = p.DictKeyTypeName ?? "object";
var valType = p.DictValueTypeName ?? "object";
// Reference type dictionaries can always be null at runtime regardless of nullable annotation
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Dictionary);");
sb.AppendLine($"{i} context.WriteVarUInt((uint){a}.Count);");
sb.AppendLine($"{i} foreach (var kvp_{s} in {a})");
sb.AppendLine($"{i} {{");
var k = $"kvp_{s}.Key";
var v = $"kvp_{s}.Value";
var ii = i + " ";
// Write key
if (p.DictKeyKind == PropertyTypeKind.String)
{
if (p.InterningFlags == 0)
sb.AppendLine($"{ii}context.StringInternEligible = false;");
else
sb.AppendLine($"{ii}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;");
sb.AppendLine($"{ii}AcBinarySerializer.WriteStringGenerated({k}, context);");
}
else if (IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum)
{
EmitWritePrimitiveValue(sb, p.DictKeyKind, k, $"dk_{s}", ii);
}
else
{
sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({k}, typeof({keyType}), context);");
}
// Write value
if (p.DictValueKind == PropertyTypeKind.String)
{
// String value: null → Null, non-null → WriteStringGenerated
sb.AppendLine($"{ii}if ({v} == null) context.WriteByte(BinaryTypeCode.Null);");
sb.AppendLine($"{ii}else");
sb.AppendLine($"{ii}{{");
if (p.InterningFlags == 0)
sb.AppendLine($"{ii} context.StringInternEligible = false;");
else
sb.AppendLine($"{ii} context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;");
sb.AppendLine($"{ii} AcBinarySerializer.WriteStringGenerated({v}, context);");
sb.AppendLine($"{ii}}}");
}
else if (IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum)
{
EmitWritePrimitiveValue(sb, p.DictValueKind, v, $"dv_{s}", ii);
}
else if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter)
{
EmitDictValueComplexWrite(sb, p, v, s, ii);
}
else
{
// Fallback for non-inlineable value types
sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({v}, typeof({valType}), context);");
}
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits inline write for a Complex+SGen dictionary value with ref tracking and metadata support.
/// Delegates marker logic to runtime WriteObjectRefMarker/MetaMarker/FullMarker bridge.
/// </summary>
private static void EmitDictValueComplexWrite(StringBuilder sb, PropInfo p, string v, string s, string i)
{
var writer = p.DictValueWriterClassName!;
sb.AppendLine($"{i}if ({v} == null) {{ context.WriteByte(BinaryTypeCode.Null); }}");
var dvRefSuffix = p.DictValueIsIId ? "IId" : "All";
if (!p.DictValueNeedsRefScan && !p.DictValueEnableMetadata)
{
// No ref, no metadata. Combined check+inc before marker write.
sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({v}, context); }}");
}
else if (p.DictValueNeedsRefScan && !p.DictValueEnableMetadata)
{
sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{dvRefSuffix}()) {writer}.Instance.WriteProperties({v}, context);");
}
else if (!p.DictValueNeedsRefScan && p.DictValueEnableMetadata)
{
sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);");
}
else
{
sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{dvRefSuffix}({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);");
}
}
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);");
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;
}
}
#region Reader Code Generation
/// <summary>
/// Generates the IGeneratedBinaryReader implementation for a type.
/// Phase 1: handles markerless path (no UseMetadata). UseMetadata/ChainMode → runtime fallback.
/// Eliminates: GetWrapper dictionary lookup, CreateInstance delegate, property setter delegates,
/// AccessorType switch dispatch, ReadValue dispatch table.
/// </summary>
private static string GenReader(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;");
sb.AppendLine();
if (!string.IsNullOrEmpty(ci.Namespace))
sb.AppendLine($"namespace {ci.Namespace};");
sb.AppendLine();
sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedReader : IGeneratedBinaryReader");
sb.AppendLine("{");
sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedReader Instance = new();");
sb.AppendLine();
// ReadProperties — reads all properties into an existing instance (mirrors WriteProperties)
// No depth safety net on deserialize: wire format is linear + finite, the serializer-side counter
// already prevents pathological depth in well-formed payloads.
sb.AppendLine(" public void ReadProperties<TInput>(object value, AcBinaryDeserializer.BinaryDeserializationContext<TInput> context)");
sb.AppendLine(" where TInput : struct, IBinaryInputBase");
sb.AppendLine(" {");
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
// Emit property reads — markerless for primitive types, markered for the rest
foreach (var p in ci.Properties)
{
sb.AppendLine();
EmitReadProp(sb, p, " ", ci.EnableMetadata);
}
sb.AppendLine(" }");
sb.AppendLine();
// ReadObject — IGeneratedBinaryReader implementation (delegates to ReadProperties)
sb.AppendLine(" public object? ReadObject<TInput>(AcBinaryDeserializer.BinaryDeserializationContext<TInput> context, int cacheIndex)");
sb.AppendLine(" where TInput : struct, IBinaryInputBase");
sb.AppendLine(" {");
sb.AppendLine($" var obj = new {ci.FullTypeName}();");
sb.AppendLine(" if (cacheIndex >= 0)");
sb.AppendLine(" context.RegisterInternedValueAt(cacheIndex, obj);");
sb.AppendLine(" ReadProperties<TInput>(obj, context);");
sb.AppendLine(" return obj;");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// Emits inline read code for a single property.
/// Markerless types: read raw value directly (no type code in stream).
/// Markered types: read type code byte, then dispatch.
/// Mirrors the serializer's EmitProp symmetry.
/// </summary>
private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata)
{
var a = $"obj.{p.Name}";
// Markerless types: read raw value directly — mirrors EmitMarkerless in writer
if (IsMarkerless(p.TypeKind))
{
if (p.TypeKind == PropertyTypeKind.Enum)
sb.AppendLine($"{i}{{ var ev = context.ReadVarInt(); {a} = Unsafe.As<int, {p.TypeNameForTypeof}>(ref ev); }}");
else
EmitReadMarkerless(sb, p.TypeKind, a, i);
return;
}
// String FastWire markerless fast-path: int32 sentinel header (-1 = null, 0 = empty, N > 0 = content).
// Wire-symmetric with `WriteStringGenerated` (SGen) and `WriteStringUtf16Markerless` (Runtime).
// Skips the typeCode-read entirely in FastWire mode; falls through to markered dispatch in Compact.
if (p.TypeKind == PropertyTypeKind.String)
{
sb.AppendLine($"{i}if (context.FastWire)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} {a} = context.ReadStringUtf16Markerless()!;");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var tc_{p.Name} = context.ReadByte();");
sb.AppendLine($"{i} if (tc_{p.Name} != BinaryTypeCode.PropertySkip)");
sb.AppendLine($"{i} {{");
EmitReadString(sb, a, $"tc_{p.Name}", i + " ");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
return;
}
// Markered types: read type code, then dispatch
var tc = $"tc_{p.Name}";
sb.AppendLine($"{i}var {tc} = context.ReadByte();");
// PropertySkip → leave default
sb.AppendLine($"{i}if ({tc} != BinaryTypeCode.PropertySkip)");
sb.AppendLine($"{i}{{");
// Nullable value types
if (IsNullableVTKind(p.TypeKind))
{
sb.AppendLine($"{i} if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
EmitReadMarkeredValue(sb, Underlying(p.TypeKind), a, tc, i + " ", p, nullable: true);
sb.AppendLine($"{i} }}");
}
else
{
switch (p.TypeKind)
{
case PropertyTypeKind.String:
EmitReadString(sb, a, tc, i + " ");
break;
case PropertyTypeKind.Complex:
EmitReadComplex(sb, p, a, tc, i + " ");
break;
case PropertyTypeKind.Collection:
EmitReadCollection(sb, p, a, tc, i + " ");
break;
case PropertyTypeKind.Dictionary:
EmitReadDictionary(sb, p, a, tc, i + " ");
break;
default:
// Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback
sb.AppendLine($"{i} context._position--;");
if (p.IsNullable)
sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}));");
else
sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;");
break;
}
}
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly.
/// </summary>
private static void EmitReadMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i)
{
switch (k)
{
case PropertyTypeKind.Int32: sb.AppendLine($"{i}{a} = context.ReadVarInt();"); break;
case PropertyTypeKind.Int64: sb.AppendLine($"{i}{a} = context.ReadVarLong();"); break;
case PropertyTypeKind.Double: sb.AppendLine($"{i}{a} = context.ReadDoubleUnsafe();"); break;
case PropertyTypeKind.Single: sb.AppendLine($"{i}{a} = context.ReadSingleUnsafe();"); break;
case PropertyTypeKind.Decimal: sb.AppendLine($"{i}{a} = context.ReadDecimalUnsafe();"); break;
case PropertyTypeKind.DateTime: sb.AppendLine($"{i}{a} = context.ReadDateTimeUnsafe();"); break;
case PropertyTypeKind.Guid: sb.AppendLine($"{i}{a} = context.ReadGuidUnsafe();"); break;
case PropertyTypeKind.Byte: sb.AppendLine($"{i}{a} = context.ReadByte();"); break;
case PropertyTypeKind.Int16: sb.AppendLine($"{i}{a} = context.ReadInt16Unsafe();"); break;
case PropertyTypeKind.UInt16: sb.AppendLine($"{i}{a} = context.ReadUInt16Unsafe();"); break;
case PropertyTypeKind.UInt32: sb.AppendLine($"{i}{a} = context.ReadVarUInt();"); break;
case PropertyTypeKind.UInt64: sb.AppendLine($"{i}{a} = context.ReadVarULong();"); break;
case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}{a} = new System.TimeSpan(context.ReadRaw<long>());"); break;
case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break;
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break;
}
}
/// <summary>
/// Emits inline string read from type code. Handles all H2Q6 (v3 wire format) string markers:
/// FixStrAscii (ASCII short, 135-166), StringAscii (ASCII long, 167),
/// StringSmall/Medium/Big (non-ASCII tiers, 91/94/103),
/// StringInternFirstSmall/Medium (interning tiers, 104/105),
/// StringInterned (cache ref, 92), StringEmpty (93), Null.
///
/// FixStrAscii is checked first as the hot path for short ASCII property names; non-ASCII
/// tier markers carry both <c>charLen</c> and <c>utf8Len</c> in fixed-width headers (1-pass decode).
/// </summary>
private static void EmitReadString(StringBuilder sb, string a, string tc, string i)
{
// FixStrAscii is the hot path — most short strings (property names) are ASCII.
sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStrAscii({tc}))");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var falen = BinaryTypeCode.DecodeFixStrAsciiLength({tc});");
sb.AppendLine($"{i} {a} = falen == 0 ? string.Empty : context.ReadAsciiBytesAsString(falen);");
sb.AppendLine($"{i}}}");
// Switch gives O(1) dispatch via JIT jump table for the remaining markers.
sb.AppendLine($"{i}else switch ({tc})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} case BinaryTypeCode.StringInterned:");
sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());");
sb.AppendLine($"{i} break;");
// H2Q6 StringSmall — non-ASCII utf8Len ≤ 255 — wire: [charLen:8][utf8Len:8][bytes], 1-pass decode.
// FastWire mode shares the marker value (=91); reader dispatches by mode.
sb.AppendLine($"{i} case BinaryTypeCode.StringSmall:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (context.FastWire)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} // Collection/dictionary element strings: markered FastWire body — int32 charLen + UTF-16 bytes.");
sb.AppendLine($"{i} // (Property-level strings take a separate markerless path in EmitReadProp; this case handles");
sb.AppendLine($"{i} // the markered StringSmall variant emitted by WriteStringWithDispatch from collection/runtime paths.)");
sb.AppendLine($"{i} var fwlen = context.ReadInt32Unsafe();");
sb.AppendLine($"{i} {a} = context.ReadStringUtf16(fwlen);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var sshdr = context.ReadTwoBytesUnsafe();");
sb.AppendLine($"{i} var sscharLen = (byte)sshdr;");
sb.AppendLine($"{i} var ssbyteLen = (byte)(sshdr >> 8);");
sb.AppendLine($"{i} {a} = ssbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(sscharLen, ssbyteLen);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
// H2Q6 StringMedium — utf8Len ≤ 65535 — single uint read packs charLen:16 + utf8Len:16
sb.AppendLine($"{i} case BinaryTypeCode.StringMedium:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var smpacked = context.ReadUInt32Unsafe();");
sb.AppendLine($"{i} var smcharLen = (ushort)smpacked;");
sb.AppendLine($"{i} var smbyteLen = (ushort)(smpacked >> 16);");
sb.AppendLine($"{i} {a} = smbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(smcharLen, smbyteLen);");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
// H2Q6 StringBig — utf8Len > 65535 — single ulong read packs charLen:32 + utf8Len:32
sb.AppendLine($"{i} case BinaryTypeCode.StringBig:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var sbpacked = context.ReadUInt64Unsafe();");
sb.AppendLine($"{i} var sbcharLen = (int)(uint)sbpacked;");
sb.AppendLine($"{i} var sbbyteLen = (int)(uint)(sbpacked >> 32);");
sb.AppendLine($"{i} {a} = sbbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(sbcharLen, sbbyteLen);");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.StringAscii:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var salen = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} {a} = salen == 0 ? string.Empty : context.ReadAsciiBytesAsString(salen);");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
// H2Q6 interning — Small tier
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstSmall:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.DisableStringCaching();");
sb.AppendLine($"{i} var iscIdx = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var ishdr = context.ReadTwoBytesUnsafe();");
sb.AppendLine($"{i} var ischarLen = (byte)ishdr;");
sb.AppendLine($"{i} var isbyteLen = (byte)(ishdr >> 8);");
sb.AppendLine($"{i} var isv = isbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(ischarLen, isbyteLen);");
sb.AppendLine($"{i} context.RegisterInternedValueAt(iscIdx, isv);");
sb.AppendLine($"{i} {a} = isv;");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
// H2Q6 interning — Medium tier — single uint header read
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.DisableStringCaching();");
sb.AppendLine($"{i} var imcIdx = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var impacked = context.ReadUInt32Unsafe();");
sb.AppendLine($"{i} var imcharLen = (ushort)impacked;");
sb.AppendLine($"{i} var imbyteLen = (ushort)(impacked >> 16);");
sb.AppendLine($"{i} var imv = imbyteLen == 0 ? string.Empty : context.ReadStringUtf8WithCharLen(imcharLen, imbyteLen);");
sb.AppendLine($"{i} context.RegisterInternedValueAt(imcIdx, imv);");
sb.AppendLine($"{i} {a} = imv;");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
sb.AppendLine($"{i} {a} = null;");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} case BinaryTypeCode.StringEmpty:");
sb.AppendLine($"{i} {a} = string.Empty;");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits inline read for a Complex property.
/// SGen reader only runs in non-metadata mode → ObjectWithMetadata never appears.
/// Compile-time ChildNeedsRefScan eliminates ObjectRefFirst/ObjectRef when provably unused.
/// Non-nullable + no ref → ZERO branches (tc consumed but ignored).
/// No SGen → runtime fallback via ReadValueGenerated.
/// </summary>
private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i)
{
if (!p.HasGeneratedWriter)
{
// No SGen reader — runtime fallback (rewind + ReadValueGenerated)
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} context._position--;");
sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}));");
sb.AppendLine($"{i}}}");
}
else
{
sb.AppendLine($"{i}context._position--;");
sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;");
}
return;
}
var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
var cast = $"({p.TypeNameForTypeof})";
if (!p.ChildNeedsRefScan)
{
// Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream
// Inline: parent creates instance, calls ReadProperties directly (mirrors EmitDirectObjectWrite)
// FixObj slot bytes (0..SlotCount-1) are also valid markers here — populate slot cache
// to keep _nextRuntimeSlot in sync with the serializer's _nextTypeSlot counter.
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc}); if ({tc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1; }}");
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
sb.AppendLine($"{i} {a} = rc_{p.Name};");
sb.AppendLine($"{i}}}");
}
else
{
// ZERO branches — tc is always Object or FixObj
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc}); if ({tc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1; }}");
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
sb.AppendLine($"{i} {a} = rc_{p.Name};");
sb.AppendLine($"{i}}}");
}
}
else
{
// Ref tracking possible — switch on tc (Object / ObjectRefFirst / [Null] / ObjectRef / <Object).
// The 4 known TypeCode constants are emitted as switch cases — the JIT compiles them as a
// jump-table for O(1) dispatch (vs the previous if-else chain's sequential ==-compares).
// The polymorphic FixObj range-check (tc < Object) goes into the default branch — runtime
// bridge path is rare on a typical SGen graph, so default fall-through is acceptable.
// Inline: parent creates instance + handles cache registration.
sb.AppendLine($"{i}switch ({tc})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} case BinaryTypeCode.Object:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
sb.AppendLine($"{i} {a} = rc_{p.Name};");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRefFirst:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var ci_{p.Name} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} context.RegisterInternedValueAt(ci_{p.Name}, rc_{p.Name});");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
sb.AppendLine($"{i} {a} = rc_{p.Name};");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
if (p.IsNullable)
sb.AppendLine($"{i} case BinaryTypeCode.Null: break;");
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRef:");
sb.AppendLine($"{i} {a} = {cast}context.GetInternedObject((int)context.ReadVarUInt())!;");
sb.AppendLine($"{i} break;");
// FixObj slot (0..SlotCount-1): same type via FixObj marker (non-meta, non-ref mode).
// Populate slot cache to keep _nextRuntimeSlot in sync with the serializer.
sb.AppendLine($"{i} default:");
sb.AppendLine($"{i} if ({tc} < BinaryTypeCode.Object)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.GetWrapper(typeof({p.TypeNameForTypeof}), {tc});");
sb.AppendLine($"{i} if ({tc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {tc} + 1;");
sb.AppendLine($"{i} var rc_{p.Name} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(rc_{p.Name}, context);");
sb.AppendLine($"{i} {a} = rc_{p.Name};");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i}}}");
}
}
/// <summary>
/// Returns true when collection element reading can be inlined (no runtime ReadValue dispatch needed).
/// </summary>
private static bool CanInlineCollectionRead(PropInfo p)
{
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter) return true;
if (p.ElementKind == PropertyTypeKind.String) return true;
if (p.ElementKind == PropertyTypeKind.Enum) return true;
if (IsMarkerless(p.ElementKind)) return true; // all primitives
return false;
}
/// <summary>
/// Emits inline read for a Collection property.
/// Known collection kind + inlineable element → inline Array loop with direct element reads.
/// Else → runtime fallback via ReadValueGenerated.
/// </summary>
private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i)
{
// Check if we can inline: known collection shape + inlineable element type
if (p.CollectionKind != null && CanInlineCollectionRead(p))
{
EmitReadCollectionInline(sb, p, a, tc, i);
return;
}
// Runtime fallback
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} context._position--;");
sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}));");
sb.AppendLine($"{i}}}");
}
else
{
sb.AppendLine($"{i}context._position--;");
sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}))!;");
}
}
/// <summary>
/// Emits inline read for a Dictionary property.
/// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...].
/// Keys and values are read inline when their types are known (primitive/string/Complex+SGen).
/// </summary>
private static void EmitReadDictionary(StringBuilder sb, PropInfo p, string a, string tc, string i)
{
var s = p.Name;
var keyType = p.DictKeyTypeName ?? "object";
var valType = p.DictValueTypeName ?? "object";
// Can we inline key/value reads?
var canInlineKey = p.DictKeyKind == PropertyTypeKind.String || IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum;
var canInlineValue = p.DictValueKind == PropertyTypeKind.String || IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum
|| (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter);
var canInline = canInlineKey || canInlineValue; // partial inline is still beneficial
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Dictionary)");
}
else
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Dictionary)");
}
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var dict_{s} = new System.Collections.Generic.Dictionary<{keyType}, {valType}>(cnt_{s});");
sb.AppendLine($"{i} for (var di_{s} = 0; di_{s} < cnt_{s}; di_{s}++)");
sb.AppendLine($"{i} {{");
// Read key
if (canInlineKey)
EmitReadDictElement(sb, p.DictKeyKind, keyType, $"dk_{s}", s, i + " ", null, false);
else
sb.AppendLine($"{i} var dk_{s} = ({keyType})AcBinaryDeserializer.ReadValueGenerated(context, typeof({keyType}))!;");
// Read value
if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter)
{
var valReader = p.DictValueWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
var vtc = $"vtc_{s}";
sb.AppendLine($"{i} var {vtc} = context.ReadByte();");
sb.AppendLine($"{i} {valType}? dv_{s} = null;");
sb.AppendLine($"{i} if ({vtc} == BinaryTypeCode.Object)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var rv_{s} = new {valType}();");
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
sb.AppendLine($"{i} dv_{s} = rv_{s};");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var rv_{s} = new {valType}();");
sb.AppendLine($"{i} context.RegisterInternedValueAt(rci_{s}, rv_{s});");
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
sb.AppendLine($"{i} dv_{s} = rv_{s};");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)");
sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;");
sb.AppendLine($"{i} else if ({vtc} != BinaryTypeCode.Null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context._position--;");
sb.AppendLine($"{i} dv_{s} = ({valType}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({valType}));");
sb.AppendLine($"{i} }}");
}
else if (canInlineValue)
EmitReadDictElement(sb, p.DictValueKind, valType, $"dv_{s}", s, i + " ", null, true);
else
sb.AppendLine($"{i} var dv_{s} = ({valType}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({valType}));");
// Add to dictionary
sb.AppendLine($"{i} if (dk_{s} != null) dict_{s}[dk_{s}] = dv_{s}!;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {a} = dict_{s};");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits inline read for a single dictionary key or value element.
/// Reads type code byte, then dispatches based on element kind.
/// </summary>
private static void EmitReadDictElement(StringBuilder sb, PropertyTypeKind kind, string typeName, string varName, string propSuffix, string i, PropInfo? p, bool isRefType)
{
var etc = $"{varName}_tc";
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
if (kind == PropertyTypeKind.String)
{
sb.AppendLine($"{i}{typeName}? {varName} = null;");
EmitReadString(sb, varName, etc, i);
}
else if (kind == PropertyTypeKind.Enum)
{
sb.AppendLine($"{i}{typeName} {varName} = default;");
sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Enum)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var eb = context.ReadByte();");
sb.AppendLine($"{i} int eiv;");
sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) eiv = BinaryTypeCode.DecodeTinyInt(eb);");
sb.AppendLine($"{i} else eiv = context.ReadVarInt();");
sb.AppendLine($"{i} {varName} = ({typeName})(object)eiv;");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({etc})) {varName} = ({typeName})(object)BinaryTypeCode.DecodeTinyInt({etc});");
}
else
{
// Primitive value type — never nullable
sb.AppendLine($"{i}{typeName} {varName} = default;");
EmitReadMarkeredValueForKind(sb, kind, varName, etc, i);
}
}
/// <summary>
/// Emits markered value read by kind only (no PropInfo needed). For dict key/value inline reads.
/// </summary>
private static void EmitReadMarkeredValueForKind(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i)
{
switch (k)
{
case PropertyTypeKind.Int32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();");
break;
case PropertyTypeKind.Int64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {a} = context.ReadVarLong();");
break;
case PropertyTypeKind.Boolean:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {a} = true;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {a} = false;");
break;
case PropertyTypeKind.Double:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {a} = context.ReadDoubleUnsafe();");
break;
case PropertyTypeKind.Single:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {a} = context.ReadSingleUnsafe();");
break;
case PropertyTypeKind.Decimal:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {a} = context.ReadDecimalUnsafe();");
break;
case PropertyTypeKind.DateTime:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {a} = context.ReadDateTimeUnsafe();");
break;
case PropertyTypeKind.Guid:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {a} = context.ReadGuidUnsafe();");
break;
case PropertyTypeKind.Byte:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (byte)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {a} = context.ReadByte();");
break;
case PropertyTypeKind.Int16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (short)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {a} = context.ReadInt16Unsafe();");
break;
case PropertyTypeKind.UInt16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ushort)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {a} = context.ReadUInt16Unsafe();");
break;
case PropertyTypeKind.UInt32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (uint)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {a} = context.ReadVarUInt();");
break;
case PropertyTypeKind.UInt64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ulong)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {a} = context.ReadVarULong();");
break;
case PropertyTypeKind.TimeSpan:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {a} = context.ReadTimeSpanUnsafe();");
break;
case PropertyTypeKind.DateTimeOffset:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {a} = context.ReadDateTimeOffsetUnsafe();");
break;
}
}
/// <summary>
/// Emits inline collection read: Array marker already consumed as tc.
/// Reads count + loops with direct element reads (Complex with SGen, or primitive/string/enum).
/// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
/// </summary>
private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i)
{
var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter;
var elemType = p.ElementFullTypeName!;
var s = p.Name;
// Null check
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Array)");
}
else
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Array)");
}
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();");
// Create collection + loop based on kind
if (p.CollectionKind == "Array")
{
sb.AppendLine($"{i} var col_{s} = new {elemType}[cnt_{s}];");
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
sb.AppendLine($"{i} {{");
if (isComplexElement)
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null);
sb.AppendLine($"{i} }}");
}
else if (p.CollectionKind == "Counted" && p.CollectionAddMethod != null)
{
// Concrete custom collection — use actual type + correct add method
if (p.CollectionHasCapacityCtor)
sb.AppendLine($"{i} var col_{s} = new {p.TypeNameForTypeof}(cnt_{s});");
else
sb.AppendLine($"{i} var col_{s} = new {p.TypeNameForTypeof}();");
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
sb.AppendLine($"{i} {{");
if (isComplexElement)
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, p.CollectionAddMethod);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod);
sb.AppendLine($"{i} }}");
}
else // List, IndexedCollection, Counted-interface → List<T> with Add
{
sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});");
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
sb.AppendLine($"{i} {{");
if (isComplexElement)
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null);
sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} {a} = col_{s};");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits per-element read inside collection loop.
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
/// !needsRefScan → only Object/Null possible → 1 branch per element.
/// </summary>
private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemTypeName, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan, string? addMethod = null)
{
var etc = $"etc_{propSuffix}";
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
var addCall = addMethod ?? "Add";
var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);";
var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = re_{propSuffix};" : $"col_{propSuffix}.{addCall}(re_{propSuffix});";
if (!needsRefScan)
{
// No ref tracking → only Object, FixObj or Null in stream — inline ReadProperties
// FixObj slot: populate slot cache to keep _nextRuntimeSlot in sync.
sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} if ({etc} < BinaryTypeCode.Object) {{ context.GetWrapper(typeof({elemTypeName}), {etc}); if ({etc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {etc} + 1; }}");
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
sb.AppendLine($"{i} {assignExpr}");
sb.AppendLine($"{i}}}");
}
else
{
// Switch on etc (Object / ObjectRefFirst / Null / ObjectRef / <Object). The JIT emits the
// 4 known TypeCode constants as a jump-table (O(1) dispatch); the polymorphic FixObj
// range-check (etc < Object) goes into the default branch. Object hot-path stays first.
sb.AppendLine($"{i}switch ({etc})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} case BinaryTypeCode.Object:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
sb.AppendLine($"{i} {assignExpr}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRefFirst:");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var ci_{propSuffix} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
sb.AppendLine($"{i} context.RegisterInternedValueAt(ci_{propSuffix}, re_{propSuffix});");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
sb.AppendLine($"{i} {assignExpr}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} case BinaryTypeCode.Null:");
sb.AppendLine($"{i} {assignNull}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i} case BinaryTypeCode.ObjectRef:");
if (isArray)
sb.AppendLine($"{i} col_{propSuffix}[{indexVar}] = {elemCast}context.GetInternedObject((int)context.ReadVarUInt())!;");
else
sb.AppendLine($"{i} col_{propSuffix}.{addCall}({elemCast}context.GetInternedObject((int)context.ReadVarUInt())!);");
sb.AppendLine($"{i} break;");
// FixObj slot (0..SlotCount-1): same type via FixObj marker.
// Populate slot cache to keep _nextRuntimeSlot in sync with the serializer.
sb.AppendLine($"{i} default:");
sb.AppendLine($"{i} if ({etc} < BinaryTypeCode.Object)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.GetWrapper(typeof({elemTypeName}), {etc});");
sb.AppendLine($"{i} if ({etc} >= context._nextRuntimeSlot) context._nextRuntimeSlot = {etc} + 1;");
sb.AppendLine($"{i} var re_{propSuffix} = new {elemTypeName}();");
sb.AppendLine($"{i} {reader}.Instance.ReadProperties(re_{propSuffix}, context);");
sb.AppendLine($"{i} {assignExpr}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} break;");
sb.AppendLine($"{i}}}");
}
}
/// <summary>
/// Emits per-element read for non-Complex collection elements (String, primitive, Enum).
/// Reads type code byte, then dispatches based on ElementKind.
/// </summary>
private static void EmitReadNonComplexCollectionElement(StringBuilder sb, PropInfo p, string indexVar, string propSuffix, string i, bool isArray, string? addMethod)
{
var addCall = addMethod ?? "Add";
var elemType = p.ElementFullTypeName!;
var colRef = $"col_{propSuffix}";
// String element FastWire markerless fast-path — same wire as property-level (int32 sentinel header).
// All FastWire string writes funnel through `WriteStringWithDispatch.FastWire = WriteStringUtf16Markerless`,
// so collection elements use the same markerless format. Skips the etc-read entirely in FastWire mode.
if (p.ElementKind == PropertyTypeKind.String)
{
var tempVar = $"sv_{propSuffix}";
sb.AppendLine($"{i}string? {tempVar};");
sb.AppendLine($"{i}if (context.FastWire)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} {tempVar} = context.ReadStringUtf16Markerless();");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var etc_{propSuffix} = context.ReadByte();");
sb.AppendLine($"{i} {tempVar} = null;");
EmitReadString(sb, tempVar, $"etc_{propSuffix}", i + " ");
sb.AppendLine($"{i}}}");
if (isArray)
sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar}!;");
else
sb.AppendLine($"{i}{colRef}.{addCall}({tempVar}!);");
return;
}
var etc = $"etc_{propSuffix}";
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
if (p.ElementKind == PropertyTypeKind.Enum)
{
// Enum element: Enum marker or TinyInt
var tempVar = $"ev_{propSuffix}";
sb.AppendLine($"{i}{elemType} {tempVar} = default;");
sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Enum)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var eb = context.ReadByte();");
sb.AppendLine($"{i} int eiv;");
sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) eiv = BinaryTypeCode.DecodeTinyInt(eb);");
sb.AppendLine($"{i} else eiv = context.ReadVarInt();");
sb.AppendLine($"{i} {tempVar} = ({elemType})(object)eiv;");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({etc})) {tempVar} = ({elemType})(object)BinaryTypeCode.DecodeTinyInt({etc});");
if (isArray)
sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar};");
else
sb.AppendLine($"{i}{colRef}.{addCall}({tempVar});");
}
else
{
// Primitive element: read markered value
var tempVar = $"pv_{propSuffix}";
sb.AppendLine($"{i}{elemType} {tempVar} = default;");
// Create a minimal PropInfo-like context for EmitReadMarkeredValue
EmitReadMarkeredValue(sb, p.ElementKind, tempVar, etc, i, p, nullable: false);
if (isArray)
sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar};");
else
sb.AppendLine($"{i}{colRef}.{addCall}({tempVar});");
}
}
/// <summary>
/// Emits markered value read for primitive types (with type code already read).
/// Handles TinyInt encoding for integer types.
/// </summary>
private static void EmitReadMarkeredValue(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i, PropInfo p, bool nullable)
{
var assign = nullable ? $"{a} = " : $"{a} = ";
switch (k)
{
case PropertyTypeKind.Int32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();");
break;
case PropertyTypeKind.Int64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {assign}context.ReadVarLong();");
break;
case PropertyTypeKind.Boolean:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {assign}true;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {assign}false;");
break;
case PropertyTypeKind.Double:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {assign}context.ReadDoubleUnsafe();");
break;
case PropertyTypeKind.Single:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {assign}context.ReadSingleUnsafe();");
break;
case PropertyTypeKind.Decimal:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {assign}context.ReadDecimalUnsafe();");
break;
case PropertyTypeKind.DateTime:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {assign}context.ReadDateTimeUnsafe();");
break;
case PropertyTypeKind.Guid:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {assign}context.ReadGuidUnsafe();");
break;
case PropertyTypeKind.Byte:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(byte)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {assign}context.ReadByte();");
break;
case PropertyTypeKind.Int16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(short)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {assign}context.ReadInt16Unsafe();");
break;
case PropertyTypeKind.UInt16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ushort)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {assign}context.ReadUInt16Unsafe();");
break;
case PropertyTypeKind.UInt32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(uint)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {assign}context.ReadVarUInt();");
break;
case PropertyTypeKind.UInt64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ulong)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {assign}context.ReadVarULong();");
break;
case PropertyTypeKind.Enum:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Enum)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var eb = context.ReadByte();");
sb.AppendLine($"{i} int ev;");
sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) ev = BinaryTypeCode.DecodeTinyInt(eb);");
sb.AppendLine($"{i} else ev = context.ReadVarInt();");
sb.AppendLine($"{i} {assign}({p.TypeNameForTypeof})(object)ev;");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({tc})) {assign}({p.TypeNameForTypeof})(object)BinaryTypeCode.DecodeTinyInt({tc});");
break;
case PropertyTypeKind.TimeSpan:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {assign}context.ReadTimeSpanUnsafe();");
break;
case PropertyTypeKind.DateTimeOffset:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {assign}context.ReadDateTimeOffsetUnsafe();");
break;
}
}
#endregion
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";
var readerRef = string.IsNullOrEmpty(ci.Namespace)
? $"{ci.ClassName}_GeneratedReader"
: $"{ci.Namespace}.{ci.ClassName}_GeneratedReader";
sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);");
sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.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;
}
/// <summary>
/// Computes whether a type needs scan pass work, split into ref tracking and string interning.
/// Uses a per-call HashSet to guard against circular references (no static cache —
/// static state is unsafe in incremental generators as it persists across builds).
/// Returns (needsRefScan, needsInternScan) — these are independent axes.
/// </summary>
private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type)
{
return ComputeNeedsScanCore(type, new HashSet<string>());
}
private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet<string> visiting)
{
// Circular reference guard: if already visiting this type, assume true (safe fallback)
var key = type.ToDisplayString();
if (!visiting.Add(key))
return (true, true, true);
// Read [AcBinarySerializable] flags
var attr = type.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
bool enableIdTracking = true, enableRefHandling = true, enableInternString = true;
if (attr != null)
{
if (attr.ConstructorArguments.Length == 1)
{
var all = (bool)attr.ConstructorArguments[0].Value!;
enableIdTracking = enableRefHandling = enableInternString = all;
}
else if (attr.ConstructorArguments.Length == 4)
{
enableIdTracking = (bool)attr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)attr.ConstructorArguments[2].Value!;
enableInternString = (bool)attr.ConstructorArguments[3].Value!;
}
}
// IId tracking: active in OnlyId + All modes
var isIId = enableIdTracking && type.AllInterfaces.Any(i =>
i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
var needsIdScan = isIId;
// Non-IId ref tracking: active only in All mode
var needsAllRefScan = !isIId && enableRefHandling;
var needsInternScan = false;
// Check properties for string interning or complex children
foreach (var p in GetAllSerializablePropertySymbols(type))
{
// Early exit: if all flags are already true, no need to check more properties
if (needsIdScan && needsAllRefScan && needsInternScan) break;
var kind = GetKind(p.Type);
// String with interning?
if (enableInternString && kind == PropertyTypeKind.String)
{
var internAttr = p.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute");
if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!))
needsInternScan = true;
}
// Complex child → recurse
if (kind == PropertyTypeKind.Complex)
{
var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type;
var childFlags = ComputeNeedsScanCore(resolved, visiting);
needsIdScan |= childFlags.needsIdScan;
needsAllRefScan |= childFlags.needsAllRefScan;
needsInternScan |= childFlags.needsInternScan;
}
// Collection → check element type
if (kind == PropertyTypeKind.Collection)
{
var elemType = GetCollectionElementType(p.Type);
if (elemType != null)
{
var elemKind = GetKind(elemType);
if (enableInternString && elemKind == PropertyTypeKind.String)
needsInternScan = true;
if (elemKind == PropertyTypeKind.Complex)
{
var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType;
var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting);
needsIdScan |= elemFlags.needsIdScan;
needsAllRefScan |= elemFlags.needsAllRefScan;
needsInternScan |= elemFlags.needsInternScan;
}
}
}
// Dictionary → check key and value types
if (kind == PropertyTypeKind.Dictionary)
{
var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type);
if (keyType != null && enableInternString && GetKind(keyType) == PropertyTypeKind.String)
needsInternScan = true;
if (valueType != null)
{
var valKind = GetKind(valueType);
if (enableInternString && valKind == PropertyTypeKind.String)
needsInternScan = true;
if (valKind == PropertyTypeKind.Complex)
{
var resolvedVal = valueType is INamedTypeSymbol nv ? nv.OriginalDefinition : valueType;
var valFlags = ComputeNeedsScanCore(resolvedVal, visiting);
needsIdScan |= valFlags.needsIdScan;
needsAllRefScan |= valFlags.needsAllRefScan;
needsInternScan |= valFlags.needsInternScan;
}
}
}
}
return (needsIdScan, needsAllRefScan, needsInternScan);
}
#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:
/// derived → base, each level sorted alphabetically, with ignore attribute filtering.
/// </summary>
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
{
// Use hierarchy-walking helper — order matches runtime TypeMetadataBase
var props = GetAllSerializablePropertySymbols(resolvedType);
return props.Select(p => ComputeFnvHash(p.Name)).ToArray();
}
#endregion
/// <summary>
/// Collects all serializable property symbols from the full inheritance hierarchy.
/// Order matches runtime TypeMetadataBase.GetUnfilteredProperties exactly:
/// derived → base, each level sorted alphabetically by name.
/// Filters: public, get+set, non-indexer, non-static, no ignore attributes.
/// Deduplicates by name (most-derived override wins).
/// </summary>
private static List<IPropertySymbol> GetAllSerializablePropertySymbols(ITypeSymbol typeSymbol)
{
var result = new List<IPropertySymbol>();
var seen = new HashSet<string>();
for (var currentType = typeSymbol as INamedTypeSymbol;
currentType != null && currentType.SpecialType != SpecialType.System_Object;
currentType = currentType.BaseType)
{
var levelProps = new List<IPropertySymbol>();
foreach (var member in currentType.GetMembers())
{
if (member is IPropertySymbol p &&
p.DeclaredAccessibility == Accessibility.Public &&
p.GetMethod != null && p.SetMethod != null &&
!p.IsIndexer && !p.IsStatic &&
seen.Add(p.Name)) // dedup: most-derived wins
{
var hasIgnore = p.GetAttributes().Any(a =>
{
var name = a.AttributeClass?.Name ?? "";
return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute";
});
if (hasIgnore) continue;
levelProps.Add(p);
}
}
// Sort each level alphabetically — matches runtime OrderBy(p => p.Name, Ordinal)
levelProps.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
result.AddRange(levelProps);
}
return result;
}
#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;
// Dictionary detection: must come before IEnumerable<T> (Dictionary implements both)
if (type is INamedTypeSymbol dictNt && dictNt.IsGenericType)
{
var orig = dictNt.OriginalDefinition.ToDisplayString();
if (orig == "System.Collections.Generic.IDictionary<TKey, TValue>" ||
orig == "System.Collections.Generic.Dictionary<TKey, TValue>" ||
dictNt.AllInterfaces.Any(ifc => ifc.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary<TKey, TValue>"))
return PropertyTypeKind.Dictionary;
}
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&lt;T&gt;, T[], IList&lt;T&gt;, IEnumerable&lt;T&gt;.
/// 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;
}
/// <summary>
/// Extracts key and value types from Dictionary&lt;K,V&gt; or IDictionary&lt;K,V&gt;.
/// </summary>
private static (ITypeSymbol? keyType, ITypeSymbol? valueType) GetDictionaryKeyValueTypes(ITypeSymbol type)
{
if (type is INamedTypeSymbol nt && nt.IsGenericType)
{
var orig = nt.OriginalDefinition.ToDisplayString();
if (orig == "System.Collections.Generic.Dictionary<TKey, TValue>" ||
orig == "System.Collections.Generic.IDictionary<TKey, TValue>")
return (nt.TypeArguments[0], nt.TypeArguments[1]);
var iface = nt.AllInterfaces.FirstOrDefault(i =>
i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary<TKey, TValue>");
if (iface != null)
return (iface.TypeArguments[0], iface.TypeArguments[1]);
}
return (null, 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&lt;T&gt;</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; }
/// <summary>True if EnablePropertyFilterFeature is enabled — controls per-property HasPropertyFilter
/// guard emission in WriteProperties / ScanObject. When false, the filter check is omitted entirely
/// → leaner generated code on the hot path (typical for high-throughput types that never use a filter).</summary>
public bool EnablePropertyFilter { get; }
/// <summary>When true, type subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool NeedsIdScan { get; }
/// <summary>When true, type subtree has non-IId ref tracking (active only in All mode).</summary>
public bool NeedsAllRefScan { get; }
/// <summary>When true, type subtree needs string interning scan.</summary>
public bool NeedsInternScan { get; }
/// <summary>Derived: NeedsIdScan || NeedsAllRefScan.</summary>
public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan;
/// <summary>Derived: any scan axis active.</summary>
public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan;
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool needsIdScan, bool needsAllRefScan, bool needsInternScan)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
}
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 when declared property type is System.Object. Runtime type dispatch needed.</summary>
public bool IsObjectDeclaredType { 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&lt;T&gt; → 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&lt;T&gt;.</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", "IndexedCollection", "Counted", 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; }
/// <summary>Add method for Counted concrete collections. null → List&lt;T&gt;.Add(), "Add" → HashSet/SortedSet, "Enqueue" → Queue, "AddLast" → LinkedList.</summary>
public string? CollectionAddMethod { get; }
/// <summary>True if the concrete Counted collection has a capacity constructor (HashSet, Queue).</summary>
public bool CollectionHasCapacityCtor { get; }
// Dictionary metadata — set when TypeKind == Dictionary
/// <summary>Key type kind for dictionary properties.</summary>
public PropertyTypeKind DictKeyKind { get; }
/// <summary>Value type kind for dictionary properties.</summary>
public PropertyTypeKind DictValueKind { get; }
/// <summary>Key type name for generated code.</summary>
public string? DictKeyTypeName { get; }
/// <summary>Value type name for generated code.</summary>
public string? DictValueTypeName { get; }
/// <summary>True if dictionary value type has [AcBinarySerializable].</summary>
public bool DictValueHasGeneratedWriter { get; }
/// <summary>Generated writer class name for dictionary value type.</summary>
public string? DictValueWriterClassName { get; }
/// <summary>True if dictionary value type implements IId&lt;T&gt;.</summary>
public bool DictValueIsIId { get; }
/// <summary>When false, dict value type skips inline metadata.</summary>
public bool DictValueEnableMetadata { get; }
/// <summary>FNV-1a hash of dict value type name.</summary>
public int DictValueTypeNameHash { get; }
/// <summary>FNV-1a hashes of dict value type's properties.</summary>
public int[]? DictValuePropertyHashes { get; }
/// <summary>When true, dict value subtree has IId types needing scan.</summary>
public bool DictValueNeedsIdScan { get; }
/// <summary>When true, dict value subtree has non-IId ref tracking.</summary>
public bool DictValueNeedsAllRefScan { get; }
/// <summary>When true, dict value subtree needs string interning scan.</summary>
public bool DictValueNeedsInternScan { get; }
/// <summary>Derived: DictValueNeedsIdScan || DictValueNeedsAllRefScan.</summary>
public bool DictValueNeedsRefScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan;
/// <summary>Derived: any dict value scan axis active.</summary>
public bool DictValueNeedsScan => DictValueNeedsIdScan || DictValueNeedsAllRefScan || DictValueNeedsInternScan;
// 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; }
/// <summary>When true, child subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool ChildNeedsIdScan { get; }
/// <summary>When true, child subtree has non-IId ref tracking (active only in All mode).</summary>
public bool ChildNeedsAllRefScan { get; }
/// <summary>When true, child subtree needs string interning scan.</summary>
public bool ChildNeedsInternScan { get; }
/// <summary>Derived: ChildNeedsIdScan || ChildNeedsAllRefScan.</summary>
public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan;
/// <summary>Derived: any child scan axis active.</summary>
public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan;
/// <summary>When true, element subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool ElementNeedsIdScan { get; }
/// <summary>When true, element subtree has non-IId ref tracking (active only in All mode).</summary>
public bool ElementNeedsAllRefScan { get; }
/// <summary>When true, element subtree needs string interning scan.</summary>
public bool ElementNeedsInternScan { get; }
/// <summary>Derived: ElementNeedsIdScan || ElementNeedsAllRefScan.</summary>
public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan;
/// <summary>Derived: any element scan axis active.</summary>
public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan;
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
bool isObjectDeclaredType = false,
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,
string? collectionAddMethod = null, bool collectionHasCapacityCtor = false,
PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown, PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown,
string? dictKeyTypeName = null, string? dictValueTypeName = null,
bool dictValueHasGeneratedWriter = false, string? dictValueWriterClassName = null,
bool dictValueIsIId = false, bool dictValueEnableMetadata = true,
int dictValueTypeNameHash = 0, int[]? dictValuePropertyHashes = null,
bool dictValueNeedsIdScan = true, bool dictValueNeedsAllRefScan = true, bool dictValueNeedsInternScan = true,
int childTypeNameHash = 0, int[]? childPropertyHashes = null,
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null,
bool childEnableMetadata = true, bool elementEnableMetadata = true,
bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true,
bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true)
{
Name = n;
TypeName = tn;
TypeNameForTypeof = tnForTypeof;
TypeKind = tk;
IsNullable = nullable;
IsObjectDeclaredType = isObjectDeclaredType;
HasGeneratedWriter = hasGeneratedWriter;
IsIId = isIId;
WriterClassName = writerClassName;
IdTypeName = idTypeName;
ElementKind = elementKind;
ElementHasGeneratedWriter = elementHasGenWriter;
ElementIsIId = elementIsIId;
ElementWriterClassName = elementWriterClassName;
ElementIdTypeName = elementIdTypeName;
CollectionKind = collectionKind;
ElementFullTypeName = elementFullTypeName;
CollectionAddMethod = collectionAddMethod;
CollectionHasCapacityCtor = collectionHasCapacityCtor;
DictKeyKind = dictKeyKind;
DictValueKind = dictValueKind;
DictKeyTypeName = dictKeyTypeName;
DictValueTypeName = dictValueTypeName;
DictValueHasGeneratedWriter = dictValueHasGeneratedWriter;
DictValueWriterClassName = dictValueWriterClassName;
DictValueIsIId = dictValueIsIId;
DictValueEnableMetadata = dictValueEnableMetadata;
DictValueTypeNameHash = dictValueTypeNameHash;
DictValuePropertyHashes = dictValuePropertyHashes;
DictValueNeedsIdScan = dictValueNeedsIdScan;
DictValueNeedsAllRefScan = dictValueNeedsAllRefScan;
DictValueNeedsInternScan = dictValueNeedsInternScan;
ChildTypeNameHash = childTypeNameHash;
ChildPropertyHashes = childPropertyHashes;
ElementTypeNameHash = elementTypeNameHash;
ElementPropertyHashes = elementPropertyHashes;
ChildEnableMetadata = childEnableMetadata;
ElementEnableMetadata = elementEnableMetadata;
ChildNeedsIdScan = childNeedsIdScan;
ChildNeedsAllRefScan = childNeedsAllRefScan;
ChildNeedsInternScan = childNeedsInternScan;
ElementNeedsIdScan = elementNeedsIdScan;
ElementNeedsAllRefScan = elementNeedsAllRefScan;
ElementNeedsInternScan = elementNeedsInternScan;
// 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, Dictionary,
NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64,
NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime,
NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum
}