From 28e9956b8fd066b8789830106795e06594b47140 Mon Sep 17 00:00:00 2001 From: Robert Putnam Date: Fri, 24 Jul 2026 11:29:53 -0600 Subject: [PATCH 1/2] fix(fsharp): propagate DerivedVariable IsReferenced to same-named Arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteFSharpMethod prefixes unreferenced method arguments with `_` to satisfy the F# compiler's unused-variable warnings. The check used arg.IsReferenced, but a method argument and a DerivedVariable that aliases the same name are distinct Variable instances — so marking one referenced had no effect on the other. Concrete case: Wolverine's HandlerChain adds ContextVariable("context", IMessageContext) to DerivedVariables so middleware can resolve IMessageContext. When a middleware frame resolves that alias and marks it referenced, the actual HandleAsync argument (`context: MessageContext`) stayed IsReferenced = false. WriteFSharpMethod then emitted `_context` in the signature while the generated body still used `context`, producing FS0039 ("value or constructor 'context' is not defined") on every handler that had IMessageContext-consuming middleware. Fix: before building the argument list in WriteFSharpMethod, walk Arguments and set IsReferenced = true on any whose Usage matches a referenced DerivedVariable. Setting it on the Variable object itself (inside the JasperFx assembly, so `internal set` is reachable) keeps all downstream codegen — including frames that read arg.IsReferenced directly — consistent with the updated state. --- src/JasperFx/CodeGeneration/GeneratedMethod.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/JasperFx/CodeGeneration/GeneratedMethod.cs b/src/JasperFx/CodeGeneration/GeneratedMethod.cs index 578a005..7d65251 100644 --- a/src/JasperFx/CodeGeneration/GeneratedMethod.cs +++ b/src/JasperFx/CodeGeneration/GeneratedMethod.cs @@ -205,6 +205,21 @@ public void WriteFSharpMethod(ISourceWriter writer) Header?.Write(writer); + // Propagate IsReferenced from DerivedVariables to Arguments that share the same Usage name. + // Handles the case where a method argument (e.g. `context: MessageContext`) is exposed under + // an interface alias in DerivedVariables (e.g. `ContextVariable("context", IMessageContext)`): + // middleware frames resolve the alias and mark it referenced, but the original argument object + // is a distinct Variable instance and would otherwise receive an erroneous `_` prefix. + // Setting IsReferenced here (within the JasperFx assembly, so `internal set` is accessible) + // ensures all downstream F# code-generation that reads arg.IsReferenced is also consistent. + foreach (var arg in Arguments) + { + if (!arg.IsReferenced && DerivedVariables.Any(d => d.Usage == arg.Usage && d.IsReferenced)) + { + arg.IsReferenced = true; + } + } + var arguments = Arguments.Select(x => $"{(x.IsReferenced ? x.Usage : "_" + x.Usage)}: {x.VariableType.FSharpName()}").Join(", "); var returnType = ReturnType.FSharpName(); var keyword = Overrides ? "override" : "member"; From fa7da8fd8ddd6a317918629376d4c6a2d2ae0759 Mon Sep 17 00:00:00 2001 From: Robert Putnam Date: Fri, 24 Jul 2026 11:39:27 -0600 Subject: [PATCH 2/2] tests for derived usages --- src/CodegenTests/FSharpGenerationTests.cs | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/CodegenTests/FSharpGenerationTests.cs b/src/CodegenTests/FSharpGenerationTests.cs index ba81e49..336f87f 100644 --- a/src/CodegenTests/FSharpGenerationTests.cs +++ b/src/CodegenTests/FSharpGenerationTests.cs @@ -89,6 +89,17 @@ public interface IFSharpSyncTaskHandler Task HandleAsync(string name); } +// Types for the DerivedVariable → Argument IsReferenced propagation test. +// Models the Wolverine pattern where a concrete argument (e.g. MessageContext) is +// exposed under an interface alias (e.g. IMessageContext) in DerivedVariables. +public interface IFakeBusContext { } +public class FakeBusContext : IFakeBusContext { } + +public interface IFakeBusContextHandler +{ + Task HandleAsync(FakeBusContext context); +} + public interface IFSharpTupleConsumer { void Consume(); @@ -371,6 +382,39 @@ public void generates_let_bang_binding_for_async_tuple_return() code.ShouldContain("let! struct (red, _, _) = _target.AsyncReturnTuple()"); } + [Fact] + public void argument_is_not_prefixed_with_underscore_when_derived_variable_with_same_name_is_referenced() + { + var assembly = new GeneratedAssembly(new GenerationRules("Some.Generated")); + var type = assembly.AddType("GeneratedFakeContextHandler", typeof(IFakeBusContextHandler)); + var method = type.MethodFor(nameof(IFakeBusContextHandler.HandleAsync)); + + // Simulate the Wolverine pattern: the concrete argument (FakeBusContext context) is + // exposed under an interface alias in DerivedVariables, and middleware marks the alias + // as referenced without touching the original Argument object. + // InternalsVisibleTo lets us set `IsReferenced` directly here. + var derivedContext = new Variable(typeof(IFakeBusContext), "context"); + derivedContext.IsReferenced = true; + method.DerivedVariables.Add(derivedContext); + + // This frame does NOT use `context` directly — it represents middleware that only + // needs the interface alias. + var service = new InjectedField(typeof(FSharpControlService), "service"); + method.Frames.Add(new MethodCall(typeof(FSharpControlService), nameof(FSharpControlService.Record)) + { + Target = service + }); + + var code = assembly.GenerateFSharpCode(); + + // WriteFSharpMethod must propagate IsReferenced from the DerivedVariable to the + // Argument with the matching Usage name. Without the fix the argument would be + // emitted as `_context` (FS1182 suppression prefix), breaking any frame body that + // references `context`. + code.ShouldContain("member this.HandleAsync(context:"); + code.ShouldNotContain("member this.HandleAsync(_context:"); + } + public class UnsupportedFrame : SyncFrame { public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)