Unexpected warning '[FS3511] This state machine is not statically compilable'
We have a number of problems where tasks are not being statically compiled.
- One is below.
- Another is https://github.com/dotnet/fsharp/issues/13657
- For top-level tasks, see https://github.com/dotnet/fsharp/issues/12038
See also https://github.com/demetrixbio/Plough.WebApi/pull/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
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.
Probably related with #12038.
Just for the record:
In that issue, @dsyme said:
It's not a super-critical show-stopping bug because
- the task still runs correctly
- it's rare to define tasks like this in project code (however it may happen a lot in notebooks)
- tasks at the top-level like this are never perf-critical
Note that points 2 and 3 don't apply here.
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.
@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.
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

The type of proxy is InvocationProps<'impl> -> Task<InvocationResult>.
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)
}
@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 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
taskimplemented 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 withwinget install Microsoft.DotNet.SDK.7btw thanks @thinkbeforecoding), just wanted to make sure that using the very latest version didn't fix the warning.
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
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
expris anApp, then re-run it thoughTryReduceExpr.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) smLogs 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.