diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f906261..350631e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,10 @@ "Bash(dir:*)", "Bash(git stash:*)", "WebFetch(domain:github.com)", - "Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")" + "Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")", + "Bash(sort:*)", + "WebFetch(domain:neuecc.medium.com)", + "WebFetch(domain:raw.githubusercontent.com)" ] } } diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 38ea653..b89c018 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -59,11 +59,67 @@ public class AcBinarySourceGenerator : IIncrementalGenerator }); if (hasIgnore) continue; + // String interning attribútum detektálás (null = no attr, true/false = explicit) + bool? stringInternAttr = null; + 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) 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 (→ needs ref tracking in generated code) + var kind = GetKind(p.Type); + bool hasGenWriter = false; + bool propTypeIsIId = false; + string? writerClassName = null; + if (kind == PropertyTypeKind.Complex) + { + // Resolve to the actual type symbol (strip nullable annotation for ref types) + // For SharedTag? → SharedTag. OriginalDefinition handles generic types. + var resolvedType = p.Type is INamedTypeSymbol namedPropType + ? namedPropType.OriginalDefinition + : p.Type; + + hasGenWriter = resolvedType.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + // Skip nested types (we don't generate writers for them) + if (hasGenWriter && resolvedType is INamedTypeSymbol nt && nt.ContainingType != null) + hasGenWriter = false; + + if (hasGenWriter) + { + propTypeIsIId = resolvedType.AllInterfaces.Any(i => + i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); + // Writer class: {Namespace}.{TypeName}_GeneratedWriter + var ns = resolvedType.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : resolvedType.ContainingNamespace.ToDisplayString(); + writerClassName = string.IsNullOrEmpty(ns) + ? $"{resolvedType.Name}_GeneratedWriter" + : $"{ns}.{resolvedType.Name}_GeneratedWriter"; + } + } + properties.Add(new PropInfo( p.Name, - p.Type.ToDisplayString(), - GetKind(p.Type), - p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type))); + typeDisplayName, + typeNameForTypeof, + kind, + p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type), + stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName)); } } @@ -105,6 +161,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); + // ReferenceHandlingMode is needed when any Complex property has direct object write + if (ci.Properties.Any(p => p.HasGeneratedWriter)) + sb.AppendLine("using AyCode.Core.Serializers;"); sb.AppendLine(); if (!string.IsNullOrEmpty(ci.Namespace)) sb.AppendLine($"namespace {ci.Namespace};"); @@ -137,7 +196,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { sb.AppendLine($"{i}if ({a}.HasValue)"); sb.AppendLine($"{i}{{"); - EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", i + " "); + EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + " "); sb.AppendLine($"{i}}}"); sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);"); return; @@ -155,29 +214,37 @@ public class AcBinarySourceGenerator : IIncrementalGenerator switch (p.TypeKind) { case PropertyTypeKind.String: + if (p.InterningFlags == 0) + sb.AppendLine($"{i}context.StringInternEligible = false;"); + else + sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;"); sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); break; case PropertyTypeKind.Complex: - // Complex object: use WriteObjectGenerated (skips type dispatch in WriteValueNonPrimitive) - if (p.IsNullable) + // Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely + // when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise. + if (p.HasGeneratedWriter) + EmitDirectObjectWrite(sb, p, a, i); + else if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, {a}.GetType(), context, depth);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); } else - sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, {a}.GetType(), context, depth);"); + sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; case PropertyTypeKind.Collection: + // typeof() instead of GetType() — avoids virtual dispatch if (p.IsNullable) { sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); } else - sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); break; default: - EmitSkip(sb, p.TypeKind, a, i); + EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i); break; } } @@ -224,7 +291,93 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } - private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string i) + /// + /// Emits direct object write — bypasses GetWrapper + WriteObject entirely. + /// Writes marker bytes + calls child GeneratedWriter.WriteProperties inline. + /// For IId types: inlines TryConsumeWritePlanEntry for ref tracking cursor alignment. + /// Falls back to WriteObjectGenerated when context.IsDirectObjectWrite is false (UseMetadata/PropertyFilter). + /// + private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i) + { + var writer = p.WriterClassName; + var nextDepth = "depth + 1"; + + if (p.IsNullable) + { + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)"); + } + else + { + sb.AppendLine($"{i}if (context.IsDirectObjectWrite)"); + } + + sb.AppendLine($"{i}{{"); + + // MaxDepth check — matches WriteObjectGenerated + sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + + if (p.IsIId) + { + // IId type: inline ref tracking from WriteObject + // context.UseTypeReferenceHandling gate: ReferenceHandling != None && IsIId + // For IId types, IsIId is always true, so: ReferenceHandling != None + sb.AppendLine($"{i} if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + // 2+ occurrence → ObjectRef + cacheIndex (no properties written) + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + // First occurrence → ObjectRefFirst + cacheIndex + properties + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + // No ref tracking entry at this visit → plain Object marker + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + sb.AppendLine($"{i} }}"); + } + else + { + // Non-IId type: usually no ref tracking needed. + // Exception: ReferenceHandling == All tracks ALL objects → scan pass increments ScanVisitIndex, + // so write pass must also consume from WritePlan to keep cursor aligned. + // Guard: fall back to WriteObjectGenerated for All mode (rare). OnlyId mode is safe (non-IId skipped). + sb.AppendLine($"{i} if (context.ReferenceHandling == ReferenceHandlingMode.All)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} else"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + sb.AppendLine($"{i} }}"); + } + + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i}}}"); + + // Fallback for non-direct mode (UseMetadata=true or HasPropertyFilter=true) + if (p.IsNullable) + sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + else + { + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);"); + } + } + + private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i) { switch (k) { @@ -293,12 +446,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});"); break; default: - sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);"); + sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({typeName}), context, depth);"); break; } } - private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string i) + private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i) { switch (k) { @@ -310,7 +463,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break; case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break; case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break; - default: EmitSkip(sb, k, a, i); break; + default: EmitSkip(sb, k, a, typeName, i); break; } } @@ -410,10 +563,44 @@ internal sealed class PropInfo { public string Name { get; } public string TypeName { get; } + /// + /// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types). + /// + public string TypeNameForTypeof { get; } public PropertyTypeKind TypeKind { get; } public bool IsNullable { get; } - public PropInfo(string n, string tn, PropertyTypeKind tk, bool nullable) - { Name = n; TypeName = tn; TypeKind = tk; IsNullable = nullable; } + /// + /// 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). + /// + public int InterningFlags { get; } + + /// True if the Complex property type has [AcBinarySerializable] → has a generated writer. + public bool HasGeneratedWriter { get; } + /// True if the Complex property type implements IId<T> → needs ref tracking in write pass. + public bool IsIId { get; } + /// Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter. + public string? WriterClassName { get; } + + public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, + bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null) + { + Name = n; + TypeName = tn; + TypeNameForTypeof = tnForTypeof; + TypeKind = tk; + IsNullable = nullable; + HasGeneratedWriter = hasGeneratedWriter; + IsIId = isIId; + WriterClassName = writerClassName; + // 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 diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 5cd4e8e..1cb9a79 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -220,6 +220,13 @@ public static partial class AcBinarySerializer public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool UseMetadata => Options.UseMetadata; public bool UseGeneratedCode => Options.UseGeneratedCode; + + /// + /// True when generated writers can bypass WriteObject entirely and write markers + properties inline. + /// Requires: no UseMetadata (no inline metadata tracking), no PropertyFilter (no per-prop filtering). + /// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types. + /// + public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter; public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 7a20084..0d0a37f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1069,11 +1069,12 @@ public static partial class AcBinarySerializer // Source-generated fast path: bypass the entire switch/delegate loop. // Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties) // and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking. - // String interning is NOT safe: generated code doesn't set StringInternEligible → cursor mismatch. + // String interning is safe: generated code uses pre-computed interningFlags bit-check + // matching runtime UseStringPropertyInterning — cursor alignment guaranteed for all modes. if (context.UseGeneratedCode) { var generatedWriter = wrapper.GeneratedWriter; - if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.UseStringInterning) + if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata) { generatedWriter.WriteProperties(value, context, nextDepth); return;