Skip to content

Add DateTimeOffset support to TypeScript source generator - #459

Open
aminparsa18 wants to merge 1 commit into
Cysharp:mainfrom
aminparsa18:feature/typescript-datetimeoffset
Open

Add DateTimeOffset support to TypeScript source generator#459
aminparsa18 wants to merge 1 commit into
Cysharp:mainfrom
aminparsa18:feature/typescript-datetimeoffset

Conversation

@aminparsa18

Copy link
Copy Markdown

Problem

DateTime is a Roslyn SpecialType, so TypeScriptMember.ConvertFromSymbol special-cases it into a Date / writeDate / readDate mapping. DateTimeOffset is not a SpecialType (Roslyn only special-cases DateTime), and unlike Guid it has no dedicated named-type check. So any [MemoryPackable, GenerateTypeScript] type with a DateTimeOffset member currently throws MEMPACK031 ("... is not supported type in typescript generation") the moment it's annotated.

Related: #335 (DateOnly/TimeOnly support - same class of gap) went unanswered and was closed by the stale-bot. This PR ships a tested fix rather than another issue.

Fix

  • TypeScriptMember.cs: add a DateTimeOffset branch in ConvertFromSymbol, right next to the existing Guid check, using the ReferenceSymbols.System_DateTimeOffset symbol that's already cached but was only used once elsewhere (an unrelated unmanaged-struct-layout check). Maps to TS Date with BinaryOperationMethod = "DateTimeOffset", so the existing templating - including the Nullable<T> unwrapping path - automatically emits writer.writeDateTimeOffset(...) / reader.readDateTimeOffset() (and the nullable variants for DateTimeOffset?), the same way every other BinaryOperationMethod already works.
  • TypeScriptRuntime.cs: add writeDateTimeOffset/readDateTimeOffset and their nullable counterparts to the embedded runtime-template strings, next to the existing writeDate/readDate.

Wire format

MemoryPack registers DateTimeOffset via UnmanagedFormatter<DateTimeOffset> (a raw struct blit - see MemoryPackFormatterProvider.WellknownTypes.cs), so the byte layout isn't documented anywhere. I verified it empirically with a throwaway console app dumping MemoryPackSerializer.Serialize output for zero/positive/negative offsets, MinValue, MaxValue, and sub-millisecond-tick values, and cross-checked the result against this repo's own MemoryLayoutTest.DateTimeOffsetLayout test.

The actual layout is:

  • bytes [0..4): offsetMinutes as a signed 32-bit int (little-endian)
  • bytes [4..8): padding
  • bytes [8..16): the UTC ticks (not locally-adjusted ticks), with the top 2 DateTimeKind bits masked off - same trick writeDate/readDate already use for DateTime

My first pass assumed a 64-bit offset field with locally-adjusted ticks, which happened to pass for zero/positive offsets but silently produced wrong instants for negative offsets - exactly the kind of bug that's easy to ship if this format is trusted from a template instead of measured. To make sure this can't regress silently, I added MemoryLayoutTest.DateTimeOffsetWireFormatMatchesTypeScriptRuntimeAlgorithm, which pins MemoryPackSerializer's actual byte output against the exact algorithm the TS runtime uses, across zero/positive/negative offsets and Min/Max/epoch values - this also guards against a runtime layout change the same way DateTimeOffsetLayout already does for the raw struct.

Testing

  • GeneratorDiagnosticsTest.TypeScript.cs: new GenerateTypeScriptDateTimeOffset test proves a DateTimeOffset member no longer throws MEMPACK031, and that the generated .ts contains writeDateTimeOffset/readDateTimeOffset calls for a plain member and the nullable variants for a DateTimeOffset? member. The existing MEMPACK031 test (using System.Version, still unsupported) is untouched.
  • MemoryLayoutTest.cs: new DateTimeOffsetWireFormatMatchesTypeScriptRuntimeAlgorithm theory (6 cases) described above.
  • Full suite, matching CI's own invocation:
    dotnet build -c Release
    dotnet test -c Release --no-build
    
    164/164 passed.

🤖 Generated with Claude Code

DateTimeOffset is not a Roslyn SpecialType (only DateTime is), and had no
dedicated check in TypeScriptMember.ConvertFromSymbol the way Guid does, so
any [MemoryPackable, GenerateTypeScript] type with a DateTimeOffset member
threw MEMPACK031 ("not supported type in typescript generation").

- TypeScriptMember.cs: map DateTimeOffset to TS `Date` via the already-cached
  ReferenceSymbols.System_DateTimeOffset symbol (previously unused outside an
  unrelated unmanaged-layout check), with BinaryOperationMethod = "DateTimeOffset"
  so the existing templating (including the Nullable<T> unwrapping path) emits
  writeDateTimeOffset/readDateTimeOffset (and nullable variants) automatically.
- TypeScriptRuntime.cs: add writeDateTimeOffset/readDateTimeOffset and their
  nullable counterparts to the embedded runtime templates.

Wire format: MemoryPack registers DateTimeOffset via UnmanagedFormatter<T> (a
raw struct blit), so the byte layout is undocumented/implementation-defined.
I verified it empirically (throwaway console app dumping serialized bytes for
zero/positive/negative offsets, MinValue, MaxValue, sub-millisecond ticks) and
cross-checked it against this repo's own MemoryLayoutTest.DateTimeOffsetLayout
test: the layout is `int offsetMinutes` (4 bytes) + 4 bytes padding + the UTC
ticks with the top 2 DateTimeKind bits masked off (8 bytes) - the offset field
is a 32-bit int, not 16/64-bit as a first pass assumed, and it holds the *UTC*
ticks (not locally-adjusted ticks). Getting this wrong silently produces wrong
instants for non-UTC offsets, so I added a regression test
(MemoryLayoutTest.DateTimeOffsetWireFormatMatchesTypeScriptRuntimeAlgorithm)
that pins MemoryPackSerializer's actual byte output against the exact
algorithm the TS runtime uses, across zero/positive/negative offsets and
Min/Max/epoch values - this also guards against a runtime layout change the
same way DateTimeOffsetLayout already does for the raw struct.

Also added GenerateTypeScriptDateTimeOffset, proving a DateTimeOffset member no
longer throws MEMPACK031 and that the generated .ts calls
writeDateTimeOffset/readDateTimeOffset (and the nullable variants for
DateTimeOffset?).

Full suite (`dotnet build -c Release && dotnet test -c Release --no-build`,
matching CI) passes: 164/164.

Related: Cysharp#335 (DateOnly/TimeOnly request, same class of gap, went unanswered
and was closed by the stale-bot - this PR ships a tested fix instead of
another issue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant