dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.94k stars 788 forks source link

F# interactive duplicates type definition if implicitly loaded 2+ times from other .fsx files #15701

Open DunetsNM opened 1 year ago

DunetsNM commented 1 year ago

I have a hierarchy of .fsx scripts where entry point script #load a few helper scripts. Some of the leaf-level scripts loaded twice or more times which F# interactive refuses to compile.

Repro steps

  1. Create a temp folder for fsx scripts , unpack this archive here and go to step 6 (or create files manually in steps 2 to 5) SharedTypeFsiBug.zip
  2. Create 1_SharedType.fsx file with this code
namespace BugDemo.Shared

module Types =
    type SharedType = Hello | World
  1. Create 2_Func1_Return_SharedType.fsx file with this code
#load "./1_SharedType.fsx"

namespace BugDemo.Shared

open BugDemo.Shared.Types

module Funcs2 =
    let func2 (_: int) : SharedType = SharedType.World
  1. Create 2_Func2_Return_SharedType.fsx file with this code
#load "./1_SharedType.fsx"

namespace BugDemo.Shared

open BugDemo.Shared.Types

module Funcs2 =
    let func2 (_: int) : SharedType = SharedType.World
  1. Create 3_Main.fsx file with this code:
#load "1_SharedType.fsx"
#load "./2_Func1_Return_SharedType.fsx"
#load "./2_Func2_Return_SharedType.fsx"

open BugDemo.Shared.Types
open BugDemo.Shared.Funcs1
open BugDemo.Shared.Funcs2

let x: SharedType = func1 1
let y: SharedType = func2 2
System.Console.WriteLine ("Are x and y equal? " + (x = y).ToString())
  1. Run dotnet fsi ./3_Main.fsx

Expected behavior

Script compiles runs and prints "Are x and y equal? False"

Actual behavior

Compilation fails with the error:

3_Main.fsx(9,21): error FS0001: This expression was expected to have type
    'FSI_0003.BugDemo.Shared.Types.SharedType'
but here has type
    'FSI_0002.BugDemo.Shared.Types.SharedType'

Known workarounds

Need to create yet another top level script that has one line #load "./3_Main.fsx" and run it.
This way F# interactive somehow properly deduplicates repeated #load directives during compilation and runs script successfully

Related information

Provide any related information (optional):

DunetsNM commented 1 year ago

Not sure if there are any technical reasons behind this design, maybe it's not a bug but feature? Then this behavior needs to be documented. Currently https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/ simply states that #load

Reads a source file, compiles it, and runs it.

a bit of background, I switched my project from fake-cli tool (dotnet fake) to plain F# interactive and ran into this bug. Scripts that fake-cli runs fine started to fail in dotnet fsi so I had to create extra wrapper script for each entry-point script

vzarytovskii commented 1 year ago

Yeah, this is a by design behaviour. Not a bug

smoothdeveloper commented 1 year ago

I don't know the underpinnings, but I overall think it fits something akin to #4772

few things to consider:

the message which deals with showing "type A was expected but type A was given" was improved to carry the assembly name.

DunetsNM commented 1 year ago

so this is by design that running the very same .fsx script file directly via dotnet fsi and indirectly via #load (see the workaround) results in different behavior?

As mentioned above docs simply say that #load just

Reads a source file, compiles it, and runs it.

This sounds very much the same as what dotnet fsi meant to do : read the source file, compile it and run it.

I didn't read the fsi source code but my hunch is that when #load invoked from the entry point script it spawns a separate compilation dynamically , so if two #load directives happen to declare same types they'll end up in two separate dynamic assemblies.

However each individual #load directive is clever enough to fetch all the transient dependencies (nested #load directives in loaded script) and compile it once as a whole thing - hence why described workaround works.

The question is, why dotnet fsi does not simply do the same? And why is it a feature rather than a bug? The most naive fix would be if under the hood fsi simply createad a temp .fsx script with a single #load line and compiled that instead of the supplied .fsx file (but probably it's also not hard to achieve without a temp file)