Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Building expecto test apps as NativeAOT #498

Open
Numpsy opened this issue Jul 20, 2024 · 6 comments
Open

Building expecto test apps as NativeAOT #498

Numpsy opened this issue Jul 20, 2024 · 6 comments

Comments

@Numpsy
Copy link
Contributor

Numpsy commented Jul 20, 2024

I'm not sure how sensible / feasible doing this is, but:

If I try to build the minimal test app from the readme:

open Expecto

let tests =
  test "A simple test" {
    let subject = "Hello World"
    Expect.equal subject "Hello World" "The strings should equal"
  }

[<EntryPoint>]
let main args =
  runTestsWithCLIArgs [] args tests

as .NET 8.0 using NativeAOT, then the test app fails to run with this exception:

Unhandled Exception: System.TypeInitializationException: A type initializer threw an exception. To determine which type, inspect the InnerException's StackTrace property.
 ---> System.TypeLoadException: Attempted to load a type that was not created during ahead of time compilation.
   at Internal.Runtime.CompilerHelpers.ThrowHelpers.ThrowUnavailableType() + 0x2b
   at Microsoft.FSharp.Core.FSharpFunc`2.InvokeFast[V](FSharpFunc`2, T, TResult) + 0x1a
   at <StartupCode$Expecto>.$Logging..cctor() + 0x4b3
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xb9
   --- End of inner exception stack trace ---
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x14a
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnGCStaticBase(StaticClassConstructionContext*, Object) + 0xd
   at Expecto.Logging.ANSIOutputWriter.setColourLevel(ColourLevel) + 0x63
   at Expecto.Tests.runTestsWithCancel@600(CancellationToken, Impl.ExpectoConfig, Test) + 0x20
   at Expecto.Tests.runTestsWithCLIArgs(IEnumerable`1, String[], Test) + 0x1d
   at ex_test_2!<BaseAddress>+0x2268a0

(I've seen a few variations of the call stack with other test apps, depending on if they use runTestsInAssembly or variations like that).

If I build the app with full AOT analysis enabled then I get a few instances of warnings like this:

ILC : AOT analysis warning IL3054: Microsoft.FSharp.Core.OptimizedClosures.FSharpFunc`3<FSharpFunc`2<FSharpFunc`2<Unit,Int64>,FSharpFunc`2<FSharpFunc`2<String[],Logger>
,FSharpFunc`2<Object,LoggingConfig>>>,DVar`1<FSharpFunc`2<Unit,Int64>>,DVar`1<FSharpFunc`2<FSharpFunc`2<String[],Logger>,FSharpFunc`2<Object,LoggingConfig>>>>: Generic
expansion to 'Microsoft.FSharp.Core.OptimizedClosures.Invoke@3756<FSharpFunc`2<FSharpFunc`2<Unit,Int64>,FSharpFunc`2<FSharpFunc`2<String[],Logger>,FSharpFunc`2<Object,L
oggingConfig>>>,DVar`1<FSharpFunc`2<Unit,Int64>>,DVar`1<FSharpFunc`2<FSharpFunc`2<String[],Logger>,FSharpFunc`2<Object,LoggingConfig>>>>' was aborted due to generic rec
ursion. An exception will be thrown at runtime if this codepath is ever reached. Generic recursion also negatively affects compilation speed and the size of the compila
tion output. It is advisable to remove the source of the generic recursion by restructuring the program around the source of recursion. The source of generic recursion
might include: 'Microsoft.FSharp.Core.FSharpFunc`2', '<StartupCode$FSharp-Core>.$Prim-types.op_Implicit@3875', '<StartupCode$FSharp-Core>.$Prim-types.op_Implicit@3880-1
', '<StartupCode$FSharp-Core>.$Prim-types.op_Implicit@3883-2', '<StartupCode$FSharp-Core>.$Prim-types.op_Implicit@3886-3', '<StartupCode$FSharp-Core>.$Prim-types.FromCo
nverter@3889', '<StartupCode$FSharp-Core>.$Prim-types.ToConverter@3892', 'Microsoft.FSharp.Core.OptimizedClosures.FSharpFunc`3', 'Microsoft.FSharp.Core.OptimizedClosure
s.FSharpFunc`4', 'Microsoft.FSharp.Core.OptimizedClosures.FSharpFunc`5', 'Microsoft.FSharp.Core.OptimizedClosures.FSharpFunc`6', 'Microsoft.FSharp.Core.OptimizedClosure
s.Invoke@3756', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3763', 'Microsoft.FSharp.Core.OptimizedClosures.Invoke@3770-1', 'Microsoft.FSharp.Core.OptimizedClosures.
Adapt@3779-1', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3783-2', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3797-3', 'Microsoft.FSharp.Core.OptimizedClosures.
Adapt@3802-4', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3806-5', 'Microsoft.FSharp.Core.OptimizedClosures.Invoke@3809-2', 'Microsoft.FSharp.Core.OptimizedClosures
.Invoke@3817-3', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3827-6', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3832-7', 'Microsoft.FSharp.Core.OptimizedClosure
s.Adapt@3837-8', 'Microsoft.FSharp.Core.OptimizedClosures.Adapt@3841-9' [S:\DevTest\ex_test_2\ex_test_2.fsproj]

Which looks like something in the logging setup is generating recursive generics too complicated for the AOT compiler to handle.
I don't know anything about how the DVar stuff works and haven't investigated any further that this so far, but leaving this here in case anyone else has any ideas about if it's possible to get this to work.

@Numpsy Numpsy changed the title Build expecto test apps as NativeAOT Building expecto test apps as NativeAOT Jul 20, 2024
@farlee2121
Copy link
Collaborator

I appreciate the well-researched suggestion!

I've also been poking at the logging recently, since there are several outstanding issues
#454, #480, #482

The DVar type is used for managing global logger state (like semaphore instances) behind the scenes.

From the poking I've done so far, I think there's a route to de-globalize the logger. That alone might be enough to solve the AOT problem, since it could eliminate DVar. If not, it'd also simplify the route to supporting Microsoft.Extensions.Logging which I'd guess is supported by AOT compile

@Numpsy
Copy link
Contributor Author

Numpsy commented Jul 21, 2024

Microsoft.Extensions.Logging.Abstractions can be used in AOT apps (I have cli tools at work using it for console logging that work in AOT compiles).

On another related note - there are a bunch of places in Expecto that use sprintf for string formatting, and that can have issues with AOT builds.
Looking at the code, I think at least some of those could be changed to use string interpolation instead which tends to work better, so are there any thoughts or objections on changing that?

@farlee2121
Copy link
Collaborator

I don't see any issue with the sprintf subs you made. It was just interpolating strings, so there shouldn't be any issues with type-specific formatting. It also shouldn't make formatting evaluations any more eager.

@Numpsy
Copy link
Contributor Author

Numpsy commented Jul 22, 2024

In a simple test app, that change seems to be enough to get a

testTask "I am (should fail)" {
  "╰〳 ಠ 益 ಠೃ 〵╯" |> Expect.equal true false
}

to run and print out the correct results in a build with simplified logging :-)
I still get an error using Expect.equal on records rather than base types, but one thing at once.

@farlee2121
Copy link
Collaborator

Interesting, I wouldn't have expected %s to behave any different than the .ToString() implied in string interpolation.

@Numpsy
Copy link
Contributor Author

Numpsy commented Jul 23, 2024

I believe that some of the issues with (s)printf are down to all the machinery for partial application and such, which interpolation doesn't have so there's less scope for issues (though I don't know why there appears to be some situations where sprintf "%A" int works in NativeAOT and sprintf "%i" int throws :-( )

There's also a specific optimization in recent compilers that can convert interpolations that only involve strings into calls to String.Concat which simplifies things - dotnet/fsharp#16556 - though you might still need langVersion set to preview to get that ATM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants