Skip to content

static let bindings will fail unexpectedly if that binding uses a no-arg case in a Discriminating Union (DU) type #16431

@patrickallensimpson

Description

@patrickallensimpson

Issue

static let bindings will fail unexpectedly if that binding uses a no-arg case in a Discriminating Union (DU) type. This happens because the static constructor for the DU and the static constructor for the DU's let bindings which has $ prefixed to the name call each other in a particular order and before the backing fields of the DU have been initialized.

This can only be observed if you have at least two compilation units and the DU is inside a module and you then reference the static let binding from some other static member of the DU.

Here is a example:

Test.fs

module Test

type ABC =
    | A
    | B
    | C of int

    static let ab' = [ A; B ]
    static member ab = ab'

    static let c0' = ABC.C 0
    static member c0 = c0'

Program.fs

open Test
printfn "ABC.c0: %A" ABC.c0
printfn "ABC.ab: %A" ABC.ab

This will print:

ABC.c0: C 0
ABC.ab: [null; null]

We expect of course that the result should have been:

ABC.c0: C 0
ABC.ab: [A; B]

This is occurring due to following code being generated in ABC's static constructor and it's interaction with $Test modules static constructor.

    static ABC()
    {
      $Test.init@ = 0;              // this line is to make sure that the parent        
                                    // module's static initializer is executed prior to 
                                    // running the rest of this code.
      int init = $Test.init@;
      Test.ABC._unique_A = (Test.ABC) new Test.ABC._A();
      Test.ABC._unique_B = (Test.ABC) new Test.ABC._B();
    }

and here is the static constructor for the Test module:

    static $Test()
    {
      Test.ABC.ab' = FSharpList<Test.ABC>.Cons(Test.ABC.get_A(), FSharpList<Test.ABC>.Cons(Test.ABC.get_B(), FSharpList<Test.ABC>.get_Empty()));
      Test.ABC.init@3 = 0;
      Test.ABC.c0' = Test.ABC.NewC(0);
      Test.ABC.init@03 = 1;
    }

You can now see the issue that arises, the ABC static constructor causes the parent Test modules static constructor to execute before initializing the backing fields for the no-arg Union cases. The static constructor for the Test module in turn uses the ABC get methods that access those uninitialized backending fields returning null.

I understand why the module initializer is setting the fields, because that way accessing any other module elements will cause all the module contained types to initialize together. But I would then propose that the module's init@ reference in the DU static constructor should happen after backing fields for no-arg cases are initialized.

Workaround

To handle this issue I used a lazy expression instead for the initialization of the static let binding this means that accessing of the backing field is delayed until this binding is actually accessed after the static constructors complete. The use of lazy also means I'm caching the value so we don't re-compute the values again and again as a normal static member would do. The only downside is that we are now re-directing through a Lazy value which is also heap allocated. It might make sense to change Lazy into a value type so that we can avoid the indirection. If I understand the VM correctly if the Lazy type is a value type then the cached value inside would simply be an additional offset from the fields offset inside the containing type.

Example:

module Test

type ABC =
    | A
    | B
    | C of int

    static let ab' = lazy [ ABC.get_A(); ABC.get_B() ] // use of lazy here to workaround a recursive initialization issue between the parent module and this DU.
    static member ab = ab'.Value

    static let c0' = ABC.C 0
    static member c0 = c0'

I'd be happy to help make this change if someone would point to the section of the compiler that handles the generation of DU static constructors.

Metadata

Metadata

Assignees

Labels

Area-Compiler-CodeGenIlxGen, ilwrite and things at the backendBugImpact-High(Internal MS Team use only) Describes an issue with extreme impact on existing code.

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions