Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected warning '[FS3511] This state machine is not statically compilable' #12839

Open
BoundedChenn31 opened this issue Mar 14, 2022 · 11 comments
Assignees
Labels
Area-Compiler-StateMachines Sequence, list, task and other state machine compilation Bug Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code.
Milestone

Comments

@BoundedChenn31
Copy link
Contributor

BoundedChenn31 commented Mar 14, 2022

We have a number of problems where tasks are not being statically compiled.

See also demetrixbio/Plough.WebApi#5 as an example where a library hit this.


Repro steps

I had this problem in more complicated code, however it boils down to something like this:

type Foo = { X: int option }

type BigRecord =
    {
        a1: string
        a2: string
        a3: string
        a4: string
        a5: string
        a6: string
        a7: string
        a8: string
        a9: string
        a10: string
        a11: string
        a12: string
        a13: string
        a14: string
        a15: string
        a16: string
        a17: string
        a18: string
        a19: string
        a20: string
        a21: string
        a22: string
        a23: string
        a24: string
        a25: string
        a26: string
        a27: string
        a28: string
        a29: string
        a30: string
        a31: string
        a32: string
        a33: string
        a34: string
        a35: string
        a36: string // no warning if at least one field removed

        a37Optional: string option
    }

let testStateMachine (bigRecord: BigRecord) =
    task {
        match Some 5 with // no warn if this match removed and only inner one kept
        | Some _ ->
            match Unchecked.defaultof<Foo>.X with // no warning if replaced with `match Some 5 with`
            | Some _ ->
                let d = { bigRecord with a37Optional = None } // no warning if d renamed as _ or ignore function used
                ()
            | None -> ()
        | _ -> ()
    }

printfn "Hello from F#"

Then compile it in release mode.

Original code doesn't have Unchecked.defaultof and gets Foo from another function but I wasn't able to create minimal example without it. I guess, it doesn't really matter and root cause is the same.

Expected behavior

No warning or better explanation of the problem and how to fix it.

Actual behavior

When compiling in Release mode:

dotnet build -c Release

Program.fs(46, 5): [FS3511] This state machine is not statically compilable. A resumable code invocation at '(46,4--46,8)' could not be reduced. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.

Known workarounds

In original code I had to create another function that works with d and inline usage let d = { bigRecord with a37Optional = None } variable at line 51:

...
match Unchecked.defaultof<Foo>.X with
| Some _ ->
    do! testStateMachineInner { bigRecord with a37Optional = None }
| None -> ()
...

Related information

.NET SDK (reflecting any global.json):
Version: 6.0.201
Commit: ef40e6aa06

Runtime Environment:
OS Name: Windows
OS Version: 10.0.19044
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\6.0.201\

Host (useful for support):
Version: 6.0.3
Commit: c24d9a9c91

@KevinRansom KevinRansom added the Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code. label Mar 14, 2022
@dsyme dsyme added Area-Compiler Area-Compiler-StateMachines Sequence, list, task and other state machine compilation and removed Area-Compiler labels Mar 30, 2022
@cmeeren
Copy link
Contributor

cmeeren commented Apr 19, 2022

I experience something similar. Here is my repro. Let me know if I should post a separate issue for this.

type T = {
  A: string
  B: string
  C: int64
}

let f () =
  task {
    // No warning if removing outer or inner match
    match Error "" with
    | Error _ -> ()
    | Ok _ ->
        match Error "" with
        | Error _ -> return failwith ""  // No warning if replacing with () as above
        | Ok _ ->
            match Some (Unchecked.defaultof<T>, Unchecked.defaultof<unit>) with  // No warning if using literal () or removing second tuple element
            | None -> return failwith ""
            | Some (info, _a) ->  // No warning if renaming '_a' to '_'
                printfn $"{info.A}"  // No warning if removing
                printfn $"{info.B}"  // No warning if removing
                printfn $"{info.C}"  // No warning if removing
                printfn $"{2 + 2}"  // No warning if removing or replacing with constant string
                return failwith ""  // No warning if removing or replacing with 'return ()' or '()'
  }

In reducing this repro, it seemed extremely arbitrary to me what caused the problem and what doesn't. For the original code (which is a Giraffe HttpHandler for serving file downloads), I am left with no apparent recourse other than to ignore FS3511 for the whole project, and live with whatever the performance impacts are.

@abelbraaksma
Copy link
Contributor

Probably related with #12038.

@cmeeren
Copy link
Contributor

cmeeren commented Apr 27, 2022

Just for the record:

In that issue, @dsyme said:

