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
19 changes: 19 additions & 0 deletions src/CodegenTests.FSharp/FSharpCodegenSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,29 @@ public static GeneratedAssembly BuildSampleAssembly()
AddResourceType(assembly);
AddOrderHandlerType(assembly);
AddSyncTaskHandlerType(assembly);
AddCalculatorType(assembly);

return assembly;
}

/// <summary>
/// A subclass that overrides a base abstract method and calls an inherited instance method via a
/// local (this-qualified) <see cref="MethodCall" />. Exercises the named-self F# emit: the member
/// renders as <c>override this.Compute</c> and the call as <c>this.Bump(seed)</c> (jasperfx#393).
/// </summary>
private static void AddCalculatorType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedCalculator", typeof(CalculatorBase));
var method = type.MethodFor(nameof(CalculatorBase.Compute));
var seed = method.Arguments[0];

var bump = new MethodCall(typeof(CalculatorBase), nameof(CalculatorBase.Bump)) { IsLocal = true };
bump.Arguments[0] = seed;
method.Frames.Add(bump);

method.Frames.Add(new ReturnFrame(bump.ReturnVariable!));
}

/// <summary>
/// A synchronous body behind a non-generic <c>Task</c> signature: the side effect runs, then
/// the method yields <c>Task.CompletedTask</c> (AsyncMode.None + Task return).
Expand Down
24 changes: 15 additions & 9 deletions src/CodegenTests.FSharpFixture/Generated.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type GeneratedGreeter(greetingService: FSharpCodegenTarget.GreetingService) =
let _greetingService = greetingService

interface FSharpCodegenTarget.IGreeter with
member _.Greet(name: string) : string =
member this.Greet(name: string) : string =
// Generated by JasperFx F# code generation (jasperfx#383)
let salutation = FSharpCodegenTarget.Salutation(name)
let result_of_CreateGreeting = _greetingService.CreateGreeting(salutation)
Expand All @@ -21,7 +21,7 @@ type GeneratedAsyncGreeter(greetingService: FSharpCodegenTarget.GreetingService)
let _greetingService = greetingService

