[WIP]: MethodImplOption.Async support
Description
Fixes #19056
Checklist
-
[ ] Test cases added
-
[ ] Performance benchmarks added in case of performance changes
-
[ ] Release notes entry updated:
Please make sure to add an entry with short succinct description of the change as well as link to this pull request to the respective release notes file, if applicable.
Release notes files:
- If anything under
src/Compilerhas been changed, please make sure to make an entry indocs/release-notes/.FSharp.Compiler.Service/<version>.md, where<version>is usually "highest" one, e.g.42.8.200 - If language feature was added (i.e.
LanguageFeatures.fsiwas changed), please add it todocs/release-notes/.Language/preview.md - If a change to
FSharp.Corewas made, please make sure to editdocs/release-notes/.FSharp.Core/<version>.mdwhere version is "highest" one, e.g.8.0.200.
Information about the release notes entries format can be found in the documentation. Example:
- More inlines for Result module. (PR #16106)
- Correctly handle assembly imports with public key token of 0 length. (Issue #16359, PR #16363)
*
while!(Language suggestion #1038, PR #14238)
If you believe that release notes are not necessary for this PR, please add
NO_RELEASE_NOTESlabel to the pull request. - If anything under
:heavy_exclamation_mark: Release notes required
@TheAngryByrd,
[!CAUTION] No release notes found for the changed paths (see table below).
Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.
The following format is recommended for this repository:
* <Informative description>. ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.
If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.
You can open this PR in browser to add release notes: open in github.dev
| Change path | Release notes path | Description |
|---|---|---|
src/Compiler |
docs/release-notes/.FSharp.Compiler.Service/11.0.0.md | No release notes found or release notes format is not correct |
We should make sure ildasm version that the CI uses actually does support async flag
We should make sure
ildasmversion that the CI uses actually does supportasyncflag
Yeah, I had that changed locally to use the latest but I didn't commit it. Definitely part of my verification work.
As noted in: https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md
Async methods also do not have matching return type conventions as sync methods. For sync methods, the stack should contain a value convertible to the stated return type before the ret instruction. For async methods, the stack should be empty in the case of Task or ValueTask, or the type argument > in the case of Task<T> or ValueTask<T>.
For the following F# code we get this IL:
[<MethodImpl(MethodImplOptions.Async)>]
let doThing () =
task {
do! Task.Yield()
return 42
}
.method public static class [System.Runtime]System.Threading.Tasks.Task`1<int32>
doThing() cil managed async
{
// Code size 79 (0x4f)
.maxstack 4
.locals init (class [IcedTasks]IcedTasks.TaskBase_Net10.TaskBuilderRuntime V_0,
class [IcedTasks]IcedTasks.TaskBase_Net10.TaskBuilderRuntime V_1,
class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit,valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>> V_2,
class [IcedTasks]IcedTasks.TaskBase_Net10.TaskBuilderBaseRuntime V_3,
valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32> V_4,
valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32> V_5,
valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32> V_6,
valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32> V_7,
valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32> V_8)
IL_0000: call class [IcedTasks]IcedTasks.TaskBase_Net10.TaskBuilderRuntime [IcedTasks]IcedTasks.Polyfill.TasksRuntime.TaskBuilder::get_task()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: stloc.1
IL_0008: ldloc.0
IL_0009: stloc.3
IL_000a: ldloc.0
IL_000b: newobj instance void ILSpySamples.TaskRuntime/doThing@1::.ctor(class [IcedTasks]IcedTasks.TaskBase_Net10.TaskBuilderRuntime)
IL_0010: stloc.2
IL_0011: ldloc.2
IL_0012: ldnull
IL_0013: callvirt instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit,valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>>::Invoke(!0)
IL_0018: stloc.s V_4
IL_001a: ldloc.s V_4
IL_001c: stloc.s V_5
IL_001e: ldloc.s V_5
IL_0020: stloc.s V_6
IL_0022: ldloca.s V_6
IL_0024: call instance bool valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>::get_IsCompleted()
IL_0029: ldc.i4.0
IL_002a: ceq
IL_002c: nop
IL_002d: brfalse.s IL_0039
IL_002f: ldloc.s V_4
IL_0031: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::UnsafeAwaitAwaiter<valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>>(!!0)
IL_0036: nop
IL_0037: br.s IL_003a
IL_0039: nop
IL_003a: ldloc.s V_4
IL_003c: stloc.s V_7
IL_003e: ldloc.s V_7
IL_0040: stloc.s V_8
IL_0042: ldloca.s V_8
IL_0044: call instance !0 valuetype [System.Runtime]System.Runtime.CompilerServices.ValueTaskAwaiter`1<int32>::GetResult()
IL_0049: call class [System.Runtime]System.Threading.Tasks.Task`1<!!0> [System.Runtime]System.Threading.Tasks.Task::FromResult<int32>(!!0)
IL_004e: ret
} // end of method TaskRuntime::doThing
Let's take a quick look at the C# equivalent:
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<Features>$(Features);runtime-async=on</Features>
<NoWarn>$(NoWarn);SYSLIB5007</NoWarn>
public async Task<int> DoThingAsync()
{
await Task.Yield();
return 42;
}
.method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task`1<int32>
DoThingAsync() cil managed async
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 01 00 00 )
.custom instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 )
// Code size 46 (0x2e)
.maxstack 1
.locals init (valuetype [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable/YieldAwaiter V_0,
valuetype [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable V_1,
int32 V_2)
IL_0000: nop
IL_0001: call valuetype [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable [System.Runtime]System.Threading.Tasks.Task::Yield()
IL_0006: stloc.1
IL_0007: ldloca.s V_1
IL_0009: call instance valuetype [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable/YieldAwaiter [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable::GetAwaiter()
IL_000e: stloc.0
IL_000f: ldloca.s V_0
IL_0011: call instance bool [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable/YieldAwaiter::get_IsCompleted()
IL_0016: brtrue.s IL_001f
IL_0018: ldloc.0
IL_0019: call void [System.Runtime]System.Runtime.CompilerServices.AsyncHelpers::UnsafeAwaitAwaiter<valuetype [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable/YieldAwaiter>(!!0)
IL_001e: nop
IL_001f: ldloca.s V_0
IL_0021: call instance void [System.Runtime]System.Runtime.CompilerServices.YieldAwaitable/YieldAwaiter::GetResult()
IL_0026: nop
IL_0027: ldc.i4.s 42
IL_0029: stloc.2
IL_002a: br.s IL_002c
IL_002c: ldloc.2
IL_002d: ret
} // end of method Thingy::DoThingAsync
I had trouble overcoming this limitation, until I remembered I can cheat by using the fun old trick of:
module internal Unsafe =
let inline cast<'a, 'b> (a: 'a) : 'b =
(# "" a : 'b #)
which does seem to make the compiler and IL happy.
Ok I'm actually dumb, I forget to set DOTNET_RuntimeAsync=1. However, I now get a nonsense result (which is still the same root cause of returning a task and not the value directly)
❯ dotnet run -f net10.0
Result: 1015063456
However I can cheat by using the fun old trick of:
module internal Unsafe =
let inline cast<'a, 'b> (a: 'a) : 'b =
(# "" a : 'b #)
❯ dotnet run -f net10.0
Result: 42
@TheAngryByrd :
I would love to have this return-type subsumption supported built-in, but I think that would just lead to fully admitting the runtime async the compiler. (i.e. this conversion between returned value and the declared return type of the method is so ad-hoc, that I do not see how to relate it to existing mechanisms like implicit conversions etc. )
Just to be sure the differences are noticed, you may also want to try:
public async Task<int> DoThingAsync()
{
await Task.Delay(1);
return 42;
}
There are some differences because Delay returns a Task, while Yield returns a custom awaitable.