It's not a super-critical show-stopping bug because

  1. the task still runs correctly
  2. it's rare to define tasks like this in project code (however it may happen a lot in notebooks)
  3. tasks at the top-level like this are never perf-critical

Note that points 2 and 3 don't apply here.

@kerams
Copy link
Contributor

kerams commented Jun 25, 2022

Chiming in with my repro.

let getDeckWithSchema cs (requester: UserId option) (deckId: DeckId) includeSections now = task {
    use cmd = getDeckWithSchemaCmd cs
    let! res = cmd.TaskAsyncExecute (defNeg1 requester, %deckId, toDbTime now, includeSections)

    match res.ResultSet1 with
    | [| r |] ->
        let deck = mapDeck r
        let atts = Array.map mapAttributeDefinition res.ResultSet3
        let cols = Array.map mapColumnDefinition res.ResultSet4
        let sections = Array.map mapSection res.ResultSet2

        return ValueSome (deck, atts, cols, sections)
    | _ ->
        return ValueNone }

A resumable code invocation at 'let! res = cmd.TaskAsyncExecute (defNeg1 requester, %deckId, toDbTime now, includeSections)' could note be reduced.

TaskAsyncExecute is a provided method, but dozens of other functions that use it don't produce the warning.

@abelbraaksma
Copy link
Contributor

@kerams, can you make the repro reproducible, please? I.e., by adding stubs for the functions that you omitted? That will make it easier to analyze what's going on.

@dsyme dsyme self-assigned this Aug 16, 2022
Juriyx pushed a commit to Juriyx/FSharp.Data.GraphQL that referenced this issue Sep 4, 2022
Juriyx pushed a commit to Juriyx/FSharp.Data.GraphQL that referenced this issue Sep 10, 2022
xperiandri pushed a commit to fsprojects/FSharp.Data.GraphQL that referenced this issue Sep 10, 2022
@vzarytovskii vzarytovskii added this to the Backlog milestone Sep 21, 2022
@kerams
Copy link
Contributor

kerams commented Oct 1, 2022

Another one.

To reproduce it, get this PR, dotnet tool restore, open the solution and try to build Fable.Remoting.Giraffe in Release.

warning FS3511: This state machine is not statically compilable. A resumable code invocation at '(46,12--46,18)' could not be reduced.

FableGiraffeAdapter.fs
devenv_5uZqpO7qpe

The type of proxy is InvocationProps<'impl> -> Task<InvocationResult>.

@natalie-o-perret
Copy link
Contributor

natalie-o-perret commented Oct 26, 2022

We keep bumping into that issue every now and again, often when there are for loops wrapped in a task CE, for instance like below:

let writeToZipOutputStreamTask (level: Deflater.CompressionLevel) outputStream (source: (string * byte array) seq) =
    task {
        use zipOutputStream = new ZipOutputStream(outputStream)
        zipOutputStream.SetLevel(int32 level)

        for name, bytes in source do
            let zipEntry = ZipEntry(name=name)
            do! zipOutputStream.PutNextEntryAsync(zipEntry)
            do! zipOutputStream.WriteAsync(bytes)
    }

The workaround that has worked for us is to simply leverage the relevant enumerator +MoveNext() + Current property:

let writeToZipOutputStreamTask (level: Deflater.CompressionLevel) outputStream (source: (string * byte array) seq) =
    task {
        use zipOutputStream = new ZipOutputStream(outputStream)
        zipOutputStream.SetLevel(int32 level)

        use sourceEnumerator = source.GetEnumerator()
        while sourceEnumerator.MoveNext() do
            let name, bytes = sourceEnumerator.Current
            let zipEntry = ZipEntry(name=name)
            do! zipOutputStream.PutNextEntryAsync(zipEntry)
            do! zipOutputStream.WriteAsync(bytes)
    }

@abelbraaksma
Copy link
Contributor

@natalie-o-perret in your first snippet you have use sourceEnumerator = source.GetEnumerator(), which isn’t used anywhere. Is that necessary for the repro?

From the looks of it, it appears that it has something to do with the combination of disposable types. What library are you referencing? Maybe we can dumb this sample down even further and do some more investigation.

Not sure all cases here are related, though.

We should also emphasise that there’s no danger to this warning. In certain cases it may not even perform so much noticeably slower (it depends, I know). It merely means you get the ‘old style’ binding that would’ve been used were task implemented pre F# 6.0, without statically compiled resumable state.

Still, would be nice if we could solve this :).

@natalie-o-perret
Copy link
Contributor

natalie-o-perret commented Nov 8, 2022

