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 fb24a4a33daa08..49e225a9c92104 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 @@ -23,6 +23,11 @@ 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 flag affects how + // ValueTaskSourceContinuation handling computes the flags to pass to + // IValueTaskSource.OnCompleted. + ValueTaskAdaptedToTask = 1 << 3, AllContinuationFlags = ContinueOnThreadPool | ContinueOnCapturedSynchronizationContext | ContinueOnCapturedTaskScheduler, @@ -832,7 +837,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; } diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index d085c08701d27f..cfd6aeefe02398 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1786,6 +1786,9 @@ 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. diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index f139770f71ddaa..8f30e675fa8a42 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 8ab52df52815b2..71650cb2a0d1ad 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -5006,6 +5006,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); @@ -5495,6 +5496,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 8b5ddcd11603dc..54a7d492bdaf36 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -4533,6 +4533,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 4a3583c8aba49f..a16f2922b20ec6 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -5862,70 +5862,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); @@ -5933,7 +5875,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; @@ -5949,7 +5891,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; @@ -5986,6 +5928,243 @@ const BYTE* Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, return nullptr; } +//------------------------------------------------------------------------ +// impMatchStlocLdloca: +// 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 +// +// 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; + 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; +} + +//------------------------------------------------------------------------ +// 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, + 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) + { + // 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]) + { + // 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 */ @@ -9053,17 +9232,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 4266be5b1207f0..8489098270afa8 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7270,6 +7270,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. @@ -7290,8 +7299,10 @@ void Compiler::impSetupAsyncCall( } else { + asyncInfo.IsValueTaskAsTask = (prefixFlags & PREFIX_IS_ADAPTED_FROM_VALUETASK) != 0; + if (opts.Tier0OptimizationEnabled() && ((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; @@ -11785,6 +11796,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) { @@ -11792,6 +11811,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 3ce8c9e8f13250..890322a782d5dd 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/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index 7e233e358ba6c3..8971a637a45ee6 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -367,34 +367,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) @@ -423,6 +399,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; @@ -433,21 +445,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; } @@ -485,7 +556,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); @@ -1791,6 +1862,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); 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; 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..f79fd2c01fe910 --- /dev/null +++ b/src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs @@ -0,0 +1,169 @@ +// 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(_ => + { + SynchronizationContext prevContext = Current; + try + { + SetSynchronizationContext(this); + d(state); + } + finally + { + SetSynchronizationContext(prevContext); + } + }, 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