diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
index f0739da..127c897 100644
--- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
+++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
@@ -18,32 +18,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{
private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute";
- // ────────────────────────────────────────────────────────────────────────────────────────────
- // TEMPORARY (2026-05-08) — A/B test feature gates for hot-path overhead measurement.
- //
- // The generated SGen `WriteProperties` / `ScanObject` methods emit two kinds of overhead-blocks
- // that are unconditionally present today but rarely exercised in typical workloads:
- //
- // 1. PropertyFilter guard (`UsePropertyFilter`) — every non-markerless property emit-site
- // checks `context.HasPropertyFilter` + filter-context allocation + lambda-call.
- // The benchmark workload never sets a property-filter → branch is always false →
- // pure overhead (CPU cycles + i-cache pressure on the hot path).
- //
- // 2. Polymorphic object-with-type-name emit (`UsePolymorphType`) — `System.Object` declared
- // properties emit `ObjectWithTypeName` marker + `WriteStringUtf8(AssemblyQualifiedName)`
- // under `!context.UseMetadata`. Same: rarely used in typical DTO graphs.
- //
- // Setting either to `false` skips the corresponding emit at compile time → leaner generated
- // code. The bench measures the actual delta vs MemPack apples-to-apples (which has neither
- // of these features).
- //
- // Long-term: these flags will move to `[AcBinarySerializable(UsePropertyFilter = false, ...)]`
- // attribute properties so consumers can opt out per type. Until then, keep both `false` for
- // benchmark-vs-MemPack measurements; flip to `true` for production where the features are needed.
- // ────────────────────────────────────────────────────────────────────────────────────────────
- // UsePropertyFilter const removed — replaced by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]`
- // attribute flag, propagated through SerializableClassInfo.EnablePropertyFilter to EmitProp/EmitScanProp.
- private const bool UsePolymorphType = true;
+ // Feature gates on the SGen-emitted writer / scan code are driven by `[AcBinarySerializable]`
+ // attribute flags. Two such gates are wired through SerializableClassInfo:
+ // • EnablePropertyFilter → omits the per-property `HasPropertyFilter` branch when false.
+ // • EnablePolymorphDetect → omits the `ObjectWithTypeName + AQN` prefix on `System.Object`-
+ // declared properties when false (then ACBIN002 guards misuse).
+ // Both default `true`; opt-out is per type via the attribute ctor parameters.
private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
id: "ACBIN001",
@@ -56,26 +36,26 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
///
/// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as System.Object requires
/// polymorphic-prefix emit (ObjectWithTypeName) so the deserializer can resolve the
- /// concrete runtime type. When is false, the prefix is
- /// suppressed and the wire silently corrupts on round-trip (FixObj slot byte against
- /// typeof(object) at read-time → 0-byte object wrapper → reader position drifts →
- /// downstream DECIMAL_DRIFT / IndexOutOfRangeException).
+ /// concrete runtime type. When the type opts out of the feature via
+ /// [AcBinarySerializable(enablePolymorphDetectFeature: false)], the prefix is suppressed
+ /// and the wire silently corrupts on round-trip (FixObj slot byte against typeof(object)
+ /// at read-time → 0-byte object wrapper → reader position drifts → downstream
+ /// DECIMAL_DRIFT / IndexOutOfRangeException).
///
/// Surface the misconfiguration at build time so the silent corruption is structurally
/// impossible. Three escape hatches for the developer:
- /// 1. Enable the polymorphic feature ( = true, or — once the
- /// planned [AcBinarySerializable(EnablePolymorphicFeature = true)] flag lands — set
- /// it on the type).
+ /// 1. Enable the polymorph-detect feature on the type
+ /// ([AcBinarySerializable(...enablePolymorphDetectFeature: true)] — default true).
/// 2. Change the property type to a concrete type (no polymorphism needed).
- /// 3. Mark the property with [AcBinaryIgnore] — ignored properties are filtered
- /// out at property enumeration, so this diagnostic does not fire for them.
+ /// 3. Mark the property with [AcBinaryIgnore] — ignored properties are filtered out
+ /// at property enumeration, so this diagnostic does not fire for them.
///
private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new(
id: "ACBIN002",
- title: "Polymorphic property requires polymorphic feature enabled",
- messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but polymorphic serialization in the source generator is disabled (UsePolymorphType=false). " +
+ 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 polymorphic serialization, (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].",
+ "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);
@@ -111,6 +91,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
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)
@@ -124,10 +105,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
enableInternString = all;
enableMetadata = all;
enablePropertyFilter = all;
+ enablePolymorphDetect = all;
}
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
{
- // Four bool ctor: (metadata, idTracking, refHandling, internString) — filter defaults to true
+ // 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!;
@@ -135,13 +117,23 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
else if (binarySerializableAttr.ConstructorArguments.Length == 5)
{
- // Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter)
+ // 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))
@@ -423,7 +415,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
var selfScanFlags = ComputeNeedsScan(typeSymbol);
- return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
+ return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, enablePolymorphDetect, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
}
private static void Execute(ImmutableArray classes, SourceProductionContext context)
@@ -447,15 +439,16 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
///
/// ACCORE-BIN-I-T7K3 guard: emits
/// (ACBIN002) for every System.Object-declared property on any
- /// [AcBinarySerializable] type while is false.
- /// Short-circuits when the feature is enabled — no per-property work needed.
+ /// [AcBinarySerializable] type whose EnablePolymorphDetectFeature is false.
+ /// Per-class gating: types with the feature enabled (default) skip the check entirely; only
+ /// opt-out types are scanned for misuse.
///
private static void DetectAndReportPolymorphicMisuse(List classes, SourceProductionContext spc)
{
- if (UsePolymorphType) return; // Feature enabled → polymorphic prefix is emitted, no misuse possible.
-
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)
@@ -603,7 +596,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
foreach (var p in ci.Properties)
{
sb.AppendLine();
- EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter);
+ EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter, ci.EnablePolymorphDetect);
}
sb.AppendLine();
@@ -757,7 +750,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" }");
}
- private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter)
+ private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect)
{
var a = $"obj.{p.Name}";
@@ -825,14 +818,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// System.Object property: runtime type unknown at compile time.
// Write ObjectWithTypeName prefix so deserializer can resolve the concrete type.
// Use value.GetType() for runtime type dispatch (not typeof(object)).
- // Gated by `UsePolymorphType` (TEMPORARY const) — `false` skips the type-name emit
- // entirely (deser will use the property's declared type, which is `object` so the
- // round-trip would fail on polymorphic instances; safe ONLY when the workload is
- // known not to use polymorphic object-typed properties — true for the benchmark).
+ // Gated by `[AcBinarySerializable(enablePolymorphDetectFeature: ...)]` — `false`
+ // skips the type-name emit entirely. Misuse (false + non-null object prop at runtime)
+ // is caught at build time by ACBIN002 in DetectAndReportPolymorphicMisuse.
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else");
sb.AppendLine($"{i}{{");
- if (UsePolymorphType)
+ if (enablePolymorphDetect)
{
sb.AppendLine($"{i} if (!context.UseMetadata)");
sb.AppendLine($"{i} {{");
@@ -3065,6 +3057,12 @@ internal sealed class SerializableClassInfo
/// 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).
public bool EnablePropertyFilter { get; }
+ /// True if EnablePolymorphDetectFeature is enabled — controls ObjectWithTypeName + AQN
+ /// prefix emit on System.Object-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 object property
+ /// will be serialized on this type, or all such properties are excluded via [AcBinaryIgnore].
+ public bool EnablePolymorphDetect { get; }
/// When true, type subtree has IId types needing scan (active in OnlyId + All).
public bool NeedsIdScan { get; }
/// When true, type subtree has non-IId ref tracking (active only in All mode).
@@ -3075,8 +3073,8 @@ internal sealed class SerializableClassInfo
public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan;
/// Derived: any scan axis active.
public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan;
- public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool needsIdScan, bool needsAllRefScan, bool needsInternScan)
- { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; EnablePropertyFilter = enablePropertyFilter; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
+ public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect, 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; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
}
internal sealed class PropInfo
diff --git a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
index 2bbd17c..fe30016 100644
--- a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
+++ b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
@@ -4,12 +4,32 @@ namespace AyCode.Core.Serializers.Attributes;
///
/// Marks a class or struct for Source Generator based binary serialization.
-/// When applied, the Source Generator will create optimized serialize/deserialize methods
-/// at compile time, eliminating the need for runtime reflection or compiled expressions.
+/// When applied, the Source Generator creates optimized serialize/deserialize methods at compile
+/// time, eliminating the need for runtime reflection or compiled expressions.
///
///
-/// If this attribute is not present, the serializer falls back to the existing
-/// compiled expression based approach which works for all types.
+/// If this attribute is not present, the serializer falls back to the existing
+/// compiled-expression-based approach which works for all types.
+///
+/// Feature flag semantics — each Enable*Feature ctor parameter controls whether
+/// the corresponding code-block is emitted by the source generator, NOT whether the feature
+/// is active at runtime:
+///
+/// - true (default): the SGen-emitted writer / reader / scan code includes
+/// the feature's branch. Runtime activation is then governed by the matching
+/// AcBinarySerializerOptions setting (e.g. UseStringInterning,
+/// ReferenceHandling, PropertyFilter, UseMetadata). The developer keeps the
+/// choice per-call by setting the options accordingly.
+/// - false: the corresponding code-block is omitted from the generated
+/// output entirely. Slightly leaner hot-path (fewer branches, less i-cache pressure on the JIT'd
+/// bodies), but the feature is permanently off for this type regardless of runtime options.
+/// A matching AcBinarySerializerOptions setting is silently ignored for this specific type.
+///
+///
+/// Set a flag to false only when you can guarantee no consumer will ever need that
+/// feature on this type (typical for high-throughput message DTOs where wire size and serialize
+/// CPU dominate, and a feature like PropertyFilter is genuinely never used). Otherwise leave
+/// it at the default true.
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public sealed class AcBinarySerializableAttribute : Attribute