Skip to content

F# code generation foundation harness (GH-2969)#2971

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

F# code generation foundation harness (GH-2969)#2971
jeremydmiller merged 3 commits into
mainfrom
feat-2969-fsharp-codegen-foundation

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

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.GenerateFSharpCode seam, 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 base Frame is a nuget type and Wolverine frames inherit SyncFrame/MethodCall directly (no shared Wolverine base).
  • wolverine-diagnostics fsharp-coverage (Diagnostics/WolverineDiagnosticsCommand.cs) — reflects over loaded Wolverine.* assemblies and buckets every Frame into implemented / intentionally-skipped / remaining. Classification is precise: a frame that overrides GenerateCode (custom C#) but not GenerateFSharpCode stays remaining rather than silently inheriting a generic base rendering.
  • First real override: MessageContextFrame.GenerateFSharpCodelet 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-in Generated.fs; FSharp.Core pinned (DisableImplicitFSharpCoreReference + explicit ref) to dodge the documented CS1705/FS0193 version mismatch.
  • Wolverine.Core.FSharpTests (C# xUnit) — compile-gate that regenerates Generated.fs from real frames and shells dotnet build on the fixture (one-time FS0193 retry), plus an fsharp-coverage smoke test.

Solution + CI

  • wolverine_fsharp.slnx at the repo root — not in wolverine.slnx/wolverine_slim.slnx (per the issue: too many projects in the main solution).
  • .github/workflows/fsharp.yml — path-filtered to Frame/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)

namespace Wolverine.Core.FSharpFixture.Generated

type GeneratedFoundationProbe() =
    interface Wolverine.Core.FSharpContracts.IFoundationProbe with
        member _.Run(runtime: Wolverine.Runtime.IWolverineRuntime) : unit =
            // Generated by Wolverine F# code generation (GH-2969 foundation probe)
            let messageContext = Wolverine.Runtime.MessageContext(runtime)
            ()

Verification

  • dotnet build wolverine_fsharp.slnx -c Release — clean.
  • Compile-gate test — generated F# compiles via dotnet build.
  • fsharp-coverage8 implemented / 0 skipped / 36 remaining / 44 loaded (Wolverine.dll), no throw.
  • Full dotnet build wolverine.slnx -c Release regression — 0 warnings, 0 errors.

Not in this PR (next steps)

  • Phase A coverage: F# emit on the handler/middleware frames. The real work is the mid-chain return rework in HandlerContinuationFrame / SimpleValidationHandlerFrame / RequirementResultHandlerFrame (F#'s expression-body model has no early return — Next must render inside an if/else branch). The two "hard" frames the issue names (ReadEnvelopeHeaderFrame, TryCatchFinallyFrame) are unreachable in a minimal chain and will be skip-marked.
  • Phases B–E (sagas/persistence, Http, Grpc, long tail), the behavioural run-under-host depth, the F# sample, and the docs page.

Part of #2969.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits May 29, 2026 12:53
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)
@jeremydmiller jeremydmiller merged commit 706c52e 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