# BINARY — Strict SGen NuGet package (working doc)

Brainstorming / planning document for the **`AcBinary.Strict`** NuGet package — a SGen-only build target of the same source tree, opt-in for consumers who can guarantee `[AcBinarySerializable]` coverage and want max perf + min publish size (especially NativeAOT). Sibling to `BINARY_BYTECODE_OPTIMIZATION.md` + `BINARY_SGEN_OPTIMIZATION.md`. NOT a TODO entry — design notes.

## Concept

Two NuGet packages from the same source tree, distinguished by an MSBuild `DefineConstants`:

| Package | Constants | Target audience |
|---|---|---|
| **`AcBinary`** (default) | (none) | Drop-in JSON-like UX — 3rd-party DTOs without attributes, gradual opt-in via `[AcBinarySerializable]` on hot-paths. The **market USP**. |
| **`AcBinary.Strict`** (opt-in) | `SGEN_ONLY` | Max perf + min AOT publish — every type must be `[AcBinarySerializable]`-decorated, Roslyn analyzer enforces at build time. |

**Wire format**: identical. A wire emitted by `AcBinary.Strict` is fully readable by `AcBinary` (downward compatible). The reverse holds only when the Hybrid path emitted no runtime-only markers (polymorphic `ObjectWithTypeName`, etc.) — typically true when the consumer is fully `[AcBinarySerializable]`-decorated.

## Market positioning — the Hybrid is NOT second-class

Common reason teams avoid MemPack: **kötelező attribute mindenhol**, 3rd-party DTOs nem szerializálhatók, mindent vagy semmit. The AcBinary Hybrid removes this barrier:
- 3rd-party type → runtime reflection fallback, works out of the box
- gradual opt-in: add `[AcBinarySerializable]` only to identified hot-paths over time
- no big-bang refactor — coexistence of SGen + reflection types in the same graph

The Strict package is **the optional max-perf niche**, not a replacement. Consumers who already disciplined every DTO (`[AcBinarySerializable]` on each, no polymorphic `object` properties, no runtime-typed collections) graduate to Strict for additional AOT-perf gains.

## Performance gain — sources

Naïve estimate: "remove the runtime fallback path" — JIT inline budget freed up, ~2-5% hot path gain.

Realistic estimate is **larger**, because the SGen-emit itself can simplify when no runtime-cooperation is required:

| Aspect | Hybrid (current) | Strict-only | Gain estimate |
|---|---|---|---|
| Property complex-child write | `WriteObjectGenerated(value, type, ctx)` → `GetWrapper(type)` → `WriteObject` → `UseTypeReferenceHandling` check → `WriteObjectWithRefHandling/Meta/Properties` | `ChildClass_GeneratedWriter.Instance.WriteProperties(value, ctx)` direct | ~5-8 method-frames eliminated |
| Wrapper-slot lookup | `context._wrapperSlots[s_wrapperSlot]` (array load + bounds check) | direct static field on the generated writer class | array dispatch eliminated |
| Tracker dispatch | `wrapper.TryTrackInt32(id, visitIndex, ref ctx.NextCacheIndexRef, ...)` virtual on wrapper field | static method on the generated writer class | virtual dispatch eliminated |
| Cold-start | `BinarySerializeTypeMetadata` ctor — reflection + Expression.Compile + GlobalMetadataCache | **eliminated entirely** | first-call latency ~50-100× faster (only JIT Tier-0 remains) |
| Generator registry | `GeneratedWriterRegistry.TryGet(type, out writer)` dict lookup | compile-time bind → static field | dict-overhead eliminated |
| Collection element emit | `EmitWriteCollectionElement` 4 branches (no-ref/ref/meta/both) | only 1-2 branches (no runtime-meta needed) | smaller emit IL, smaller JIT code |

**Realistic Ser hot-path gain: -10..-20%** (not 2-5%). Plus **NativeAOT publish size -30..-50%** (no reflection IL preserved, no `Expression.Compile`, no `IDictionary`/`IEnumerable` interface dispatch tables).

**Cold-start dramatic change**: first SignalR message / first cache deserialization ~100× faster on Strict (no reflection + no Expression.Compile + no metadata-dict build).

## Refactor prerequisite — physical separation

Scattered `#if !SGEN_ONLY` directives mid-method are a maintenance liability. Phase 0 (refactor-only, 0 behavior change): **physical file separation** via `partial class`:

```
AcBinarySerializer.cs                          ← SGen + shared (always compiled)
AcBinarySerializer.RuntimeFallback.cs          ← Hybrid-only (file-level `#if !SGEN_ONLY`)
AcBinaryDeserializer.cs                        ← SGen + shared
AcBinaryDeserializer.RuntimeFallback.cs        ← Hybrid-only
AcBinaryDeserializer.TypeReaderTable.cs        ← Hybrid-only (256-slot dispatch table)
BinarySerializeTypeMetadata.cs                 ← shared (reduced ctor)
BinarySerializeTypeMetadata.Reflection.cs      ← Hybrid-only (reflection + Expression.Compile)
```

`partial class` keeps the public surface unchanged; the body splits across files. **One `#if !SGEN_ONLY` directive per file at the top**, not 50 scattered through the method body. Bridge-methods (e.g. `WriteValueGenerated`, `ReadValueGenerated`) become `partial method` declarations — one body in `.RuntimeFallback.cs` (Hybrid), another in a `SGenBridge.cs` (Strict throws `NotSupportedException` or compile-error).

This refactor is **valuable on its own** — even if the Strict NuGet never ships, the codebase is clearer (Hybrid path explicit, SGen path explicit, no comingled logic).

## Phased implementation

