Add inline dictionary read support to AcBinary generator

Enable optimized codegen for Dictionary<TKey, TValue> and IDictionary<TKey, TValue> properties. The generator now analyzes key/value types and emits direct binary read loops for primitive, string, enum, and complex types with generated readers. PropInfo is extended with dictionary metadata. Improves performance and type safety by eliminating reflection and runtime type dispatch for supported dictionary types. Also refactors collection reading logic and updates dependency graph and scanning for dictionary value types.
This commit is contained in:
Loretta 2026-02-28 14:38:50 +01:00
parent 8f665c5c4d
commit 2aa2eecccd
1 changed files with 447 additions and 16 deletions

View File

@ -181,6 +181,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
string? elemWriterClassName = null; string? elemWriterClassName = null;
string? elemIdTypeName = null; string? elemIdTypeName = null;
string? collKind = null; string? collKind = null;
string? collAddMethod = null;
bool collHasCapacityCtor = false;
string? elemFullTypeName = null; string? elemFullTypeName = null;
int elementTypeNameHash = 0; int elementTypeNameHash = 0;
int[]? elementPropertyHashes = null; int[]? elementPropertyHashes = null;
@ -211,6 +213,22 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
"System.Collections.Generic.LinkedList<T>" => "Counted", "System.Collections.Generic.LinkedList<T>" => "Counted",
_ => null _ => 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 // For Complex element types, check for generated writer
@ -250,6 +268,43 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
} }
} }
// 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;
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.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";
}
}
}
}
properties.Add(new PropInfo( properties.Add(new PropInfo(
p.Name, p.Name,
typeDisplayName, typeDisplayName,
@ -259,6 +314,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
p.Type.SpecialType == SpecialType.System_Object, p.Type.SpecialType == SpecialType.System_Object,
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
collAddMethod, collHasCapacityCtor,
dictKeyKind, dictValueKind, dictKeyTypeName, dictValueTypeName, dictValueHasGenWriter, dictValueWriterClassName,
childTypeNameHash, childPropertyHashes, childTypeNameHash, childPropertyHashes,
elementTypeNameHash, elementPropertyHashes, elementTypeNameHash, elementPropertyHashes,
propEnableMetadata, elemEnableMetadata, propEnableMetadata, elemEnableMetadata,
@ -342,6 +399,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target)) if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target))
edges.Add(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; adjacency[ci.FullTypeName] = edges;
} }
@ -542,7 +604,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var scanProps = ci.Properties.Where(p => var scanProps = ci.Properties.Where(p =>
p.TypeKind == PropertyTypeKind.String || p.TypeKind == PropertyTypeKind.String ||
p.TypeKind == PropertyTypeKind.Complex || p.TypeKind == PropertyTypeKind.Complex ||
p.TypeKind == PropertyTypeKind.Collection).ToList(); p.TypeKind == PropertyTypeKind.Collection ||
p.TypeKind == PropertyTypeKind.Dictionary).ToList();
// Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed // Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed
var hasStringScan = scanProps.Any(p => var hasStringScan = scanProps.Any(p =>
@ -681,6 +744,16 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
break; break;
case PropertyTypeKind.Dictionary:
// Dictionary: always runtime fallback via WriteValueGenerated (which calls WriteDictionary)
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
}
else
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
break;
default: default:
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i);
break; break;
@ -774,6 +847,17 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
case PropertyTypeKind.Collection: case PropertyTypeKind.Collection:
EmitScanCollection(sb, p, a, i); EmitScanCollection(sb, p, a, i);
break; break;
case PropertyTypeKind.Dictionary:
// Dictionary scan: runtime fallback via ScanValueGenerated
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} != null)");
sb.AppendLine($"{i} AcBinarySerializer.ScanValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
}
else
sb.AppendLine($"{i}AcBinarySerializer.ScanValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
break;
} }
if (!IsMarkerless(p.TypeKind)) if (!IsMarkerless(p.TypeKind))
@ -1500,6 +1584,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
EmitReadCollection(sb, p, a, tc, i + " "); EmitReadCollection(sb, p, a, tc, i + " ");
break; break;
case PropertyTypeKind.Dictionary:
EmitReadDictionary(sb, p, a, tc, i + " ");
break;
default: default:
// Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback // Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback
sb.AppendLine($"{i} context._position--;"); sb.AppendLine($"{i} context._position--;");
@ -1631,15 +1719,27 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
} }
} }
/// <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> /// <summary>
/// Emits inline read for a Collection property. /// Emits inline read for a Collection property.
/// Complex element with SGen + known collection kind → inline Array loop with direct element reader calls. /// Known collection kind + inlineable element → inline Array loop with direct element reads.
/// Else → runtime fallback via ReadValueGenerated. /// Else → runtime fallback via ReadValueGenerated.
/// </summary> /// </summary>
private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i) private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i)
{ {
// Check if we can inline: need SGen element reader + known collection shape + Array marker // Check if we can inline: known collection shape + inlineable element type
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null) if (p.CollectionKind != null && CanInlineCollectionRead(p))
{ {
EmitReadCollectionInline(sb, p, a, tc, i); EmitReadCollectionInline(sb, p, a, tc, i);
return; return;
@ -1662,16 +1762,186 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
} }
} }
/// <summary>
/// Emits inline read for a Dictionary property.
/// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...].
/// Keys and values are read inline when their types are known (primitive/string/Complex+SGen).
/// </summary>
private static void EmitReadDictionary(StringBuilder sb, PropInfo p, string a, string tc, string i)
{
var s = p.Name;
var keyType = p.DictKeyTypeName ?? "object";
var valType = p.DictValueTypeName ?? "object";
// Can we inline key/value reads?
var canInlineKey = p.DictKeyKind == PropertyTypeKind.String || IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum;
var canInlineValue = p.DictValueKind == PropertyTypeKind.String || IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum
|| (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter);
var canInline = canInlineKey || canInlineValue; // partial inline is still beneficial
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Dictionary)");
}
else
{
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Dictionary)");
}
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var dict_{s} = new System.Collections.Generic.Dictionary<{keyType}, {valType}>(cnt_{s});");
sb.AppendLine($"{i} var nd_{s} = depth + 1;");
sb.AppendLine($"{i} for (var di_{s} = 0; di_{s} < cnt_{s}; di_{s}++)");
sb.AppendLine($"{i} {{");
// Read key
if (canInlineKey)
EmitReadDictElement(sb, p.DictKeyKind, keyType, $"dk_{s}", s, i + " ", null, false);
else
sb.AppendLine($"{i} var dk_{s} = ({keyType})AcBinaryDeserializer.ReadValueGenerated(context, typeof({keyType}), nd_{s})!;");
// 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} dv_{s} = ({valType}){valReader}.Instance.ReadObject(context, nd_{s}, -1)!;");
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)");
sb.AppendLine($"{i} dv_{s} = ({valType}){valReader}.Instance.ReadObject(context, nd_{s}, (int)context.ReadVarUInt())!;");
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}), nd_{s});");
sb.AppendLine($"{i} }}");
}
else if (canInlineValue)
EmitReadDictElement(sb, p.DictValueKind, valType, $"dv_{s}", s, i + " ", null, true);
else
sb.AppendLine($"{i} var dv_{s} = ({valType}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({valType}), nd_{s});");
// Add to dictionary
sb.AppendLine($"{i} if (dk_{s} != null) dict_{s}[dk_{s}] = dv_{s}!;");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} {a} = dict_{s};");
sb.AppendLine($"{i}}}");
}
/// <summary>
/// Emits inline read for a single dictionary key or value element.
/// Reads type code byte, then dispatches based on element kind.
/// </summary>
private static void EmitReadDictElement(StringBuilder sb, PropertyTypeKind kind, string typeName, string varName, string propSuffix, string i, PropInfo? p, bool isRefType)
{
var etc = $"{varName}_tc";
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
if (kind == PropertyTypeKind.String)
{
sb.AppendLine($"{i}{typeName}? {varName} = null;");
EmitReadString(sb, varName, etc, i);
}
else if (kind == PropertyTypeKind.Enum)
{
sb.AppendLine($"{i}{typeName} {varName} = default;");
sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Enum)");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var eb = context.ReadByte();");
sb.AppendLine($"{i} int eiv;");
sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) eiv = BinaryTypeCode.DecodeTinyInt(eb);");
sb.AppendLine($"{i} else eiv = context.ReadVarInt();");
sb.AppendLine($"{i} {varName} = ({typeName})(object)eiv;");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({etc})) {varName} = ({typeName})(object)BinaryTypeCode.DecodeTinyInt({etc});");
}
else
{
// Primitive value type — never nullable
sb.AppendLine($"{i}{typeName} {varName} = default;");
EmitReadMarkeredValueForKind(sb, kind, varName, etc, i);
}
}
/// <summary>
/// Emits markered value read by kind only (no PropInfo needed). For dict key/value inline reads.
/// </summary>
private static void EmitReadMarkeredValueForKind(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i)
{
switch (k)
{
case PropertyTypeKind.Int32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();");
break;
case PropertyTypeKind.Int64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {a} = context.ReadVarInt();");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {a} = context.ReadVarLong();");
break;
case PropertyTypeKind.Boolean:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {a} = true;");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {a} = false;");
break;
case PropertyTypeKind.Double:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {a} = context.ReadDoubleUnsafe();");
break;
case PropertyTypeKind.Single:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {a} = context.ReadSingleUnsafe();");
break;
case PropertyTypeKind.Decimal:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {a} = context.ReadDecimalUnsafe();");
break;
case PropertyTypeKind.DateTime:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {a} = context.ReadDateTimeUnsafe();");
break;
case PropertyTypeKind.Guid:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {a} = context.ReadGuidUnsafe();");
break;
case PropertyTypeKind.Byte:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (byte)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {a} = context.ReadByte();");
break;
case PropertyTypeKind.Int16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (short)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {a} = context.ReadInt16Unsafe();");
break;
case PropertyTypeKind.UInt16:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ushort)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {a} = context.ReadUInt16Unsafe();");
break;
case PropertyTypeKind.UInt32:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (uint)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {a} = context.ReadVarUInt();");
break;
case PropertyTypeKind.UInt64:
sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {a} = (ulong)BinaryTypeCode.DecodeTinyInt({tc});");
sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {a} = context.ReadVarULong();");
break;
case PropertyTypeKind.TimeSpan:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {a} = context.ReadTimeSpanUnsafe();");
break;
case PropertyTypeKind.DateTimeOffset:
sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {a} = context.ReadDateTimeOffsetUnsafe();");
break;
}
}
/// <summary> /// <summary>
/// Emits inline collection read: Array marker already consumed as tc. /// Emits inline collection read: Array marker already consumed as tc.
/// Reads count + loops with direct element reader calls. /// Reads count + loops with direct element reads (Complex with SGen, or primitive/string/enum).
/// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance. /// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
/// </summary> /// </summary>
private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i) private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i)
{ {
var reader = p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"); var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter;
var elemType = p.ElementFullTypeName!; var elemType = p.ElementFullTypeName!;
var elemCast = $"({elemType})";
var s = p.Name; var s = p.Name;
// Null check // Null check
@ -1687,23 +1957,45 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i}{{"); sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();"); sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();");
sb.AppendLine($"{i} var nd_{s} = depth + 1;"); if (isComplexElement)
sb.AppendLine($"{i} var nd_{s} = depth + 1;");
// Create collection based on kind // Create collection + loop based on kind
if (p.CollectionKind == "Array") if (p.CollectionKind == "Array")
{ {
sb.AppendLine($"{i} var col_{s} = new {elemType}[cnt_{s}];"); 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} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); if (isComplexElement)
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null);
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
} }
else // List, IndexedCollection, Counted — all use List<T> with Add 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})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, p.CollectionAddMethod);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod);
sb.AppendLine($"{i} }}");
}
else // List, IndexedCollection, Counted-interface → List<T> with Add
{ {
sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});"); sb.AppendLine($"{i} 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} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan); if (isComplexElement)
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan);
else
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null);
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
} }
@ -1716,13 +2008,14 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback. /// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
/// !needsRefScan → only Object/Null possible → 1 branch per element. /// !needsRefScan → only Object/Null possible → 1 branch per element.
/// </summary> /// </summary>
private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan) private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan, string? addMethod = null)
{ {
var etc = $"etc_{propSuffix}"; var etc = $"etc_{propSuffix}";
sb.AppendLine($"{i}var {etc} = context.ReadByte();"); sb.AppendLine($"{i}var {etc} = context.ReadByte();");
var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.Add(null!);"; var addCall = addMethod ?? "Add";
var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.Add("; var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);";
var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.{addCall}(";
var assignEnd = isArray ? ";" : ");"; var assignEnd = isArray ? ";" : ");";
if (!needsRefScan) if (!needsRefScan)
@ -1744,6 +2037,63 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
} }
} }
/// <summary>
/// Emits per-element read for non-Complex collection elements (String, primitive, Enum).
/// Reads type code byte, then dispatches based on ElementKind.
/// </summary>
private static void EmitReadNonComplexCollectionElement(StringBuilder sb, PropInfo p, string indexVar, string propSuffix, string i, bool isArray, string? addMethod)
{
var etc = $"etc_{propSuffix}";
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
var addCall = addMethod ?? "Add";
var elemType = p.ElementFullTypeName!;
var colRef = $"col_{propSuffix}";
if (p.ElementKind == PropertyTypeKind.String)
{
// String element: FixStr / String / StringInternFirst / StringInterned / Null / StringEmpty
var tempVar = $"sv_{propSuffix}";
sb.AppendLine($"{i}string? {tempVar} = null;");
EmitReadString(sb, tempVar, etc, i);
if (isArray)
sb.AppendLine($"{i}{colRef}[{indexVar}] = {tempVar}!;");
else
sb.AppendLine($"{i}{colRef}.{addCall}({tempVar}!);");
}
else 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> /// <summary>
/// Emits markered value read for primitive types (with type code already read). /// Emits markered value read for primitive types (with type code already read).
/// Handles TinyInt encoding for integer types. /// Handles TinyInt encoding for integer types.
@ -1994,6 +2344,28 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
} }
} }
} }
// 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); return (needsIdScan, needsAllRefScan, needsInternScan);
@ -2081,6 +2453,15 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset; if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset;
if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum; if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum;
if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection; 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)) if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T))
return PropertyTypeKind.Collection; return PropertyTypeKind.Collection;
if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex; if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex;
@ -2110,6 +2491,26 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
return null; return null;
} }
/// <summary>
/// Extracts key and value types from Dictionary&lt;K,V&gt; or IDictionary&lt;K,V&gt;.
/// </summary>
private static (ITypeSymbol? keyType, ITypeSymbol? valueType) GetDictionaryKeyValueTypes(ITypeSymbol type)
{
if (type is INamedTypeSymbol nt && nt.IsGenericType)
{
var orig = nt.OriginalDefinition.ToDisplayString();
if (orig == "System.Collections.Generic.Dictionary<TKey, TValue>" ||
orig == "System.Collections.Generic.IDictionary<TKey, TValue>")
return (nt.TypeArguments[0], nt.TypeArguments[1]);
var iface = nt.AllInterfaces.FirstOrDefault(i =>
i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IDictionary<TKey, TValue>");
if (iface != null)
return (iface.TypeArguments[0], iface.TypeArguments[1]);
}
return (null, null);
}
private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32; private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32;
private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch
@ -2204,6 +2605,24 @@ internal sealed class PropInfo
public string? CollectionKind { get; } public string? CollectionKind { get; }
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary> /// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
public string? ElementFullTypeName { get; } public string? ElementFullTypeName { get; }
/// <summary>Add method for Counted concrete collections. null → List&lt;T&gt;.Add(), "Add" → HashSet/SortedSet, "Enqueue" → Queue, "AddLast" → LinkedList.</summary>
public string? CollectionAddMethod { get; }
/// <summary>True if the concrete Counted collection has a capacity constructor (HashSet, Queue).</summary>
public bool CollectionHasCapacityCtor { get; }
// Dictionary metadata — set when TypeKind == Dictionary
/// <summary>Key type kind for dictionary properties.</summary>
public PropertyTypeKind DictKeyKind { get; }
/// <summary>Value type kind for dictionary properties.</summary>
public PropertyTypeKind DictValueKind { get; }
/// <summary>Key type name for generated code.</summary>
public string? DictKeyTypeName { get; }
/// <summary>Value type name for generated code.</summary>
public string? DictValueTypeName { get; }
/// <summary>True if dictionary value type has [AcBinarySerializable].</summary>
public bool DictValueHasGeneratedWriter { get; }
/// <summary>Generated writer class name for dictionary value type.</summary>
public string? DictValueWriterClassName { get; }
// UseMetadata inline hash-ek (Complex/Collection child típushoz) // UseMetadata inline hash-ek (Complex/Collection child típushoz)
/// <summary>FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter.</summary> /// <summary>FNV-1a hash of child type name (Complex property). Only set when HasGeneratedWriter.</summary>
@ -2244,6 +2663,10 @@ internal sealed class PropInfo
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, 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, PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, 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,
int childTypeNameHash = 0, int[]? childPropertyHashes = null, int childTypeNameHash = 0, int[]? childPropertyHashes = null,
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, int elementTypeNameHash = 0, int[]? elementPropertyHashes = null,
bool childEnableMetadata = true, bool elementEnableMetadata = true, bool childEnableMetadata = true, bool elementEnableMetadata = true,
@ -2267,6 +2690,14 @@ internal sealed class PropInfo
ElementIdTypeName = elementIdTypeName; ElementIdTypeName = elementIdTypeName;
CollectionKind = collectionKind; CollectionKind = collectionKind;
ElementFullTypeName = elementFullTypeName; ElementFullTypeName = elementFullTypeName;
CollectionAddMethod = collectionAddMethod;
CollectionHasCapacityCtor = collectionHasCapacityCtor;
DictKeyKind = dictKeyKind;
DictValueKind = dictValueKind;
DictKeyTypeName = dictKeyTypeName;
DictValueTypeName = dictValueTypeName;
DictValueHasGeneratedWriter = dictValueHasGeneratedWriter;
DictValueWriterClassName = dictValueWriterClassName;
ChildTypeNameHash = childTypeNameHash; ChildTypeNameHash = childTypeNameHash;
ChildPropertyHashes = childPropertyHashes; ChildPropertyHashes = childPropertyHashes;
ElementTypeNameHash = elementTypeNameHash; ElementTypeNameHash = elementTypeNameHash;
@ -2291,7 +2722,7 @@ internal enum PropertyTypeKind
{ {
Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64, Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64,
Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum, Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum,
Collection, Complex, Collection, Complex, Dictionary,
NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64, NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64,
NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime, NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime,
NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum