From 5ee695f3632bbffd2c8d508078c160e7450302ab Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 15:24:27 +0200 Subject: [PATCH 01/14] Recognize Task<->ValueTask adaptions in async versions Support looking through `return new ValueTask(TaskReturningFunction())` and `return ValueTaskReturningFunction().AsTask()` in async versions. These can be transformed into async calls. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 15 +- src/coreclr/inc/corinfo.h | 13 +- src/coreclr/jit/async.cpp | 5 + src/coreclr/jit/compiler.h | 4 + src/coreclr/jit/gentree.h | 5 + src/coreclr/jit/importer.cpp | 286 ++++++++++++++---- src/coreclr/jit/importercalls.cpp | 25 +- src/coreclr/jit/namedintrinsiclist.h | 3 + .../src/System/Threading/Tasks/ValueTask.cs | 4 + 9 files changed, 281 insertions(+), 79 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index 3501630d114f6e..4774fd507629bb 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -22,28 +22,31 @@ internal enum ContinuationFlags ContinueOnThreadPool = 1 << 0, ContinueOnCapturedSynchronizationContext = 1 << 1, ContinueOnCapturedTaskScheduler = 1 << 2, + // This is an await of valueTask.AsTask() + // (e.g. valueTask.AsTask() returned from an async version) + ValueTaskAdaptedToTask = 1 << 3, - AllContinuationFlags = ContinueOnThreadPool | ContinueOnCapturedSynchronizationContext | ContinueOnCapturedTaskScheduler, + AllContinuationFlags = ContinueOnThreadPool | ContinueOnCapturedSynchronizationContext | ContinueOnCapturedTaskScheduler | ValueTaskAdaptedToTask, // The flags encode where in the continuation various members are stored. // If the encoded index is 0, it means no such member is present. // Otherwise the exact offset of the member is computed as // DataOffset + (index - 1) * PointerSize // - ExecutionContextIndexFirstBit = 3, + ExecutionContextIndexFirstBit = 4, ExecutionContextIndexNumBits = 2, - ContinuationContextIndexFirstBit = 5, + ContinuationContextIndexFirstBit = 6, ContinuationContextIndexNumBits = 2, - ExceptionIndexFirstBit = 7, + ExceptionIndexFirstBit = 8, ExceptionIndexNumBits = 3, // For JIT, the continuation stores space for every possible type of // async callee's result. We need to represent the offset to each of // these, so we allocate the rest of the bits for this. - ResultIndexFirstBit = 10, - ResultIndexNumBits = 22, + ResultIndexFirstBit = 11, + ResultIndexNumBits = 21, } // Keep in sync with CORINFO_AsyncResumeInfo in corinfo.h diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 4dfa9d39bc10c7..cfd6aeefe02398 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1786,26 +1786,29 @@ enum CorInfoContinuationFlags // If this bit is set the continuation context is a TaskScheduler that // we should continue on. CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER = 1 << 2, + // If this bit is set this is an await of valueTask.AsTask() + // (common pattern when returning a value-task in an async version) + CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK = 1 << 3, // The flags encode where in the continuation various members are stored. // If the encoded index is 0, it means no such member is present. // Otherwise the exact offset of the member is computed as // OFFSETOF__CORINFO_Continuation__data + (index - 1) * PointerSize - CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT = 3, + CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT = 4, CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_NUM_BITS = 2, - CORINFO_CONTINUATION_CONTEXT_INDEX_FIRST_BIT = 5, + CORINFO_CONTINUATION_CONTEXT_INDEX_FIRST_BIT = 6, CORINFO_CONTINUATION_CONTEXT_INDEX_NUM_BITS = 2, - CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT = 7, + CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT = 8, CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS = 3, // For JIT, the continuation stores space for every possible type of // async callee's result. We need to represent the offset to each of // these, so we allocate the rest of the bits for this. - CORINFO_CONTINUATION_RESULT_INDEX_FIRST_BIT = 10, - CORINFO_CONTINUATION_RESULT_INDEX_NUM_BITS = 22, + CORINFO_CONTINUATION_RESULT_INDEX_FIRST_BIT = 11, + CORINFO_CONTINUATION_RESULT_INDEX_NUM_BITS = 21, }; struct CORINFO_ASYNC_INFO diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 21d19968671854..499cef36e68103 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -2217,6 +2217,11 @@ void AsyncTransformation::CreateSuspension(BasicBlock* call continuationFlags |= CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL; } + if (callInfo.IsValueTaskAsTask) + { + continuationFlags |= CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK; + } + newContinuation = m_compiler->gtNewLclvNode(newContinuationVar, TYP_REF); unsigned flagsOffset = m_compiler->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd); GenTree* flagsNode = m_compiler->gtNewIconNode((ssize_t)continuationFlags, TYP_INT); diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index f11aa03e81c123..3d7c35fd64d37f 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -5001,6 +5001,7 @@ class Compiler PREFIX_IS_TASK_AWAIT = 0x00000080, PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT = 0x00000100, PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT = 0x00000200, + PREFIX_IS_ADAPTED_FROM_VALUETASK = 0x00000400, }; static void impValidateMemoryAccessOpcode(const BYTE* codeAddr, const BYTE* codeEndp, bool volatilePrefix); @@ -5480,6 +5481,9 @@ class Compiler bool impMatchIsInstBooleanConversion(const BYTE* codeAddr, const BYTE* codeEndp, int* consumed); const BYTE* impMatchTaskAwaitPattern(const BYTE* codeAddr, const BYTE* codeEndp, int* configVal, IL_OFFSET* awaitOffset); + bool impMatchAsyncVersionTailCall(const BYTE* codeAddr, const BYTE* codeEndp, int* prefixFlags, int* numBytesMatched); + bool impMatchStlocLdloca(const BYTE** codeAddr, const BYTE* codeEndp, unsigned* lclNum); + bool impCheckOptimizeAwait(IL_OFFSET awaitOffset); GenTree* impCastClassOrIsInstToTree( diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index b66874618c106b..6b002b654b5b86 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -4535,6 +4535,11 @@ struct AsyncCallInfo // records that behavior. ::ContinuationContextHandling ContinuationContextHandling = ContinuationContextHandling::None; + // Is this 'await valueTask.AsTask()'? These come with special semantics as + // they no longer transparently forward continuation context handling to an + // underlying IValueTaskSource, if present. + bool IsValueTaskAsTask = false; + // Tail awaits do not generate suspension points and the JIT instead // directly returns the callee's continuation to the caller. bool IsTailAwait = false; diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 1e1b25904f41c7..b796eb00004770 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5861,70 +5861,12 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, { // ConfigureAwait on a ValueTask will start with stloc/ldloca. // The longest encoding should fit in the length we asked for above. - uint8_t maybeStLoc = getU1LittleEndian(nextOpcode); - const BYTE* nextTmp = nextOpcode + 1; - int stlocNum = -1; - switch (maybeStLoc) + unsigned stlocNum = BAD_VAR_NUM; + if (impMatchStlocLdloca(&nextOpcode, codeEndp, &stlocNum)) { - case CEE_STLOC_0: - stlocNum = 0; - break; - case CEE_STLOC_1: - stlocNum = 1; - break; - case CEE_STLOC_2: - stlocNum = 2; - break; - case CEE_STLOC_3: - stlocNum = 3; - break; - case CEE_STLOC_S: - stlocNum = getU1LittleEndian(nextTmp); - nextTmp += 1; - break; - case CEE_PREFIX1: - uint16_t maybeStLocWide = (uint16_t)256 + getU1LittleEndian(nextTmp); - nextTmp += 1; - if (maybeStLocWide == CEE_STLOC) - { - stlocNum = getU2LittleEndian(nextTmp); - nextTmp += 2; - } - break; - } - - // if it was a stloc, check for matching ldloca - if (stlocNum != -1) - { - uint8_t maybeLdLoca = getU1LittleEndian(nextTmp); - nextTmp += 1; - int ldlocaNum = -1; - switch (maybeLdLoca) - { - case CEE_LDLOCA_S: - ldlocaNum = getU1LittleEndian(nextTmp); - nextTmp += 1; - break; - case CEE_PREFIX1: - uint16_t maybeLdLocaWide = (uint16_t)256 + getU1LittleEndian(nextTmp); - nextTmp += 1; - if (maybeLdLocaWide == CEE_LDLOCA) - { - ldlocaNum = getU2LittleEndian(nextTmp); - nextTmp += 2; - } - break; - } - - // no ldloca or locals did not match, this can't be await pattern - if (stlocNum != ldlocaNum) - return nullptr; - // locals match, but no space for ConfigureAwait call, this can't be await pattern - if (nextTmp + 2 * (1 + sizeof(mdToken)) >= codeEndp) + if (nextOpcode + 2 * (1 + sizeof(mdToken)) >= codeEndp) return nullptr; - - nextOpcode = nextTmp; } uint8_t nextOp = getU1LittleEndian(nextOpcode); @@ -5985,6 +5927,216 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, return nullptr; } +bool Compiler::impMatchStlocLdloca(const BYTE** codeAddr, const BYTE* codeEndp, unsigned* lclNum) +{ + *lclNum = BAD_VAR_NUM; + const BYTE* code = *codeAddr; + if (code >= codeEndp) + { + return false; + } + + unsigned matchedLclNum = BAD_VAR_NUM; + BYTE opcode = *code; + code++; + if ((opcode >= CEE_STLOC_0) && (opcode <= CEE_STLOC_3)) + { + matchedLclNum = opcode - CEE_STLOC_0; + } + else if (opcode == CEE_STLOC_S) + { + if (code >= codeEndp) + { + return false; + } + + matchedLclNum = *code; + code++; + } + else if (opcode == CEE_PREFIX1) + { + if (code >= codeEndp) + { + return false; + } + + uint16_t maybeStLocWide = (uint16_t)256 + *code; + code++; + if ((maybeStLocWide != CEE_STLOC) || (code + 1 >= codeEndp)) + { + return false; + } + + matchedLclNum = getU2LittleEndian(code); + code += 2; + } + else + { + return false; + } + + if (code >= codeEndp) + { + return false; + } + + opcode = *code; + code++; + if (opcode == CEE_LDLOCA_S) + { + if (code >= codeEndp) + { + return false; + } + + if (*code != matchedLclNum) + { + return false; + } + + code++; + } + else if (opcode == CEE_PREFIX1) + { + if (code >= codeEndp) + { + return false; + } + + uint16_t maybeLdLocaWide = (uint16_t)256 + *code; + code++; + if ((maybeLdLocaWide != CEE_LDLOCA) || (code + 1 >= codeEndp)) + { + return false; + } + + if (getU2LittleEndian(code) != matchedLclNum) + { + return false; + } + code += 2; + } + else + { + return false; + } + + *lclNum = matchedLclNum; + *codeAddr = code; + return true; +} + +bool Compiler::impMatchAsyncVersionTailCall(const BYTE* codeAddr, + const BYTE* codeEndp, + int* prefixFlags, + int* numBytesMatched) +{ + const BYTE* nextOpcode = codeAddr; + + // Look for call; ret + if ((nextOpcode < codeEndp) && (*nextOpcode == CEE_RET)) + { + *numBytesMatched = 1; + return true; + } + + // Look for call; stloc X; ldloca X; call AsTask(); ret + unsigned vtLclNum; + if (impMatchStlocLdloca(&nextOpcode, codeEndp, &vtLclNum)) + { + if ((nextOpcode >= codeEndp) || (*nextOpcode != CEE_CALL)) + { + return false; + } + + nextOpcode++; + + // Quick check for ret before we resolve the token + if (nextOpcode + sizeof(mdToken) >= codeEndp || (*(nextOpcode + sizeof(mdToken)) != CEE_RET)) + { + return false; + } + + CORINFO_RESOLVED_TOKEN callTok; + impResolveToken(nextOpcode, &callTok, CORINFO_TOKENKIND_Method); + + if (!eeIsIntrinsic(callTok.hMethod)) + { + return false; + } + + NamedIntrinsic ni = lookupNamedIntrinsic(callTok.hMethod); + if ((ni != NI_System_Threading_Tasks_ValueTask_AsTask) && (ni != NI_System_Threading_Tasks_ValueTask_1_AsTask)) + { + return false; + } + + nextOpcode += sizeof(mdToken); + nextOpcode++; // matched CEE_RET already + + JITDUMP("Matched \"return ValueTaskReturn().AsTask()\"\n"); + *prefixFlags |= PREFIX_IS_ADAPTED_FROM_VALUETASK; + *numBytesMatched = (int)(nextOpcode - codeAddr); + return true; + } + + // Look for call; newobj ValueTask; ret + if ((nextOpcode < codeEndp) && (*nextOpcode == CEE_NEWOBJ)) + { + nextOpcode++; + + // Quick check for ret before we resolve the token + if (nextOpcode + sizeof(mdToken) >= codeEndp || (*(nextOpcode + sizeof(mdToken)) != CEE_RET)) + { + return false; + } + + CORINFO_RESOLVED_TOKEN ctorTok; + impResolveToken(nextOpcode, &ctorTok, CORINFO_TOKENKIND_NewObj); + + if (!eeIsIntrinsic(ctorTok.hMethod)) + { + return false; + } + + NamedIntrinsic ni = lookupNamedIntrinsic(ctorTok.hMethod); + if ((ni != NI_System_Threading_Tasks_ValueTask__ctor) && (ni != NI_System_Threading_Tasks_ValueTask_1__ctor)) + { + return false; + } + + CORINFO_SIG_INFO sig; + info.compCompHnd->getMethodSig(ctorTok.hMethod, &sig); + + if (sig.numArgs != 1) + { + return false; + } + + if (info.compRetType != TYP_VOID) + { + assert((sig.sigInst.classInstCount == 1) && (sig.sigInst.methInstCount == 0)); + CORINFO_CLASS_HANDLE paramClass = info.compCompHnd->getArgClass(&sig, sig.args); + if (paramClass == sig.sigInst.classInst[0]) + { + // This is "class ValueTask { ValueTask(T value) }" overload + // which is not what we are looking for. That one gets folded + // by impFoldAwaitedTopOfStack. + return false; + } + } + + nextOpcode += sizeof(mdToken); + nextOpcode++; // matched CEE_RET already + + JITDUMP("Matched \"return new ValueTask(TaskReturn())\"\n"); + *numBytesMatched = (int)(nextOpcode - codeAddr); + return true; + } + + return false; +} + /***************************************************************************** * Import the instr for the given basic block */ @@ -9052,17 +9204,17 @@ void Compiler::impImportBlockCode(BasicBlock* block) if (compIsAsyncVersion()) { - if ((codeAddr + sz < codeEndp) && (getU1LittleEndian(codeAddr + sz) == CEE_RET) && - ((info.compFlags & CORINFO_FLG_SYNCH) == 0)) + int numBytesMatched; + if (((info.compFlags & CORINFO_FLG_SYNCH) == 0) && + impMatchAsyncVersionTailCall(codeAddr + sz, codeEndp, &prefixFlags, &numBytesMatched)) { JITDUMP("\nRecognized tail-call in async version\n"); - awaitOffset = (IL_OFFSET)(codeAddr - 1 - info.compCode); isAwait = true; + awaitOffset = (IL_OFFSET)(codeAddr - 1 - info.compCode); prefixFlags |= PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT; - - // Consume the ret opcode. Note `codeAddr` points at the unconsumed token; + // Consume number of bytes matched. Note `codeAddr` points at the unconsumed token; // the main loop will still do `codeAddr += sz` (token size) after this case. - codeAddrAfterMatch = codeAddr + 1; + codeAddrAfterMatch = codeAddr + numBytesMatched; } } else diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index a0340343c58c44..86055c8fe9cc8f 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7268,6 +7268,15 @@ void Compiler::impSetupAsyncCall( return; } + // We cannot inline if the callee returns valueTask.AsTask() in an + // async version. We need to preserve the continuation in this case to + // be able to mark it with CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK. + if ((prefixFlags & PREFIX_IS_ADAPTED_FROM_VALUETASK) != 0) + { + compInlineResult->NoteFatal(InlineObservation::CALLEE_AWAIT); + return; + } + // For async versions of synchronous methods all async calls are in // tail position. Inlining is simple for these cases: we can just // inherit all context handling from the inlining call. @@ -7288,8 +7297,10 @@ void Compiler::impSetupAsyncCall( } else { + asyncInfo.IsValueTaskAsTask = (prefixFlags & PREFIX_IS_ADAPTED_FROM_VALUETASK) != 0; + if (opts.OptimizationEnabled() && ((prefixFlags & PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT) != 0) && - (call->gtReturnType == info.compRetType)) + (call->gtReturnType == info.compRetType) && !asyncInfo.IsValueTaskAsTask) { CORINFO_METHOD_HANDLE exactCalleeHnd = ((call->AsCall()->gtCallType != CT_USER_FUNC) || call->AsCall()->IsVirtual()) ? nullptr : methHnd; @@ -11783,6 +11794,14 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method) { result = NI_System_Threading_Tasks_ValueTask_get_CompletedTask; } + else if (strcmp(methodName, ".ctor") == 0) + { + result = NI_System_Threading_Tasks_ValueTask__ctor; + } + else if (strcmp(methodName, "AsTask") == 0) + { + result = NI_System_Threading_Tasks_ValueTask_AsTask; + } } else if (strcmp(className, "ValueTask`1") == 0) { @@ -11790,6 +11809,10 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method) { result = NI_System_Threading_Tasks_ValueTask_1__ctor; } + else if (strcmp(methodName, "AsTask") == 0) + { + result = NI_System_Threading_Tasks_ValueTask_1_AsTask; + } } } } diff --git a/src/coreclr/jit/namedintrinsiclist.h b/src/coreclr/jit/namedintrinsiclist.h index 5f96a30903d1d2..b19bf10f89a194 100644 --- a/src/coreclr/jit/namedintrinsiclist.h +++ b/src/coreclr/jit/namedintrinsiclist.h @@ -172,8 +172,11 @@ enum NamedIntrinsic : unsigned short NI_System_Threading_Tasks_ValueTask_FromResult, NI_System_Threading_Tasks_ValueTask_get_CompletedTask, + NI_System_Threading_Tasks_ValueTask__ctor, + NI_System_Threading_Tasks_ValueTask_AsTask, NI_System_Threading_Tasks_ValueTask_1__ctor, + NI_System_Threading_Tasks_ValueTask_1_AsTask, // These two are special marker IDs so that we still get the inlining profitability boost NI_System_Numerics_Intrinsic, diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs index dbb08c49559c07..3fea4a5dd7c345 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs @@ -71,6 +71,7 @@ namespace System.Threading.Tasks /// Initialize the with a that represents the operation. /// The task. [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Intrinsic] public ValueTask(Task task) { if (task == null) @@ -174,6 +175,7 @@ obj is ValueTask && /// It will either return the wrapped task object if one exists, or it'll /// manufacture a new task object to represent the result. /// + [Intrinsic] public Task AsTask() { object? obj = _obj; @@ -497,6 +499,7 @@ public ValueTask(TResult result) /// Initialize the with a that represents the operation. /// The task. [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Intrinsic] public ValueTask(Task task) { if (task == null) @@ -576,6 +579,7 @@ public bool Equals(ValueTask other) => /// It will either return the wrapped task object if one exists, or it'll /// manufacture a new task object to represent the result. /// + [Intrinsic] public Task AsTask() { object? obj = _obj; From f688392c5e8027f6efda2ad63933bdacae835cd2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 16:01:12 +0200 Subject: [PATCH 02/14] Add tests --- .../async-versions-task-adapters.cs | 161 ++++++++++++++++++ .../async-versions-task-adapters.csproj | 5 + .../async-versions.il} | 2 +- .../async-versions.ilproj} | 0 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs create mode 100644 src/tests/async/async-versions-task-adapters/async-versions-task-adapters.csproj rename src/tests/async/{asyncversions/asyncversions.il => async-versions/async-versions.il} (99%) rename src/tests/async/{asyncversions/asyncversions.ilproj => async-versions/async-versions.ilproj} (100%) diff --git a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs new file mode 100644 index 00000000000000..6d87dac0f07c16 --- /dev/null +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; +using Xunit; + +public class Async2TaskAdapters +{ + // Wrapping a runtime-async Task in a ValueTask (ValueTask(Task) constructor) and + // awaiting the resulting ValueTask should suspend and resume correctly. + [Fact] + public static void TestTaskToValueTask() + { + SynchronizationContext prevContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(new MySyncContext()); + WrapTaskInValueTask().GetAwaiter().GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(prevContext); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask WrapTaskInValueTask() + { + return new ValueTask(YieldingTask()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task YieldingTask() + { + await Task.Yield(); + } + + // Converting a ValueTask backed by an IValueTaskSource into a Task via AsTask() + // and awaiting it should complete correctly. + [Fact] + public static void TestValueTaskSourceToTask() + { + SynchronizationContext prevContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(new MySyncContext()); + ConvertSourceToTask().GetAwaiter().GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(prevContext); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Task ConvertSourceToTask() + { + return SourceValueTask().AsTask(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask SourceValueTask() + { + return new ValueTask(new Source(), 1); + } + + // Verifies that the two ValueTask> constructor overloads are handled correctly: + // - return new ValueTask>(TaskOfIntReturningFunction()) passes a Task, which binds + // to the ValueTask(TResult result) constructor, wrapping the Task as a value. + // - return new ValueTask>(TaskOfTaskOfIntReturningFunction()) passes a Task>, + // which binds to the ValueTask(Task task) constructor, an actual async call. + [Fact] + public static void TestValueTaskOfTaskValueVersusAsync() + { + WrapValueVersusAsync().GetAwaiter().GetResult(); + } + + private static async Task WrapValueVersusAsync() + { + // Value case: a suspended Task is passed to the ValueTask>(TResult result) + // constructor, so the ValueTask holds it as an already-available value. The ValueTask is + // completed even while the inner Task is still suspended. + TaskCompletionSource valueGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + ValueTask> valueCase = WrapTaskValue(valueGate); + Assert.True(valueCase.IsCompletedSuccessfully); + Task innerFromValue = await valueCase; + Assert.False(innerFromValue.IsCompleted); + valueGate.SetResult(); + Assert.Equal(42, await innerFromValue); + + // Async case: a suspended Task> is passed to the ValueTask>(Task task) + // constructor, so the ValueTask is backed by that task and is not completed until the outer + // task completes. + TaskCompletionSource asyncGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + ValueTask> asyncCase = WrapTaskTask(asyncGate); + Assert.False(asyncCase.IsCompleted); + asyncGate.SetResult(); + Task innerFromAsync = await asyncCase; + Assert.Equal(123, await innerFromAsync); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask> WrapTaskValue(TaskCompletionSource gate) + { + return new ValueTask>(TaskOfIntReturningFunction(gate)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ValueTask> WrapTaskTask(TaskCompletionSource gate) + { + return new ValueTask>(TaskOfTaskOfIntReturningFunction(gate)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskOfIntReturningFunction(TaskCompletionSource gate) + { + await gate.Task; + return 42; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task> TaskOfTaskOfIntReturningFunction(TaskCompletionSource gate) + { + await gate.Task; + return Task.FromResult(123); + } + + private class Source : IValueTaskSource + { + public void GetResult(short token) + { + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return ValueTaskSourceStatus.Pending; + } + + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + { + Assert.Equal(ValueTaskSourceOnCompletedFlags.None, flags); + continuation(state); + } + } + + private class MySyncContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object state) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + SetSynchronizationContext(this); + d(state); + }, null); + } + } +} diff --git a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.csproj b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.csproj new file mode 100644 index 00000000000000..197767e2c4e249 --- /dev/null +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tests/async/asyncversions/asyncversions.il b/src/tests/async/async-versions/async-versions.il similarity index 99% rename from src/tests/async/asyncversions/asyncversions.il rename to src/tests/async/async-versions/async-versions.il index 602bab480b851f..62ac6aa095cb73 100644 --- a/src/tests/async/asyncversions/asyncversions.il +++ b/src/tests/async/async-versions/async-versions.il @@ -10,7 +10,7 @@ .assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) .ver 4:0:0:0 } .assembly extern xunit.core {} -.assembly asyncversions +.assembly 'async-versions' { } diff --git a/src/tests/async/asyncversions/asyncversions.ilproj b/src/tests/async/async-versions/async-versions.ilproj similarity index 100% rename from src/tests/async/asyncversions/asyncversions.ilproj rename to src/tests/async/async-versions/async-versions.ilproj From 3de295a0d6fbbfaaff94970ca9c3416f50a0ddc6 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 16:20:10 +0200 Subject: [PATCH 03/14] feedback --- src/coreclr/jit/importer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index b796eb00004770..56b0423376d310 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5874,7 +5874,7 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, if ((nextOp != CEE_LDC_I4_0 && nextOp != CEE_LDC_I4_1) || (nextNextOp != CEE_CALL && nextNextOp != CEE_CALLVIRT)) { - if (stlocNum != -1) + if (stlocNum != BAD_VAR_NUM) { // we had stloc/ldloca, we must see ConfigAwait return nullptr; @@ -5890,7 +5890,7 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, if (!eeIsIntrinsic(nextCallTok.hMethod) || lookupNamedIntrinsic(nextCallTok.hMethod) != NI_System_Threading_Tasks_Task_ConfigureAwait) { - if (stlocNum != -1) + if (stlocNum != BAD_VAR_NUM) { // we had stloc/ldloca, we must see ConfigAwait return nullptr; From 69b46f5afb276c21bd7cd2a3a781b6e3bd3ac103 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 16:24:35 +0200 Subject: [PATCH 04/14] Make usage more clear --- .../Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index 4774fd507629bb..d4fc375b747482 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -22,11 +22,13 @@ internal enum ContinuationFlags ContinueOnThreadPool = 1 << 0, ContinueOnCapturedSynchronizationContext = 1 << 1, ContinueOnCapturedTaskScheduler = 1 << 2, - // This is an await of valueTask.AsTask() - // (e.g. valueTask.AsTask() returned from an async version) + // This is an await of valueTask.AsTask() (e.g. valueTask.AsTask() + // returned from an async version). This flag affects how + // ValueTaskSourceContinuation handling computes the flags to pass to + // IValueTaskSource.OnCompleted. ValueTaskAdaptedToTask = 1 << 3, - AllContinuationFlags = ContinueOnThreadPool | ContinueOnCapturedSynchronizationContext | ContinueOnCapturedTaskScheduler | ValueTaskAdaptedToTask, + AllContinuationFlags = ContinueOnThreadPool | ContinueOnCapturedSynchronizationContext | ContinueOnCapturedTaskScheduler, // The flags encode where in the continuation various members are stored. // If the encoded index is 0, it means no such member is present. @@ -828,7 +830,8 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) // the direct AsyncHelpers.Await(ValueTask/ValueTask) path. // In either case, that can only happen in nontransparent/user code. Continuation contWithContinueFlags = valueTaskSourceCont; - while ((contWithContinueFlags.Flags & ContinuationFlags.AllContinuationFlags) == 0 && contWithContinueFlags.Next != null) + while ((contWithContinueFlags.Flags & (ContinuationFlags.AllContinuationFlags | ContinuationFlags.ValueTaskAdaptedToTask)) == 0 && + contWithContinueFlags.Next != null) { contWithContinueFlags = contWithContinueFlags.Next; } From cacdec0b4b27842b849d4b24d31060adec28bd48 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 16:25:46 +0200 Subject: [PATCH 05/14] More feedback --- .../async-versions-task-adapters.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs index 6d87dac0f07c16..f79fd2c01fe910 100644 --- a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs @@ -153,8 +153,16 @@ public override void Post(SendOrPostCallback d, object state) { ThreadPool.UnsafeQueueUserWorkItem(_ => { - SetSynchronizationContext(this); - d(state); + SynchronizationContext prevContext = Current; + try + { + SetSynchronizationContext(this); + d(state); + } + finally + { + SetSynchronizationContext(prevContext); + } }, null); } } From ebc9b259b2a59956f8a83dfa98b731d3d1b0701a Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 1 Jul 2026 20:18:14 +0200 Subject: [PATCH 06/14] ILScanner part --- .../IL/ILImporter.Scanner.cs | 183 ++++++++++++++---- 1 file changed, 150 insertions(+), 33 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index ab198087de1b90..581ebefd3688f0 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -355,34 +355,10 @@ private bool MatchTaskAwaitPattern() // so check for that. if (reader.Size > 2 * (1 + sizeof(int))) { - opcode = reader.ReadILOpcode(); - - // ConfigureAwait on a ValueTask will start with stloc/ldloca. - int stlocNum = opcode switch - { - >= ILOpcode.stloc_0 and <= ILOpcode.stloc_3 => opcode - ILOpcode.stloc_0, - ILOpcode.stloc => reader.ReadILUInt16(), - ILOpcode.stloc_s => reader.ReadILByte(), - _ => -1, - }; - - // if it was a stloc, check for matching ldloca - if (stlocNum != -1) - { - opcode = reader.ReadILOpcode(); - int ldlocaNum = opcode switch - { - ILOpcode.ldloca_s => reader.ReadILByte(), - ILOpcode.ldloca => reader.ReadILUInt16(), - _ => -1, - }; - - if (stlocNum != ldlocaNum) - return false; - - opcode = reader.ReadILOpcode(); - } + if (!MatchStlocLdloca(ref reader, out int stlocNum)) + stlocNum = -1; + opcode = reader.ReadILOpcode(); if (opcode is (not ILOpcode.ldc_i4_0) and (not ILOpcode.ldc_i4_1)) { if (stlocNum != -1) @@ -411,6 +387,42 @@ private bool MatchTaskAwaitPattern() && IsAsyncHelpersAwait((MethodDesc)_methodIL.GetObject(reader.ReadILToken())); } + private static bool MatchStlocLdloca(ref ILReader reader, out int stlocNum) + { + stlocNum = -1; + ILReader tempReader = reader; + + ILOpcode opcode = tempReader.ReadILOpcode(); + + // ConfigureAwait on a ValueTask will start with stloc/ldloca. + stlocNum = opcode switch + { + >= ILOpcode.stloc_0 and <= ILOpcode.stloc_3 => opcode - ILOpcode.stloc_0, + ILOpcode.stloc => tempReader.ReadILUInt16(), + ILOpcode.stloc_s => tempReader.ReadILByte(), + _ => -1, + }; + + if (stlocNum == -1) + { + return false; + } + + opcode = tempReader.ReadILOpcode(); + int ldlocaNum = opcode switch + { + ILOpcode.ldloca_s => tempReader.ReadILByte(), + ILOpcode.ldloca => tempReader.ReadILUInt16(), + _ => -1, + }; + + if (stlocNum != ldlocaNum) + return false; + + reader = tempReader; + return true; + } + private ILReader GetRemainingBlockIL() { int nextBBOffset = _currentOffset; @@ -421,21 +433,80 @@ private ILReader GetRemainingBlockIL() return new ILReader(new ReadOnlySpan(_ilBytes, _currentOffset, nextBBOffset - _currentOffset)); } - private bool MatchTailCallAwait(MethodDesc method) + private bool MatchTailCallAwait() { // Create ILReader for what's left in the basic block - var reader = GetRemainingBlockIL(); + ILReader remainingReader = GetRemainingBlockIL(); - if (!reader.HasNext) + if (!remainingReader.HasNext) return false; - ILOpcode opcode = reader.ReadILOpcode(); - if (opcode == ILOpcode.ret && method.Signature.ReturnsTaskOrValueTask()) + ILReader reader = remainingReader; + // Look for call; ret + if (reader.ReadILOpcode() == ILOpcode.ret) { _prevMatchedAwaitTailCallRetPostOffset = _currentOffset + reader.Offset; return true; } + reader = remainingReader; + // Look for call; stloc X; ldloca X; call AsTask(); ret + if (MatchStlocLdloca(ref reader, out _)) + { + if (reader.ReadILOpcode() != ILOpcode.call) + { + return false; + } + + int callToken = reader.ReadILToken(); + + // Verify ret first before we go resolving metadata + if (reader.ReadILOpcode() != ILOpcode.ret) + { + return false; + } + + if (!IsValueTaskAsTask((MethodDesc)_methodIL.GetObject(callToken))) + { + return false; + } + + _prevMatchedAwaitTailCallRetPostOffset = _currentOffset + reader.Offset; + return true; + } + + // Look for call; newobj ValueTask; ret + reader = remainingReader; + if (reader.ReadILOpcode() == ILOpcode.newobj) + { + int ctorToken = reader.ReadILToken(); + + if (reader.ReadILOpcode() != ILOpcode.ret) + { + return false; + } + + MethodDesc ctorMethod = (MethodDesc)_methodIL.GetObject(ctorToken); + if (!IsValueTaskCtor(ctorMethod)) + { + return false; + } + + MethodSignature sig = ctorMethod.GetTypicalMethodDefinition().Signature; + if (sig.Length != 1) + { + return false; + } + + if (sig[0] is not MetadataType mt || !IsTaskType(mt)) + { + return false; + } + + _prevMatchedAwaitTailCallRetPostOffset = _currentOffset + reader.Offset; + return true; + } + return false; } @@ -473,7 +544,7 @@ private void ImportCall(ILOpcode opcode, int token) // Don't get async variant of Delegate.Invoke method; the pointed to method is not an async variant either. allowAsyncVariant = allowAsyncVariant && !method.OwningType.IsDelegate; - if (allowAsyncVariant && (_canonMethod.SupportsAsyncVersionCodegen() ? MatchTailCallAwait(method) : MatchTaskAwaitPattern())) + if (allowAsyncVariant && (_canonMethod.SupportsAsyncVersionCodegen() ? MatchTailCallAwait() : MatchTaskAwaitPattern())) { MethodDesc asyncVariantMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(method); MethodDesc asyncVariantRuntimeDeterminedMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(runtimeDeterminedMethod); @@ -1779,6 +1850,52 @@ private static bool IsTaskConfigureAwait(MethodDesc method) return false; } + private static bool IsValueTaskAsTask(MethodDesc method) + { + if (method.IsIntrinsic && method.Name == "AsTask"u8) + { + MetadataType owningType = method.OwningType as MetadataType; + if (owningType != null) + { + Utf8Span typeName = owningType.Name; + return owningType.Module == method.Context.SystemModule + && owningType.Namespace == "System.Threading.Tasks"u8 + && (typeName == "ValueTask"u8 || typeName == "ValueTask`1"u8); + } + } + + return false; + } + + private static bool IsValueTaskCtor(MethodDesc method) + { + if (method.IsIntrinsic && method.Name == ".ctor"u8) + { + MetadataType owningType = method.OwningType as MetadataType; + if (owningType != null) + { + Utf8Span typeName = owningType.Name; + return owningType.Module == method.Context.SystemModule + && owningType.Namespace == "System.Threading.Tasks"u8 + && (typeName == "ValueTask"u8 || typeName == "ValueTask`1"u8); + } + } + + return false; + } + + private static bool IsTaskType(MetadataType type) + { + if (type.Module != type.Context.SystemModule + || type.Namespace != "System.Threading.Tasks"u8) + { + return false; + } + + Utf8Span name = type.Name; + return name == "Task"u8 || name == "Task`1"u8; + } + private DefType GetWellKnownType(WellKnownType wellKnownType) { return _compilation.TypeSystemContext.GetWellKnownType(wellKnownType); From 7fb6ba7af949fc207f35e7d1fe2145f0dd06fe32 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 7 Jul 2026 15:02:25 +0200 Subject: [PATCH 07/14] Add function headers --- src/coreclr/jit/importer.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index e0c0af6f7efadb..ade404bbfe8808 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5928,6 +5928,18 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, return nullptr; } +//------------------------------------------------------------------------ +// impMatchStlocLdloca: +// Match stloc followed by ldloca, and returne the local number if matched. +// +// Arguments: +// codeAddr - IL pointer to first opcode +// codeEndp - End of IL code stream +// lclNum - [out] local variable number if matched +// +// Returns: +// True if the IL is a stloc followed by a ldloca; otherwise false. +// bool Compiler::impMatchStlocLdloca(const BYTE** codeAddr, const BYTE* codeEndp, unsigned* lclNum) { *lclNum = BAD_VAR_NUM; @@ -6027,6 +6039,19 @@ bool Compiler::impMatchStlocLdloca(const BYTE** codeAddr, const BYTE* codeEndp, return true; } +//------------------------------------------------------------------------ +// impMatchAsyncVersionTailCall: +// See if a call can be matched to be a tailcall in an async version. +// +// Arguments: +// codeAddr - IL pointer to first opcode +// codeEndp - End of IL code stream +// prefixFlags - [out] flags indicating the presence of specific prefixes +// numBytesMatched - [out] number of bytes matched in the pattern +// +// Returns: +// True if the IL is a tailcall in an async version; otherwise false. +// bool Compiler::impMatchAsyncVersionTailCall(const BYTE* codeAddr, const BYTE* codeEndp, int* prefixFlags, From 9cfdf7a7ffa9e8687130433f8165ba0331929760 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 7 Jul 2026 15:21:20 +0200 Subject: [PATCH 08/14] Nit --- src/coreclr/jit/importer.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index ade404bbfe8808..a16f2922b20ec6 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5930,12 +5930,12 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, //------------------------------------------------------------------------ // impMatchStlocLdloca: -// Match stloc followed by ldloca, and returne the local number if matched. +// Match stloc followed by ldloca, and return the local number if matched. // // Arguments: -// codeAddr - IL pointer to first opcode -// codeEndp - End of IL code stream -// lclNum - [out] local variable number if matched +// codeAddr - IL pointer to first opcode +// codeEndp - End of IL code stream +// lclNum - [out] local variable number if matched // // Returns: // True if the IL is a stloc followed by a ldloca; otherwise false. @@ -6141,6 +6141,8 @@ bool Compiler::impMatchAsyncVersionTailCall(const BYTE* codeAddr, if (info.compRetType != TYP_VOID) { + // Since we validated above that this instance is being returned + // this can only be ValueTask at this point. assert((sig.sigInst.classInstCount == 1) && (sig.sigInst.methInstCount == 0)); CORINFO_CLASS_HANDLE paramClass = info.compCompHnd->getArgClass(&sig, sig.args); if (paramClass == sig.sigInst.classInst[0]) From 3c9159fd5dda915c26911d8b4b694c6905ba5423 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 8 Jul 2026 16:25:43 +0200 Subject: [PATCH 09/14] Reorder --- src/coreclr/jit/importer.cpp | 80 ++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index a16f2922b20ec6..94e0f123ea5931 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -6066,46 +6066,6 @@ bool Compiler::impMatchAsyncVersionTailCall(const BYTE* codeAddr, return true; } - // Look for call; stloc X; ldloca X; call AsTask(); ret - unsigned vtLclNum; - if (impMatchStlocLdloca(&nextOpcode, codeEndp, &vtLclNum)) - { - if ((nextOpcode >= codeEndp) || (*nextOpcode != CEE_CALL)) - { - return false; - } - - nextOpcode++; - - // Quick check for ret before we resolve the token - if (nextOpcode + sizeof(mdToken) >= codeEndp || (*(nextOpcode + sizeof(mdToken)) != CEE_RET)) - { - return false; - } - - CORINFO_RESOLVED_TOKEN callTok; - impResolveToken(nextOpcode, &callTok, CORINFO_TOKENKIND_Method); - - if (!eeIsIntrinsic(callTok.hMethod)) - { - return false; - } - - NamedIntrinsic ni = lookupNamedIntrinsic(callTok.hMethod); - if ((ni != NI_System_Threading_Tasks_ValueTask_AsTask) && (ni != NI_System_Threading_Tasks_ValueTask_1_AsTask)) - { - return false; - } - - nextOpcode += sizeof(mdToken); - nextOpcode++; // matched CEE_RET already - - JITDUMP("Matched \"return ValueTaskReturn().AsTask()\"\n"); - *prefixFlags |= PREFIX_IS_ADAPTED_FROM_VALUETASK; - *numBytesMatched = (int)(nextOpcode - codeAddr); - return true; - } - // Look for call; newobj ValueTask; ret if ((nextOpcode < codeEndp) && (*nextOpcode == CEE_NEWOBJ)) { @@ -6162,6 +6122,46 @@ bool Compiler::impMatchAsyncVersionTailCall(const BYTE* codeAddr, return true; } + // Look for call; stloc X; ldloca X; call AsTask(); ret + unsigned vtLclNum; + if (impMatchStlocLdloca(&nextOpcode, codeEndp, &vtLclNum)) + { + if ((nextOpcode >= codeEndp) || (*nextOpcode != CEE_CALL)) + { + return false; + } + + nextOpcode++; + + // Quick check for ret before we resolve the token + if (nextOpcode + sizeof(mdToken) >= codeEndp || (*(nextOpcode + sizeof(mdToken)) != CEE_RET)) + { + return false; + } + + CORINFO_RESOLVED_TOKEN callTok; + impResolveToken(nextOpcode, &callTok, CORINFO_TOKENKIND_Method); + + if (!eeIsIntrinsic(callTok.hMethod)) + { + return false; + } + + NamedIntrinsic ni = lookupNamedIntrinsic(callTok.hMethod); + if ((ni != NI_System_Threading_Tasks_ValueTask_AsTask) && (ni != NI_System_Threading_Tasks_ValueTask_1_AsTask)) + { + return false; + } + + nextOpcode += sizeof(mdToken); + nextOpcode++; // matched CEE_RET already + + JITDUMP("Matched \"return ValueTaskReturn().AsTask()\"\n"); + *prefixFlags |= PREFIX_IS_ADAPTED_FROM_VALUETASK; + *numBytesMatched = (int)(nextOpcode - codeAddr); + return true; + } + return false; } From 1a12a70638282276ade868229843a00063e74ad8 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 8 Jul 2026 16:47:29 +0200 Subject: [PATCH 10/14] Implement in interpreter too --- src/coreclr/interpreter/compiler.cpp | 292 +++++++++++++++++- src/coreclr/interpreter/compiler.h | 13 +- src/coreclr/interpreter/intrinsics.cpp | 14 + .../async-versions-task-adapters.cs | 28 +- 4 files changed, 325 insertions(+), 22 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 59ae0526ecd763..02f24bcf9c86b8 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -3595,10 +3595,12 @@ bool InterpCompiler::EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCa switch (ni) { case NI_System_Runtime_CompilerServices_AsyncHelpers_Await: - // AsyncHelpers.Await should never be expanded here. It is simply needed for recognition elsewhere. - return false; case NI_System_Threading_Tasks_Task_ConfigureAwait: - // ConfigureAwait should never be expanded here. It is simply needed for recognition elsewhere. + case NI_System_Threading_Tasks_ValueTask__ctor: + case NI_System_Threading_Tasks_ValueTask_AsTask: + case NI_System_Threading_Tasks_ValueTask_1__ctor: + case NI_System_Threading_Tasks_ValueTask_1_AsTask: + // These should never be expanded here. They are simply needed for recognition elsewhere. return false; case NI_System_Runtime_CompilerServices_AsyncHelpers_AsyncSuspend: if (!m_methodInfo->args.isAsyncCall()) @@ -4671,6 +4673,67 @@ static OpcodePeepElement peepRuntimeAsyncCallRetInAsyncVersion[] = { { 6, CEE_ILLEGAL } // End marker }; +static OpcodePeepElement peepRuntimeAsyncCallNewobjRetInAsyncVersion[] = { + // call or callvirt at the start + { 5, CEE_NEWOBJ }, + { 10, CEE_RET }, + { 11, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_L_L[] = { + // call or callvirt at the start + { 5, CEE_STLOC }, + { 9, CEE_LDLOCA }, + { 13, CEE_CALL }, + { 18, CEE_RET }, + { 19, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_S_L[] = { + // call or callvirt at the start + { 5, CEE_STLOC_S }, + { 7, CEE_LDLOCA }, + { 11, CEE_CALL }, + { 16, CEE_RET }, + { 17, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_L_S[] = { + // call or callvirt at the start + { 5, CEE_STLOC }, + { 9, CEE_LDLOCA_S }, + { 11, CEE_CALL }, + { 16, CEE_RET }, + { 17, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_S_S[] = { + // call or callvirt at the start + { 5, CEE_STLOC_S }, + { 7, CEE_LDLOCA_S }, + { 9, CEE_CALL }, + { 14, CEE_RET }, + { 15, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_EXACT_L[] = { + // call or callvirt at the start + // STLOC_0, STLOC_1, STLOC_2, or STLOC_3 goes here (at offset 5) + { 6, CEE_LDLOCA }, + { 10, CEE_CALL }, + { 15, CEE_RET }, + { 16, CEE_ILLEGAL } // End marker +}; + +static OpcodePeepElement peepRuntimeAsyncCallAsTaskRetInAsyncVersion_EXACT_S[] = { + // call or callvirt at the start + // STLOC_0, STLOC_1, STLOC_2, or STLOC_3 goes here (at offset 5) + { 6, CEE_LDLOCA_S }, + { 8, CEE_CALL }, + { 13, CEE_RET }, + { 14, CEE_ILLEGAL } // End marker +}; + static OpcodePeepElement peepRuntimeAsyncJmpInAsyncVersion[] = { { 0, CEE_JMP }, { 5, CEE_ILLEGAL } // End marker @@ -4686,11 +4749,19 @@ class InterpAsyncCallPeeps OpcodePeep peepCallConfigureAwaitValueTask_S_S = { peepRuntimeAsyncCallConfigureAwaitValueTask_S_S, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_S_S" }; OpcodePeep peepCallConfigureAwaitValueTask_EXACT_S = { peepRuntimeAsyncCallConfigureAwaitValueTask_EXACT_S, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_EXACT_S" }; OpcodePeep peepCallConfigureAwaitValueTask_EXACT_L = { peepRuntimeAsyncCallConfigureAwaitValueTask_EXACT_L, &InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc, &InterpCompiler::ApplyRuntimeAsyncCall, "CallConfigureAwaitValueTask_EXACT_L" }; - OpcodePeep peepCallRetInAsyncVersion = { peepRuntimeAsyncCallRetInAsyncVersion, &InterpCompiler::IsRuntimeAsyncCallRetOrJmpInAsyncVersion, &InterpCompiler::ApplyRuntimeAsyncCallRetInAsyncVersion, "CallRetInAsyncVersion" }; + + OpcodePeep peepCallRetInAsyncVersion = { peepRuntimeAsyncCallRetInAsyncVersion, &InterpCompiler::IsRuntimeAsyncCallRetOrJmpInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallRetInAsyncVersion" }; + OpcodePeep peepCallNewobjRetInAsyncVersion = { peepRuntimeAsyncCallNewobjRetInAsyncVersion, &InterpCompiler::IsRuntimeAsyncCallNewobjRetInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallNewobjRetInAsyncVersion" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_L_L = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_L_L, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_L_L" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_S_L = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_S_L, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_S_L" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_L_S = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_L_S, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_L_S" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_S_S = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_S_S, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersion, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_S_S" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_EXACT_S = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_EXACT_S, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_EXACT_S" }; + OpcodePeep peepCallAsTaskRetInAsyncVersion_EXACT_L = { peepRuntimeAsyncCallAsTaskRetInAsyncVersion_EXACT_L, &InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact, &InterpCompiler::ApplyAsyncVersionPeepSkipToRet, "CallAsTaskRetInAsyncVersion_EXACT_L" }; OpcodePeep peepJmpInAsyncVersion = { peepRuntimeAsyncJmpInAsyncVersion, &InterpCompiler::IsRuntimeAsyncCallRetOrJmpInAsyncVersion, &InterpCompiler::ApplyRuntimeAsyncCall, "JmpInAsyncVersion" }; public: - OpcodePeep* Peeps[11] = { + OpcodePeep* Peeps[9] = { &peepCall, &peepCallConfigureAwaitTask, &peepCallConfigureAwaitValueTask_L_L, @@ -4699,17 +4770,36 @@ class InterpAsyncCallPeeps &peepCallConfigureAwaitValueTask_S_S, &peepCallConfigureAwaitValueTask_EXACT_S, &peepCallConfigureAwaitValueTask_EXACT_L, + NULL }; + + OpcodePeep* AsyncVersionPeeps[10] = { &peepCallRetInAsyncVersion, + &peepCallNewobjRetInAsyncVersion, + &peepCallAsTaskRetInAsyncVersion_L_L, + &peepCallAsTaskRetInAsyncVersion_S_L, + &peepCallAsTaskRetInAsyncVersion_L_S, + &peepCallAsTaskRetInAsyncVersion_S_S, + &peepCallAsTaskRetInAsyncVersion_EXACT_S, + &peepCallAsTaskRetInAsyncVersion_EXACT_L, &peepJmpInAsyncVersion, NULL }; - - bool FindAndApplyPeep(InterpCompiler* compiler) + bool FindAndApplyPeep(InterpCompiler* compiler, bool isAsyncVersionOfSyncMethod) { #ifdef DEBUG if (!InterpConfig.JitOptimizeAwait()) return false; #endif // DEBUG + if (isAsyncVersionOfSyncMethod) + { + if (compiler->m_isSynchronized) + { + return false; + } + + return compiler->FindAndApplyPeep(AsyncVersionPeeps); + } + return compiler->FindAndApplyPeep(Peeps); } } AsyncCallPeeps; @@ -4938,6 +5028,8 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re void* calliCookie = NULL; ContinuationContextHandling continuationContextHandling = ContinuationContextHandling::None; + bool isValueTaskAdaptedToTask = false; + if (isCalli) { // Suppress uninitialized use warning. @@ -4974,13 +5066,16 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re } else { - // We expect this bool to be reset when we handle the RET instruction after it gets set. + // We expect state to be reset before we call into peeps assert(!m_asyncVersionIsTailCalling); + assert(!m_isValueTaskAdaptedToTask); - if (!newObj && m_methodInfo->args.isAsyncCall() && AsyncCallPeeps.FindAndApplyPeep(this)) + if (!newObj && m_methodInfo->args.isAsyncCall() && AsyncCallPeeps.FindAndApplyPeep(this, m_isAsyncVersionOfSyncMethod)) { resolvedCallToken = m_resolvedAsyncCallToken; continuationContextHandling = m_currentContinuationContextHandling; + isValueTaskAdaptedToTask = m_isValueTaskAdaptedToTask; + m_isValueTaskAdaptedToTask = false; } else { @@ -5741,7 +5836,7 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re if (callInfo.sig.isAsyncCall() && m_methodInfo->args.isAsyncCall()) // Async2 functions may need to suspend { - EmitSuspend(callInfo.sig.retType, continuationContextHandling); + EmitSuspend(callInfo.sig.retType, continuationContextHandling, isValueTaskAdaptedToTask); } } @@ -5965,7 +6060,7 @@ void InterpCompiler::WrapTopOfStackInAwait() m_pLastNewIns->info.pCallInfo = new (getAllocator(IMK_CallInfo)) InterpCallInfo(); m_pLastNewIns->info.pCallInfo->pCallArgs = callArgs; - EmitSuspend(awaitSig.retType, ContinuationContextHandling::None); + EmitSuspend(awaitSig.retType, ContinuationContextHandling::None, /* isValueTaskAdaptedToTask */ false); } static void SetSlotToTrue(TArray &gcRefMap, int32_t slotOffset) @@ -5982,7 +6077,7 @@ static void SetSlotToTrue(TArray &gcRefMap, int32_t slot gcRefMap.Set(slotIndex, true); } -void InterpCompiler::EmitSuspend(CorInfoType callRetType, ContinuationContextHandling continuationContextHandling) +void InterpCompiler::EmitSuspend(CorInfoType callRetType, ContinuationContextHandling continuationContextHandling, bool isValueTaskAdaptedToTask) { if (m_nextAwaitIsTail) { @@ -6276,6 +6371,11 @@ void InterpCompiler::EmitSuspend(CorInfoType callRetType, ContinuationContextHan flags |= CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL; } + if (isValueTaskAdaptedToTask) + { + flags |= CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK; + } + suspendData->flags = (CorInfoContinuationFlags)flags; suspendData->keepAliveOffset = keepAliveOffset + OFFSETOF__CORINFO_Continuation__data; @@ -7598,7 +7698,115 @@ bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip bool InterpCompiler::IsRuntimeAsyncCallRetOrJmpInAsyncVersion(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) { - if (!m_isAsyncVersionOfSyncMethod || m_isSynchronized) + if (!ResolveAsyncCallToken(ip)) + { + return false; + } + + m_currentContinuationContextHandling = ContinuationContextHandling::None; + m_asyncVersionIsTailCalling = true; + return true; +} + +int InterpCompiler::ApplyAsyncVersionPeepSkipToRet(const uint8_t* ip, OpcodePeepElement* pattern, void* computedInfo) +{ + while (pattern->opcode != CEE_RET) + { + assert(pattern->opcode != CEE_ILLEGAL); + pattern++; + } + + return pattern->offsetIntoPeep; +} + +bool InterpCompiler::IsRuntimeAsyncCallNewobjRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) +{ + CORINFO_RESOLVED_TOKEN ctorResolvedToken; + ResolveToken(getU4LittleEndian(ip + pattern[0].offsetIntoPeep + 1), CORINFO_TOKENKIND_NewObj, &ctorResolvedToken); + if (!m_compHnd->isIntrinsic(ctorResolvedToken.hMethod)) + { + return false; + } + + NamedIntrinsic ni = GetNamedIntrinsic(m_compHnd, m_methodHnd, ctorResolvedToken.hMethod); + if (ni != NI_System_Threading_Tasks_ValueTask__ctor && ni != NI_System_Threading_Tasks_ValueTask_1__ctor) + { + return false; + } + + CORINFO_SIG_INFO sig; + m_compHnd->getMethodSig(ctorResolvedToken.hMethod, &sig); + + if (sig.numArgs != 1) + { + return false; + } + + if (m_methodInfo->args.retType != CORINFO_TYPE_VOID) + { + // Since we validated that this instance is being returned + // this can only be ValueTask at this point. + assert((sig.sigInst.classInstCount == 1) && (sig.sigInst.methInstCount == 0)); + CORINFO_CLASS_HANDLE paramClass = m_compHnd->getArgClass(&sig, sig.args); + if (paramClass == sig.sigInst.classInst[0]) + { + // This is "class ValueTask { ValueTask(T value) }" overload + // which is not what we are looking for + return false; + } + } + + if (!ResolveAsyncCallToken(ip)) + { + return false; + } + + m_currentContinuationContextHandling = ContinuationContextHandling::None; + m_asyncVersionIsTailCalling = true; + return true; +} + +bool InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) +{ + uint32_t stLocVar = 0; + uint32_t ldlocaVar = 0; + + switch (*(ip + pattern[0].offsetIntoPeep)) + { + case CEE_STLOC_S: + stLocVar = (ip + pattern[0].offsetIntoPeep)[1]; + break; + default: + // Must be STLOC + stLocVar = getU2LittleEndian(ip + pattern[0].offsetIntoPeep + 2); + break; + } + + switch (*(ip + pattern[1].offsetIntoPeep)) + { + case CEE_LDLOCA_S: + ldlocaVar = (ip + pattern[1].offsetIntoPeep)[1]; + break; + default: + // Must be LDLOCA + ldlocaVar = getU2LittleEndian(ip + pattern[1].offsetIntoPeep + 2); + break; + } + + if (ldlocaVar != stLocVar) + { + return false; + } + + CORINFO_RESOLVED_TOKEN asTaskResolvedToken; + ResolveToken(getU4LittleEndian(ip + pattern[2].offsetIntoPeep + 1), CORINFO_TOKENKIND_Method, &asTaskResolvedToken); + if (!m_compHnd->isIntrinsic(asTaskResolvedToken.hMethod)) + { + return false; + } + + NamedIntrinsic ni = GetNamedIntrinsic(m_compHnd, m_methodHnd, asTaskResolvedToken.hMethod); + if (ni != NI_System_Threading_Tasks_ValueTask_AsTask && ni != NI_System_Threading_Tasks_ValueTask_1_AsTask) { return false; } @@ -7610,13 +7818,65 @@ bool InterpCompiler::IsRuntimeAsyncCallRetOrJmpInAsyncVersion(const uint8_t* ip, m_currentContinuationContextHandling = ContinuationContextHandling::None; m_asyncVersionIsTailCalling = true; + m_isValueTaskAdaptedToTask = true; return true; } -int InterpCompiler::ApplyRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo) +bool InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) { - // Only skip the call, not the RET we matched. - return 5; + // IL pattern: + // CALL | CALLVIRT at offset 0 + // # STLOC_0 | STLOC_1 | STLOC_2 | STLOC_3 at offset 5 + // # LDLOCA | LDLOCA_S at offset 6 + // # CALL + // # RET + + uint32_t stLocVar = 0; + uint32_t ldlocaVar = 0; + + switch (*(ip + pattern[0].offsetIntoPeep - 1)) + { + case CEE_STLOC_0: + stLocVar = 0; + break; + case CEE_STLOC_1: + stLocVar = 1; + break; + case CEE_STLOC_2: + stLocVar = 2; + break; + case CEE_STLOC_3: + stLocVar = 3; + break; + default: + return false; + } + + switch (*(ip + pattern[0].offsetIntoPeep)) + { + case CEE_LDLOCA_S: + ldlocaVar = (ip + pattern[0].offsetIntoPeep)[1]; + break; + default: + // Must be LDLOCA + ldlocaVar = getU2LittleEndian(ip + pattern[0].offsetIntoPeep + 2); + break; + } + + if (ldlocaVar != stLocVar) + { + return false; + } + + if (!ResolveAsyncCallToken(ip)) + { + return false; + } + + m_currentContinuationContextHandling = ContinuationContextHandling::None; + m_asyncVersionIsTailCalling = true; + m_isValueTaskAdaptedToTask = true; + return true; } int InterpCompiler::ApplyConvRUnR4Peep(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo) diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index a459090f1df827..e1b7f7313e3035 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -711,6 +711,11 @@ class InterpCompiler bool m_nextAwaitIsTail = false; bool m_asyncVersionIsTailCalling = false; + // If true, this is a recognized await of ValueTaskReturn().AsTask(). This + // does not have strictly the same semantics as 'await ValueTaskReturn()' + // so some special treatment is needed. + bool m_isValueTaskAdaptedToTask = false; + // Table of mappings of leave instructions to the first finally call island the leave // needs to execute. TArray m_leavesTable; @@ -1003,11 +1008,15 @@ class InterpCompiler bool IsRuntimeAsyncCall(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallConfigureAwaitTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); + bool IsRuntimeAsyncCallNewobjRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); + NamedIntrinsic GetNamedIntrinsicFrom(const uint8_t* ip); + bool IsRuntimeAsyncCallAsTaskRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); + bool IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallRetOrJmpInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); int ApplyRuntimeAsyncCall(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo) { return -1; } - int ApplyRuntimeAsyncCallRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo); + int ApplyAsyncVersionPeepSkipToRet(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo); ContinuationContextHandling m_currentContinuationContextHandling = ContinuationContextHandling::None; CORINFO_RESOLVED_TOKEN m_resolvedAsyncCallToken; @@ -1022,7 +1031,7 @@ class InterpCompiler void EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool readonly, bool tailcall, bool newObj, bool isCalli); void WrapTopOfStackInAwait(); void EmitRet(CORINFO_METHOD_INFO* methodInfo); - void EmitSuspend(CorInfoType callRetType, ContinuationContextHandling ContinuationContextHandling); + void EmitSuspend(CorInfoType callRetType, ContinuationContextHandling continuationContextHandling, bool isValueTaskAdaptedToTask); void EmitCalli(bool isTailCall, void* calliCookie, int callIFunctionPointerVar, CORINFO_SIG_INFO* callSiteSig); bool EmitNamedIntrinsicCall(NamedIntrinsic ni, bool nonVirtualCall, CORINFO_CLASS_HANDLE clsHnd, CORINFO_METHOD_HANDLE method, CORINFO_SIG_INFO sig); void EmitLdind(InterpType type, CORINFO_CLASS_HANDLE clsHnd, int32_t offset); diff --git a/src/coreclr/interpreter/intrinsics.cpp b/src/coreclr/interpreter/intrinsics.cpp index 0bdcb718d5b6e7..38e31e24a1cf08 100644 --- a/src/coreclr/interpreter/intrinsics.cpp +++ b/src/coreclr/interpreter/intrinsics.cpp @@ -170,6 +170,20 @@ NamedIntrinsic GetNamedIntrinsic(COMP_HANDLE compHnd, CORINFO_METHOD_HANDLE comp !strcmp(className, "ValueTask`1") || !strcmp(className, "ValueTask")) return NI_System_Threading_Tasks_Task_ConfigureAwait; } + else if (!strcmp(className, "ValueTask")) + { + if (!strcmp(methodName, ".ctor")) + return NI_System_Threading_Tasks_ValueTask__ctor; + else if (!strcmp(methodName, "AsTask")) + return NI_System_Threading_Tasks_ValueTask_AsTask; + } + else if (!strcmp(className, "ValueTask`1")) + { + if (!strcmp(methodName, ".ctor")) + return NI_System_Threading_Tasks_ValueTask_1__ctor; + else if (!strcmp(methodName, "AsTask")) + return NI_System_Threading_Tasks_ValueTask_1_AsTask; + } } return NI_Illegal; diff --git a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs index f79fd2c01fe910..32e68dc523b8ed 100644 --- a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs @@ -19,7 +19,7 @@ public static void TestTaskToValueTask() try { SynchronizationContext.SetSynchronizationContext(new MySyncContext()); - WrapTaskInValueTask().GetAwaiter().GetResult(); + WrapTaskInValueTaskCaller().GetAwaiter().GetResult(); } finally { @@ -27,6 +27,11 @@ public static void TestTaskToValueTask() } } + private static async Task WrapTaskInValueTaskCaller() + { + await WrapTaskInValueTask(); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static ValueTask WrapTaskInValueTask() { @@ -48,7 +53,7 @@ public static void TestValueTaskSourceToTask() try { SynchronizationContext.SetSynchronizationContext(new MySyncContext()); - ConvertSourceToTask().GetAwaiter().GetResult(); + ConvertSourceToTaskCaller().GetAwaiter().GetResult(); } finally { @@ -56,6 +61,11 @@ public static void TestValueTaskSourceToTask() } } + private static async Task ConvertSourceToTaskCaller() + { + await ConvertSourceToTask(); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static Task ConvertSourceToTask() { @@ -85,7 +95,7 @@ private static async Task WrapValueVersusAsync() // constructor, so the ValueTask holds it as an already-available value. The ValueTask is // completed even while the inner Task is still suspended. TaskCompletionSource valueGate = new(TaskCreationOptions.RunContinuationsAsynchronously); - ValueTask> valueCase = WrapTaskValue(valueGate); + ValueTask> valueCase = WrapTaskValueCaller(valueGate); Assert.True(valueCase.IsCompletedSuccessfully); Task innerFromValue = await valueCase; Assert.False(innerFromValue.IsCompleted); @@ -96,19 +106,29 @@ private static async Task WrapValueVersusAsync() // constructor, so the ValueTask is backed by that task and is not completed until the outer // task completes. TaskCompletionSource asyncGate = new(TaskCreationOptions.RunContinuationsAsynchronously); - ValueTask> asyncCase = WrapTaskTask(asyncGate); + ValueTask> asyncCase = WrapTaskTaskCaller(asyncGate); Assert.False(asyncCase.IsCompleted); asyncGate.SetResult(); Task innerFromAsync = await asyncCase; Assert.Equal(123, await innerFromAsync); } + private static async ValueTask> WrapTaskValueCaller(TaskCompletionSource gate) + { + return await WrapTaskValue(gate); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static ValueTask> WrapTaskValue(TaskCompletionSource gate) { return new ValueTask>(TaskOfIntReturningFunction(gate)); } + private static async ValueTask> WrapTaskTaskCaller(TaskCompletionSource gate) + { + return await WrapTaskTask(gate); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static ValueTask> WrapTaskTask(TaskCompletionSource gate) { From 6f37b74e6423032f50dd5470d900a1a72d2975ad Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 8 Jul 2026 17:14:38 +0200 Subject: [PATCH 11/14] Some feedback --- src/coreclr/interpreter/compiler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index e1b7f7313e3035..e9a75a03537cf4 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -1009,7 +1009,6 @@ class InterpCompiler bool IsRuntimeAsyncCallConfigureAwaitTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallNewobjRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); - NamedIntrinsic GetNamedIntrinsicFrom(const uint8_t* ip); bool IsRuntimeAsyncCallAsTaskRetInAsyncVersion(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); bool IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc(const uint8_t* ip, OpcodePeepElement* peep, void** computedInfo); From dbe355ab6169c23db3624b794c31675aed5aeb76 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 9 Jul 2026 10:26:17 +0200 Subject: [PATCH 12/14] Check for AsTask --- src/coreclr/interpreter/compiler.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 02f24bcf9c86b8..b1bcf1c254eda8 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -7868,6 +7868,19 @@ bool InterpCompiler::IsRuntimeAsyncCallAsTaskRetInAsyncVersionExact(const uint8_ return false; } + CORINFO_RESOLVED_TOKEN asTaskResolvedToken; + ResolveToken(getU4LittleEndian(ip + pattern[1].offsetIntoPeep + 1), CORINFO_TOKENKIND_Method, &asTaskResolvedToken); + if (!m_compHnd->isIntrinsic(asTaskResolvedToken.hMethod)) + { + return false; + } + + NamedIntrinsic ni = GetNamedIntrinsic(m_compHnd, m_methodHnd, asTaskResolvedToken.hMethod); + if (ni != NI_System_Threading_Tasks_ValueTask_AsTask && ni != NI_System_Threading_Tasks_ValueTask_1_AsTask) + { + return false; + } + if (!ResolveAsyncCallToken(ip)) { return false; From 7ccc1654b5bceb947100b3933385048ced6e621d Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 13 Jul 2026 16:22:36 +0200 Subject: [PATCH 13/14] Add HasNext checks to ILScanner Valid IL can run out of bytes in these checks because the bytes we are matching on span only to the next basic block start, not until the end of the IL. --- .../aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index 8971a637a45ee6..d7b441d08c07fd 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -404,6 +404,9 @@ private static bool MatchStlocLdloca(ref ILReader reader, out int stlocNum) stlocNum = -1; ILReader tempReader = reader; + if (!tempReader.HasNext) + return false; + ILOpcode opcode = tempReader.ReadILOpcode(); // ConfigureAwait on a ValueTask will start with stloc/ldloca. @@ -420,6 +423,9 @@ private static bool MatchStlocLdloca(ref ILReader reader, out int stlocNum) return false; } + if (!tempReader.HasNext) + return false; + opcode = tempReader.ReadILOpcode(); int ldlocaNum = opcode switch { @@ -465,7 +471,7 @@ private bool MatchTailCallAwait() // Look for call; stloc X; ldloca X; call AsTask(); ret if (MatchStlocLdloca(ref reader, out _)) { - if (reader.ReadILOpcode() != ILOpcode.call) + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.call) { return false; } @@ -473,7 +479,7 @@ private bool MatchTailCallAwait() int callToken = reader.ReadILToken(); // Verify ret first before we go resolving metadata - if (reader.ReadILOpcode() != ILOpcode.ret) + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ret) { return false; } @@ -493,7 +499,7 @@ private bool MatchTailCallAwait() { int ctorToken = reader.ReadILToken(); - if (reader.ReadILOpcode() != ILOpcode.ret) + if (!reader.HasNext || reader.ReadILOpcode() != ILOpcode.ret) { return false; } From de7fed6cc6142834d60d5f4b050d95960a14fbc5 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 13 Jul 2026 16:39:31 +0200 Subject: [PATCH 14/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../async-versions-task-adapters.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs index 32e68dc523b8ed..509874dc4e14e5 100644 --- a/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs @@ -173,16 +173,16 @@ public override void Post(SendOrPostCallback d, object state) { ThreadPool.UnsafeQueueUserWorkItem(_ => { - SynchronizationContext prevContext = Current; - try - { - SetSynchronizationContext(this); - d(state); - } - finally - { - SetSynchronizationContext(prevContext); - } + SynchronizationContext prevContext = Current; + try + { + SetSynchronizationContext(this); + d(state); + } + finally + { + SetSynchronizationContext(prevContext); + } }, null); } }