| Phase | Scope | Risk | Behavior change |
|---|---|---|---|
| **0 (refactor-only)** | Physical separation: runtime-fallback code → `.RuntimeFallback.cs` partial files. Same build target, no `#if` directives yet. | very low | none |
| **1 (Strict build target)** | New MSBuild `Configuration: SGenOnly` with `<DefineConstants>$(DefineConstants);SGEN_ONLY</DefineConstants>`. File-level `#if !SGEN_ONLY` on the runtime-only partial files. Bridge-method bodies are `partial` — Hybrid bind in `.RuntimeFallback.cs`, Strict bind throws/errors. | low | Strict consumers cannot serialize undecorated types (build / runtime error) |
| **2 (SGen-emit simplification — the BIG perf gain)** | Generator emits direct-call pattern in Strict mode instead of bridges. `EmitDirectCollectionWrite` / `EmitReadComplex` / `EmitWriteProp` branches collapse. Slot-array dispatch replaced with static field references. | medium | hot-path perf jumps -10..-20% on Strict |
| **3 (metadata-dict elimination)** | `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` ctor reflection paths gone in Strict. `GlobalMetadataCache` dictionary unused in Strict. Cold-start dramatic improvement. | medium | first-use latency ~100× faster on Strict |
| **4 (Roslyn analyzer)** | Strict-mode analyzer: build-error if a property type lacks `[AcBinarySerializable]` (and isn't a known primitive), if `object`-typed property has no `[AcBinaryPolymorphic(types: typeof(A), typeof(B))]`, etc. Hybrid-mode same checks → warning only (perf hint). | low | better developer experience |
| **5 (NuGet packaging + ADR)** | Separate `AcBinary.Strict.nupkg` publish target. ADR-level documentation. Strict-consumer guide. | low | ship to consumers |

Phases can be **partially shipped** — e.g. Phase 0 alone is valuable as a codebase tidy-up. Phase 1 ships the build target without performance changes. Phase 2 is the actual perf-gain. Phase 5 is the consumer-facing release.

## Wire-format compatibility matrix

| Producer | Consumer | Compatible? |
|---|---|---|
| AcBinary (Hybrid, fully decorated graph) | AcBinary.Strict | ✓ (no runtime-only markers emitted) |
| AcBinary (Hybrid, with undecorated children) | AcBinary.Strict | ✗ — may contain `ObjectWithTypeName` / polymorph markers Strict doesn't decode |
| AcBinary.Strict | AcBinary (Hybrid) | ✓ always (Strict wire ⊂ Hybrid wire) |
| AcBinary.Strict | AcBinary.Strict | ✓ |

The analyzer in Phase 4 enforces the "fully decorated graph" property on Strict consumers — so the practical incompatibility (row 2) cannot occur in disciplined Strict deployments.

## Maintenance cost — ~10% CI overhead

- Source repo: single
- Build targets: Release + SGenOnly (2× build)
- NuGet packages: 2 (`.nupkg`)
- Test suite: same source, conditional skip on runtime-fallback-only tests under `SGEN_ONLY` (e.g. polymorph-without-attribute tests)
- CI time: ~2× (acceptable)

**Not a duplicated codebase** — same source tree, MSBuild constants differ.

## Open questions / considerations

- **Polymorph opt-in attribute**: Strict-mode requires `[AcBinaryPolymorphic(types: typeof(A), typeof(B))]` on `object`-typed properties (closed type set, compile-time bound). The Hybrid path uses runtime `value.GetType()` + `ObjectWithTypeName` — that flow doesn't exist in Strict. Spec out the attribute shape + emit pattern before Phase 4.
- **`IDictionary<,>` / `IEnumerable<>` runtime-typed**: Strict-mode demands closed generic type arguments. Generic `Dictionary<string, object>` with polymorphic values may not be supported (or requires explicit poly-attribute on the value-type).
- **Existing reference-handling tracker**: `wrapper.TryTrackInt32(id, ...)` is a virtual call on the wrapper. Phase 2 needs a strategy — move tracker state to `[ThreadStatic]` on the context, or generate per-type static trackers (latter more JIT-friendly).
- **3rd-party type-coverage tooling**: Strict-consumers need a Roslyn diagnostic that lists every type in the project's type-graph that lacks `[AcBinarySerializable]` — guides the gradual migration from Hybrid to Strict.
- **Naming**: `AcBinary.Strict` is descriptive but verbose. Alternatives: `AcBinary.Sealed`, `AcBinary.AOT`, `AcBinary.Lean`. Decision deferred to Phase 5.

## Cross-references

- `BINARY_SGEN.md` — current SGen architecture (hybrid execution model, bridge methods, wrapper slot system)
- `BINARY_SGEN_OPTIMIZATION.md` — SGen per-property emit micro-optimization brainstorming
- `BINARY_BYTECODE_OPTIMIZATION.md` — wire-format marker layout reorganization (orthogonal — Strict NuGet doesn't change wire format)
- `BINARY_ISSUES.md#accore-bin-i-n6q3` — cold-start cost chain (Phase 3 addresses this directly)
- `BINARY_TODO.md#accore-bin-t-w9f1` — compile-time metadata generation (a precondition for Phase 3 — eliminates `Expression.Compile` cold-start)
- `BINARY_TODO.md#accore-bin-t-t5j8` — JIT Tier-1 warmup (residual cold-start after Phase 3)
- `BINARY_TODO.md#accore-bin-t-n4p8` — Hybrid-side reference-property null-check parity fix (2026-05-23). Phase 2 collapsing structurally eliminates this regression class: the fallback `WriteObjectGenerated` ag (which the bug lived on) does not emit in Strict mode — every Complex property routes through `EmitDirectObjectWrite` because all child types are guaranteed-decorated.