interface FSharpCodegenTarget.IAsyncGreeter with
member _.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =
member this.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =
task {
// Async greeting handler (jasperfx#383)
let salutation = FSharpCodegenTarget.Salutation(name)
Expand All @@ -33,7 +33,7 @@ type GeneratedDirectAsyncGreeter(greetingService: FSharpCodegenTarget.GreetingSe
let _greetingService = greetingService

interface FSharpCodegenTarget.IDirectAsyncGreeter with
member _.GreetDirectAsync(name: string) : System.Threading.Tasks.Task<string> =
member this.GreetDirectAsync(name: string) : System.Threading.Tasks.Task<string> =
// Returns the service Task directly (jasperfx#383)
let salutation = FSharpCodegenTarget.Salutation(name)
_greetingService.CreateGreetingAsync(salutation)
Expand All @@ -42,7 +42,7 @@ type GeneratedAccumulator(accumulatorService: FSharpCodegenTarget.AccumulatorSer
let _accumulatorService = accumulatorService

interface FSharpCodegenTarget.IAccumulator with
member _.Accumulate() : FSharpCodegenTarget.MutableBox =
member this.Accumulate() : FSharpCodegenTarget.MutableBox =
let mutable mutableBox = FSharpCodegenTarget.MutableBox()
mutableBox <- _accumulatorService.Advance(mutableBox)
mutableBox
Expand All @@ -51,7 +51,7 @@ type GeneratedConditionalGreeter(controlFlowService: FSharpCodegenTarget.Control
let _controlFlowService = controlFlowService

interface FSharpCodegenTarget.IConditionalGreeter with
member _.Describe(input: string) : string =
member this.Describe(input: string) : string =
if isNull input then
_controlFlowService.Fallback()
else
Expand All @@ -61,15 +61,15 @@ type GeneratedToggle(controlFlowService: FSharpCodegenTarget.ControlFlowService)
let _controlFlowService = controlFlowService

interface FSharpCodegenTarget.IToggle with
member _.Toggle(flag: bool) : unit =
member this.Toggle(flag: bool) : unit =
if flag then
_controlFlowService.Record()

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

interface FSharpCodegenTarget.IResource with
member _.Run() : unit =
member this.Run() : unit =
_controlFlowService.Begin()
try
_controlFlowService.Record()
Expand All @@ -81,7 +81,7 @@ type GeneratedOrderHandler(confirmationFactory: FSharpCodegenTarget.Confirmation
let _orderRepository = orderRepository

interface FSharpCodegenTarget.IOrderHandler with
member _.Handle(command: FSharpCodegenTarget.PlaceOrder) : System.Threading.Tasks.Task<FSharpCodegenTarget.OrderConfirmation> =
member this.Handle(command: FSharpCodegenTarget.PlaceOrder) : System.Threading.Tasks.Task<FSharpCodegenTarget.OrderConfirmation> =
task {
// Handle a PlaceOrder command (jasperfx#383)
let order = FSharpCodegenTarget.Order(command)
Expand All @@ -94,7 +94,13 @@ type GeneratedSyncTaskHandler(controlFlowService: FSharpCodegenTarget.ControlFlo
let _controlFlowService = controlFlowService

interface FSharpCodegenTarget.ISyncTaskHandler with
member _.HandleAsync(name: string) : System.Threading.Tasks.Task =
member this.HandleAsync(name: string) : System.Threading.Tasks.Task =
_controlFlowService.Record()
System.Threading.Tasks.Task.CompletedTask

type GeneratedCalculator() =
inherit FSharpCodegenTarget.CalculatorBase()
override this.Compute(seed: int) : int =
let result_of_Bump = this.Bump(seed)
result_of_Bump

8 changes: 4 additions & 4 deletions src/CodegenTests/FSharpGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void emits_expected_fsharp_for_a_type_implementing_an_interface_with_an_i
code.ShouldContain("type GeneratedGreeter(service: CodegenTests.FSharpGreetingService) =");
code.ShouldContain("let _service = service");
code.ShouldContain("interface CodegenTests.IFSharpGreeter with");
code.ShouldContain("member _.Greet(name: string) : string =");
code.ShouldContain("member this.Greet(name: string) : string =");
code.ShouldContain("// a comment");
code.ShouldContain("let result_of_CreateGreeting = _service.CreateGreeting(name)");

Expand Down Expand Up @@ -142,7 +142,7 @@ public void wraps_an_async_method_body_in_a_task_computation_expression()

var code = assembly.GenerateFSharpCode();

code.ShouldContain("member _.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =");
code.ShouldContain("member this.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =");
code.ShouldContain("task {");
// await inside the computation expression binds with let!
code.ShouldContain("let! result_of_CreateGreetingAsync = _service.CreateGreetingAsync(name)");
Expand All @@ -168,7 +168,7 @@ public void emits_a_bare_trailing_task_expression_for_return_from_last_node()

var code = assembly.GenerateFSharpCode();

code.ShouldContain("member _.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =");
code.ShouldContain("member this.GreetAsync(name: string) : System.Threading.Tasks.Task<string> =");
// No state machine: the Task is returned directly, with no task block and no `return!`.
code.ShouldNotContain("task {");
code.ShouldNotContain("return!");
Expand Down Expand Up @@ -284,7 +284,7 @@ public void emits_task_completed_task_for_a_synchronous_task_returning_method()

var code = assembly.GenerateFSharpCode();

code.ShouldContain("member _.HandleAsync(name: string) : System.Threading.Tasks.Task =");
code.ShouldContain("member this.HandleAsync(name: string) : System.Threading.Tasks.Task =");
code.ShouldContain("_service.Record()");
// No state machine for a synchronous body — just yield a completed Task.
code.ShouldNotContain("task {");
Expand Down
16 changes: 16 additions & 0 deletions src/FSharpCodegenTarget/Contracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ public interface ISyncTaskHandler
Task HandleAsync(string name);
}

/// <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
/// inherited instance <c>Bump</c> via a local (this-qualified) MethodCall — exercises the named-self
/// F# emit so inherited instance members resolve (jasperfx#393).
/// </summary>
public abstract class CalculatorBase
{
public int Bump(int value)
{
return value + 1;
}

public abstract int Compute(int seed);
}

public class ControlFlowService
{
public string Fallback()
Expand Down
5 changes: 4 additions & 1 deletion src/JasperFx/CodeGeneration/Frames/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,10 @@ private string fsharpDetermineTarget()
{
if (IsLocal)
{
return string.Empty;
// A local call targets a method on the generated type itself. C# relies on an implicit
// `this`; F# has none, so qualify with the named self identifier emitted by
// GeneratedMethod.WriteFSharpMethod. See jasperfx#393.
return "this.";
}

var target = Method.IsStatic
Expand Down
8 changes: 5 additions & 3 deletions src/JasperFx/CodeGeneration/GeneratedMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,11 @@ public void WriteFSharpMethod(ISourceWriter writer)
var returnType = ReturnType.FSharpName();
var keyword = Overrides ? "override" : "member";

// `_` self identifier avoids an unused-`this` warning; instance let-bound
// fields are reachable directly by name regardless of the self identifier.
writer.Write($"BLOCK:{keyword} _.{MethodName}({arguments}) : {returnType} =");
// A named `this` self identifier is required so emitted frames can call inherited instance
// members (e.g. a base class's `WriteJsonAsync`/`ReadJsonAsync`) — F# has no implicit `this`,
// and `member _.` would discard the instance. F# does not warn on an unused member self
// identifier (unlike an unused `let` binding), so this is safe. See jasperfx#393.
writer.Write($"BLOCK:{keyword} this.{MethodName}({arguments}) : {returnType} =");

if (AsyncMode == AsyncMode.AsyncTask)
{
Expand Down
Loading