# AcBinary Source Generation (SGen)

Source-generated serialization architecture, hybrid execution model, bridge methods, and code generation patterns. For modifying SGen writers or the runtime ↔ SGen boundary.

> Wire format: `BINARY_FORMAT.md` | Options: `BINARY_OPTIONS.md` | Implementation: `BINARY_IMPLEMENTATION.md` | Writers: `BINARY_WRITERS.md`

## Overview

Two execution modes, seamlessly interoperable in a single serialization run:

| Mode | Trigger | Property access | Type dispatch | Performance |
|------|---------|----------------|---------------|-------------|
| **SGen** | `[AcBinarySerializable]` + `UseGeneratedCode=true` | `Unsafe.As<T>` direct field | Compile-time known | Fastest |
| **Runtime** | No attribute or `UseGeneratedCode=false` | Compiled delegates (`GetValue`) | Reflection + interface checks | Flexible |

## Interfaces

### IGeneratedBinaryWriter

```csharp
void WriteProperties<TOutput>(object value, BinarySerializationContext<TOutput> context, int depth);
void ScanObject<TOutput>(object value, BinarySerializationContext<TOutput> context, int depth);
void ScanForDuplicates<TOutput>(object value, BinarySerializationContext<TOutput> context);
```

- `WriteProperties`: writes all properties directly (no marker, no ref handling — caller handles)
- `ScanObject`: recursive graph walk for duplicates (strings + IId objects)
- `ScanForDuplicates`: entry point — `HasCaching` check + `ScanObject` + `SortWritePlan`

### IGeneratedBinaryReader

```csharp
object? ReadObject<TInput>(BinaryDeserializationContext<TInput> context, int depth, int cacheIndex);
void ReadProperties<TInput>(object value, BinaryDeserializationContext<TInput> context, int depth);
```

## SGen Root Fast Path

**Problem:** Runtime dispatch chain for root object: ~12 calls, ~8 branches before first property byte. MemoryPack: ~2 calls.

**Solution:** `AcBinarySerializer.Serialize<T>` checks for GeneratedWriter before the full dispatch chain:

```
if (options.UseGeneratedCode)
    wrapper = GetWrapper(runtimeType)
    if (wrapper.GeneratedWriter != null)
        ScanForDuplicates → WriteHeader → WriteObject(wrapper) → return
// else: full path (IQueryable/Expression + WriteValue dispatch)
```

**Skipped steps:** `is IQueryable` check, `IsExpressionType`, `TryWritePrimitive` (Type.GetTypeCode + 15-case switch), `WriteValueNonPrimitive` (4 interface checks + GetWrapper), 2 method call levels.

**Safety guarantees:**
- GeneratedWriter exists → type is NOT IQueryable, Expression, primitive, byte[], IDictionary, IEnumerable
- SGen only generates for `[AcBinarySerializable]` object model types
- Root always uses `value.GetType()` → no declared vs runtime type mismatch (polymorphism safe)
- `WriteObject` handles FixObj slots, UseMetadata, RefHandling → wire format identical
- Hybrid children use bridge methods → unchanged behavior

**Applies to:** both `Serialize<T>(T, options)` (byte[]) and `Serialize<T>(T, IBufferWriter, options)`.

## Hybrid Execution Model

SGen root type with non-SGen child types works transparently. SGen-generated `WriteProperties` calls bridge methods for unknown child types:

```
SGen Root (WriteProperties)
  ├─ SGen Child A → direct SGen→SGen call (WriteProperties)
  ├─ Runtime Child B → WriteValueGenerated (bridge → WriteValueNonPrimitive)
  ├─ String property → WriteStringGenerated (bridge → WriteString)
  └─ SGen Child C with runtime grandchild
       ├─ SGen grandchild → direct call
       └─ Runtime grandchild → WriteObjectGenerated (bridge → GetWrapper + WriteObject)
```

**Key:** SGen types can call **directly** into other SGen types' `WriteProperties` (zero dispatch). Non-SGen children fall back to runtime via bridge methods.

## Bridge Methods

Located in `AcBinarySerializer.cs` region "Generated Writer Bridge Methods". Called by SGen-generated code to transition to runtime pipeline.

| Bridge | Called when | What it does |
|--------|-----------|-------------|
| `WriteValueGenerated(value, type, ctx, depth)` | SGen encounters non-SGen complex child | → `WriteValueNonPrimitive` (byte[]? IDictionary? IEnumerable? GetWrapper → WriteObject) |
| `WriteObjectGenerated(value, type, ctx, depth)` ✱ | SGen knows child is object (not collection) | → `GetWrapper(type)` → `WriteObject` |
| `WriteObjectGenerated(value, type, slot, ctx, depth)` ✱ | SGen knows child is SGen object with known slot | → `GetWrapper(type, slot)` → `WriteObject`. Slot = compile-time known wrapper index, avoids dictionary lookup |
| `WriteStringGenerated(value, ctx)` | SGen writes string property | null → PropertySkip, empty → StringEmpty, else → `WriteString` (with interning) |
| `ScanValueGenerated(value, type, ctx, depth)` | SGen scan encounters non-SGen child | → runtime `ScanValue` for reference/string tracking |

> **✱ Caller contract — null-check required.** The `WriteObjectGenerated` and `WriteValueGenerated` bridges assume non-null `value` and dereference via `Unsafe.As<T>` (or compiled-lambda property-walk) inside the target's `WriteProperties` / `WriteValueNonPrimitive`. All SGen reference-property emit branches in `GenWriter.cs` MUST emit `if ({a} == null) WriteByte(BinaryTypeCode.PropertySkip);` before calling, across every `PropertyTypeKind`:
>
> | Branch | Location | Null-check pattern |
> |---|---|---|
> | `PropertyTypeKind.Complex` → `EmitDirectObjectWrite` | line ~828 | `if ({a} == null) PropertySkip; else …` (always) |
> | `PropertyTypeKind.Complex` → `IsObjectDeclaredType` (System.Object property) | line ~295 | `if ({a} == null) PropertySkip; else …` (always) |
> | `PropertyTypeKind.Complex` → fallback `WriteObjectGenerated` | line ~309 | `if ({a} == null) PropertySkip; else WriteObjectGenerated(…)` (always — fixed in [`N4P8`](BINARY_TODO.md#accore-bin-t-n4p8)) |
> | `PropertyTypeKind.Collection` → `EmitDirectCollectionWrite` | line ~877 | `if ({a} == null) PropertySkip; else …` (always) |
> | `PropertyTypeKind.Collection` → fallback `WriteValueGenerated` | line ~321 | `if ({a} == null) PropertySkip; else WriteValueGenerated(…)` (always — fixed in [`N4P8` follow-up](BINARY_TODO.md#accore-bin-t-n4p8)) |
> | `PropertyTypeKind.Dictionary` → `EmitDirectDictionaryWrite` | line ~1037 | `if ({a} == null) PropertySkip; else …` (always) |
>
> Runtime null is possible regardless of the nullable annotation (EF lazy-load, projection gaps, detached navigation properties, missing collection initializers, cross-assembly types without compile-time nullable context). The reader-side companion `EmitReadProperty` (GenReader.cs line ~137) already wraps every markered property in `if (tc != PropertySkip) { … }`, so the `PropertySkip` marker is uniformly handled across all `PropertyTypeKind`s. `WriteStringGenerated`, by contrast, **internalizes** the null-check (one of the bridge's responsibilities) — caller emit is shorter on the string ag.

## Wrapper Slot System

Each SGen type gets a unique slot index via `AllocateWrapperSlot()` (`Interlocked.Increment`).

| Slot range | Purpose |
|-----------|---------|
| 0–63 | FixObj markers — runtime polymorphic types (dynamic assignment) |
| 64+ | SGen types (compile-time stable, sequential allocation) |

`GetWrapper(type, slot)`: first call per slot per context → populates from `GetWrapper(type)`. Subsequent calls → direct array index `_wrapperSlots[slot]`. O(1) lookup, no dictionary.

## Generated Code Patterns

### WriteProperties (generated)

```csharp
// Generated for [AcBinarySerializable] type
void WriteProperties<TOutput>(object value, BinarySerializationContext<TOutput> ctx, int depth)
{
    var obj = Unsafe.As<MyType>(value);       // No cast, no box
    ctx.WriteInt32Property(obj.Id);            // Direct typed write
    AcBinarySerializer.WriteStringGenerated(obj.Name, ctx);  // String bridge
    // SGen child → direct call:
    ChildWriter.WriteProperties(obj.Child, ctx, depth + 1);
    // Non-SGen child → bridge:
    AcBinarySerializer.WriteValueGenerated(obj.Other, typeof(OtherType), ctx, depth + 1);
}
```
```

### ScanObject (generated)

```csharp
void ScanObject<TOutput>(object value, BinarySerializationContext<TOutput> ctx, int depth)
{
    var obj = Unsafe.As<MyType>(value);
    // Track this object for ref handling
    if (!ctx.TrackObjectForScan(value)) return;  // Already visited
    // Scan string properties for interning
    ctx.ScanStringProperty(obj.Name);
    // SGen child → direct scan:
    ChildWriter.ScanObject(obj.Child, ctx, depth + 1);
    // Non-SGen child → bridge:
    AcBinarySerializer.ScanValueGenerated(obj.Other, typeof(OtherType), ctx, depth + 1);
}
```

## SGen Output Constraints

Design rules for anyone modifying `AcBinarySourceGenerator.cs`. Violating these silently regresses the SGen hot path.

**No EH regions in generated hot methods.** Generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` / `ReadObject` / `ReadProperties` MUST NOT emit `try`, `catch`, `finally`, or `using` blocks. On **.NET 9** (project minimum), the CoreCLR JIT refuses to inline any method with an EH region — `AggressiveInlining` is silently ignored, and the SGen Root Fast Path collapses to a regular call frame per property write. .NET 10 partially lifts this for same-module try-finally ([`dotnet/runtime#112998`](https://github.com/dotnet/runtime/pull/112998), merged 2025-03-20); not yet our minimum runtime, and `catch` / cross-module / P/Invoke-stub cases remain blocked even on .NET 10+. Hard rule for SGen output regardless of runtime — generated method must work on .NET 9 AND keep inline-friendliness on .NET 10+.

**Future-feature trap.** When adding generator features that *seem* to need EH:

- `[CustomSerializer]` / `[CustomDeserializer]` attribute hooks → wrap user code in a cold helper called from the generated method, not in a `try` block inline.
- `OnSerializing` / `OnDeserialized` lifecycle callbacks → cold helper.
- Validation attributes (`[Validate]`, `[Required]`) → cold helper that throws; the generated method calls it without try/catch.
- Rented-buffer cleanup (`using var pooled = ...`) → keep the `using` in the entry frame (`Serialize<T>`), never in the generated `WriteProperties`.

**Straight-line rule.** Generated hot methods: `Unsafe.As<T>` cast → null/depth check → property write or bridge call → repeat. No exception handling, no resource ownership, no early-return cleanup. Resource ownership lives at `Serialize<T>` / `Deserialize<T>` entry, not per-property.

See `BINARY_IMPLEMENTATION.md` Rule #3 (Inlining barriers) for JIT-level rationale; `BINARY_TODO.md#accore-bin-t-t5j8` for the audit task enforcing this on existing output.

## Object Marker Bridge — Metadata Caching

`WriteObjectFullMarkerIId` / `WriteObjectFullMarkerAll` in `PropertyWriters.cs`: when `UseMetadata=true`, `GetWrapper` result and `wrapper.Metadata` are cached in a local variable at the method entry. This avoids redundant `GetWrapper` + `value.GetType()` calls in the ref-handling and non-ref branches.

## Performance Characteristics

| Aspect | SGen | Runtime |
|--------|------|---------|
| Property access | `Unsafe.As<T>` (0 overhead) | Compiled delegate invoke |
| Type dispatch | Compile-time resolved | Interface checks + GetTypeCode switch |
| Wrapper lookup | Slot array index O(1) | Dictionary lookup (amortized O(1) but hashing) |
| String write | Bridge to same WriteString | Same WriteString |
| Ref handling | Same IdentityMap | Same IdentityMap |
| Wire format | **Identical** | **Identical** |
| Root dispatch | Fast path (3 checks) | Full chain (~12 calls, ~8 branches) |

## Configuration

| Option | Default | Effect on SGen |
|--------|---------|---------------|
| `UseGeneratedCode` | `true` | `false` → ignores all GeneratedWriter/Reader, uses runtime only |
| `UseMetadata` | `false` | `false` + SGen → `IsDirectObjectWrite=true` → SGen inlines property writes |
| `ReferenceHandling` | `All` | `None` + `StringInterning=None` → no scan pass (single-phase) |

## When to Use SGen

- **Hot-path types**: types serialized frequently (SignalR messages, cache entries)
- **Large object graphs**: deep nesting benefits most from zero-dispatch property access
- **Small payloads**: root fast path eliminates dispatch overhead that dominates at small sizes
- **Not needed for**: one-off serialization, 3rd-party types, Expression/IQueryable types

## Cold-Start Overhead & Planned Mitigation

First use of an SGen type per process incurs reflection + `Expression.Compile` in the `BinarySerializeTypeMetadata` / `BinaryDeserializeTypeMetadata` constructors — dominant first-call cost today. Subsequent calls reuse cached metadata and wrappers.

See `BINARY_ISSUES.md#accore-bin-i-n6q3` for the full cost chain and `BINARY_TODO.md#accore-bin-t-w9f1` / `#todo-04` for planned work:

- **ACCORE-BIN-T-W9F1 — Compile-time metadata generation.** Source generator emits pre-built `BinarySerializeTypeMetadata` (and deserialize counterpart) with hashes, flags, and `MinWriteSize` baked in. Registered from `ModuleInit` into a `GeneratedMetadataRegistry`; `GetWrapperSlow` consults it before falling back to the reflection `MetadataFactory`. A lazy `TypeMetadataBase.RuntimeInit()` builds the `Expression.Compile` property accessors only when needed (runtime-only type, or `UseGeneratedCode=false`). SGen types never touch their own runtime accessors → `RuntimeInit` skipped. Hybrid mode is unaffected: non-SGen child types continue to run the reflection factory.
- **ACCORE-BIN-T-T5J8 — JIT Tier 1 warmup.** After ACCORE-BIN-T-W9F1, JIT of generated `WriteProperties` / `ScanObject` / `ScanForDuplicates` becomes the residual first-call cost. Candidates to evaluate (measure first): `[MethodImpl(MethodImplOptions.AggressiveOptimization)]` on generated methods, background `RuntimeHelpers.PrepareMethod` from `ModuleInit`, ReadyToRun (R2R) in consuming-project publish config, code chunking for oversized methods.

Wire format is unchanged by both TODOs — this is dispatch / startup optimization only.