@natalie-o-perret in your first snippet you have use sourceEnumerator = source.GetEnumerator(), which isn’t used anywhere. Is that necessary for the repro?

From the looks of it, it appears that it has something to do with the combination of disposable types. What library are you referencing? Maybe we can dumb this sample down even further and do some more investigation.

Not sure all cases here are related, though.

We should also emphasise that there’s no danger to this warning. In certain cases it may not even perform so much noticeably slower (it depends, I know). It merely means you get the ‘old style’ binding that would’ve been used were task implemented pre F# 6.0, without statically compiled resumable state.

Still, would be nice if we could solve this :).

Thanks, it was a bad copy-paste hiccup when submitting my comment at the time.

Here is a full sample for repro purposes:

open System.IO
open System.Text
open System.Threading.Tasks

open FSharpPlus

open FSharp.Collections

open ICSharpCode.SharpZipLib.Zip
open ICSharpCode.SharpZipLib.Zip.Compression


[<RequireQualifiedAccess>]
module ZipOutputStream =
    let writeToTask1 (level: Deflater.CompressionLevel) outputStream (source: (string * byte array) seq) =
        task {
            use zipOutputStream = new ZipOutputStream(outputStream)
            zipOutputStream.SetLevel(int32 level)

            for name, bytes in source do
                let zipEntry = ZipEntry(name=name)
                do! zipOutputStream.PutNextEntryAsync(zipEntry)
                do! zipOutputStream.WriteAsync(bytes)
        }

    let writeToTask2 (level: Deflater.CompressionLevel) outputStream (source: (string * byte array) seq) =
        task {
            use zipOutputStream = new ZipOutputStream(outputStream)
            zipOutputStream.SetLevel(int32 level)

            use sourceEnumerator = source.GetEnumerator()
            while sourceEnumerator.MoveNext() do
                let name, bytes = sourceEnumerator.Current
                let zipEntry = ZipEntry(name=name)
                do! zipOutputStream.PutNextEntryAsync(zipEntry)
                do! zipOutputStream.WriteAsync(bytes)
        }

