Module `do` is often not evaluated, static initializers
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 22h
- Merged PRs (30d)
- 144
Description
This is a known issue, or feature so to say. The language spec (screenshot below) has an extensive list of reasons when _static initialisers_ aren't evaluated. The spec doesn't specifically mention `module do` there, but it applies.
The [docs say about this just the following](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/do-bindings):
> Use a `do` binding when you want to execute code independently of a function or value definition. The expression in a `do` binding must return `unit`. Code in a top-level `do` binding is executed when the module is initialized. The keyword `do` is optional.
This isn't very clear and arguably not true. The common idea (as often stated in online posts) is that the `do` binding is executed "on first access", basically similarly to how `static do` works in classes. This is incorrect.
If you manage to find the section in the F# spec and you disentangle it, it turns out that the `do` is _only_ executed if it's either in the same file as `main` (or a script file), or if it has a referential dependency on mutable state (i.e., a `let mutable`), or a normal `let` that happens to be bound to something not "constant-like".
### Repro steps
There are many subtleties to this, but as a "simplest example":
```f#
module Test =
do printfn "Hello world"
let f() = printfn "Doing something interesting"
```
In another file, or reference the above through a project reference, call `Test.f()`. You will notice that `Hello world` is never printed.
### Expected behavior
Module `do` gets executed on first access to the module.
### Actual behavior
Module `do` does not get executed ever. However, as soon as you try to reproduce it by dumping it in a script file or copying it to FSI, you will see that it does get executed.
### Known workarounds
Force a referential dependency on a mutual. This is far from trivial and has led to many discussions and hard-to-diagnose bugs.
Now the `do` will get executed:
```f#
module Test =
let mutable __force = 42
do printfn "Hello world"
let f() =
__force <- __force + 1
printfn "Doing something interesting"
```
### Related information
Perhaps we can improve by issuing a warning when initializing code is not being executed? Or, conversely, add an opt-in attribute that would add the `module do` to the static constructor of the module, as opposed to the `StartupCode$File` cctor (which contains the static fields, forcing the initialization only when there's some mutable state or non-trivial let binding).
[From the spec](https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf):

Contributor guide
Assessment
This issue has not been assessed yet.