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
45 changes: 45 additions & 0 deletions jasperfx.sln
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.SourceGenerator",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.SourceGenerator.Tests", "src\JasperFx.SourceGenerator.Tests\JasperFx.SourceGenerator.Tests.csproj", "{2952E461-E26E-4B9F-83D1-C4D1944B1F7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FSharpCodegenTarget", "src\FSharpCodegenTarget\FSharpCodegenTarget.csproj", "{10EEDBCD-9A90-437A-A30B-D4D712126289}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CodegenTests.FSharpFixture", "src\CodegenTests.FSharpFixture\CodegenTests.FSharpFixture.fsproj", "{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodegenTests.FSharp", "src\CodegenTests.FSharp\CodegenTests.FSharp.csproj", "{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -417,6 +423,42 @@ Global
{2952E461-E26E-4B9F-83D1-C4D1944B1F7C}.Release|x64.Build.0 = Release|Any CPU
{2952E461-E26E-4B9F-83D1-C4D1944B1F7C}.Release|x86.ActiveCfg = Release|Any CPU
{2952E461-E26E-4B9F-83D1-C4D1944B1F7C}.Release|x86.Build.0 = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|x64.ActiveCfg = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|x64.Build.0 = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|x86.ActiveCfg = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Debug|x86.Build.0 = Debug|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|Any CPU.Build.0 = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|x64.ActiveCfg = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|x64.Build.0 = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|x86.ActiveCfg = Release|Any CPU
{10EEDBCD-9A90-437A-A30B-D4D712126289}.Release|x86.Build.0 = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|x64.ActiveCfg = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|x64.Build.0 = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|x86.ActiveCfg = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Debug|x86.Build.0 = Debug|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|Any CPU.Build.0 = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|x64.ActiveCfg = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|x64.Build.0 = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|x86.ActiveCfg = Release|Any CPU
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8}.Release|x86.Build.0 = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|x64.ActiveCfg = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|x64.Build.0 = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|x86.ActiveCfg = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Debug|x86.Build.0 = Debug|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|Any CPU.Build.0 = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|x64.ActiveCfg = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|x64.Build.0 = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|x86.ActiveCfg = Release|Any CPU
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -438,5 +480,8 @@ Global
{8FB8216F-216F-480F-9519-A5893F7F3151} = {A1BBEFB2-1FDB-4A32-8D00-DD5AA8E1E43A}
{E48F04F5-A8A3-4C47-9965-53C9D5DDC806} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{2952E461-E26E-4B9F-83D1-C4D1944B1F7C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{10EEDBCD-9A90-437A-A30B-D4D712126289} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{610F3BB6-CD20-4AFB-8B60-2B1632ED97D8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{818BB6C9-1E47-4939-8F7B-59858CFB5B1C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
37 changes: 37 additions & 0 deletions src/CodegenTests.FSharp/CodegenTests.FSharp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
F# code-generation integration tests (jasperfx#383).

This project is intentionally NOT wired into any ./build.sh / Nuke test target:
its single test shells out to `dotnet build` against the checked-in F# fixture,
which is too slow/heavy for the fast unit run. It is still part of jasperfx.sln so
it compiles with the rest of the solution. Run it explicitly with:
dotnet test src/CodegenTests.FSharp
-->

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\JasperFx\JasperFx.csproj" />
<ProjectReference Include="..\FSharpCodegenTarget\FSharpCodegenTarget.csproj" />
</ItemGroup>

</Project>
283 changes: 283 additions & 0 deletions src/CodegenTests.FSharp/FSharpCodegenSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
using System.Runtime.CompilerServices;
using FSharpCodegenTarget;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;

namespace CodegenTests.FSharp;

/// <summary>
/// Builds the canonical "milestone 1" <see cref="GeneratedAssembly" /> for F# code
/// generation (jasperfx#383): a sealed type that implements a C# interface, takes a
/// dependency through its constructor, constructs a value object, calls the injected
/// service, and returns a result. This single sample exercises all five frames wired up
/// for F# emit — <see cref="CommentFrame" />, <see cref="ConstructorFrame" />,
/// <see cref="MethodCall" />, <see cref="CodeFrame" />, and <see cref="ReturnFrame" /> —
/// plus constructor injection and interface implementation.
/// </summary>
public static class FSharpCodegenSample
{
public static GeneratedAssembly BuildSampleAssembly()
{
var assembly = new GeneratedAssembly(new GenerationRules("FSharpCodegenTarget.Generated"));

var type = assembly.AddType("GeneratedGreeter", typeof(IGreeter));
var method = type.MethodFor(nameof(IGreeter.Greet));

// 1. A leading comment.
method.Frames.Add(new CommentFrame("Generated by JasperFx F# code generation (jasperfx#383)"));

// 2. Construct a value object from the method argument: let salutation = Salutation(name)
var salutationCtor = typeof(Salutation).GetConstructors().Single();
method.Frames.Add(new ConstructorFrame(typeof(Salutation), salutationCtor));

// 3. Call the constructor-injected service:
// let result_of_CreateGreeting = _greetingService.CreateGreeting(salutation)
var service = new InjectedField(typeof(GreetingService), "greetingService");
var call = new MethodCall(typeof(GreetingService), nameof(GreetingService.CreateGreeting))
{
Target = service
};
method.Frames.Add(call);

// 4. A raw code line that creates a new variable: let result = result_of_CreateGreeting.ToUpper()
var result = new Variable(typeof(string), "result");
var codeFrame = (CodeFrame)method.Frames.Code("let {0} = {1}.ToUpper()", result, call.ReturnVariable!);
((ICodeFrame)codeFrame).Creates(result);

// 5. Return the result (rendered as a trailing F# expression).
method.Frames.Add(new ReturnFrame(result));

AddAsyncType(assembly);
AddDirectAsyncType(assembly);
AddAccumulatorType(assembly);
AddConditionalType(assembly);
AddToggleType(assembly);
AddResourceType(assembly);
AddOrderHandlerType(assembly);
AddSyncTaskHandlerType(assembly);

return assembly;
}

/// <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).
/// </summary>
private static void AddSyncTaskHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedSyncTaskHandler", typeof(ISyncTaskHandler));
var method = type.MethodFor(nameof(ISyncTaskHandler.HandleAsync));

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

/// <summary>
/// A Wolverine-shaped async handler: construct a domain object from the command, <c>do!</c>
/// an async repository save, build a confirmation with a sync factory call, and return it.
/// Exercises mixed sync/async frames inside a single <c>task { }</c> body.
/// </summary>
private static void AddOrderHandlerType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedOrderHandler", typeof(IOrderHandler));
var method = type.MethodFor(nameof(IOrderHandler.Handle));
var command = method.Arguments[0];

method.Frames.Add(new CommentFrame("Handle a PlaceOrder command (jasperfx#383)"));

var orderCtor = typeof(Order).GetConstructors().Single();
var ctorFrame = new ConstructorFrame(typeof(Order), orderCtor);
ctorFrame.Parameters[0] = command;
method.Frames.Add(ctorFrame);

var repository = new InjectedField(typeof(IOrderRepository), "orderRepository");
var save = new MethodCall(typeof(IOrderRepository), nameof(IOrderRepository.SaveAsync))
{
Target = repository
};
save.Arguments[0] = ctorFrame.Variable;
method.Frames.Add(save);

var factory = new InjectedField(typeof(ConfirmationFactory), "confirmationFactory");
var create = new MethodCall(typeof(ConfirmationFactory), nameof(ConfirmationFactory.Create))
{
Target = factory
};
create.Arguments[0] = ctorFrame.Variable;
method.Frames.Add(create);

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

/// <summary>
/// A null-guard that returns one of two values — F# <c>if isNull x then ... else ...</c>
/// as the trailing (return) expression (IfElseNullGuardFrame).
/// </summary>
private static void AddConditionalType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedConditionalGreeter", typeof(IConditionalGreeter));
var method = type.MethodFor(nameof(IConditionalGreeter.Describe));
var input = method.Arguments[0];

var service = new InjectedField(typeof(ControlFlowService), "controlFlowService");

var fallback = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback))
{
Target = service, ReturnAction = ReturnAction.Return
};
var echo = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Echo))
{
Target = service, ReturnAction = ReturnAction.Return
};
echo.Arguments[0] = input;