[<RequireQualifiedAccess>]
module Task =
    let getAwaiterResult (t: Task<'T>) = t.GetAwaiter().GetResult()

[<EntryPoint>]
let main _ =
    use ms1 = new MemoryStream()
    use ms2 = new MemoryStream()
    let pseudoFiles = seq {
        for c in 'a' .. 'z' do
            let s = string c
            yield s, Encoding.UTF8.GetBytes(s)
    }
    ZipOutputStream.writeToTask1 Deflater.CompressionLevel.BEST_COMPRESSION ms1 pseudoFiles |> Task.getAwaiterResult
    ZipOutputStream.writeToTask2 Deflater.CompressionLevel.BEST_COMPRESSION ms2 pseudoFiles |> Task.getAwaiterResult

    0

Notes about dependencies:

  • SharpZipLib, version: 1.4.1
  • FSharpPlus, version: 1.2.5

Building it with the release configuration flag:

# natalie-perret @ nppc in ~\Desktop\Personal\playground\fsharp\FSharpPlayground [05:57:10]
$ dotnet build -c release
MSBuild version 17.4.0+18d5aef85 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
C:\Users\natalie-perret\Desktop\Personal\playground\fsharp\FSharpPlayground\Program.fs(16,9): warning FS3511: This state machine is not statically comp
ilable. A resumable code invocation at '(20,12--20,15)' could not be reduced. An alternative dynamic implementation will be used, which may be slower. 
Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning. [C:\Users\natalie-perret\Desktop\Per
sonal\playground\fsharp\FSharpPlayground\FSharpPlayground.fsproj]
  FSharpPlayground -> C:\Users\natalie-perret\Desktop\Personal\playground\fsharp\FSharpPlayground\bin\Release\net7.0\FSharpPlayground.dll

Build succeeded.

C:\Users\natalie-perret\Desktop\Personal\playground\fsharp\FSharpPlayground\Program.fs(16,9): warning FS3511: This state machine is not statically comp
ilable. A resumable code invocation at '(20,12--20,15)' could not be reduced. An alternative dynamic implementation will be used, which may be slower. 
Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning. [C:\Users\natalie-perret\Desktop\Per
sonal\playground\fsharp\FSharpPlayground\FSharpPlayground.fsproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:06.11

Note about the .NET version in use:

  • <TargetFramework>net7.0</TargetFramework> (installed on the release date / conf with winget install Microsoft.DotNet.SDK.7 btw thanks @thinkbeforecoding), just wanted to make sure that using the very latest version didn't fix the warning.

@TheAngryByrd
Copy link
Contributor

TheAngryByrd commented Apr 7, 2024

I just ran into this issue with FsToolkit.Errorhandling dealing with using multiple and!s:

cancellableTaskValidation {
    let! a = Ok 3
    and! b = Choice1Of2 2
    and! c = CancellableTaskValidation.ok 1
    return a + b - c
}

I ended up looking into it and came to the same fix as in #14930 of if the expr is an App, then re-run it though TryReduceExpr.

It'll take a while to get a log of the expression it's trying to reduce, but it's effectively the same problem as listed in the PR:

Invoke (let arg0 = System.Collections.Generic.IEnumerator`1.get_Current [e]
        body@1 #0 arg0 #1 arg0) sm

Logs from my fork:

         RepeatBindAndApplyOuterDefinitions for App(Invoke, [Let(arg0_p0, Op(TupleFieldGet(..), ..), Let(arg0_p1, .., ..)), sm])...
         expanding defns and reducing App(Invoke, [Let(arg0_p0, Op(TupleFieldGet(..), ..), Let(arg0_p1, .., ..)), sm])...
         found delegate invoke in possible reduction, f = Let(arg0_p0, Op(TupleFieldGet(..), result), Let(arg0_p1, Op(TupleFieldGet(..), ..), App(.., [.., .., ..]))), args now [sm]...
         expanding defns and reducing Let(arg0_p0, Op(TupleFieldGet(..), result), Let(arg0_p1, Op(TupleFieldGet(..), ..), App(.., [.., .., ..])))...
         TryReduceApp Let
         "let arg0_p0 =
               #0 result
         let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         TryReduceApp Let
         "let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         failed TryReduceApp
         "continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         App(continuation@1-3, [arg0_p0, Op(TupleFieldGet(..), arg0_p1), Op(TupleFieldGet(..), arg0_p1)])
         failed to reduce "let arg0_p0 =
               #0 result
         let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         After RepeatBindAndApplyOuterDefinitions:
         Invoke (let arg0_p0 =
                       #0 result
                 let arg0_p1 =
                       #1 result
                 continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1) sm

@vzarytovskii
Copy link
Member

I just ran into this issue with FsToolkit.Errorhandling dealing with using multiple and!s:

cancellableTaskValidation {
    let! a = Ok 3
    and! b = Choice1Of2 2
    and! c = CancellableTaskValidation.ok 1
    return a + b - c
}

I ended up looking into it and came to the same fix as in #14930 of if the expr is an App, then re-run it though TryReduceExpr.

It'll take a while to get a log of the expression it's trying to reduce, but it's effectively the same problem as listed in the PR:

Invoke (let arg0 = System.Collections.Generic.IEnumerator`1.get_Current [e]
        body@1 #0 arg0 #1 arg0) sm

Logs from my fork:

         RepeatBindAndApplyOuterDefinitions for App(Invoke, [Let(arg0_p0, Op(TupleFieldGet(..), ..), Let(arg0_p1, .., ..)), sm])...
         expanding defns and reducing App(Invoke, [Let(arg0_p0, Op(TupleFieldGet(..), ..), Let(arg0_p1, .., ..)), sm])...
         found delegate invoke in possible reduction, f = Let(arg0_p0, Op(TupleFieldGet(..), result), Let(arg0_p1, Op(TupleFieldGet(..), ..), App(.., [.., .., ..]))), args now [sm]...
         expanding defns and reducing Let(arg0_p0, Op(TupleFieldGet(..), result), Let(arg0_p1, Op(TupleFieldGet(..), ..), App(.., [.., .., ..])))...
         TryReduceApp Let
         "let arg0_p0 =
               #0 result
         let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         TryReduceApp Let
         "let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         failed TryReduceApp
         "continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         App(continuation@1-3, [arg0_p0, Op(TupleFieldGet(..), arg0_p1), Op(TupleFieldGet(..), arg0_p1)])
         failed to reduce "let arg0_p0 =
               #0 result
         let arg0_p1 =
               #1 result
         continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1"
         After RepeatBindAndApplyOuterDefinitions:
         Invoke (let arg0_p0 =
                       #0 result
                 let arg0_p1 =
                       #1 result
                 continuation@1-3 arg0_p0 #0 arg0_p1 #1 arg0_p1) sm

We can probably make this change as a ad-hoc fix for these cases. Problem is that to properly fix majority of those issues, it will take a deeper investigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compiler-StateMachines Sequence, list, task and other state machine compilation Bug Impact-Low (Internal MS Team use only) Describes an issue with limited impact on existing code.
Projects
Status: New
Development

No branches or pull requests

9 participants