Skip to content

Land F# code generation Phase B on main (GH-2969)#2974

Merged
jeremydmiller merged 4 commits into
mainfrom
feat-2969-fsharp-codegen-foundation
May 29, 2026
Merged

Land F# code generation Phase B on main (GH-2969)#2974
jeremydmiller merged 4 commits into
mainfrom
feat-2969-fsharp-codegen-foundation

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Catch-up PR. #2971 merged the feat-2969-fsharp-codegen-foundation branch to main at its foundation+Phase-A state, but #2973 (Phase B — in-memory saga frames) merged into this same branch ~18 seconds later. So Phase B is in the branch but never reached main. This PR brings it across.

Diff is Phase B only (foundation + Phase A are already on main):

  • F# emit for the in-memory saga frame set (CreateNewSagaFrame, ResolveSagaFrame, SagaStoreOrDeleteFrame, ConditionalSagaInsertFrame, SetSagaIdFrame, SetSagaIdFromSagaFrame, AssertSagaStateExistsFrame, PullSagaIdFromMessageFrame, PullSagaIdFromEnvelopeFrame).
  • The CountingSaga fixture + multi-chain rendering.

Already reviewed/merged as #2973 into the branch; this is purely the branch→main hop. fsharp-coverage after this lands: 24 implemented / 2 skipped / 18 remaining.

Part of #2969.

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits May 29, 2026 13:27
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>
F# code generation Phase B: in-memory saga frames (GH-2969)
…H-2969)

Phase C of the F# code-generation audit (#2969) — Wolverine.Http. Stands up the
HTTP F# fixture surface and renders a real HttpChain to compilable F# for the
static-response (GET) path.

- New per-surface trio under src/Testing/: Wolverine.Http.FSharpContracts
  (endpoint classes), Wolverine.Http.FSharpFixture (F#, checked-in Generated.fs),
  Wolverine.Http.FSharpTests (compile gate + HTTP fsharp-coverage smoke test).
  Added to wolverine_fsharp.slnx; CI now runs `dotnet test wolverine_fsharp.slnx`
  across both the Core and Http surfaces.
- HTTP chains render with no web host via HttpChain.ChainFor<T>(x => x.Method(),
  httpGraph) + httpGraph.StartAssembly(rules) + AssembleTypes + GenerateFSharpCode.
  The generated type subclasses the public HttpHandler base.
- WriteStringFrame emits F# (HttpHandler.WriteString is static, so it resolves
  without a `this`). A `[WolverineGet]` endpoint returning a string renders and
  compiles end to end.

Deferred — blocked on the JasperFx F# self-identifier gap (to be filed upstream):
HttpHandler.ReadJsonAsync<T> and WriteJsonAsync<T> are INSTANCE methods called
unqualified in the generated handler, which F# cannot resolve from a
`member _.Handle` body. So ReadJsonBody / WriteJsonFrame (the JSON POST path)
can't emit F# until JasperFx emits a named self for generated members. The POST
endpoint is kept in the contracts for that follow-up but is not rendered yet.
Same root gap as RecordMessageCausationFrame (Phase B).

fsharp-coverage with Wolverine.Http loaded: 25 implemented / 2 skipped /
54 remaining of 81 Frame types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F# code generation Phase C (partial): static-response HTTP endpoints (GH-2969)
@jeremydmiller jeremydmiller merged commit 04c1784 into main May 29, 2026
24 checks passed
This was referenced Jun 1, 2026
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