3276 lines
176 KiB
C#
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<T> / T[] where T is a Complex type with generated writer.
|
|
/// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely.
|
|
/// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]...
|
|
/// Handles both UseMetadata=true and false inline — no fallback to WriteValueGenerated.
|
|
/// </summary>
|
|
private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.ElementWriterClassName;
|
|
|
|
// 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<T>, T[], IList<T>, IEnumerable<T>.
|
|
/// Returns null if the element type cannot be determined.
|
|
/// </summary>
|
|
private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type)
|
|
{
|
|
// T[] → element type
|
|
if (type is IArrayTypeSymbol arrayType)
|
|
return arrayType.ElementType;
|
|
|
|
// Generic collections: List<T>, IList<T>, ICollection<T>, IEnumerable<T>
|
|
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
|
|
{
|
|
// Direct: List<T>, HashSet<T>, etc. — first type argument
|
|
var iface = namedType.AllInterfaces
|
|
.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
|
|
if (iface != null)
|
|
return iface.TypeArguments[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts key and value types from Dictionary<K,V> or IDictionary<K,V>.
|
|
/// </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<T></summary>
|
|
public bool IsIId { get; }
|
|
/// <summary>The Id type name ("int", "long", "System.Guid") if IsIId, null otherwise</summary>
|
|
public string? IdTypeName { get; }
|
|
/// <summary>True if EnableRefHandlingFeature is enabled — controls non-IId All mode tracking code emission.</summary>
|
|
public bool EnableRefHandling { get; }
|
|
/// <summary>FNV-1a hash of ClassName (matches runtime SourceType.Name hash)</summary>
|
|
public int TypeNameHash { get; }
|
|
/// <summary>FNV-1a hash of each property name, in property order</summary>
|
|
public int[] PropertyNameHashes { get; }
|
|
/// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
|
|
public bool EnableMetadata { get; }
|
|
/// <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<T> → needs ref tracking in write pass.</summary>
|
|
public bool IsIId { get; }
|
|
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
|
public string? WriterClassName { get; }
|
|
/// <summary>Id type name ("int", "long", "System.Guid") for IId child types. Null if not IId.</summary>
|
|
public string? IdTypeName { get; }
|
|
|
|
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
|
|
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
|
|
public PropertyTypeKind ElementKind { get; }
|
|
/// <summary>True if collection element type has [AcBinarySerializable].</summary>
|
|
public bool ElementHasGeneratedWriter { get; }
|
|
/// <summary>True if collection element type implements IId<T>.</summary>
|
|
public bool ElementIsIId { get; }
|
|
/// <summary>Generated writer class name for collection element type.</summary>
|
|
public string? ElementWriterClassName { get; }
|
|
/// <summary>Id type name for collection element IId types. Null if not IId.</summary>
|
|
public string? ElementIdTypeName { get; }
|
|
/// <summary>Collection type: "List", "Array", "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<T>.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<T>.</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
|
|
}
|