F# code generation for the pre-generated code model (#383) — Milestone 1#384
Merged
Conversation
Milestone 1: emit F# as an alternative to C# for the static (pre-generated)
code model only — never in-memory Roslyn compilation. The C# path is untouched;
every new per-frame member is virtual with a default-throw NotSupportedException
so existing frames keep compiling and working.
Phase 1 — writer + seam:
- FSharpSourceWriter : ISourceWriter, where BLOCK/END/FinishBlock indent/dedent
without emitting braces (F# significant whitespace); the ISourceWriter
interface proved F#-sufficient (no IFSharpSourceWriter needed).
- Frame.GenerateFSharpCode virtual default-throw (names the frame).
- Variable.FSharpAssignmentUsage + Mutable flag (let vs let mutable).
- Type.FSharpName() with F# primitive aliases (double->float, void->unit, ...),
arrays, closed generics; throws loudly on open generics / tuples / by-ref.
Phase 2 — outer emit layers:
- GeneratedAssembly.GenerateFSharpCode (namespace + open + types at column 0).
- GeneratedType.WriteFSharp (primary ctor, let-bound fields, interface-member
grouping, base-class inherit).
- GeneratedMethod.WriteFSharpMethod (member signature; drives the same arranged
frame chain; sync body + task { } async wrapper with literal CE braces).
Phase 3 — five frames: CommentFrame, CodeFrame, ConstructorFrame, MethodCall,
ReturnFrame. F# is expression-oriented, so ReturnFrame emits a bare trailing
expression when synchronous and `return x` only inside a task block.
Phase 4 — fixtures + command + gate:
- FSharpCodegenTarget (C# contracts), CodegenTests.FSharpFixture (checked-in
.fsproj + committed Generated.fs), CodegenTests.FSharp (codegen-fsharp command,
sample builder, and an integration test that regenerates Generated.fs and
shells out to `dotnet build`, asserting exit 0). The integration-test project
is intentionally NOT wired into ./build.sh / Nuke test targets.
Acceptance: a trivial generated F# type compiles (dotnet build exit 0), the full
C# suite stays green, and unimplemented frames throw a clear NotSupportedException
naming themselves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an async contract (IAsyncGreeter + GreetingService.CreateGreetingAsync) and a
second generated type to the fixture sample whose method is asynchronous, so the
body is wrapped in a `task { }` computation expression with `let!` (await) and
`return`. Regenerated Generated.fs now contains both the synchronous and the
asynchronous type, and the compile gate proves both compile (dotnet build exit 0).
Also adds a fast async-emit unit test in CodegenTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…LastNode (#383) Unify the F# emit rule: a `task { }` computation expression is emitted iff AsyncMode == AsyncTask; everything else is a bare F# expression body. let mutable (ReturnAction.Assign): - MethodCall.AssignResultTo now marks the target Variable as Mutable, so its first binding renders `let mutable x = ...` and the assignment site renders `x <- ...`. Harmless on the C# path and a no-op for arguments/injected fields (never rendered via FSharpAssignmentUsage). Idiomatic ReturnFromLastNode: - When the single trailing async call IS the return value, emit the Task expression directly (e.g. `member _.M(...) = svc.FooAsync(args)`) instead of wrapping it in `task { return! ... }` — no unnecessary state machine. Fixture gains GeneratedDirectAsyncGreeter (bare trailing Task) and GeneratedAccumulator (let mutable + <-); both compile via the gate. Adds fast emit unit tests for each. Full C# suite stays green (CodegenTests 383, CoreTests 439, + AOT smoke). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#383) Add GenerateFSharpCode to the control-flow frames, mapping C# statement blocks onto F# expressions: - CompositeFrame: parallel F# chaining + a `generateFSharpCode` hook (default-throw, naming the frame) for subclasses; GenerateCode stays sealed/untouched. - IfBlock: `if <condition> then` with a brace-free indented body (condition is a raw passthrough, like CodeFrame). - IfElseNullGuardFrame: `if isNull x then ... else ...` (an expression — both branches yield the trailing value), and its nested IfNullGuardFrame: `if not (isNull x) then`. - TryFinallyWrapperFrame: `try <body> finally <cleanup>`. Gated by three new fixture types (GeneratedConditionalGreeter, GeneratedToggle, GeneratedResourceRunner) that compile via `dotnet build`, plus fast emit unit tests. Full C# suite stays green (CodegenTests 386, CoreTests 439, + AOT smoke). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GenerateFSharpCode now emits `open` statements derived from each generated type's base type, implemented interfaces, and every generated method's return type and argument types (recursing into generic arguments) — not just injected-field namespaces. Implemented as an F#-only GeneratedAssembly.AllReferencedFSharpNamespaces so the shared C# AllReferencedNamespaces / GenerateCode path is untouched. All emitted F# names remain fully qualified, so these opens are convenience rather than a correctness requirement. Generated.fs now also opens System and System.Threading.Tasks. Adds a unit test; fixture still compiles via the gate; full C# suite green (the lone CommandLineTests blip was the known-flaky console-capture test CommandExecutorTester, which passes on isolated/repeat runs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A representative multi-frame handler that surfaces gaps before the real Wolverine
frame set: construct a domain object from the command, `do!` an async repository
save, build a confirmation with a sync factory call, and `return` it — all inside a
single `task { }` body (mixed sync + async frames). Gated via `dotnet build` and a
fast emit unit test. No core library changes; CodegenTests 388 green on net9/net10.
Generated shape:
member _.Handle(command: PlaceOrder) : Task<OrderConfirmation> =
task {
let order = FSharpCodegenTarget.Order(command)
do! _orderRepository.SaveAsync(order)
let orderConfirmation = _confirmationFactory.Create(order)
return orderConfirmation
}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ate (#383) WriteFSharpMethod now completes the async-mode matrix: a synchronous body behind a non-generic Task signature (AsyncMode.None, no ReturnVariable) emits the side-effect frames followed by a trailing `System.Threading.Tasks.Task.CompletedTask` — the F# equivalent of C#'s `return Task.CompletedTask;`. This is the common Wolverine "synchronous handler with a Task signature" shape. Gated by a new GeneratedSyncTaskHandler fixture type + a fast unit test. Also harden the compile gate: a nested `dotnet build` (build-inside-test) can rarely trip an internal F# compiler crash (FS0193 in the auto-generated AssemblyAttributes.fs) unrelated to the generated source. The gate now retries the build once on that specific internal-error signature only — a genuine F# error in Generated.fs is deterministic and persists across the retry, so this cannot mask a real failure. Full C# suite green (CodegenTests 389, CoreTests 439, + AOT smoke). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Implements Milestone 1 of #383 and several follow-on increments: emit F# as an alternative to C# for the pre-generated (static) code model only (never in-memory Roslyn). The C# path is untouched — every new per-frame member is
virtualwith a default-throwNotSupportedException, so existing frames keep compiling and working.Writer + seam (Phase 1)
CodeGeneration/FSharp/FSharpSourceWriter.cs:ISourceWriter—BLOCK:/END/FinishBlockindent/dedent without braces (F# significant whitespace); line writing + backtick→"identical to the C# writer.Frame.GenerateFSharpCode(...)—virtual, default-throws naming the frame.Variable.FSharpAssignmentUsage+Mutableflag (letvslet mutable).Type.FSharpName()— F# primitive aliases (double→float,void→unit, …), arrays, closed generics; throws loudly on open generics / tuples / by-ref.Outer emit layers (Phase 2)
GeneratedAssembly.GenerateFSharpCode(namespace+open+ types at column 0)GeneratedType.WriteFSharp(primary ctor,let _x = xfields, interface-member grouping, base-classinherit)GeneratedMethod.WriteFSharpMethod(member signature; drives the same arranged frame chain)Frames
CommentFrame,CodeFrame,ConstructorFrame,MethodCall,ReturnFrame. F# is expression-oriented, soReturnFrameemits a bare trailing expression outside a task block andreturn xinside one.CompositeFrame(parallel F# chaining + agenerateFSharpCodehook),IfBlock(if cond then),IfElseNullGuardFrame(if isNull x then … else …) + itsIfNullGuardFrame(if not (isNull x) then), andTryFinallyWrapperFrame(try … finally …).Async-mode matrix & emit refinements
A
task { }block is emitted iffAsyncMode == AsyncTask:AsyncTask→task { … let! … return … }ReturnFromLastNode→ the trailing Task is returned directly (member _.M(...) = svc.FooAsync(args)) — notask { }/return!None+ non-genericTaskreturn → side-effect frames then a trailingTask.CompletedTask(the synchronous-handler-with-a-Task-signature shape)let mutable→MethodCall.AssignResultTomarks its targetMutable, so a reassigned local renderslet mutable x = …thenx <- …(no-op on the C# path and for args/injected fields)opencoverage → derived from injected fields plus base/interface/return/argument types (recursing into generic args), via an F#-onlyAllReferencedFSharpNamespacesFixtures + command + gate (Phase 4)
FSharpCodegenTarget(C# contracts),CodegenTests.FSharpFixture(checked-in.fsproj+ committedGenerated.fs),CodegenTests.FSharp(thecodegen-fsharpcommand, sample builder, and the integration test that regeneratesGenerated.fsand shells out todotnet build)../build.sh/ Nuke test targets. Run it withdotnet test src/CodegenTests.FSharp. The gate retries once on a transient internal F# compiler crash (FS0193) — a real error inGenerated.fsis deterministic and still fails.task { }async, direct-async (ReturnFromLastNode), sync-Task (Task.CompletedTask),let mutablereassignment,if/then,if/elsenull guard,try/finally, and a Wolverine-shaped async handler (construct →do!save → sync factory →return).Acceptance criteria
dotnet build(exit 0)NotSupportedExceptionnaming itselfStill deferred (guarded by the default-throw seam)
Task<T>returned from a synchronous body (would needTask.FromResult)Commits
task { }gatelet mutable+ idiomaticReturnFromLastNodeopencoverageTask-returning methods (Task.CompletedTask) + gate hardening🤖 Generated with Claude Code