Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
JasperFx.Events, JasperFx.Events.SourceGenerator) set
<Version>$(JasperFxVersion)</Version> so they always release together. Bump this one
value to release the whole set. -->
<JasperFxVersion>2.2.6</JasperFxVersion>
<JasperFxVersion>2.2.7</JasperFxVersion>
<LangVersion>13</LangVersion>
<NoWarn>1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618</NoWarn>
<Authors>Jeremy D. Miller;Jaedyn Tonee</Authors>
Expand Down
115 changes: 115 additions & 0 deletions src/CodegenTests.FSharp/FSharpCodegenSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,125 @@ public static GeneratedAssembly BuildSampleAssembly()
AddCalculatorType(assembly);
AddCastType(assembly);
AddScopedConsumerType(assembly);
AddActivityEmitterType(assembly);
AddNowHandlerType(assembly);
AddValueTaskHandlerType(assembly);
AddMemberAccessType(assembly);
AddArrayHandlerType(assembly);
AddLazyHandlerType(assembly);

return assembly;
}

/// <summary>
/// Appends an activity event — exercises <see cref="AppendActivityEventFrame" />. F# has no
/// null-conditional operator, so the body emits an explicit <c>if not (isNull Activity.Current)</c>
/// guard and pipes the chained <c>AddEvent</c> result to <c>ignore</c>.
/// </summary>
private static void AddActivityEmitterType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedActivityEmitter", typeof(IActivityEmitter));
var method = type.MethodFor(nameof(IActivityEmitter.Emit));
method.Frames.Add(new AppendActivityEventFrame("sample.activity.event"));
}

/// <summary>
/// Resolves the current time through <see cref="NowTimeVariableSource" /> and passes it to a
/// service — exercises <see cref="NowFetchFrame" /> (<c>let now = System.DateTime.UtcNow</c>).
/// </summary>
private static void AddNowHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedNowHandler", typeof(INowHandler));
var method = type.MethodFor(nameof(INowHandler.Stamp));
method.Sources.Add(new NowTimeVariableSource());

var service = new InjectedField(typeof(ClockService), "clockService");
var call = new MethodCall(typeof(ClockService), nameof(ClockService.Stamp))
{
Target = service,
ReturnAction = ReturnAction.Return
};
method.Frames.Add(call);
}

/// <summary>
/// A synchronous body behind a <see cref="ValueTask{T}" /> signature — exercises
/// <see cref="ReturnValueTask" />: the trailing F# expression is <c>ValueTask&lt;string&gt;(result)</c>.
/// </summary>
private static void AddValueTaskHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedValueTaskHandler", typeof(IValueTaskHandler));
var method = type.MethodFor(nameof(IValueTaskHandler.HandleAsync));

var service = new InjectedField(typeof(ControlFlowService), "controlFlowService");
var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback))
{
Target = service
};
method.Frames.Add(call);
method.Frames.Add(new ReturnValueTask(typeof(string)));
}

/// <summary>
/// Reads a member off a constructed object — exercises <see cref="MemberAccessFrame" />
/// (<c>let value = mutableBox.Value</c>).
/// </summary>
private static void AddMemberAccessType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedMemberAccessHandler", typeof(IMemberAccessHandler));
var method = type.MethodFor(nameof(IMemberAccessHandler.Read));

var ctor = new ConstructorFrame(typeof(MutableBox), typeof(MutableBox).GetConstructors().Single());
method.Frames.Add(ctor);

var member = typeof(MutableBox).GetProperty(nameof(MutableBox.Value))!;
var access = new MemberAccessFrame(typeof(MutableBox), member, "value");
method.Frames.Add(access);

method.Frames.Add(new ReturnFrame(access.Variable));
}

/// <summary>
/// Builds an array literal from two injected elements — exercises <see cref="CreateArrayFrame" />
/// (<c>let things = [| thingA; thingB |]</c>).
/// </summary>
private static void AddArrayHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedArrayHandler", typeof(IArrayHandler));
var method = type.MethodFor(nameof(IArrayHandler.Build));

var thingA = new InjectedField(typeof(Thing), "thingA");
var thingB = new InjectedField(typeof(Thing), "thingB");
var array = new CreateArrayFrame(typeof(Thing[]), typeof(Thing), new Variable[] { thingA, thingB });
method.Frames.Add(array);

method.Frames.Add(new ReturnFrame(array.Variable));
}

