F# code generation Phase B: in-memory saga frames (GH-2969)#2973
Merged
jeremydmiller merged 1 commit intoMay 29, 2026
Merged
Conversation
Phase B of the F# code-generation audit (#2969): teach the in-memory stateful saga frame set to emit F#, and render a real saga's start + continue chains to compilable F#. A string-keyed CountingSaga (Start + Handle) drives the fixture. Saga frame F# overrides: - CreateNewSagaFrame: `let saga = T()`. - ResolveSagaFrame / SagaStoreOrDeleteFrame / ConditionalSagaInsertFrame: composites that delegate to their inner frames' F# and render the surrounding `if/else` (IsCompleted -> delete/update; not-completed -> insert). - SetSagaIdFrame / SetSagaIdFromSagaFrame: SetSagaId + a null-guarded SetTag block (no `?.`, pipe SetTag to ignore). - AssertSagaStateExistsFrame: `raise (UnknownSagaException(typeof<T>, sagaId))`. - PullSagaIdFromMessageFrame / PullSagaIdFromEnvelopeFrame: the saga-id resolvers. The string branch uses `if isNull x then envelope.SagaId else x`; the Guid/numeric branch uses F#'s auto-tupled `match Type.TryParse(s) with | true, v -> ... | _ -> ...`, with Unchecked.defaultof<T> as the zero/default sentinel. In-memory Load/Store/Delete are plain MethodCalls (already emit F#); the not-found null-guard comes from JasperFx's IfElseNullGuardFrame (already F#). fsharp-coverage after Phase B: 24 implemented / 2 skipped / 18 remaining of 44 Frame types in Wolverine.dll (up from 15/2/27 at Phase A). Deferred to a Phase B follow-up (not reachable from a minimal in-memory saga, so not exercised here): CreateMissingSagaFrame (reassigns the loaded-saga var; needs the load variable marked mutable), the store-specific LoadSagaOperation / SagaOperation / EnrollAndFetchSagaStorageFrame wrappers, ShouldProceedGuardFrame (ResequencerSaga), the Marten/Polecat data-requirement frames (separate assemblies / per-store fixtures), and the behavioural run-step (TypeLoadMode.Static). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 1, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase B of the F# code-generation audit (#2969). Teaches the in-memory stateful saga frame set to emit F#, and renders a real saga's start + continue chains to compilable F#.
A string-keyed
CountingSaga(Start+Handle) drives the fixture; the driver renders both its chains alongside the Phase A handlers.Saga frame F# overrides
CreateNewSagaFrame—let saga = T().ResolveSagaFrame/SagaStoreOrDeleteFrame/ConditionalSagaInsertFrame— composites that delegate to their inner frames' F# and render the surroundingif/else(IsCompleted()→ delete/update; not-completed → insert).SetSagaIdFrame/SetSagaIdFromSagaFrame—SetSagaId+ a null-guardedSetTagblock (no?.; pipeSetTagtoignore).AssertSagaStateExistsFrame—raise (UnknownSagaException(typeof<T>, sagaId)).PullSagaIdFromMessageFrame/PullSagaIdFromEnvelopeFrame— the saga-id resolvers. The string branch usesif isNull x then envelope.SagaId else x; the Guid/numeric branch uses F#'s auto-tupledmatch Type.TryParse(s) with | true, v -> … | _ -> …, withUnchecked.defaultof<T>as the zero/default sentinel.In-memory
Load/Store/Deleteare plainMethodCalls (already emit F#); the not-found null-guard comes from JasperFx'sIfElseNullGuardFrame(already F#).Generated F# (excerpt — the saga continue chain)
Verification
Generated.fs(4 handler chains + 2 saga chains) compiles viadotnet build.fsharp-coverage— 24 implemented / 2 skipped / 18 remaining of 44 (up from 15/2/27 at Phase A).dotnet build wolverine_fsharp.slnx -c Release— clean.dotnet build wolverine.slnx -c Releaseregression — 0 warnings, 0 errors.Deferred (not reachable from a minimal in-memory saga, so not exercised here)
CreateMissingSagaFrame(reassigns the loaded-saga var → needs the loadVariablemarkedMutableso F# renderslet mutable; only used for start-or-continue /NotFound).LoadSagaOperation/SagaOperation/EnrollAndFetchSagaStorageFrame<,>.ShouldProceedGuardFrame(ResequencerSaga).ThrowRequiredDataMissingExceptionFrame,ConstructSpecificationFrame, …) — separate assemblies; need per-store fixture projects (the issue's open per-store-granularity question).TypeLoadMode.Static+InvokeAsync+ assert) — Phase B stays at compile-gate depth for now, like Phase A.Part of #2969.
🤖 Generated with Claude Code