Fsharp: Improve error reporting: importing modules from an assembly compiled as Exe but without explicit EntryPoint should raise a warning

Created on 22 Jul 2016  路  12Comments  路  Source: dotnet/fsharp

what

F# users not acquainted with module static initialization semantics in exe project (which apparently differ from library project) might expect a different result when doing this:

steps

C:\tmp\repro>more main.fs
module Foo
  type Buildable = Buildable of obj
  let o = Buildable null
C:\tmp\repro>fsc main.fs
Microsoft (R) F# Compiler version 14.0.23413.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

C:\tmp\repro>more test.fsx
#r @"main.exe"
printfn "%A" Foo.o
C:\tmp\repro>fsi test.fsx
<null>

C:\tmp\repro>

Actual results

The static type initializer for module Foo wasn't executed and Foo.o will always remain null.

Expected results

If we want to preserve that behaviour, I think a warning when referencing a .exe containing F# modules would help user to understand that modules might not be initialized as intended.

If we could fix the behaviour while maintaining compatibility / non breaking change, that would IMHO be ideal.

Feature Improvement

Most helpful comment

/bump on this issue. This suddenly becomes a bigger problem as .NET Core test projects get compiled as Exe targeting netcoreapp1.x. Now if your entire test consists of one file containing the following:

module MyTest
open Xunit

let myValue = [ 2 ]

[<Fact>]
let ``myValue is populated`` () =
  let [ x ] = myValue in Assert.StrictEqual(2, x)

This test will fail at runtime with a NullReferenceException, as myValue will never be set. fsc creates an implicit main function which would do the initialization, but when running tests, main is never called. As a workaround, I have been adding a dummy Program.fs which moves myValue's initialization to a static constructor:

module Program
let main _ = 0

This workaround is non-intuitive, and will catch those creating tests for .NET Core F# projects off guard.

All 12 comments

@jmmk I simplified the problem you faced.

Other issues that could be relevant (not really sure): #1150 #1288

This is by design for a host of technical issues related to concurrency and debugging. The right approach is to use an explicit [<EntryPoint>] in your main.exe.

a warning when referencing a .exe containing F# modules would help user to understand that modules might not be initialized as intended.

Yes, that would be appropriate. Please change the title to that as a design suggestion (since this is actually by design)

Renamed:

Improve error reporting: importing modules from an assembly compiled as Exe but without explicit EntryPoint should raise a warning

Hi @smoothdeveloper!

Thanks for referencing me here. However, I'm not sure what action you're meaning I should take. Is there a workaround I should be applying here for Prime - EG - should I put an explicit entry point into Prime.exe? I'm not sure what static module initialization might be missing since I think all the initialization in Prime is explicit - but I could easily be mistaken!

@bryanedds yes if you intend prime to remain a .exe, @dsyme mentions that using [<EntryPoint>] would restore the usual module initialization in static constructors.

I'm not sure if your code has things that normally gets initialized in static constructors (such as any let bound values which are not integral type AFAIU and maybe others), you can see the minimal reproduce steps to better understand implication.

/bump on this issue. This suddenly becomes a bigger problem as .NET Core test projects get compiled as Exe targeting netcoreapp1.x. Now if your entire test consists of one file containing the following:

module MyTest
open Xunit

let myValue = [ 2 ]

[<Fact>]
let ``myValue is populated`` () =
  let [ x ] = myValue in Assert.StrictEqual(2, x)

This test will fail at runtime with a NullReferenceException, as myValue will never be set. fsc creates an implicit main function which would do the initialization, but when running tests, main is never called. As a workaround, I have been adding a dummy Program.fs which moves myValue's initialization to a static constructor:

module Program
let main _ = 0

This workaround is non-intuitive, and will catch those creating tests for .NET Core F# projects off guard.

@dsyme as an alternative solution (discussing the issue with @alxandr ):

the fsc will generate the empty main if there isnt a function with [<EntryPoint>] defined and --target:exe

is too complicated to implement? or compatibility reasons?

@enricosada I believe it already does this - the question is the style of triggers used for the initialization of the top-level bindings in the final file

If there is an explicit main, then the initialization logic is placed in a .cctor - if there is no explicit main, it is placed in the implicit main method itself.

I believe we wanted to avoid a .cctor because there are corner cases where they cause deadlock, e.g. if threads are activated that themselves access the statically initialized data. But perhaps it's wrong in retrospect and we should always use the .cctor.

See IlxGen.fs for details. I think it's also covered in the F# Language Spec

I'd just like to voice that this is one of the most surprising behaviours I've ever encountered in F#, and I was pretty sure there was a compiler bug in F# for core messing things up. Given that this is probably something people will hit when they start using dotnet test. I at least tend to remove things that I don't know why exists, cause I like having control over my project and know why/how everything is connected. As such, I removed the entrypoint in my test assemblies, because having an empty entrypoint in a test assembly, that will never be run as an app makes no sense. And then I started getting NullReferenceExceptions for no apparent reason.

Another good reason to change this:

#if NETCOREAPP2_0
[<EntryPoint>]
let main _ = failwithf "YoloDev.Expecto.TestSdk is not intended to be run as a program"
#endif

If you're making a test adapter, you need to do stuff like this. Cause it's an exe for netcoreapp, and a library for net461.

I just hit this for the first time and was _very_ surprised. I encountered it in updating some projects to the new tooling & .NET standard. Since I didn't specify OutputType on my unit test projects, they defaulted to executables and, when running the tests, values were unexpectedly null.

I came here looking for a compiler bug, actually! 馃榾

I'd put in a vote to always use static constructors, since this is rather spooky. 馃懟

Was this page helpful?
0 / 5 - 0 ratings