fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Backwards compatibility broken

Open gusty opened this issue 10 months ago • 4 comments

The following code is compiled differently in F#9, depending on which SDK is used ( 8 vs 9, in both cases targeting net8 )

type Default3 = class end
type Default2 = class inherit Default3 end
type Default1 = class inherit Default2 end

type IsAltLeftZero =
    inherit Default1

    static member inline IsAltLeftZero (_: ref<'T>   when 'T : struct    , _mthd: Default3) = false
    static member inline IsAltLeftZero (_: ref<'T>   when 'T : not struct, _mthd: Default2) = false

    static member inline IsAltLeftZero (t: ref<'At                               , _mthd: Default1) = (^At : (static member IsAltLeftZero : _ -> _) t.Value)
    static member inline IsAltLeftZero (_: ref< ^t> when ^t: null and ^t: struct , _mthd: Default1) = ()

    static member        IsAltLeftZero (t: ref<option<_>  > , _mthd: IsAltLeftZero) = Option.isSome t.Value
    static member        IsAltLeftZero (t: ref<voption<_>  >, _mthd: IsAltLeftZero) = ValueOption.isSome t.Value

    static member        IsAltLeftZero (t: ref<Result<_,_>> , _mthd: IsAltLeftZero) = match t.Value with (Ok _        ) -> true | _ -> false
    static member        IsAltLeftZero (t: ref<Choice<_,_>> , _mthd: IsAltLeftZero) = match t.Value with (Choice1Of2 _) -> true | _ -> false

    static member inline Invoke (x: 'At) : bool =
        let inline call (mthd : ^M, input: ^I) =
            ((^M or ^I) : (static member IsAltLeftZero : _*_ -> _) (ref input), mthd)
        call(Unchecked.defaultof<IsAltLeftZero>, x)

Looking at the produced type for IsAltLeftZero there are some diffs in the Invoke$W method (I guess W stands for witness)

// F#8 => Boolean Invoke$W[At](FSharpFunc`2[At,Boolean], FSharpFunc`2[FSharpRef`1[At],FSharpFunc`2[IsAltLeftZero,Boolean]], At);
// F#9 => Boolean Invoke$W[At](                          FSharpFunc`2[FSharpRef`1[At],FSharpFunc`2[IsAltLeftZero,Boolean]], At);

At the end of the day, the problem is when using the produced dll, from F# 9, the following call will fail when compiled with sdk 8

> IsAltLeftZero.Invoke None ;; 

  IsAltLeftZero.Invoke None ;; 
  ---------------------^^^^

stdin(4,22): error FS0001: '.Invoke' does not support the type ''a option', because the latter lacks the required (real or built-in) member 'IsAltLeftZero'

Known workarounds

The only workaround is recompile everything in F#9

Note: I this is a major breaking change. Running SRTP code from FSharpPlus breaks in so many places (See https://github.com/fsprojects/FSharpPlus/issues/613) that I will be really surprised if this affects only F#+ code.

gusty avatar Feb 27 '25 19:02 gusty

To summarize: Lib 9, Consumer 8 -> this is the main scenario detected as being broken Lib 9, Consumer also 9 -> works Lib 8, Consumer 9 -> works

And the problem arises for consumers passing in Option<> only ? (because my first-glance hypothesis is this affects types that UseNullAsTrueValue)

T-Gro avatar Feb 27 '25 20:02 T-Gro

No, I think it's rather:

Lib 8, Consumer 9 -> this is the main scenario detected as being broken Lib 9, Consumer also 9 -> works Lib 9, Consumer 8 -> works (although not 100% sure about this one)

gusty avatar Feb 27 '25 22:02 gusty

Is there any update on this? I'm under the impression that if this is not fixed in time, it will be harder as there might be more dlls compiled with the incorrect code around.

gusty avatar May 30 '25 04:05 gusty

Hi @gusty , this is not fixed yet.

I tried investigating it a few times, but so far not identified an approach for fixing that. It still remains a priority item to get addressed.

T-Gro avatar May 30 '25 08:05 T-Gro

@gusty :

Can I please ask for your help for correctly reproducing and verifying this?

// Picking 1.6.1. to make sure it has been built with older SDK
#r "nuget: FSharpPlus, 1.6.1"
FSharpPlus.Control.IsAltLeftZero.Invoke None

This should follow the Lib 8, Consumer 9 -> this is the main scenario detected as being broken scenario

T-Gro avatar Jul 31 '25 14:07 T-Gro

@T-Gro I just managed to reproduce it. Steps:

  • Create a new project, an F# class lib should be enough. Name it something like ReproFailure, target .net8, after the namespace paste the code from the repro.

  • Create a global.json like this:

{
  "sdk": {
    "version": "8.0.0",
    "rollForward": "latestFeature",
    "allowPrerelease": true
  },

  "additionalSdks": [
    "5.0.405",
    "6.0.201",
    "7.0.100"
  ]
}
  • Compile it

  • Now try this from an fsi with langversion 9

#r @"C:\Repos\ReproFailure\bin\Debug\net8.0\ReproFailure.dll"

open ReproFailure

let x = IsAltLeftZero.Invoke None 

Then you'll see the error I gave you in the description of this issue. Hope this helps, otherwise please let me know.

gusty avatar Jul 31 '25 16:07 gusty

THanks for the steps, @gusty .

I have created the repro with the mentioned global.json, dotnet --version in that folder states 8.0.412.

I then tried to reproduce the issue by loading it from fsi, trying both F# Interactive version 13.9.300.0 for F# 9.0 which comes with .NET SDK 9.0.302. And also with the latest, F# Interactive version 14.0.100.0 for F# 10.0 from .NET 10.0.100-preview.6.25358.103.

For both of these, the code snippet runs just fine with:

> let x = IsAltLeftZero.Invoke None;;
val x: bool = false

I find it suspicious that the bug would be fixed by accident by one of the other bugfixes I did for nullness, but I cannot rule it out.

Please

For your langversion=9 repro, can you please let me know your exact dotnet --version ? I will then try with the same one.

Bug

I definitely believe there is an issue with importing older assemblies, I do have a hypothesis related to "null ambivalent handling" and a fix prepared - but it bothers my that I am unable to repro the original issue on my end ;; to verify the fix addresses the right thing.

T-Gro avatar Aug 05 '25 13:08 T-Gro

I have created the repro with the mentioned global.json, dotnet --version in that folder states 8.0.412.

I have 8.0.303

Then on the interactive (with langversion=9) I have:

Microsoft (R) F# Interactive version 12.9.101.0 for F# 9.0

I can send you the dll if you want.

Also, you can have a look at your compiled dll and see if looks like the F#8 I reported in the original message, or the F#9 one. That will give you a hint about the problem trying to get the same repro and will point to the dll generation or the dll reading, being different that what I have.

gusty avatar Aug 05 '25 15:08 gusty

F# Interactive version 12.9.101.0 for F# 9.0 has not been updated, is it possible this is a very early .NET 9 SDK (or Visual Studio from that time) which did not receive any further updates?

If you have that (v8 created) .dll at hand and could send it to me, I could verify it at latest available SDK as well as against the code currently in main.

The .dll created by my 8.0.412 version for the repro also has different .dll, so I would prefer if I could have your .dll exactly.

	public static bool Invoke$W<At>(FSharpFunc<FSharpRef<At>, FSharpFunc<IsAltLeftZero, bool>> isAltLeftZero, At x)
	{
		object call = $Library.call@23-2.@_instance;
		Tuple<IsAltLeftZero, At> tuple = new Tuple<IsAltLeftZero, At>(null, x);
		IsAltLeftZero item = tuple.Item1;
		At item2 = tuple.Item2;
		return FSharpFunc<FSharpRef<At>, simple_repro.IsAltLeftZero>.InvokeFast(isAltLeftZero, Operators.Ref(item2), item);
	}

T-Gro avatar Aug 06 '25 07:08 T-Gro

Or, if there is a specific version of F#+ that already has this problem, then I could #r "nuget: that version - I could then make it part of our regular test suite to keep it there, to verify behavior of new code against older F#+ (unlike a .dll which I cannot keep around in the repo)

T-Gro avatar Aug 06 '25 07:08 T-Gro

Please send me your email address by slack ( I just messaged you ) so I can send you the dll.

Regarding F#+ I think all versions have this problem, as we didn't move from .net 8 at the moment. This was discovered with the latest version, which is 1.7.0 which was built using Github's CI.

gusty avatar Aug 06 '25 07:08 gusty

I just bumped a giraffe project with lots of F#+ from TF net8.0 to TF net10.0 and saw lots of compiler errors around Reader.map and Seq.traverse pipes composed in functions without type annotations. Warnings are present with both F#+ 1.6.1 and 1.7.0. Something like below, but hundreds of these:

A unique overload for method 'Delay' could not be determined based on type information prior to this program point. A type annotation may be needed.

Known return type: Reader<HttpContext,NestedElements<RoomViewModel,ParallelElements<XmlNode>>>

Known type parameters: < Control.Delay , (unit -> Reader<HttpContext,NestedElements<RoomViewModel,ParallelElements<XmlNode>>>) , Control.Delay >

Candidates:
 - static member Control.Delay.Delay: _mthd: Internals.Default1 * (unit -> ^t) * 'a1 -> unit when ^t: null and ^t: struct
 - static member Control.Delay.Delay: _mthd: Internals.Default1 * x: (unit -> ^I) * Control.Delay -> ^I when ^I: (static member Delay: (unit -> ^I) -> ^I)
 - static member Control.Delay.Delay: _mthd: Internals.Default3 * x: (unit -> ^Monad<'T>) * Internals.Default1 -> ^Monad<'T> when (Control.Bind or ^a1 or ^Monad<'T>) : (static member (>>=) : ^a1 * (unit -> ^Monad<'T>) -> ^Monad<'T>) and (Control.Return or ^a1) : (static member Return: ^a1 * Control.Return -> (unit -> ^a1))
A unique overload for method '<*>' could not be determined based on type information prior to this program point. A type annotation may be needed.
Known return type: FSharpPlus.Data.Reader<System.IServiceProvider,Giraffe.ViewEngine.HtmlElements.XmlNode Microsoft.FSharp.Collections.list>
Known type parameters: < struct (FSharpPlus.Data.Reader<System.IServiceProvider,(Giraffe.ViewEngine.HtmlElements.XmlNode Microsoft.FSharp.Collections.list -> Giraffe.ViewEngine.HtmlElements.XmlNode Microsoft.FSharp.Collections.list)> * FSharpPlus.Data.Reader<System.IServiceProvider,Giraffe.ViewEngine.HtmlElements.XmlNode Microsoft.FSharp.Collections.list>) , FSharpPlus.Data.Reader<System.IServiceProvider,Giraffe.ViewEngine.HtmlElements.XmlNode Microsoft.FSharp.Collections.list> , FSharpPlus.Control.Apply >
Candidates:
 - static member FSharpPlus.Control.Apply.``<*>`` : struct (^Applicative<'T->'U> * ^Applicative<'T>) * _output: ^Applicative<'U> * [<System.Runtime.InteropServices.Optional>] _mthd: FSharpPlus.Internals.Default1 -> ^Applicative<'U> when (^Applicative<'T->'U> or ^Applicative<'T> or ^Applicative<'U>) : (static member (<*>) : ^Applicative<'T->'U> * ^Applicative<'T> -> ^Applicative<'U>)
 - static member FSharpPlus.Control.Apply.``<*>`` : struct (^Monad<'T->'U> * ^Monad<'T>) * _output: ^Monad<'U> * [<System.Runtime.InteropServices.Optional>] _mthd: FSharpPlus.Internals.Default2 -> ^Monad<'U> when (^Monad<'T->'U> or ^Monad<'U>) : (static member (>>=) : ^Monad<'T->'U> * (('T -> 'U) -> ^Monad<'U>) -> ^Monad<'U>) and (^Monad<'T> or ^Monad<'U>) : (static member (>>=) : ^Monad<'T> * ('T -> ^Monad<'U>) -> ^Monad<'U>) and ^Monad<'U> : (static member Return: 'U -> ^Monad<'U>)
 - static member FSharpPlus.Control.Apply.``<*>`` : struct (^t * ^u) * _output: ^r * _mthd: FSharpPlus.Internals.Default1 -> ('a3 -> 'a3) when ^t: null and ^t: struct and ^u: null and ^u: struct and ^r: null and ^r: struct
dotnet --info
.NET SDK:
 Version:           10.0.100-preview.7.25380.108
 Commit:            30000d883e
 Workload version:  10.0.100-manifests.613fefda
 MSBuild version:   17.15.0-preview-25380-108+30000d883

FSharp.Core is pinned to 10.0.100-preview7.25380.108

It seems relevant to me eye, but please let me know if I'm posting to a wrong thread.

anpin avatar Sep 05 '25 12:09 anpin

At first sight it seems related to this bug report. Is there a way you could create a min repro?

gusty avatar Sep 05 '25 14:09 gusty

I tried with a simple fsx, but couldn't find a reproduction case yet. Will try again at another time.

anpin avatar Sep 05 '25 15:09 anpin

Thanks, it would help for sure. An .fsx with a #r "nuget:.." reference to F#+ would work great, as it is something we can also keep around as a regression test.

Indeed, the static member FSharpPlus.Control.Apply.``<*>`` : struct (^t * ^u) * _output: ^r * _mthd: FSharpPlus.Internals.Default1 -> ('a3 -> 'a3) when ^t: null and ^t: struct and ^u: null and ^u: struct and ^r: null and ^r: struct part seems related.

T-Gro avatar Sep 08 '25 12:09 T-Gro