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.9k stars 783 forks source link

VerificationException on runtime, due to upcast to obj inside the task state machine #16154

Open Thorium opened 1 year ago

Thorium commented 1 year ago

There I was, minding my own business, of writing generic higher order functions in asynchronous code, when she hit me, the task-state-machine. She has hit me before, but this time I think she left some traces of her, so I could saw a glimpse of that monster. So I'll report it here, even this will probably be closed "as designed", it might help some other guy struggling in seduction by her.

I haven't been able to repro it in short code, yet, but this is what is going on. Observe the following code:

open System.Linq
type Shape = {x: int; y: int}

let returnFilter condition =
    task {
        if condition = "a" then
            let filter1 : IQueryable<Shape> -> IQueryable<Shape> =
                fun data -> data.Where(fun a -> a.x = 1)
            return Some filter1
        elif condition = "b" then
            let filter2 : IQueryable<Shape> -> IQueryable<Shape> =
                fun data -> data.Where(fun a -> a.y <> 2)
            return Some filter2
        else
            return None
    }

...and then composing these filters back and forth, e.g.

    let data = [{x = 1; y = 1}; {x = 2; y = 2}].AsQueryable()
    task {
        let! getfilter1 = returnFilter "a"
        let! getfilter2 = returnFilter "b"
        match getfilter1, getfilter2 with
        | Some filter1, Some filter2 ->
            return data |> filter2 |> filter1 |> Seq.toList
        // ...and so on...

Expected behavior

Silly me, by the code I expected the type of returnFilter is: string -> Task<(IQueryable<Shape> -> IQueryable<Shape>) option>

Actual behavior

Well, the actual type of returnFilter is: string -> Task<(#IQueryable<Shape> -> IQueryable<Shape>) option>

The code compiled well. But this hash # made all the difference, because combining different returnFilters together caused them to be automatically upcasted to obj.

Thus the code threw at runtime:

System.Security.VerificationException: Method returnFilter: type argument 'System.Object' violates the constraint of type parameter 'a'.
   at ...(my code location of "task{")...
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.Start[TStateMachine](TStateMachine& stateMachine)

What I historically do is change the code from task to async and all is good, but not this time:

System.Security.VerificationException: Method returnFilter: type argument 'System.Object' violates the constraint of type parameter 'a'.
    at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in D:\\a\\_work\\1\\s\\src\\FSharp.Core\\async.fs:line 510
   at ...(mycode)...
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\\a\\_work\\1\\s\\src\\FSharp.Core\\async.fs:line 114
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at ...(mycode)...

Known workarounds

Do write the parameters explicitly. You can even declare a type for your function:

type ShapeFilter = IQueryable<Shape> -> IQueryable<Shape>
let returnFilter condition : Task<Option<ShapeFilter>>=
   //... and now the compiler is happy, and the runtime is happy.

Related information

Provide any related information (optional):

T-Gro commented 1 year ago

(to be verified if/since-when this is a regression)