F# Compiler Guide

F# Interactive Code Generation

F# Interactive (dotnet fsi) accepts incremental code fragments. This capability is also used by hosted execution capability of the FSharp.Compiler.Service API which is used to build the F# kernel for .NET Interactive notebooks.

Historically F# Interactive code was emitted into a single dynamic assembly using Reflection.Emit and ilreflect.fs (meaning one assembly that is continually growing). However, .NET Core Reflection.Emit does not support the emit of debug symbols for dynamic assemblies, so in Feb 2022 we switched to emitting multiple non-dynamic assemblies (meaning assemblies dynamically created in-memory using ilwrite.fs, and loaded, but not growing).

The assemblies are named:



Compat switch

There is a switch fsi --multiemit that turns on the use of multi-assembly generation (when it is off, we use Reflection Emit for single-dynamic-assembly generation). This is on by default for .NET Core, and off by default for .NET Framework for compat reasons.

Are multiple assemblies too costly?

There is general assumption in this that on modern dev machines (where users execute multiple interactions) then generating 50, 100 or 1,000 or 10,000 dynamic assemblies by repeated manual execution of code is not a problem: the extra overheads of multiple assemblies compared to one dynamic assembly is of no real significance in developer REPL scenarios given the vast amount of memory available on modern 64-bit machines.

Quick check: adding 10,000 let x = 1;; interactions to .NET Core dotnet fsi adds about 300MB to the FSI.EXE process, meaning 30K/interaction. A budget of 1GB for interactive fragments (reasonable on a 64-bit machine), and an expected maximum of 10000 fragments before restart (that's a lot!), then each fragment can take up to 100K. This is well below the cost of a new assembly.

Additionally, these costs are not substantially reduced if --multiemit is disabled, so they've always been the approximate costs of F# Interactive fragment generation.

Internals and accessibility across fragments

Generating into multiple assemblies raises issues for some things that are assembly bound such as "internals" accessibility. In a first iteration of this we had a failing case here:

> artifacts
// Fragment 1
> let internal f() = 1;;
val internal f: unit -> int

// Fragment 2 - according to existing rules it is allowed to access internal things of the first
System.MethodAccessException: Attempt by method '<StartupCode$FSI_0003>FSI_0003.main@()' to access method 'FSI_0002.f()' failed.
   at <StartupCode$FSI_0003>FSI_0003.main@()

This is because we are now generating into multiple assemblies. Another bug was this:

> artifacts
// Fragment 1 - not `x` becomes an internal field of the class
> type C() =
>    let mutable x = 1
>    member _.M() = x
> ;;
// Fragment 2 - inlining 'M()' gave an access to the internal field `x`
> C().M();;

According to the current F# scripting programming model (the one checked in the editor), the "internal" thing should be accessible in subsequent fragments. Should this be changed? No:

Because of this we emit IVTs for the next 30 FSI-ASSEMBLYnnn assemblies on each assembly fragment, giving a warning when an internal thing is accessed across assembly boundaries within that 30 (reporting it as a deprecated feature), and give an error if internal access happens after that.

From a compat perspective this seems reasonable, and the compat flag is available to return the whole system to generate-one-assembly behavior.

type unit = Unit
<summary>The type 'unit', which has only one value "()". This value is special and always uses the representation 'null'.</summary>
<category index="1">Basic Types</category>
Multiple items
val int : value:'T -> int (requires member op_Explicit)
<summary>Converts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Int32.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted int</returns>

[<Struct>] type int = int32
<summary>An abbreviation for the CLI type <see cref="T:System.Int32" />.</summary>
<category>Basic Types</category>

type int<'Measure> = int
<summary>The type of 32-bit signed integer numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Int32" />.</summary>
<category>Basic Types with Units of Measure</category>
namespace System
Multiple items
type MethodAccessException = inherit MemberAccessException new : unit -> unit + 3 overloads
<summary>The exception that is thrown when there is an invalid attempt to access a method, such as accessing a private method from partially trusted code.</summary>

System.MethodAccessException() : System.MethodAccessException
System.MethodAccessException(message: string) : System.MethodAccessException
System.MethodAccessException(message: string, inner: exn) : System.MethodAccessException