Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.7.0] - 2026-04-30

### Added

- **Declarative semantic-type registry (#166)**: Schemas can now annotate fields with `semanticType` (e.g. `UTCTimestampNanos`, `LocalMktDate`, `MonthYear`) and the generator emits a sibling typed accessor `{Field}Value` next to the raw wire field — without changing the wire layout. Eight FIX/SBE built-in converters ship out of the box (`UTCTimestamp`, `UTCTimestampNanos`, `UTCTimestampMicros`, `UTCTimestampMillis`, `UTCDateOnly`, `LocalMktDate`, `MonthYear`, `Boolean`) producing strongly typed `DateTime` / `DateOnly` / `(int Year, int Month)` / `bool` results from the underlying primitive. Optional fields produce a nullable accessor that returns `null` on the SBE null sentinel. Field-level `semanticType` wins; otherwise the field inherits its referenced type's `semanticType` (the common FIX/B3 pattern of declaring `<type semanticType="..."/>`). Fields whose type already produces a typed helper struct (e.g. `LocalMktDate` → `DateOnly` via `DateHelper`) are left untouched to avoid double conversion. The raw wire accessor is **never** replaced — the typed accessor is always additive.
- **User-extensible converters via `[assembly: SbeSemanticType("Name", typeof(MyConverter))]`**: Any user type implementing `ISbeSemanticConverter<TWire, TSemantic>` (a static-abstract interface emitted into every consuming compilation as `SbeSourceGenerator.Runtime.ISbeSemanticConverter`) can be registered against any `semanticType` string and overrides the built-in. The generator scans assembly attributes via a syntax-first incremental pipeline, validates that the converter's `TWire` matches the schema field's wire `SpecialType`, and reports diagnostics on misregistration. Built-ins are seeded automatically; users only declare what they want to override or add.
- **`partial` on non-blittable generated types (#167)**: The dispatcher (`SbeDispatcher`), handler interface (`ISbeMessageHandler`), per-message version maps (`{Msg}VersionMap`), zero-copy readers (`{Msg}DataReader`), and validation extension classes (`{X}Validation`) are now emitted as `partial`. Consumers can extend them in user code without forking the generator — for example, adding instrumentation hooks to the dispatcher, default methods to the handler interface, custom lookups to a version map, or domain-specific helpers to a `DataReader`. Layout-bearing blittable structs were already `partial`; this fills in the remaining surface intentionally, while the semantic-type registry (#166) provides the safe path for adding typed accessors without touching wire layout.
- **`SBE016` diagnostic** — *Semantic converter wire-type mismatch*: emitted when a user-registered converter declares a `TWire` that does not match the schema field's wire primitive. The accessor is suppressed for that field; raw access is unaffected.
- **`SBE017` diagnostic** — *Semantic converter does not implement `ISbeSemanticConverter<,>`*: emitted when a `[SbeSemanticType]` registration points at a type that does not implement the runtime interface (or implements it with non-static members). The registration is ignored.
- **`SBE018` diagnostic** — *Semantic accessor name collision*: emitted (Warning) when the generated `{Field}Value` name would collide with an existing member; the semantic accessor is dropped to keep the surface compiling.

## [1.6.1] - 2026-04-30

### Fixed
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ A Roslyn-based source generator that converts FIX Simple Binary Encoding (SBE) X
- Explicit `blockLength` on messages
- Validation constraints (min/max ranges)
- Zero-cost `SbeDispatcher` + `ISbeMessageHandler` for devirtualized message routing
- Comprehensive build-time diagnostics (SBE001–SBE015)
- Declarative semantic-type registry mapping `semanticType="…"` → typed `{Field}Value` accessors (built-in FIX converters: `UTCTimestamp{Nanos,Micros,Millis}`, `UTCDateOnly`, `LocalMktDate`, `MonthYear`, `Boolean`; user-extensible via `[assembly: SbeSemanticType(...)]`)
- Generated dispatcher, handler interface, version maps, data readers, and validation classes are emitted as `partial` for safe consumer extension (typed accessors should prefer the semantic registry)
- Comprehensive build-time diagnostics (SBE001–SBE018)

## What's New in v1.5.0

Expand Down Expand Up @@ -370,6 +372,9 @@ The generator provides comprehensive diagnostics:
| SBE013 | Warning | Duplicate type name |
| SBE014 | Warning | sinceVersion exceeds schema version |
| SBE015 | Warning | Duplicate generated source hintName suppressed |
| SBE016 | Error | Semantic converter wire-type mismatch |
| SBE017 | Error | Semantic converter does not implement `ISbeSemanticConverter<,>` |
| SBE018 | Warning | Semantic accessor name collision |

See [Diagnostics README](./src/SbeCodeGenerator/Diagnostics/README.md) for details.

Expand Down
10 changes: 10 additions & 0 deletions src/SbeCodeGenerator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Release 1.7.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|------
SBE016 | SbeSourceGenerator | Error | Semantic converter wire-type mismatch (#166)
SBE017 | SbeSourceGenerator | Error | Semantic converter does not implement ISbeSemanticConverter<,> (#166)
SBE018 | SbeSourceGenerator | Warning | Semantic accessor name collides with an existing field (#166)

## Release 1.6.1

### New Rules
Expand Down
4 changes: 0 additions & 4 deletions src/SbeCodeGenerator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|------
15 changes: 15 additions & 0 deletions src/SbeCodeGenerator/Diagnostics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ Provides compile-time diagnostics for:
**Example**: A schema declares two `<enum name="Side">` blocks; the second pass attempts `AddSource("…/Enums/Side.cs", …)` again. Without the suppression, Roslyn would throw `ArgumentException`, abort the generator phase, and produce a cascade of `CS0246` errors against partially-emitted files.
**Resolution**: Resolve the underlying duplication in the schema (commonly a duplicate type name — see also `SBE013`) or fix the upstream code path that emitted the second source.

### SBE016: Semantic Converter Wire-Type Mismatch
**Severity**: Error
**Triggered when**: A user-registered semantic converter (`[assembly: SbeSemanticType("Name", typeof(MyConv))]`) declares a `TWire` (the first type argument of `ISbeSemanticConverter<TWire, TSemantic>`) that does not match the schema field's underlying primitive (e.g., a `ulong`-wired converter applied to a `uint16` field).
**Resolution**: Either change the converter's `TWire` to match the schema's primitive, or register a different converter for that `semanticType`. The accessor is suppressed for that field; raw access remains available.

### SBE017: Semantic Converter Does Not Implement ISbeSemanticConverter
**Severity**: Error
**Triggered when**: A type referenced in `[assembly: SbeSemanticType(..., typeof(X))]` does not implement `SbeSourceGenerator.Runtime.ISbeSemanticConverter<TWire, TSemantic>` (the static-abstract interface emitted by the generator).
**Resolution**: Make the converter type implement `ISbeSemanticConverter<TWire, TSemantic>` with `static abstract FromWire`/`ToWire` members. The registration is ignored.

### SBE018: Semantic Accessor Name Collision
**Severity**: Warning
**Triggered when**: The generated `{Field}Value` semantic accessor name collides with an existing member on the same struct (e.g. a sibling field literally named `XxxValue`).
**Resolution**: Rename either the conflicting field in the schema or refactor your field naming to avoid the suffix collision. The semantic accessor is dropped for that field; raw access is unaffected.

## Usage

Diagnostics are automatically reported during source generation. When you build a project that includes an invalid SBE schema as an additional file, you'll see these diagnostics in:
Expand Down
30 changes: 30 additions & 0 deletions src/SbeCodeGenerator/Diagnostics/SbeDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,35 @@ internal static class SbeDiagnostics
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Roslyn requires every generated source hintName to be unique. The generator now suppresses duplicates and continues, instead of aborting the entire generation phase. Resolve the underlying schema duplication or fix the upstream generator path that produced the second source.");

// SBE016: Semantic converter wire-type mismatch
public static readonly DiagnosticDescriptor SemanticConverterWireMismatch = new DiagnosticDescriptor(
id: "SBE016",
title: "Semantic converter wire-type mismatch",
messageFormat: "Semantic converter '{0}' for semanticType '{1}' expects wire type '{2}', but field '{3}.{4}' has wire type '{5}'. The {6}Value accessor will not be emitted.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "A converter registered via [assembly: SbeSemanticType(...)] (or a built-in) declares a TWire generic argument that does not match the field's actual wire primitive. Fix the registration so TWire matches the field's primitive type, or pick a different converter.");

// SBE017: Semantic converter does not implement ISbeSemanticConverter<,>
public static readonly DiagnosticDescriptor SemanticConverterMissingInterface = new DiagnosticDescriptor(
id: "SBE017",
title: "Semantic converter must implement ISbeSemanticConverter<TWire, TSemantic>",
messageFormat: "Type '{0}' is registered for semanticType '{1}' but does not implement 'SbeSourceGenerator.Runtime.ISbeSemanticConverter<TWire, TSemantic>'. The registration is ignored.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Converters registered via [assembly: SbeSemanticType(...)] must implement ISbeSemanticConverter<TWire, TSemantic> with static abstract members so the generator can validate the wire/semantic types and emit the typed accessor.");

// SBE018: Semantic accessor name collision
public static readonly DiagnosticDescriptor SemanticAccessorNameCollision = new DiagnosticDescriptor(
id: "SBE018",
title: "Semantic accessor name collides with an existing field",
messageFormat: "Cannot emit semantic accessor '{0}Value' on message '{1}' because another field with the same name already exists. The semantic accessor for semanticType '{2}' is skipped.",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The convention is to emit a typed accessor named '{Field}Value'. If a field literally named '{Field}Value' already exists in the same message, the semantic accessor would collide with it and is skipped to keep the generated code compilable.");
}
}
4 changes: 2 additions & 2 deletions src/SbeCodeGenerator/Generators/DispatcherGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class DispatcherGenerator : ICodeGenerator
sb.Append("namespace ").Append(baseNs).AppendLine(";");
sb.AppendLine();
sb.AppendLine("/// <summary>Handler interface dispatched to by <see cref=\"SbeDispatcher\"/>. Implement as a <c>struct</c> for zero-cost devirtualized dispatch.</summary>");
sb.AppendLine("public interface ISbeMessageHandler");
sb.AppendLine("public partial interface ISbeMessageHandler");
sb.AppendLine("{");
foreach (var (name, _) in messages)
{
Expand All @@ -70,7 +70,7 @@ internal class DispatcherGenerator : ICodeGenerator
sb.AppendLine("/// Because <typeparamref name=\"T\"/> is constrained to <c>struct, ISbeMessageHandler</c>, the JIT");
sb.AppendLine("/// generates a specialized version per handler type and devirtualizes every dispatch call.");
sb.AppendLine("/// </summary>");
sb.AppendLine("public static class SbeDispatcher");
sb.AppendLine("public static partial class SbeDispatcher");
sb.AppendLine("{");
sb.AppendLine("\t/// <summary>Decodes the header at the start of <paramref name=\"buffer\"/> and dispatches to the matching handler method.</summary>");
sb.AppendLine("\t/// <returns><c>true</c> if a known message was dispatched; <c>false</c> if the header could not be read or the templateId is unknown (in which case <c>OnUnknownMessage</c> is called).</returns>");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using SbeSourceGenerator.SemanticTypes;
using System.Text;

namespace SbeSourceGenerator.Generators.Fields
{
/// <summary>
/// Issue #166: emits an additional <c>{Field}Value</c> readonly property next to
/// a raw wire field, delegating to a registered semantic converter. Carries no
/// layout information (does not implement <see cref="IBlittable"/> or
/// <see cref="IBlittableMessageField"/>), so it never affects offset computation
/// in <c>SumFieldLength()</c> nor appears in the generated <c>ToString()</c>.
/// </summary>
public class SemanticAccessorDefinition : IFileContentGenerator
{
private readonly string _fieldName;
private readonly string _converterFqn;
private readonly string _semanticTypeDisplay;
private readonly bool _isOptional;
private readonly string _semanticTypeKey;
private readonly bool _isBuiltIn;

public SemanticAccessorDefinition(string fieldName, string converterFullyQualifiedName,
string semanticTypeDisplay, bool isOptional, string semanticTypeKey, bool isBuiltIn)
{
_fieldName = fieldName;
_converterFqn = converterFullyQualifiedName;
_semanticTypeDisplay = semanticTypeDisplay;
_isOptional = isOptional;
_semanticTypeKey = semanticTypeKey;
_isBuiltIn = isBuiltIn;
}

public void AppendFileContent(StringBuilder sb, int tabs = 0)
{
sb.AppendLine("/// <summary>", tabs);
sb.AppendTabs(tabs).Append("/// Typed accessor for <c>").Append(_fieldName)
.Append("</c> derived via the <c>").Append(_semanticTypeKey).Append("</c> semantic converter")
.Append(_isBuiltIn ? " (built-in)." : ".").AppendLine();
sb.AppendLine("/// </summary>", tabs);

if (_isOptional)
{
// Optional fields expose Field/HasField/Set patterns; we read via the public Field property
// (which already handles endian conversion and null-sentinel comparison).
sb.AppendTabs(tabs).Append("public readonly ").Append(_semanticTypeDisplay).Append("? ").Append(_fieldName).Append("Value => ")
.Append(_fieldName).Append(".HasValue ? ").Append(_converterFqn).Append(".FromWire(").Append(_fieldName).Append(".Value) : null;").AppendLine();
}
else
{
sb.AppendTabs(tabs).Append("public readonly ").Append(_semanticTypeDisplay).Append(" ").Append(_fieldName).Append("Value => ")
.Append(_converterFqn).Append(".FromWire(").Append(_fieldName).Append(");").AppendLine();
}
}
}
}
Loading
Loading