Skip to content

F# code generation Phase A: core handler frames (GH-2969)#2972

Merged
jeremydmiller merged 1 commit into
feat-2969-fsharp-codegen-foundationfrom
feat-2969-fsharp-phase-a
May 29, 2026
Merged

F# code generation Phase A: core handler frames (GH-2969)#2972
jeremydmiller merged 1 commit into
feat-2969-fsharp-codegen-foundationfrom
feat-2969-fsharp-phase-a

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

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 HandlerGraph chain (not a hand-built assembly) to compilable F#.

Stacked on #2971 (the Foundation PR). Base is feat-2969-fsharp-codegen-foundation; review/merge that first, then this rebases onto main. The diff below is Phase A only.

Frame F# overrides

  • MessageFramelet msg = envelope.Message :?> T (the message is obj, so a dynamic downcast).
  • TagHandlerFrame / AuditToActivityFrame — F# has no null-conditional ?., and Activity.SetTag returns the Activity, so guard if not (isNull Activity.Current) then … and pipe each call to ignore.
  • ApplyExecutionDiagnosticTagsFrame — void static call.
  • The mid-chain reworkSimpleValidationHandlerFrame, RequirementResultHandlerFrame, HandlerContinuationFrame. F# has no early return, so the C# 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 method's Task result comes from the enclosing task { } or the machinery-appended trailing Task.CompletedTask, so both branches are unit and the guard sits cleanly in statement position). Shared via FSharpEmitHelpers.WriteAbortGuard.

Skip-marked ([FSharpEmit(Skip)])

Unreachable in a minimal chain; reworked in a later phase:

  • ReadEnvelopeHeaderFrame — emits out-var TryParse + a reassigned default.
  • TryCatchFinallyFrame — imperative inheritance-ordered catch-block rewrites.

JasperFx-layer gap worked around

FSharpEmitHelpers.FSharpUsage rewrites a CastVariable's C# ((Type)x) cast (e.g. the injected ILogger<TMessage> handed to validation frames as ILogger) into an F# upcast (x :> Type). The proper fix is an F#-aware usage on JasperFx's CastVariable; to be filed upstream. (Separately, RecordMessageCausationFrame stays 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 one Generated.fs. Three C# handlers exercise all 7 newly-implemented frames across the async-cascade path and both sync continuation paths (RequirementResult + a HandlerContinuation gate) — hitting both the task { } and trailing-Task.CompletedTask abort 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)

override _.HandleAsync(context: MessageContext, cancellation: CancellationToken) : Task =
    task {
        let createName = context.Envelope.Message :?> CreateName
        WolverineTracing.ApplyExecutionDiagnosticTags(Activity.Current, context.Envelope)
        if not (isNull Activity.Current) then
            Activity.Current.SetTag("name", createName.Name) |> ignore
        let nameHandler = NameHandler()
        let stringValueIEnumerable1 = nameHandler.Validate(createName)
        if SimpleValidationContinuationPolicy.LogValidationMessages((_loggerForMessage :> ILogger), stringValueIEnumerable1) then
            ()
        else
            let outgoing1 = nameHandler.Handle(createName)
            do! context.EnqueueCascadingAsync(outgoing1)
    }

Verification

  • Compile-gate test — the regenerated Generated.fs (4 handler chains) compiles via dotnet build.
  • fsharp-coverage15 implemented / 2 skipped / 27 remaining of 44 (up from 8/0/36 at Foundation).
  • dotnet build wolverine_fsharp.slnx -c Release — clean.
  • Full dotnet build wolverine.slnx -c Release regression — 0 warnings, 0 errors.

Part of #2969.

🤖 Generated with Claude Code

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>
@jeremydmiller jeremydmiller merged commit e36e0d1 into feat-2969-fsharp-codegen-foundation May 29, 2026
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