method.Frames.Add(new IfElseNullGuardFrame(input, new Frame[] { fallback }, new Frame[] { echo }));
}

/// <summary>
/// A conditional side effect — F# <c>if flag then ...</c> (IfBlock, a CompositeFrame).
/// </summary>
private static void AddToggleType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedToggle", typeof(IToggle));
var method = type.MethodFor(nameof(IToggle.Toggle));
var flag = method.Arguments[0];

var service = new InjectedField(typeof(ControlFlowService), "controlFlowService");
var record = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Record))
{
Target = service
};

method.Frames.Add(new IfBlock(flag, record));
}

/// <summary>
/// A try/finally — F# <c>try ... finally ...</c> (TryFinallyWrapperFrame).
/// </summary>
private static void AddResourceType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedResourceRunner", typeof(IResource));
var method = type.MethodFor(nameof(IResource.Run));

var service = new InjectedField(typeof(ControlFlowService), "controlFlowService");
var begin = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Begin))
{
Target = service
};
var work = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Record))
{
Target = service
};
var end = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.End))
{
Target = service
};

method.Frames.Add(new TryFinallyWrapperFrame(begin, new Frame[] { end }));
method.Frames.Add(work);
}

/// <summary>
/// A second type whose method is asynchronous, so the body is wrapped in a
/// <c>task { }</c> computation expression with <c>let!</c> (await) and <c>return</c>.
/// </summary>
private static void AddAsyncType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedAsyncGreeter", typeof(IAsyncGreeter));
var method = type.MethodFor(nameof(IAsyncGreeter.GreetAsync));