/// <summary>
/// Resolves a service flagged for service location off an injected <see cref="IServiceProvider" />
/// — exercises <see cref="LazyServiceLocationFrame" />
/// (<c>let controlFlowService = …GetRequiredService&lt;ControlFlowService&gt;(provider)</c>).
/// </summary>
private static void AddLazyHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedLazyHandler", typeof(ILazyHandler));
var method = type.MethodFor(nameof(ILazyHandler.Handle));

// The injected IServiceProvider is the scope the LazyServiceLocationFrame resolves against;
// register it as a constructor field so FindVariable(IServiceProvider) resolves.
type.AllInjectedFields.Add(new InjectedField(typeof(IServiceProvider), "provider"));
var lazy = new LazyServiceLocationFrame(typeof(ControlFlowService));
method.Frames.Add(lazy);

var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback))
{
Target = lazy.Variable,
ReturnAction = ReturnAction.Return
};
method.Frames.Add(call);
}

/// <summary>
/// A handler that resolves a service-located <see cref="IScopedThing" /> and awaits it.
/// Exercises the scoped-DI frames: <c>use serviceScope = …CreateAsyncScope(…)</c> +
Expand Down
46 changes: 46 additions & 0 deletions src/CodegenTests.FSharpFixture/Generated.fs
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,49 @@ type GeneratedScopedConsumer(serviceScopeFactory: Microsoft.Extensions.Dependenc
do! scopedThing.DoAsync()
}

type GeneratedActivityEmitter() =
interface FSharpCodegenTarget.IActivityEmitter with
member this.Emit() : unit =
if not (isNull System.Diagnostics.Activity.Current) then System.Diagnostics.Activity.Current.AddEvent(System.Diagnostics.ActivityEvent("sample.activity.event")) |> ignore

type GeneratedNowHandler(clockService: FSharpCodegenTarget.ClockService) =
let _clockService = clockService

interface FSharpCodegenTarget.INowHandler with
member this.Stamp() : string =
let now = System.DateTime.UtcNow
_clockService.Stamp(now)

type GeneratedValueTaskHandler(controlFlowService: FSharpCodegenTarget.ControlFlowService) =
let _controlFlowService = controlFlowService

interface FSharpCodegenTarget.IValueTaskHandler with
member this.HandleAsync() : System.Threading.Tasks.ValueTask<string> =
let result_of_Fallback = _controlFlowService.Fallback()
System.Threading.Tasks.ValueTask<string>(result_of_Fallback)

type GeneratedMemberAccessHandler() =
interface FSharpCodegenTarget.IMemberAccessHandler with
member this.Read() : int =
let mutableBox = FSharpCodegenTarget.MutableBox()
let value = mutableBox.Value
value

type GeneratedArrayHandler(thingA: FSharpCodegenTarget.Thing, thingB: FSharpCodegenTarget.Thing) =
let _thingA = thingA
let _thingB = thingB

interface FSharpCodegenTarget.IArrayHandler with
member this.Build() : FSharpCodegenTarget.Thing[] =
let thingArray = [| _thingA; _thingB |]
thingArray

type GeneratedLazyHandler(provider: System.IServiceProvider) =
let _provider = provider

interface FSharpCodegenTarget.ILazyHandler with
member this.Handle() : string =
// This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else
let controlFlowService = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<FSharpCodegenTarget.ControlFlowService>(_provider)
controlFlowService.Fallback()

65 changes: 65 additions & 0 deletions src/FSharpCodegenTarget/Contracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,71 @@ public interface IScopedConsumerHandler
Task Handle();
}

// --- Contracts for the remaining JasperFx F# frame surface (jasperfx#399) ---

/// <summary>
/// A sync void handler used to exercise <c>AppendActivityEventFrame</c>: the generated body emits a
/// guarded <c>Activity.Current.AddEvent(...)</c> (F# has no <c>?.</c>, so an explicit isNull guard).
/// </summary>
public interface IActivityEmitter
{
void Emit();
}

/// <summary>
/// A handler that stamps the current time. The injected <see cref="DateTime" /> resolves through
/// <c>NowTimeVariableSource</c> → <c>NowFetchFrame</c> (<c>let now = System.DateTime.UtcNow</c>).
/// </summary>
public interface INowHandler
{
string Stamp();
}

public class ClockService
{
public string Stamp(DateTime now)
{
return now.ToString("O");
}
}

/// <summary>
/// A handler whose signature returns <see cref="ValueTask{T}" /> with a synchronous body — exercises
/// <c>ReturnValueTask</c>: the trailing F# expression constructs a <c>ValueTask&lt;string&gt;(result)</c>.
/// </summary>
public interface IValueTaskHandler
{
ValueTask<string> HandleAsync();
}

