Skip to content

Conversation

@TheAngryByrd
Copy link
Contributor

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/Compiler has been changed, please make sure to make an entry in docs/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.fsi was changed), please add it to docs/release-notes/.Language/preview.md
    • If a change to FSharp.Core was made, please make sure to edit docs/release-notes/.FSharp.Core/<version>.md where version is "highest" one, e.g. 8.0.200.

    Information about the release notes entries format can be found in the documentation.
    Example:

    If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

@github-actions
Copy link
Contributor

❗ 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

module M
open System.Threading.Tasks
open System.Runtime.CompilerServices
[<System.Runtime.CompilerServices.MethodImplAttribute(System.Runtime.CompilerServices.MethodImplOptions.Async)>]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this will fail when targeting desktop framework, you might need to use TheoryForNETCOREAPPAttribute in the test declaration.

@majocha
Copy link
Contributor

majocha commented Nov 10, 2025

We should make sure ildasm version that the CI uses actually does support async flag

@TheAngryByrd
Copy link
Contributor Author

We should make sure ildasm version that the CI uses actually does support async flag

Yeah, I had that changed locally to use the latest but I didn't commit it. Definitely part of my verification work.

@TheAngryByrd
Copy link
Contributor Author

TheAngryByrd commented Nov 21, 2025

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 or ValueTask.

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.

@TheAngryByrd

This comment was marked as outdated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

net10.0: Support MethodImpl.Async

3 participants