method.Frames.Add(new CommentFrame("Async greeting handler (jasperfx#383)"));

var salutationCtor = typeof(Salutation).GetConstructors().Single();
method.Frames.Add(new ConstructorFrame(typeof(Salutation), salutationCtor));

var service = new InjectedField(typeof(GreetingService), "greetingService");
var call = new MethodCall(typeof(GreetingService), nameof(GreetingService.CreateGreetingAsync))
{
Target = service
};
method.Frames.Add(call);

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

/// <summary>
/// A type whose single async call IS the return value, so the body is a bare trailing
/// Task expression (ReturnFromLastNode) — no <c>task { }</c> state machine.
/// </summary>
private static void AddDirectAsyncType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedDirectAsyncGreeter", typeof(IDirectAsyncGreeter));
var method = type.MethodFor(nameof(IDirectAsyncGreeter.GreetDirectAsync));

method.Frames.Add(new CommentFrame("Returns the service Task directly (jasperfx#383)"));

var salutationCtor = typeof(Salutation).GetConstructors().Single();
method.Frames.Add(new ConstructorFrame(typeof(Salutation), salutationCtor));

var service = new InjectedField(typeof(GreetingService), "greetingService");
var call = new MethodCall(typeof(GreetingService), nameof(GreetingService.CreateGreetingAsync))
{
Target = service,
// The call itself is the method's return value — no ReturnFrame after it.
ReturnAction = ReturnAction.Return
};
method.Frames.Add(call);
}

/// <summary>
/// A type that constructs a local, reassigns it from a service call, and returns it —
/// exercising F# <c>let mutable</c> + the <c>&lt;-</c> reassignment operator.
/// </summary>
private static void AddAccumulatorType(GeneratedAssembly assembly)
{
var type = assembly.AddType("GeneratedAccumulator", typeof(IAccumulator));
var method = type.MethodFor(nameof(IAccumulator.Accumulate));

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

var service = new InjectedField(typeof(AccumulatorService), "accumulatorService");
var call = new MethodCall(typeof(AccumulatorService), nameof(AccumulatorService.Advance))
{
Target = service
};
call.Arguments[0] = ctorFrame.Variable;
call.AssignResultTo(ctorFrame.Variable); // box <- service.Advance(box) (marks box mutable)
method.Frames.Add(call);

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

/// <summary>
/// Builds the sample and renders it as F# source.
/// </summary>
public static string GenerateCode()
{
return BuildSampleAssembly().GenerateFSharpCode();
}

/// <summary>
/// The checked-in fixture's <c>Generated.fs</c>, located relative to this source file so it
/// resolves regardless of the test runner's working directory or bin layout.
/// </summary>
public static string DefaultGeneratedFilePath([CallerFilePath] string thisFile = "")
{
var testProjectDir = Path.GetDirectoryName(thisFile)!;
var srcDir = Path.GetDirectoryName(testProjectDir)!;
return Path.Combine(srcDir, "CodegenTests.FSharpFixture", "Generated.fs");
}

public static string FixtureProjectPath([CallerFilePath] string thisFile = "")
{
var testProjectDir = Path.GetDirectoryName(thisFile)!;
var srcDir = Path.GetDirectoryName(testProjectDir)!;
return Path.Combine(srcDir, "CodegenTests.FSharpFixture", "CodegenTests.FSharpFixture.fsproj");
}
}
Loading
Loading