/// <summary>
/// A handler that reads a member off a constructed object — exercises <c>MemberAccessFrame</c>
/// (<c>let value = mutableBox.Value</c>).
/// </summary>
public interface IMemberAccessHandler
{
int Read();
}

/// <summary>
/// A handler that builds an array of injected elements — exercises <c>CreateArrayFrame</c>'s F# array
/// literal (<c>let things = [| thingA; thingB |]</c>).
/// </summary>
public interface IArrayHandler
{
Thing[] Build();
}

/// <summary>
/// A handler that resolves a service flagged <c>AlwaysUseServiceLocationFor</c> off an injected
/// <see cref="IServiceProvider" /> — exercises <c>LazyServiceLocationFrame</c>
/// (<c>let controlFlowService = ServiceProviderServiceExtensions.GetRequiredService&lt;…&gt;(provider)</c>).
/// </summary>
public interface ILazyHandler
{
string Handle();
}

/// <summary>
/// A base class with a public instance method (<see cref="Bump" />) and an abstract method to
/// override (<see cref="Compute" />). The generated subclass overrides <c>Compute</c> and calls the
Expand Down
10 changes: 10 additions & 0 deletions src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,15 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
// F# has no null-conditional operator; guard Activity.Current explicitly. AddEvent returns the
// Activity for chaining, so pipe the result to ignore.
writer.Write(
$"if not (isNull {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}) then {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}.AddEvent({typeof(ActivityEvent).FSharpName()}(\"{_eventName}\")) |> ignore");

Next?.GenerateFSharpCode(method, writer);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain) => [];
}
8 changes: 8 additions & 0 deletions src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine(
$"return new {typeof(ValueTask).FullNameInCode()}<{_variableType.FullNameInCode()}>({_returnValue.Usage});");
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
// Terminal frame: in F# the trailing expression IS the return value (no `return` keyword).
// ValueTask<T> is a struct, so it is constructed without `new`.
writer.WriteLine(
$"{typeof(ValueTask).FSharpName()}<{_variableType.FSharpName()}>({_returnValue.FSharpUsage})");
}
}
7 changes: 7 additions & 0 deletions src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using JasperFx.CodeGeneration.Frames;
using JasperFx.Core.Reflection;

namespace JasperFx.CodeGeneration.Model;

Expand Down Expand Up @@ -47,6 +48,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine($"var {Variable.Usage} = {Variable.VariableType.FullName}.{nameof(DateTime.UtcNow)};");
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteLine($"{Variable.FSharpAssignmentUsage} = {Variable.VariableType.FSharpName()}.{nameof(DateTime.UtcNow)}");
Next?.GenerateFSharpCode(method, writer);
}
}

#endregion
19 changes: 19 additions & 0 deletions src/JasperFx/CodeGeneration/Services/ArrayPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,23 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine($"{_serviceType.FullNameInCode()} {Variable.Usage} = new {_elementType.FullNameInCode()}[]{{{_elements.Select(x => x.Usage).Join(", ")}}};");
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
// Mirror the C# fail-fast null guard for a missing keyed singleton mirror (jasperfx#381).
foreach (var element in _elements)
{
if (element is InjectedSingleton { Descriptor: { IsKeyedService: true } descriptor }
&& EnumerableSingletons.IsMirrorKey(descriptor.ServiceKey))
{
var message = EnumerableSingletons.MissingMirrorMessage(_elementType, descriptor.ServiceKey);
writer.WriteLine(
$"if isNull {element.FSharpUsage} then raise({typeof(InvalidOperationException).FSharpName()}({CodeFormatter.Write(message)}))");
}
}

// F# array literal: [| e1; e2 |]. The element type is inferred, so no explicit annotation.
writer.WriteLine($"{Variable.FSharpAssignmentUsage} = [| {_elements.Select(x => x.FSharpUsage).Join("; ")} |]");
Next?.GenerateFSharpCode(method, writer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
$"var {Variable.Usage} = {typeof(ServiceProviderServiceExtensions).FullNameInCode()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FullNameInCode()}>({_scoped.Usage});");
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else");
writer.Write(
$"{Variable.FSharpAssignmentUsage} = {typeof(ServiceProviderServiceExtensions).FSharpName()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FSharpName()}>({_scoped.FSharpUsage})");
Next?.GenerateFSharpCode(method, writer);
}
}
6 changes: 6 additions & 0 deletions src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
writer.Write($"{Variable.FSharpAssignmentUsage} = {_parent.FSharpUsage}.{_member.Name}");
Next?.GenerateFSharpCode(method, writer);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_parent = chain.FindVariable(_targetType);
Expand Down
Loading