Skip to content

Design note and future cleanup to emit fewer (or no) FSharpTypeFunc.Specialize calls #10517

@dsyme

Description

@dsyme

I was discussing this with @TIHan today and we want to record this issue, this is just a sketch.

For code like this:

let makeThing(f1: int -> int*string, f2: byte -> byte*string) = ....

let f (z: string) =
    let g x = (x,z)
    makeThing(g, g)

the F# type checker generalises the inner function:

let f (z: string) =
    let g<'T> (x: 'T) = (x,z)
    MakeThing(g<int>, g<byte>)

In the absence of other optimizations, and to avoid code duplication, and to make compilation localised, F# often emits let g<'T> = ... as a "first class function value" (FSharpTypeFunc) and then immediately specialises that. For example the most naive code might be a bunch of closures roughly like this:

let f (z: string) =
    static type clo_g2<'T>(z:string): FSharpFunc<'T, 'T*string>
        field _z: string = z
        abstract Invoke(x:'T) = (x,this._z)

    static type clo_g(z:string): FSharpTypeFunc
        field _z: string = z
        abstract Specialize<'T> = box (new clo_g2<'T>(this._z))

    //MakeThing(g_int, g_byte) becomes:
    MakeThing((clo_g.Specialize<int> :?> (int -> int * string)),
              (clo_g.Specialize<byte> :?> (byte -> byte * string)))

Here clo_g is almost always completely superfluous, it's immediately speciailzed to two types.

But really, we should basically never (or almost never) emit FSharpTypeFunc because, as can be seen above, these things are always immediately applied to actual type arguments,

Now, IlxGen actually has a complex code path to emit "local type function closures" in the case where T is constrained (when 'T is constrained we can't use FSharpTypeFunc because that only represents unconstrained first class type functions - we only realised this late in F# 0.x when we fully connected F# to .NET generics)

We should either :

  1. always emit "local" type functions (no FSharpTypeFunc), simply by deleting this one single line, or

  2. still emit ILX closures and adjust the emit of FSharpTypeFunc and subsequent closures to avoid calls to FSharpTypeFunc.Specialize and go straight to the closure class its cody would return (clo_g2)

Notes

  • Note the currently emitted code is particularly bad when multiple generic parameters are involved, when there can be several calls to Specialize.

  • Note also that a lot of this code gets generated in debug mode, and it's possible the debugging experience degrades in some ways because of that. It would likely become better if we make the generated code simpler, but should be checked

  • The call to Specialize is ultimately built here

  • The FSharpTypeFunc closure class is emitted here

Background and relation to TType_forall and Expr.TyLambda

The existence of FSharpTypeFunc at all is a relic from the 1.x era of F# when we thought the language would have to support "first class forall types", e.g. let f (x: FORALL<'T>. 'T -> 'T). These things are however easily representable in .NET programming by classes or interfaces that have generic vitrual methods, and code generall becomes much clearer when such a subtle contract is given a name.

However we never actually removed forall types or type lambdas from the F# TypedTree and pickled format. For example, you might imagine that a Val stores its information like this:

  let f<'T> (x: 'T) = x

  f = { genericParams: ['T]
        parameters: [{name="x";type='T;attribs}] 
        returnType: [type='T; attribs] }

but actually what happens is that the type of f when used as a first class value is stored in Val is more like this:

f = { type: FORALL 'T. 'T -> 'T
      argInfos: [{"x";attribs}] 
      returnInfo: attribs }

and the body matches this:

let f = TyLambda<'T>. Lambda(x:'T). x

The information in Val gets "split and folded back together" repeatedly in routines like GetTopValTypeInFSharpForm.
Thus the TypedTree and pickled format include Expr.TyLambda (in Expressions) and TType_forall (in types). Both are a nasty relic. Also some optimization phases may, I believe, give rise to further instances of these that are not eliminated or in a neat normal form.

As an aside, the argInfos in Val only gets stored for top level (class, module) function definitions and members, and not expression-level function definitions. This even shows in F# tooling where inner function definitions do not show metadata information like argument names.

image

As an aside, this whole story really stems from the early implementation of F# as a lambda-calculus-like-thing over-emphasises the view that function definitions result in first class function values.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions