F# code generation foundation harness (GH-2969)#2971
Merged
Conversation
Lands the scaffolding the phased F# code-generation audit (#2969) depends on, without any of the Phase A frame rework. The #384 F# emitter infrastructure (Frame.GenerateFSharpCode seam, FSharpSourceWriter, GeneratedAssembly.GenerateFSharpCode) already ships in the pinned JasperFx, so this proves the regenerate -> compile pipeline end-to-end against real Wolverine frames. - Add [FSharpEmit(Skip, Reason)] marker attribute for frames that are deliberately "not applicable in F#" (an attribute rather than an override, since JasperFx's base Frame is a nuget type and Wolverine frames inherit SyncFrame/MethodCall directly). - Add `wolverine-diagnostics fsharp-coverage`: reflects over loaded Wolverine.* assemblies and buckets every Frame as implemented / intentionally-skipped / remaining. Classification is precise -- a frame that overrides GenerateCode but not GenerateFSharpCode stays "remaining" rather than inheriting a generic base rendering. - Implement the first real override: MessageContextFrame.GenerateFSharpCode (`let messageContext = MessageContext(runtime)`). - Add the F# foundation harness under src/Testing/ (3 projects mirroring the #384 fixture pattern): Wolverine.Core.FSharpContracts (C# contract), Wolverine.Core.FSharpFixture (F#, checked-in Generated.fs, FSharp.Core pinned to dodge the CS1705/FS0193 version mismatch), and Wolverine.Core.FSharpTests (C# xUnit compile-gate that regenerates Generated.fs and `dotnet build`s the fixture with a one-time FS0193 retry, plus an fsharp-coverage smoke test). - Add wolverine_fsharp.slnx at the repo root (NOT in wolverine.slnx) and a path-filtered .github/workflows/fsharp.yml. Foundation fsharp-coverage tally: 8 implemented / 0 skipped / 36 remaining of 44 Frame types in Wolverine.dll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
F# code generation Phase A: core handler frames (GH-2969)
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.
Foundation PR for the phased F# code-generation audit (#2969). This lands the scaffolding every phase depends on — without any of the Phase A frame rework — and proves the regenerate → compile pipeline end-to-end against real Wolverine frames.
The #384 emitter infrastructure (
Frame.GenerateFSharpCodeseam,FSharpSourceWriter,GeneratedAssembly.GenerateFSharpCode(),Type.FSharpName()) already ships in the pinned JasperFx, so this needs zero JasperFx changes.What's here
Production (
src/Wolverine)[FSharpEmit(Skip, Reason)]marker attribute (Configuration/FSharpEmitAttribute.cs) for frames that are deliberately not applicable in F#. An attribute rather than an override because JasperFx's baseFrameis a nuget type and Wolverine frames inheritSyncFrame/MethodCalldirectly (no shared Wolverine base).wolverine-diagnostics fsharp-coverage(Diagnostics/WolverineDiagnosticsCommand.cs) — reflects over loadedWolverine.*assemblies and buckets everyFrameinto implemented / intentionally-skipped / remaining. Classification is precise: a frame that overridesGenerateCode(custom C#) but notGenerateFSharpCodestays remaining rather than silently inheriting a generic base rendering.MessageContextFrame.GenerateFSharpCode→let messageContext = MessageContext(runtime).Harness (
src/Testing/, 3 projects mirroring the #384 fixture pattern)Wolverine.Core.FSharpContracts(C#) — shared contract the generated adapter implements.Wolverine.Core.FSharpFixture(F#) — checked-inGenerated.fs; FSharp.Core pinned (DisableImplicitFSharpCoreReference+ explicit ref) to dodge the documented CS1705/FS0193 version mismatch.Wolverine.Core.FSharpTests(C# xUnit) — compile-gate that regeneratesGenerated.fsfrom real frames and shellsdotnet buildon the fixture (one-time FS0193 retry), plus anfsharp-coveragesmoke test.Solution + CI
wolverine_fsharp.slnxat the repo root — not inwolverine.slnx/wolverine_slim.slnx(per the issue: too many projects in the main solution)..github/workflows/fsharp.yml— path-filtered toFrame/Wolverine/Http/Persistence changes + the F# projects. Intended to be made required-for-merge via branch protection on those paths.Generated F# (the committed fixture)
Verification
dotnet build wolverine_fsharp.slnx -c Release— clean.dotnet build.fsharp-coverage— 8 implemented / 0 skipped / 36 remaining / 44 loaded (Wolverine.dll), no throw.dotnet build wolverine.slnx -c Releaseregression — 0 warnings, 0 errors.Not in this PR (next steps)
returnrework inHandlerContinuationFrame/SimpleValidationHandlerFrame/RequirementResultHandlerFrame(F#'s expression-body model has no early return —Nextmust render inside anif/elsebranch). The two "hard" frames the issue names (ReadEnvelopeHeaderFrame,TryCatchFinallyFrame) are unreachable in a minimal chain and will be skip-marked.Part of #2969.
🤖 Generated with Claude Code