Refactor AcBinarySourceGenerator into partial classes
Split AcBinarySourceGenerator.cs into multiple partial class files for improved maintainability and clarity. Each major concern (models, type analysis, class info extraction, writer/reader emit, diagnostics, and module init) now resides in its own file. Updated .gitignore and settings.local.json to support the new structure. No functional changes to generator output; this is a pure organizational refactor.
This commit is contained in:
parent
638be8c52e
commit
d9ab3940eb
|
|
@ -85,7 +85,25 @@
|
|||
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core log -p --all -S \"MaxDepth\" -- AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs)",
|
||||
"Bash(git -C H:/Applications/Aycode/Source/AyCode.Core show ac6e66f^:AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs)",
|
||||
"Bash(ls -la \"H:\\\\Applications\\\\Aycode\\\\Source\\\\\" 2>&1 && echo \"---\" && ls -la \"H:\\\\Applications\\\\Aycode\\\\\" 2>&1)",
|
||||
"Bash(dotnet publish *)"
|
||||
"Bash(dotnet publish *)",
|
||||
"Bash(head -n 2998 AcBinarySourceGenerator.cs)",
|
||||
"Bash(echo \"\")",
|
||||
"Bash(mv AcBinarySourceGenerator.cs.tmp AcBinarySourceGenerator.cs)",
|
||||
"Read(//h/h/Applications/Aycode/Source/AyCode.Core/AyCode.Core.Serializers.SourceGenerator/**)",
|
||||
"Bash(head -n 2644 AcBinarySourceGenerator.cs)",
|
||||
"Bash(cat)",
|
||||
"Bash(mv tmp.cs AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR < 407 || NR > 533 { print }' AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR == 406 { print; print \"\"; print \" // DetectAndReportCycles + DetectAndReportPolymorphicMisuse + ShortTypeName moved to\"; print \" // AcBinarySourceGenerator.Diagnostics.cs.\"; next } { print }' tmp.cs)",
|
||||
"Bash(rm tmp.cs)",
|
||||
"Bash(cat /tmp/getclassinfo-body.txt)",
|
||||
"Bash(echo \"}\")",
|
||||
"Bash(awk 'NR < 44 || NR > 387 { print }' AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR == 43 { print; print \"\"; print \" // GetClassInfo extraction pass moved to AcBinarySourceGenerator.GetClassInfo.cs.\"; next } { print }' tmp.cs)",
|
||||
"Bash(awk 'NR < 69 || NR > 1257 { print }' AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR == 68 { print; print \"\"; print \" // Writer-side emit pass \\(GenWriter + GenScanProperties + EmitProp + EmitScan* + EmitDirect*Write +\"; print \" // EmitSkip + EmitVal + EmitMarkerless + helpers\\) moved to AcBinarySourceGenerator.GenWriter.cs.\"; next } { print }' tmp.cs)",
|
||||
"Bash(awk 'NR < 73 || NR > 930 { print }' AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Build-time diagnostics for the AcBinary source generator.
|
||||
///
|
||||
/// <para><b>Registered diagnostics</b>:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>ACBIN001</c> — <see cref="CircularReferenceWarning"/>: detects circular type references
|
||||
/// among <c>[AcBinarySerializable]</c> types and warns the developer to consider ref-handling mode.</item>
|
||||
/// <item><c>ACBIN002</c> — <see cref="PolymorphicPropertyWithFeatureDisabledError"/>: ACCORE-BIN-I-T7K3
|
||||
/// compile-time guard. Fires when a type opts out of <c>EnablePolymorphDetectFeature</c> AND still
|
||||
/// declares an <c>object</c> property — the SGen-emitted writer would silently corrupt the wire.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public partial class AcBinarySourceGenerator
|
||||
{
|
||||
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 the type opts out of the feature via
|
||||
/// <c>[AcBinarySerializable(enablePolymorphDetectFeature: 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 polymorph-detect feature on the type
|
||||
/// (<c>[AcBinarySerializable(...enablePolymorphDetectFeature: true)]</c> — default true).
|
||||
/// 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 EnablePolymorphDetectFeature",
|
||||
messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but EnablePolymorphDetectFeature is disabled on the type. " +
|
||||
"The generated writer would silently corrupt the wire on round-trip. " +
|
||||
"To fix: (1) enable EnablePolymorphDetectFeature on [AcBinarySerializable], (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].",
|
||||
category: "AcBinarySerializer",
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
/// <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 whose <c>EnablePolymorphDetectFeature</c> is <c>false</c>.
|
||||
/// Per-class gating: types with the feature enabled (default) skip the check entirely; only
|
||||
/// opt-out types are scanned for misuse.
|
||||
/// </summary>
|
||||
private static void DetectAndReportPolymorphicMisuse(List<SerializableClassInfo> classes, SourceProductionContext spc)
|
||||
{
|
||||
foreach (var ci in classes)
|
||||
{
|
||||
if (ci.EnablePolymorphDetect) continue; // Feature enabled → polymorphic prefix is emitted, no misuse possible.
|
||||
|
||||
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(" → ", 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Module-init emit pass: generates the static class with a <c>[ModuleInitializer]</c> method that
|
||||
/// auto-registers every generated writer / reader instance into the runtime registries
|
||||
/// (<c>AcBinarySerializer.RegisterGeneratedWriter</c> / <c>AcBinaryDeserializer.RegisterGeneratedReader</c>).
|
||||
/// Emitted once per compilation as <c>AcBinaryGeneratedWriters_Init.g.cs</c>.
|
||||
/// </summary>
|
||||
public partial class AcBinarySourceGenerator
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,884 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Reader-side emit pass: generates the <c>IGeneratedBinaryReader</c> implementation for each
|
||||
/// <c>[AcBinarySerializable]</c> type. Emits <c>ReadProperties</c> (inline property reads with marker
|
||||
/// dispatch) and <c>ReadObject</c> (entry point with cache-index registration).
|
||||
///
|
||||
/// <para>Sub-passes:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>EmitReadProp</c> — per-property read emit (markerless + markered variants).</item>
|
||||
/// <item><c>EmitReadString</c> — H2Q6 string-tier marker dispatch (FixStrAscii + tier-tables +
|
||||
/// intern cases gated by <c>EnableInternStringFeature</c>).</item>
|
||||
/// <item><c>EmitReadComplex</c> — Object / ObjectRef* / FixObj-slot dispatch for IId-typed children.</item>
|
||||
/// <item><c>EmitReadCollection</c> / <c>EmitReadCollectionInline</c> / <c>EmitReadCollectionElement</c> /
|
||||
/// <c>EmitReadNonComplexCollectionElement</c> — collection-shape inline reading.</item>
|
||||
/// <item><c>EmitReadDictionary</c> / <c>EmitReadDictElement</c> — dict-shape inline reading.</item>
|
||||
/// <item><c>EmitReadMarkeredValue</c> / <c>EmitReadMarkeredValueForKind</c> — primitive value-with-marker reads.</item>
|
||||
/// <item><c>EmitReadMarkerless</c> — markerless primitive reads (FastMode + per-property markerless types).</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public partial class AcBinarySourceGenerator
|
||||
{
|
||||
#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, ci.EnableInternString);
|
||||
}
|
||||
|
||||
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, bool enableInternString)
|
||||
{
|
||||
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 + " ", enableInternString);
|
||||
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 + " ", enableInternString);
|
||||
break;
|
||||
|
||||
case PropertyTypeKind.Complex:
|
||||
EmitReadComplex(sb, p, a, tc, i + " ");
|
||||
break;
|
||||
|
||||
case PropertyTypeKind.Collection:
|
||||
EmitReadCollection(sb, p, a, tc, i + " ", enableInternString);
|
||||
break;
|
||||
|
||||
case PropertyTypeKind.Dictionary:
|
||||
EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString);
|
||||
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, bool enableInternString)
|
||||
{
|
||||
// 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}{{");
|
||||
// Interning case (2nd+ occurrence ref) — only emit when EnableInternStringFeature is enabled
|
||||
// on this type. When disabled, the writer never emits StringInterned markers for this type's
|
||||
// properties, so the reader doesn't need to handle them. ACCORE-BIN-T-K9M3 Phase C.
|
||||
if (enableInternString)
|
||||
{
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInterned:");
|
||||
sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());");
|
||||
sb.AppendLine($"{i} break;");
|
||||
}
|
||||
// H2Q6 string-tier markers + StringAscii. Wire-decode body is shared with the runtime path
|
||||
// (TypeReaderTable + cross-type populate) — see context.ReadStringSmall/Medium/Big, ReadPlainStringAscii.
|
||||
// These markers are feature-independent: writer emits them on any string property regardless of
|
||||
// intern setting (intern is opt-in per-property via [AcStringIntern] + InternBit).
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringSmall:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadStringSmall();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringMedium:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadStringMedium();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringBig:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadStringBig();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringAscii:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadPlainStringAscii();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
// Interning first-occurrence cases — see comment above.
|
||||
if (enableInternString)
|
||||
{
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstSmall:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringSmall();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
sb.AppendLine($"{i} case BinaryTypeCode.StringInternFirstMedium:");
|
||||
sb.AppendLine($"{i} {a} = context.ReadAndRegisterInternedStringMedium();");
|
||||
sb.AppendLine($"{i} break;");
|
||||
}
|
||||
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, bool enableInternString)
|
||||
{
|
||||
// Check if we can inline: known collection shape + inlineable element type
|
||||
if (p.CollectionKind != null && CanInlineCollectionRead(p))
|
||||
{
|
||||
EmitReadCollectionInline(sb, p, a, tc, i, enableInternString);
|
||||
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, bool enableInternString)
|
||||
{
|
||||
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, enableInternString);
|
||||
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, enableInternString);
|
||||
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, bool enableInternString)
|
||||
{
|
||||
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, enableInternString);
|
||||
}
|
||||
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, bool enableInternString)
|
||||
{
|
||||
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, enableInternString);
|
||||
else
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null, enableInternString);
|
||||
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, enableInternString, p.CollectionAddMethod);
|
||||
else
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod, enableInternString);
|
||||
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, enableInternString);
|
||||
else
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null, enableInternString);
|
||||
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, bool enableInternString, 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, bool enableInternString)
|
||||
{
|
||||
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 + " ", enableInternString);
|
||||
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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,363 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Class-info extraction pass — transforms a Roslyn <see cref="GeneratorAttributeSyntaxContext"/>
|
||||
/// (a class/struct annotated with <c>[AcBinarySerializable]</c>) into the <see cref="SerializableClassInfo"/>
|
||||
/// model consumed by the emit passes (writer / reader / scan / init).
|
||||
///
|
||||
/// <para>Reads the attribute's feature flags (1-, 4-, 5-, 6-bool ctor variants), walks the inheritance
|
||||
/// hierarchy via <c>GetAllSerializablePropertySymbols</c>, and computes per-property metadata: kind,
|
||||
/// nullability, intern eligibility, complex / collection / dictionary element types, generated-writer
|
||||
/// pointers, FNV hashes for inline-metadata, and recursive scan-need flags.</para>
|
||||
/// </summary>
|
||||
public partial class AcBinarySourceGenerator
|
||||
{
|
||||
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 enablePolymorphDetect = 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;
|
||||
enablePolymorphDetect = all;
|
||||
}
|
||||
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
|
||||
{
|
||||
// Four bool ctor: (metadata, idTracking, refHandling, internString) — filter + polymorph default 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) — polymorph 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!;
|
||||
enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
|
||||
}
|
||||
else if (binarySerializableAttr.ConstructorArguments.Length == 6)
|
||||
{
|
||||
// Six bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter, polymorphDetect)
|
||||
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!;
|
||||
enablePolymorphDetect = (bool)binarySerializableAttr.ConstructorArguments[5].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, enablePolymorphDetect, enableInternString, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
// Source-generator model types — pure POCO data carriers describing a `[AcBinarySerializable]` type
|
||||
// and its serializable properties. Consumed by all emit / diagnostics / analysis passes in the partial
|
||||
// `AcBinarySourceGenerator` class (see siblings `*.GenWriter.cs`, `*.GenReader.cs`, etc.).
|
||||
|
||||
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>True if EnablePolymorphDetectFeature is enabled — controls <c>ObjectWithTypeName</c> + AQN
|
||||
/// prefix emit on <c>System.Object</c>-declared properties. When false, the prefix is suppressed
|
||||
/// AND ACBIN002 fires at build time if such a property exists on this type (guarding against silent
|
||||
/// wire corruption). Opt-out is intentional: dev guarantees no polymorphic <c>object</c> property
|
||||
/// will be serialized on this type, or all such properties are excluded via <c>[AcBinaryIgnore]</c>.</summary>
|
||||
public bool EnablePolymorphDetect { get; }
|
||||
/// <summary>True if EnableInternStringFeature is enabled — controls whether the SGen-emitted reader
|
||||
/// contains <c>StringInterned</c>, <c>StringInternFirstSmall</c>, <c>StringInternFirstMedium</c> case-ágakat.
|
||||
/// When false, those cases are omitted (the writer doesn't emit those markers when intern is off,
|
||||
/// so the reader doesn't need to handle them). Leaner switch dispatch (~30% fewer string cases) +
|
||||
/// smaller IL → faster cold-start JIT + smaller AOT publish.</summary>
|
||||
public bool EnableInternString { 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 enablePolymorphDetect, bool enableInternString, 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; EnablePolymorphDetect = enablePolymorphDetect; EnableInternString = enableInternString; 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
|
||||
}
|
||||
|
|
@ -0,0 +1,368 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace AyCode.Core.Serializers.SourceGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Type-analysis utilities for the AcBinary source generator: kind detection, FNV-1a hashing,
|
||||
/// symbol enumeration, name flattening, and recursive scan-need computation. All methods are
|
||||
/// pure functions over Roslyn symbols (no mutable state, safe to call from any emit pass).
|
||||
/// </summary>
|
||||
public partial class AcBinarySourceGenerator
|
||||
{
|
||||
/// <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>
|
||||
/// 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue