F# code generation Phase A: core handler frames (GH-2969)#2972
Merged
jeremydmiller merged 1 commit intoMay 29, 2026
Merged
Conversation
Phase A of the F# code-generation audit (#2969): teach the handler/middleware frames that make up a basic in-process handler chain to emit F#, and render a real Wolverine HandlerGraph chain (not a hand-built assembly) to compilable F#. Frame F# overrides: - MessageFrame: `let msg = envelope.Message :?> T` (obj downcast). - TagHandlerFrame / AuditToActivityFrame: F# has no null-conditional `?.`, and Activity.SetTag returns the Activity, so guard `if not (isNull Activity.Current)` and pipe each call to `ignore`. - ApplyExecutionDiagnosticTagsFrame: void static call. - SimpleValidationHandlerFrame / RequirementResultHandlerFrame / HandlerContinuationFrame: the mid-chain rework. F# has no early `return`, so `if (cond) return; <Next>` becomes `if cond then () else <Next>` — the rest of the chain renders inside `else`, and the abort branch is always `()` (the Task result comes from the enclosing `task { }` or the trailing Task.CompletedTask). Shared via FSharpEmitHelpers.WriteAbortGuard. Skip-marked with [FSharpEmit(Skip)] (unreachable in a minimal chain; reworked in a later phase): ReadEnvelopeHeaderFrame (out-var TryParse/`default`), TryCatchFinallyFrame (imperative inheritance-ordered catch blocks). FSharpEmitHelpers.FSharpUsage works around a JasperFx CastVariable gap: its Usage bakes a C# `((Type)x)` cast (e.g. injected ILogger<TMessage> -> ILogger) that is invalid F#; rewrite as an F# upcast `(x :> Type)`. Harness: the driver now stands up a minimal in-memory host, compiles the graph without starting it, and renders every contracts-assembly chain into Generated.fs. Three C# handlers exercise all 7 newly-implemented frames across the async cascade path and both sync continuation paths (RequirementResult + the HandlerContinuation gate), hitting both the `task { }` and trailing-CompletedTask abort shapes. (Authoring handlers in F# is the separate concern of #2968.) fsharp-coverage after Phase A: 15 implemented / 2 skipped / 27 remaining of 44 Frame types in Wolverine.dll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 1, 2026
Merged
This was referenced Jun 4, 2026
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 A of the F# code-generation audit (#2969). Teaches the handler/middleware frames that make up a basic in-process handler chain to emit F#, and evolves the harness to render a real Wolverine
HandlerGraphchain (not a hand-built assembly) to compilable F#.Frame F# overrides
MessageFrame—let msg = envelope.Message :?> T(the message isobj, so a dynamic downcast).TagHandlerFrame/AuditToActivityFrame— F# has no null-conditional?., andActivity.SetTagreturns theActivity, so guardif not (isNull Activity.Current) then …and pipe each call toignore.ApplyExecutionDiagnosticTagsFrame— void static call.SimpleValidationHandlerFrame,RequirementResultHandlerFrame,HandlerContinuationFrame. F# has no earlyreturn, so the C#if (cond) return; <Next>becomesif cond then () else <Next>: the rest of the chain renders insideelse, and the abort branch is always()(the method'sTaskresult comes from the enclosingtask { }or the machinery-appended trailingTask.CompletedTask, so both branches areunitand the guard sits cleanly in statement position). Shared viaFSharpEmitHelpers.WriteAbortGuard.Skip-marked (
[FSharpEmit(Skip)])Unreachable in a minimal chain; reworked in a later phase:
ReadEnvelopeHeaderFrame— emits out-varTryParse+ a reassigneddefault.TryCatchFinallyFrame— imperative inheritance-ordered catch-block rewrites.JasperFx-layer gap worked around
FSharpEmitHelpers.FSharpUsagerewrites aCastVariable's C#((Type)x)cast (e.g. the injectedILogger<TMessage>handed to validation frames asILogger) into an F# upcast(x :> Type). The proper fix is an F#-aware usage on JasperFx'sCastVariable; to be filed upstream. (Separately,RecordMessageCausationFramestays in the remaining bucket: it emits an unqualified inherited instance call, which needs F# self/base-identifier support in JasperFx's member emit — and it's off-by-default, so it isn't triggered here.)Harness
The driver now stands up a minimal in-memory host, compiles the graph without starting it (
GetServices<ICodeFileCollection>(), no Roslyn/transports), and renders every contracts-assembly chain into oneGenerated.fs. Three C# handlers exercise all 7 newly-implemented frames across the async-cascade path and both sync continuation paths (RequirementResult+ aHandlerContinuationgate) — hitting both thetask { }and trailing-Task.CompletedTaskabort shapes. (Authoring handlers in F# is the separate concern of #2968; this audit proves the codegen frames emit F#.)Generated F# (excerpt — the validation + cascading chain)
Verification
Generated.fs(4 handler chains) compiles viadotnet build.fsharp-coverage— 15 implemented / 2 skipped / 27 remaining of 44 (up from 8/0/36 at Foundation).dotnet build wolverine_fsharp.slnx -c Release— clean.dotnet build wolverine.slnx -c Releaseregression — 0 warnings, 0 errors.Part of #2969.
🤖 Generated with Claude Code