From a2713efee36efe9e71b742ce9d0e113886d12525 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 14:37:29 +0100 Subject: [PATCH 1/8] Move async variant resolution into getCallInfo --- src/coreclr/inc/corinfo.h | 26 ++++--- src/coreclr/jit/ee_il_dll.hpp | 9 ++- src/coreclr/jit/importer.cpp | 49 ++++++------- src/coreclr/jit/importercalls.cpp | 5 -- src/coreclr/jit/morph.cpp | 6 +- src/coreclr/vm/jitinterface.cpp | 118 ++++++++++++++---------------- 6 files changed, 101 insertions(+), 112 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index ae313b4cbbe162..f8094b1a082e97 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1434,14 +1434,14 @@ enum CORINFO_THIS_TRANSFORM enum CORINFO_CALLINFO_FLAGS { - CORINFO_CALLINFO_NONE = 0x0000, - CORINFO_CALLINFO_ALLOWINSTPARAM = 0x0001, // Can the compiler generate code to pass an instantiation parameters? Simple compilers should not use this flag - CORINFO_CALLINFO_CALLVIRT = 0x0002, // Is it a virtual call? - // UNUSED = 0x0004, - CORINFO_CALLINFO_DISALLOW_STUB = 0x0008, // Do not use a stub for this call, even if it is a virtual call. - CORINFO_CALLINFO_SECURITYCHECKS = 0x0010, // Perform security checks. - CORINFO_CALLINFO_LDFTN = 0x0020, // Resolving target of LDFTN - // UNUSED = 0x0040, + CORINFO_CALLINFO_NONE = 0x0000, + CORINFO_CALLINFO_ALLOWINSTPARAM = 0x0001, // Can the compiler generate code to pass an instantiation parameters? Simple compilers should not use this flag + CORINFO_CALLINFO_CALLVIRT = 0x0002, // Is it a virtual call? + CORINFO_CALLINFO_ALLOWASYNCVARIANT = 0x0004, // allow resolution to an async variant + CORINFO_CALLINFO_DISALLOW_STUB = 0x0008, // Do not use a stub for this call, even if it is a virtual call. + CORINFO_CALLINFO_SECURITYCHECKS = 0x0010, // Perform security checks. + CORINFO_CALLINFO_LDFTN = 0x0020, // Resolving target of LDFTN + // UNUSED = 0x0040, }; enum CorInfoIsAccessAllowedResult @@ -1484,10 +1484,6 @@ enum CorInfoTokenKind // token comes from devirtualizing a method CORINFO_TOKENKIND_DevirtualizedMethod = 0x800 | CORINFO_TOKENKIND_Method, - - // token comes from runtime async awaiting pattern - CORINFO_TOKENKIND_Await = 0x2000 | CORINFO_TOKENKIND_Method, - CORINFO_TOKENKIND_AwaitVirtual = 0x4000 | CORINFO_TOKENKIND_Method, }; struct CORINFO_RESOLVED_TOKEN @@ -1560,6 +1556,12 @@ struct CORINFO_CALL_INFO CORINFO_CONST_LOOKUP instParamLookup; bool wrapperDelegateInvoke; + + // If CORINFO_CALLINFO_ALLOWASYNCVARIANT was passed, this is the resolved + // async variant or NULL if no async variant was resolved. + // This is the async variant of the token's method and differs from hMethod + // of this class in cases of sharing, constrained resolution etc. + CORINFO_METHOD_HANDLE resolvedAsyncVariant; }; enum CORINFO_DEVIRTUALIZATION_DETAIL diff --git a/src/coreclr/jit/ee_il_dll.hpp b/src/coreclr/jit/ee_il_dll.hpp index 303340a446612b..512384824d1a1d 100644 --- a/src/coreclr/jit/ee_il_dll.hpp +++ b/src/coreclr/jit/ee_il_dll.hpp @@ -339,7 +339,12 @@ inline var_types Compiler::TypeHandleToVarType(CorInfoType jitType, CORINFO_CLAS return type; } -inline CORINFO_CALLINFO_FLAGS combine(CORINFO_CALLINFO_FLAGS flag1, CORINFO_CALLINFO_FLAGS flag2) +constexpr CORINFO_CALLINFO_FLAGS operator|(CORINFO_CALLINFO_FLAGS a, CORINFO_CALLINFO_FLAGS b) { - return (CORINFO_CALLINFO_FLAGS)(flag1 | flag2); + return (CORINFO_CALLINFO_FLAGS)((uint32_t)a | (uint32_t)b); +} + +inline CORINFO_CALLINFO_FLAGS& operator|=(CORINFO_CALLINFO_FLAGS& a, CORINFO_CALLINFO_FLAGS b) +{ + return a = (CORINFO_CALLINFO_FLAGS)((uint32_t)a | (uint32_t)b); } diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 70b2aeb3a46c14..c71d3a9bcd1d63 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -8600,7 +8600,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) JITDUMP(" %08X", resolvedToken.token); eeGetCallInfo(&resolvedToken, (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, - combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_LDFTN), &callInfo); + CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_LDFTN, &callInfo); // This check really only applies to intrinsic Array.Address methods if (callInfo.sig.callConv & CORINFO_CALLCONV_PARAMTYPE) @@ -8639,8 +8639,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) JITDUMP(" %08X", resolvedToken.token); eeGetCallInfo(&resolvedToken, nullptr /* constraint typeRef */, - combine(combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_LDFTN), - CORINFO_CALLINFO_CALLVIRT), + CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_LDFTN | CORINFO_CALLINFO_CALLVIRT, &callInfo); // This check really only applies to intrinsic Array.Address methods @@ -8773,7 +8772,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) _impResolveToken(CORINFO_TOKENKIND_NewObj); eeGetCallInfo(&resolvedToken, nullptr /* constraint typeRef*/, - combine(CORINFO_CALLINFO_SECURITYCHECKS, CORINFO_CALLINFO_ALLOWINSTPARAM), &callInfo); + CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_ALLOWINSTPARAM, &callInfo); mflags = callInfo.methodFlags; @@ -9000,6 +8999,14 @@ void Compiler::impImportBlockCode(BasicBlock* block) // many other places. We unfortunately embed that knowledge here. if (opcode != CEE_CALLI) { + CORINFO_CALLINFO_FLAGS callInfoFlags = + CORINFO_CALLINFO_ALLOWINSTPARAM | CORINFO_CALLINFO_SECURITYCHECKS; + + if (opcode == CEE_CALLVIRT) + { + callInfoFlags |= CORINFO_CALLINFO_CALLVIRT; + } + bool isAwait = false; int configVal = -1; // -1 not configured, 0/1 configured to false/true const BYTE* codeAddrAfterMatch = nullptr; @@ -9021,44 +9028,36 @@ void Compiler::impImportBlockCode(BasicBlock* block) { prefixFlags |= PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT; } + + callInfoFlags |= CORINFO_CALLINFO_ALLOWASYNCVARIANT; } } + _impResolveToken(CORINFO_TOKENKIND_Method); + + eeGetCallInfo(&resolvedToken, + (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, + callInfoFlags, &callInfo); + if (isAwait) { - _impResolveToken(opcode == CEE_CALLVIRT ? CORINFO_TOKENKIND_AwaitVirtual - : CORINFO_TOKENKIND_Await); - if (resolvedToken.hMethod != nullptr) + if (callInfo.resolvedAsyncVariant != NO_METHOD_HANDLE) { - // There is a runtime async variant that is implicitly awaitable, just call that. - // skip the await pattern to the last token. codeAddr = codeAddrAfterMatch; opcodeOffs = awaitOffset; + + JITDUMP("Async variant provided by VM\n"); + // Update the token as we may need it again for various runtime lookups + resolvedToken.hMethod = callInfo.resolvedAsyncVariant; } else { - // This can happen in cases when the Task-returning method is not a runtime Async - // function. For example "T M1(T arg) => arg" when called with a Task argument. - // It can also happen generally if the VM does not think using the async entry point - // is worth it. Treat these as a regular call that is Awaited. - _impResolveToken(CORINFO_TOKENKIND_Method); prefixFlags &= ~(PREFIX_IS_TASK_AWAIT | PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT); isAwait = false; JITDUMP("No async variant provided by VM, treating as regular call that is awaited\n"); } } - else - { - _impResolveToken(CORINFO_TOKENKIND_Method); - } - - eeGetCallInfo(&resolvedToken, - (prefixFlags & PREFIX_CONSTRAINED) ? &constrainedResolvedToken : nullptr, - // this is how impImportCall invokes getCallInfo - combine(combine(CORINFO_CALLINFO_ALLOWINSTPARAM, CORINFO_CALLINFO_SECURITYCHECKS), - (opcode == CEE_CALLVIRT) ? CORINFO_CALLINFO_CALLVIRT : CORINFO_CALLINFO_NONE), - &callInfo); } else { diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 62b79e697788ff..2920c260c1b5a3 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -6917,11 +6917,6 @@ void Compiler::impSetupAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned pref JITDUMP(" Continuation continues on thread pool\n"); } } - else if (opcode == CEE_CALLI) - { - // Used for unboxing/instantiating stubs - JITDUMP("Call is an async calli\n"); - } else { JITDUMP("Call is an async non-task await\n"); diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 5d8075ec7236d3..164fff65e14801 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -5358,14 +5358,14 @@ GenTree* Compiler::fgMorphTailCallViaHelpers(GenTreeCall* call, CORINFO_TAILCALL { assert(!call->tailCallInfo->GetSig()->hasTypeArg()); - CORINFO_CALL_INFO callInfo; - unsigned flags = CORINFO_CALLINFO_LDFTN; + CORINFO_CALL_INFO callInfo; + CORINFO_CALLINFO_FLAGS flags = CORINFO_CALLINFO_LDFTN; if (call->tailCallInfo->IsCallvirt()) { flags |= CORINFO_CALLINFO_CALLVIRT; } - eeGetCallInfo(call->tailCallInfo->GetToken(), nullptr, (CORINFO_CALLINFO_FLAGS)flags, &callInfo); + eeGetCallInfo(call->tailCallInfo->GetToken(), nullptr, flags, &callInfo); target = getVirtMethodPointerTree(thisPtrStubArg, call->tailCallInfo->GetToken(), &callInfo); } diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 5824d737c2255d..17998cf1a26ddd 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -1080,42 +1080,6 @@ void CEEInfo::resolveToken(/* IN, OUT */ CORINFO_RESOLVED_TOKEN * pResolvedToken th = ClassLoader::LoadArrayTypeThrowing(th); break; - case CORINFO_TOKENKIND_Await: - case CORINFO_TOKENKIND_AwaitVirtual: - { - // in rare cases a method that returns Task is not actually TaskReturning (i.e. returns T). - // we cannot resolve to an Async variant in such case. - // return NULL, so that caller would re-resolve as a regular method call - bool allowAsyncVariant = pMD->ReturnsTaskOrValueTask(); - if (allowAsyncVariant) - { - bool isDirect = tokenType == CORINFO_TOKENKIND_Await || pMD->IsStatic(); - if (!isDirect) - { - DWORD attrs = pMD->GetAttrs(); - if (pMD->GetMethodTable()->IsInterface()) - { - isDirect = !IsMdVirtual(attrs); - } - else - { - isDirect = !IsMdVirtual(attrs) || IsMdFinal(attrs) || pMD->GetMethodTable()->IsSealed(); - } - } - - if (isDirect && !pMD->IsAsyncThunkMethod()) - { - // Async variant would be a thunk. Do not resolve direct calls - // to async thunks. That just creates and JITs unnecessary - // thunks, and the thunks are harder for the JIT to optimize. - allowAsyncVariant = false; - } - } - - pMD = allowAsyncVariant ? pMD->GetAsyncVariant(/*allowInstParam*/FALSE) : NULL; - } - break; - default: // Disallow ELEMENT_TYPE_BYREF and ELEMENT_TYPE_VOID if (et == ELEMENT_TYPE_BYREF || et == ELEMENT_TYPE_VOID) @@ -4994,6 +4958,7 @@ void CEEInfo::getCallInfo( INDEBUG(memset(pResult, 0xCC, sizeof(*pResult))); pResult->stubLookup.lookupKind.needsRuntimeLookup = false; + pResult->resolvedAsyncVariant = NULL; MethodDesc* pMD = (MethodDesc *)pResolvedToken->hMethod; TypeHandle th(pResolvedToken->hClass); @@ -5145,35 +5110,6 @@ void CEEInfo::getCallInfo( // MethodDesc * pTargetMD = pMDAfterConstraintResolution; - DWORD dwTargetMethodAttrs = pTargetMD->GetAttrs(); - - pResult->exactContextNeedsRuntimeLookup = (fIsStaticVirtualMethod && !fResolvedConstraint && !constrainedType.IsNull() && constrainedType.IsCanonicalSubtype()); - - if (pTargetMD->HasMethodInstantiation()) - { - pResult->contextHandle = MAKE_METHODCONTEXT(pTargetMD); - if (pTargetMD->GetMethodTable()->IsSharedByGenericInstantiations() || TypeHandle::IsCanonicalSubtypeInstantiation(pTargetMD->GetMethodInstantiation())) - { - pResult->exactContextNeedsRuntimeLookup = TRUE; - } - } - else - { - if (!exactType.IsTypeDesc() && !pTargetMD->IsArray()) - { - // Because of .NET's notion of base calls, exactType may point to a sub-class - // of the actual class that defines pTargetMD. If the JIT decides to inline, it is - // important that they 'match', so we fix exactType here. - exactType = pTargetMD->GetExactDeclaringType(exactType.AsMethodTable()); - _ASSERTE(!exactType.IsNull()); - } - - pResult->contextHandle = MAKE_CLASSCONTEXT(exactType.AsPtr()); - if (exactType.IsSharedByGenericInstantiations()) - { - pResult->exactContextNeedsRuntimeLookup = TRUE; - } - } // // Determine whether to perform direct call @@ -5181,6 +5117,7 @@ void CEEInfo::getCallInfo( bool directCall = false; bool resolvedCallVirt = false; + DWORD dwTargetMethodAttrs = pTargetMD->GetAttrs(); if ((flags & CORINFO_CALLINFO_LDFTN) && (!fIsStaticVirtualMethod || fResolvedConstraint)) { @@ -5236,6 +5173,57 @@ void CEEInfo::getCallInfo( } } + // See if we can resolve to an async variant. Task-returning methods may + // not always have such a variant (for a T return where T is task, for + // example). + if ((flags & CORINFO_CALLINFO_ALLOWASYNCVARIANT) && pTargetMD->ReturnsTaskOrValueTask()) + { + if (!directCall || pTargetMD->IsAsyncThunkMethod()) + { + // Either a virtual call or a direct call where the async variant + // is user-defined. Switch to the async variant in these cases. + if (pMD == pTargetMD) + { + pMD = pTargetMD = pMD->GetAsyncVariant(/*allowInstParam */ FALSE); + } + else + { + pMD = pMD->GetAsyncVariant(/* allowInstParam */ FALSE); + pTargetMD = pTargetMD->GetAsyncVariant(/* allowInstParam */ FALSE); + } + + pResult->resolvedAsyncVariant = CORINFO_METHOD_HANDLE(pMD); + } + } + + pResult->exactContextNeedsRuntimeLookup = (fIsStaticVirtualMethod && !fResolvedConstraint && !constrainedType.IsNull() && constrainedType.IsCanonicalSubtype()); + + if (pTargetMD->HasMethodInstantiation()) + { + pResult->contextHandle = MAKE_METHODCONTEXT(pTargetMD); + if (pTargetMD->GetMethodTable()->IsSharedByGenericInstantiations() || TypeHandle::IsCanonicalSubtypeInstantiation(pTargetMD->GetMethodInstantiation())) + { + pResult->exactContextNeedsRuntimeLookup = TRUE; + } + } + else + { + if (!exactType.IsTypeDesc() && !pTargetMD->IsArray()) + { + // Because of .NET's notion of base calls, exactType may point to a sub-class + // of the actual class that defines pTargetMD. If the JIT decides to inline, it is + // important that they 'match', so we fix exactType here. + exactType = pTargetMD->GetExactDeclaringType(exactType.AsMethodTable()); + _ASSERTE(!exactType.IsNull()); + } + + pResult->contextHandle = MAKE_CLASSCONTEXT(exactType.AsPtr()); + if (exactType.IsSharedByGenericInstantiations()) + { + pResult->exactContextNeedsRuntimeLookup = TRUE; + } + } + if (directCall) { // Direct calls to abstract methods are not allowed From 51649662f7e0f1acf33c818ac9ba1a81f818cfec Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 14:38:12 +0100 Subject: [PATCH 2/8] Update JIT-EE GUID --- src/coreclr/inc/jiteeversionguid.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index 642b5838407f98..b1e3295a990e32 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* 8f2ee94f-5111-4b7b-97bd-5c689d476385 */ - 0x8f2ee94f, - 0x5111, - 0x4b7b, - {0x97, 0xbd, 0x5c, 0x68, 0x9d, 0x47, 0x63, 0x85} +constexpr GUID JITEEVersionIdentifier = { /* 8edbe247-3d6f-43ca-81de-648d358d36f4 */ + 0x8edbe247, + 0x3d6f, + 0x43ca, + {0x81, 0xde, 0x64, 0x8d, 0x35, 0x8d, 0x36, 0xf4} }; #endif // JIT_EE_VERSIONING_GUID_H From 1d0de49d335b76472fa4b452f74753f5bc639e5c Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 15:12:17 +0100 Subject: [PATCH 3/8] Fix build --- src/coreclr/interpreter/compiler.cpp | 41 +++++++++++++++------------- src/coreclr/interpreter/compiler.h | 1 - 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index b5924423564ee6..8fae59e677d8d9 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -4551,10 +4551,13 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re } else { + unsigned flags = CORINFO_CALLINFO_ALLOWINSTPARAM | CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_DISALLOW_STUB; + const uint8_t* callIP = m_ip; if (!newObj && m_methodInfo->args.isAsyncCall() && AsyncCallPeeps.FindAndApplyPeep(this)) { - resolvedCallToken = m_resolvedAsyncCallToken; + ResolveToken(token, CORINFO_TOKENKIND_Method, &resolvedCallToken); continuationContextHandling = m_currentContinuationContextHandling; + flags |= CORINFO_CALLINFO_ALLOWASYNCVARIANT; } else { @@ -4570,11 +4573,23 @@ void InterpCompiler::EmitCall(CORINFO_RESOLVED_TOKEN* pConstrainedToken, bool re m_compHnd->getNewHelper(resolvedCallToken.hClass, &hasSideEffects); } - CORINFO_CALLINFO_FLAGS flags = (CORINFO_CALLINFO_FLAGS)(CORINFO_CALLINFO_ALLOWINSTPARAM | CORINFO_CALLINFO_SECURITYCHECKS | CORINFO_CALLINFO_DISALLOW_STUB); if (isVirtual) - flags = (CORINFO_CALLINFO_FLAGS)(flags | CORINFO_CALLINFO_CALLVIRT); + flags |= CORINFO_CALLINFO_CALLVIRT; - m_compHnd->getCallInfo(&resolvedCallToken, pConstrainedToken, m_methodInfo->ftn, flags, &callInfo); + m_compHnd->getCallInfo(&resolvedCallToken, pConstrainedToken, m_methodInfo->ftn, (CORINFO_CALLINFO_FLAGS)flags, &callInfo); + + if (flags & CORINFO_CALLINFO_ALLOWASYNCVARIANT) + { + if (callInfo.resolvedAsyncVariant != NULL) + { + resolvedCallToken.hMethod = callInfo.resolvedAsyncVariant; + } + else + { + // Undo the IP change from the peep + m_ip = callIP + 5; + } + } if (callInfo.sig.isVarArg()) { @@ -6739,14 +6754,6 @@ int InterpCompiler::ApplyLdftnDelegateCtorPeep(const uint8_t* ip, OpcodePeepElem return -1; } -bool InterpCompiler::ResolveAsyncCallToken(const uint8_t* ip) -{ - CorInfoTokenKind tokenKind = - ip[0] == CEE_CALL ? CORINFO_TOKENKIND_Await : CORINFO_TOKENKIND_AwaitVirtual; - ResolveToken(getU4LittleEndian(ip + 1), tokenKind, &m_resolvedAsyncCallToken); - return m_resolvedAsyncCallToken.hMethod != NULL; -} - bool InterpCompiler::IsRuntimeAsyncCall(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) { CORINFO_RESOLVED_TOKEN awaitResolvedToken; @@ -6757,10 +6764,6 @@ bool InterpCompiler::IsRuntimeAsyncCall(const uint8_t* ip, OpcodePeepElement* pa return false; } - if (!ResolveAsyncCallToken(ip)) - { - return false; - } m_currentContinuationContextHandling = ContinuationContextHandling::ContinueOnCapturedContext; return true; } @@ -6825,7 +6828,7 @@ bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitTask(const uint8_t* ip, Opc return false; } - return ResolveAsyncCallToken(ip); + return true; } bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) @@ -6895,7 +6898,7 @@ bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTaskExactStLoc(const u return false; } - return ResolveAsyncCallToken(ip); + return true; } bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip, OpcodePeepElement* pattern, void** ppComputedInfo) @@ -6928,7 +6931,7 @@ bool InterpCompiler::IsRuntimeAsyncCallConfigureAwaitValueTask(const uint8_t* ip return false; } - return ResolveAsyncCallToken(ip); + 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 896642e275e3fe..5ab165b770757e 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -927,7 +927,6 @@ class InterpCompiler bool IsLdftnDelegateCtorPeep(const uint8_t* ip, OpcodePeepElement* peep, void** outComputedInfo); int ApplyLdftnDelegateCtorPeep(const uint8_t* ip, OpcodePeepElement* peep, void* computedInfo); - bool ResolveAsyncCallToken(const uint8_t* ip); enum class ContinuationContextHandling : uint8_t { ContinueOnCapturedContext, From c85622b5e7b91273e17caeb85bfae4bb28455465 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 15:28:15 +0100 Subject: [PATCH 4/8] Implement in ilc --- .../tools/Common/JitInterface/CorInfoImpl.cs | 53 +++------ .../tools/Common/JitInterface/CorInfoTypes.cs | 8 +- .../JitInterface/CorInfoImpl.RyuJit.cs | 102 +++++++++++------- src/coreclr/vm/jitinterface.cpp | 6 +- 4 files changed, 82 insertions(+), 87 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index f82c3914a4cf57..5e8a77672319d7 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1778,10 +1778,18 @@ private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pRe if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_Newarr) result = ((TypeDesc)result).MakeArrayType(); - if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await or CorInfoTokenKind.CORINFO_TOKENKIND_AwaitVirtual) + return result; + } + + private MethodDesc GetRuntimeDeterminedMethodForTokenWithTemplate(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc templateMethod) + { + object result = GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + Debug.Assert(result is MethodDesc); + + if (templateMethod.IsAsyncVariant()) result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); - return result; + return (MethodDesc)result; } private static object GetRuntimeDeterminedObjectForToken(MethodILScope methodIL, object typeOrMethodContext, mdToken token) @@ -1868,45 +1876,8 @@ private void resolveToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) _compilation.NodeFactory.MetadataManager.GetDependenciesDueToAccess(ref _additionalDependencies, _compilation.NodeFactory, (MethodIL)methodIL, method); #endif - if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await or CorInfoTokenKind.CORINFO_TOKENKIND_AwaitVirtual) - { - // in rare cases a method that returns Task is not actually TaskReturning (i.e. returns T). - // we cannot resolve to an Async variant in such case. - // return NULL, so that caller would re-resolve as a regular method call - bool allowAsyncVariant = method.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask(); - - // 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 !READYTORUN - if (allowAsyncVariant) - { - bool isDirect = pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_Await || method.IsCallEffectivelyDirect(); - if (isDirect && !method.IsAsync) - { - // Async variant would be a thunk. Do not resolve direct calls - // to async thunks. That just creates and JITs unnecessary - // thunks, and the thunks are harder for the JIT to optimize. - allowAsyncVariant = false; - } - } -#endif - - method = allowAsyncVariant - ? _compilation.TypeSystemContext.GetAsyncVariantMethod(method) - : null; - } - - if (method != null) - { - pResolvedToken.hMethod = ObjectToHandle(method); - pResolvedToken.hClass = ObjectToHandle(method.OwningType); - } - else - { - pResolvedToken.hMethod = null; - pResolvedToken.hClass = null; - } + pResolvedToken.hMethod = ObjectToHandle(method); + pResolvedToken.hClass = ObjectToHandle(method.OwningType); } else if (result is FieldDesc) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index 18e6f0092df510..905da8eccd4557 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -411,7 +411,7 @@ public enum CORINFO_CALLINFO_FLAGS CORINFO_CALLINFO_NONE = 0x0000, CORINFO_CALLINFO_ALLOWINSTPARAM = 0x0001, // Can the compiler generate code to pass an instantiation parameters? Simple compilers should not use this flag CORINFO_CALLINFO_CALLVIRT = 0x0002, // Is it a virtual call? - // UNUSED = 0x0004, + CORINFO_CALLINFO_ALLOWASYNCVARIANT = 0x0004, // allow resolution to an async variant CORINFO_CALLINFO_DISALLOW_STUB = 0x0008, // Do not use a stub for this call, even if it is a virtual call. CORINFO_CALLINFO_SECURITYCHECKS = 0x0010, // Perform security checks. CORINFO_CALLINFO_LDFTN = 0x0020, // Resolving target of LDFTN @@ -1146,6 +1146,8 @@ public unsafe struct CORINFO_CALL_INFO public byte _wrapperDelegateInvoke; public bool wrapperDelegateInvoke { get { return _wrapperDelegateInvoke != 0; } set { _wrapperDelegateInvoke = value ? (byte)1 : (byte)0; } } + + public CORINFO_METHOD_STRUCT_* resolvedAsyncVariant; } public enum CORINFO_DEVIRTUALIZATION_DETAIL @@ -1477,10 +1479,6 @@ public enum CorInfoTokenKind // token comes from resolved static virtual method CORINFO_TOKENKIND_ResolvedStaticVirtualMethod = 0x1000 | CORINFO_TOKENKIND_Method, - - // token comes from runtime async awaiting pattern - CORINFO_TOKENKIND_Await = 0x2000 | CORINFO_TOKENKIND_Method, - CORINFO_TOKENKIND_AwaitVirtual = 0x4000 | CORINFO_TOKENKIND_Method, }; // These are error codes returned by CompileMethod diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs index 052d6572e481fa..b2694fa3b1d1f6 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs @@ -374,7 +374,7 @@ private void getReadyToRunDelegateCtorHelper(ref CORINFO_RESOLVED_TOKEN pTargetM MethodDesc expectedTargetMethod = HandleToObject(pTargetMethod.hMethod); TypeDesc delegateTypeDesc = HandleToObject(delegateType); - MethodDesc targetMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pTargetMethod); + MethodDesc targetMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pTargetMethod, expectedTargetMethod); // If this was a constrained+ldftn sequence, we need to resolve the constraint TypeDesc constrainedType = null; @@ -1301,40 +1301,11 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO } } - MethodDesc targetMethod = methodAfterConstraintResolution; - // // Initialize callee context used for inlining and instantiation arguments // - - if (targetMethod.HasInstantiation) - { - pResult->contextHandle = contextFromMethod(targetMethod); - pResult->exactContextNeedsRuntimeLookup = targetMethod.IsSharedByGenericInstantiations; - } - else - { - pResult->contextHandle = contextFromType(exactType); - pResult->exactContextNeedsRuntimeLookup = exactType.IsCanonicalSubtype(CanonicalFormKind.Any); - - // Use main method as the context as long as the methods are called on the same type - if (pResult->exactContextNeedsRuntimeLookup && - pResolvedToken.tokenContext == contextFromMethodBeingCompiled() && - constrainedType == null && - exactType == MethodBeingCompiled.OwningType && - // But don't allow inlining into generic methods since the generic context won't be the same. - // The scanner won't be able to predict such inlinig. See https://github.com/dotnet/runtimelab/pull/489 - !MethodBeingCompiled.HasInstantiation) - { - var methodIL = (MethodIL)HandleToObject((void*)pResolvedToken.tokenScope); - var rawMethod = (MethodDesc)methodIL.GetMethodILDefinition().GetObject((int)pResolvedToken.token); - if (IsTypeSpecForTypicalInstantiation(rawMethod.OwningType)) - { - pResult->contextHandle = contextFromMethodBeingCompiled(); - } - } - } + MethodDesc targetMethod = methodAfterConstraintResolution; // // Determine whether to perform direct call @@ -1382,6 +1353,60 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO } } + // See if we can resolve to an async variant. Task-returning methods may + // not always have such a variant (for a T return where T is task, for + // example). + pResult->resolvedAsyncVariant = null; + if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_ALLOWASYNCVARIANT) != 0 && + method.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask() && + !method.OwningType.IsDelegate) + { + if (!directCall || targetMethod.IsAsync) + { + // Either a virtual call or a direct call where the async variant + // is user-defined. Switch to the async variant in these cases. + if (method == targetMethod) + { + method = targetMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(method); + } + else + { + method = _compilation.TypeSystemContext.GetAsyncVariantMethod(method); + targetMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(targetMethod); + } + + pResult->resolvedAsyncVariant = ObjectToHandle(method); + } + } + + if (targetMethod.HasInstantiation) + { + pResult->contextHandle = contextFromMethod(targetMethod); + pResult->exactContextNeedsRuntimeLookup = targetMethod.IsSharedByGenericInstantiations; + } + else + { + pResult->contextHandle = contextFromType(exactType); + pResult->exactContextNeedsRuntimeLookup = exactType.IsCanonicalSubtype(CanonicalFormKind.Any); + + // Use main method as the context as long as the methods are called on the same type + if (pResult->exactContextNeedsRuntimeLookup && + pResolvedToken.tokenContext == contextFromMethodBeingCompiled() && + constrainedType == null && + exactType == MethodBeingCompiled.OwningType && + // But don't allow inlining into generic methods since the generic context won't be the same. + // The scanner won't be able to predict such inlinig. See https://github.com/dotnet/runtimelab/pull/489 + !MethodBeingCompiled.HasInstantiation) + { + var methodIL = (MethodIL)HandleToObject((void*)pResolvedToken.tokenScope); + var rawMethod = (MethodDesc)methodIL.GetMethodILDefinition().GetObject((int)pResolvedToken.token); + if (IsTypeSpecForTypicalInstantiation(rawMethod.OwningType)) + { + pResult->contextHandle = contextFromMethodBeingCompiled(); + } + } + } + pResult->codePointerOrStubLookup.lookupKind.needsRuntimeLookup = false; bool allowInstParam = (flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_ALLOWINSTPARAM) != 0; @@ -1407,7 +1432,8 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO ReadyToRunHelperId lookupHelper; if (forceUseRuntimeLookup) { - MethodDesc runtimeDeterminedInterfaceMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + MethodDesc runtimeDeterminedInterfaceMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); + targetOfLookup = new ConstrainedCallInfo(runtimeDeterminedConstrainedType, runtimeDeterminedInterfaceMethod); lookupHelper = ReadyToRunHelperId.ConstrainedDirectCall; } @@ -1426,7 +1452,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO lookupMethod = targetMethod.GetMethodDefinition(); if (lookupMethod.HasInstantiation) { - var methodToGetInstantiation = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + var methodToGetInstantiation = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); lookupMethod = lookupMethod.MakeInstantiatedMethod(methodToGetInstantiation.Instantiation); } Debug.Assert(lookupMethod.GetCanonMethodTarget(CanonicalFormKind.Specific) == targetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)); @@ -1462,7 +1488,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->codePointerOrStubLookup.runtimeLookup.indirections = CORINFO.USEHELPER; pResult->codePointerOrStubLookup.lookupKind.runtimeLookupKind = GetGenericRuntimeLookupKind(HandleToObject(callerHandle)); pResult->codePointerOrStubLookup.lookupKind.runtimeLookupFlags = (ushort)ReadyToRunHelperId.MethodEntry; - pResult->codePointerOrStubLookup.lookupKind.runtimeLookupArgs = (void*)ObjectToHandle(GetRuntimeDeterminedObjectForToken(ref pResolvedToken)); + pResult->codePointerOrStubLookup.lookupKind.runtimeLookupArgs = (void*)ObjectToHandle(GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method)); } else { @@ -1566,7 +1592,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->kind = CORINFO_CALL_KIND.CORINFO_CALL_CODE_POINTER; TypeDesc runtimeDeterminedConstrainedType = (TypeDesc)GetRuntimeDeterminedObjectForToken(ref *pConstrainedResolvedToken); - MethodDesc runtimeDeterminedInterfaceMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + MethodDesc runtimeDeterminedInterfaceMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); var constrainedCallInfo = new ConstrainedCallInfo(runtimeDeterminedConstrainedType, runtimeDeterminedInterfaceMethod); var constrainedHelperId = ReadyToRunHelperId.ConstrainedDirectCall; @@ -1593,7 +1619,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->codePointerOrStubLookup.constLookup.accessType = InfoAccessType.IAT_VALUE; pResult->nullInstanceCheck = true; - MethodDesc targetOfLookup = _compilation.GetTargetOfGenericVirtualMethodCall((MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken)); + MethodDesc targetOfLookup = _compilation.GetTargetOfGenericVirtualMethodCall((MethodDesc)GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method)); _compilation.DetectGenericCycles( ((MethodILScope)HandleToObject((void*)pResolvedToken.tokenScope)).OwningMethod, @@ -1618,7 +1644,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO if (pResult->exactContextNeedsRuntimeLookup) { ComputeLookup(ref pResolvedToken, - GetRuntimeDeterminedObjectForToken(ref pResolvedToken), + GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method), ReadyToRunHelperId.VirtualDispatchCell, HandleToObject(callerHandle), ref pResult->codePointerOrStubLookup); @@ -1750,7 +1776,7 @@ private void embedGenericHandle(ref CORINFO_RESOLVED_TOKEN pResolvedToken, bool helperId = ReadyToRunHelperId.MethodDictionary; } - target = GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + target = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, md); } else if (!fEmbedParent && pResolvedToken.hField != null) { diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 17998cf1a26ddd..979d85d0b0117e 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -4958,7 +4958,6 @@ void CEEInfo::getCallInfo( INDEBUG(memset(pResult, 0xCC, sizeof(*pResult))); pResult->stubLookup.lookupKind.needsRuntimeLookup = false; - pResult->resolvedAsyncVariant = NULL; MethodDesc* pMD = (MethodDesc *)pResolvedToken->hMethod; TypeHandle th(pResolvedToken->hClass); @@ -5176,7 +5175,8 @@ void CEEInfo::getCallInfo( // See if we can resolve to an async variant. Task-returning methods may // not always have such a variant (for a T return where T is task, for // example). - if ((flags & CORINFO_CALLINFO_ALLOWASYNCVARIANT) && pTargetMD->ReturnsTaskOrValueTask()) + pResult->resolvedAsyncVariant = NULL; + if ((flags & CORINFO_CALLINFO_ALLOWASYNCVARIANT) && pMD->ReturnsTaskOrValueTask()) { if (!directCall || pTargetMD->IsAsyncThunkMethod()) { @@ -5184,7 +5184,7 @@ void CEEInfo::getCallInfo( // is user-defined. Switch to the async variant in these cases. if (pMD == pTargetMD) { - pMD = pTargetMD = pMD->GetAsyncVariant(/*allowInstParam */ FALSE); + pMD = pTargetMD = pMD->GetAsyncVariant(/* allowInstParam */ FALSE); } else { From f9224d454caa88be378ecd330208929bcc6fbde7 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 15:53:10 +0100 Subject: [PATCH 5/8] Fix when tokens refer directly to async variants --- .../tools/Common/JitInterface/CorInfoImpl.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 5e8a77672319d7..2e8d771ec9e6ea 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1763,6 +1763,19 @@ private static object ResolveTokenInScope(MethodILScope methodIL, object typeOrM return result; } + private MethodDesc GetRuntimeDeterminedMethodForTokenWithTemplate(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc templateMethod) + { + object result = GetRuntimeDeterminedObjectForToken(ref pResolvedToken); + Debug.Assert(result is MethodDesc); + + if (templateMethod.IsAsyncVariant() && !((MethodDesc)result).IsAsyncVariant()) + { + result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); + } + + return (MethodDesc)result; + } + private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) { // Since RyuJIT operates on canonical types (as opposed to runtime determined ones), but the @@ -1781,17 +1794,6 @@ private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pRe return result; } - private MethodDesc GetRuntimeDeterminedMethodForTokenWithTemplate(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc templateMethod) - { - object result = GetRuntimeDeterminedObjectForToken(ref pResolvedToken); - Debug.Assert(result is MethodDesc); - - if (templateMethod.IsAsyncVariant()) - result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); - - return (MethodDesc)result; - } - private static object GetRuntimeDeterminedObjectForToken(MethodILScope methodIL, object typeOrMethodContext, mdToken token) { object result = ResolveTokenInScope(methodIL, typeOrMethodContext, token); From eb135f9d27dcac737f05afa5359d8506043b9445 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 16:01:48 +0100 Subject: [PATCH 6/8] Update ILScanner --- .../IL/ILImporter.Scanner.cs | 113 ++++++++---------- 1 file changed, 49 insertions(+), 64 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 4394d08ad8240b..ad7c175edb94b4 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -449,60 +449,6 @@ private void ImportCall(ILOpcode opcode, int token) Debug.Assert(false); break; } - // Are we scanning a call within a state machine? - if (opcode is ILOpcode.call or ILOpcode.callvirt - && _canonMethod.IsAsyncCall()) - { - // Add dependencies on infra to do suspend/resume. We only need to do this once per method scanned. - if (!_asyncDependenciesReported && method.IsAsync) - { - _asyncDependenciesReported = true; - - const string asyncReason = "Async state machine"; - - var resumptionStub = new AsyncResumptionStub(_canonMethod, _compilation.TypeSystemContext.GeneratedAssembly.GetGlobalModuleType()); - _dependencies.Add(_compilation.NodeFactory.MethodEntrypoint(resumptionStub), asyncReason); - - _dependencies.Add(_factory.ConstructedTypeSymbol(_compilation.TypeSystemContext.ContinuationType), asyncReason); - - DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8); - - _dependencies.Add(_compilation.GetHelperEntrypoint(ReadyToRunHelper.AllocContinuation), asyncReason); - _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason); - _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null)), asyncReason); - _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason); - _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContextsOnSuspension"u8, null)), asyncReason); - } - - // If this is the task await pattern, we're actually going to call the variant - // so switch our focus to the variant. - - // in rare cases a method that returns Task is not actually TaskReturning (i.e. returns T). - // we cannot resolve to an Async variant in such case. - bool allowAsyncVariant = method.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask(); - - // 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) - { - bool isDirect = opcode == ILOpcode.call || method.IsCallEffectivelyDirect(); - if (isDirect && !method.IsAsync) - { - // Async variant would be a thunk. Do not resolve direct calls - // to async thunks. That just creates and JITs unnecessary - // thunks, and the thunks are harder for the JIT to optimize. - allowAsyncVariant = false; - } - } - - if (allowAsyncVariant && MatchTaskAwaitPattern()) - { - runtimeDeterminedMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(runtimeDeterminedMethod); - method = _factory.TypeSystemContext.GetAsyncVariantMethod(method); - } - } - if (opcode == ILOpcode.newobj) { TypeDesc owningType = runtimeDeterminedMethod.OwningType; @@ -671,16 +617,6 @@ private void ImportCall(ILOpcode opcode, int token) MethodDesc targetMethod = methodAfterConstraintResolution; - bool exactContextNeedsRuntimeLookup; - if (targetMethod.HasInstantiation) - { - exactContextNeedsRuntimeLookup = targetMethod.IsSharedByGenericInstantiations; - } - else - { - exactContextNeedsRuntimeLookup = exactType.IsCanonicalSubtype(CanonicalFormKind.Any); - } - // // Determine whether to perform direct call // @@ -712,6 +648,55 @@ private void ImportCall(ILOpcode opcode, int token) } } + // Are we scanning a call within a state machine? + if (opcode is ILOpcode.call or ILOpcode.callvirt && _canonMethod.IsAsyncCall()) + { + if (MatchTaskAwaitPattern() + && method.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask() && + !method.OwningType.IsDelegate) + { + if (!directCall || targetMethod.IsAsync) + { + // Either a virtual call or a direct call where the async variant + // is user-defined. Switch to the async variant in these cases. + method = _compilation.TypeSystemContext.GetAsyncVariantMethod(method); + targetMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(targetMethod); + runtimeDeterminedMethod = _factory.TypeSystemContext.GetAsyncVariantMethod(runtimeDeterminedMethod); + } + } + + // Add dependencies on infra to do suspend/resume. We only need to do this once per method scanned. + if (!_asyncDependenciesReported && method.IsAsync) + { + _asyncDependenciesReported = true; + + const string asyncReason = "Async state machine"; + + var resumptionStub = new AsyncResumptionStub(_canonMethod, _compilation.TypeSystemContext.GeneratedAssembly.GetGlobalModuleType()); + _dependencies.Add(_compilation.NodeFactory.MethodEntrypoint(resumptionStub), asyncReason); + + _dependencies.Add(_factory.ConstructedTypeSymbol(_compilation.TypeSystemContext.ContinuationType), asyncReason); + + DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8); + + _dependencies.Add(_compilation.GetHelperEntrypoint(ReadyToRunHelper.AllocContinuation), asyncReason); + _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason); + _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null)), asyncReason); + _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason); + _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContextsOnSuspension"u8, null)), asyncReason); + } + } + + bool exactContextNeedsRuntimeLookup; + if (targetMethod.HasInstantiation) + { + exactContextNeedsRuntimeLookup = targetMethod.IsSharedByGenericInstantiations; + } + else + { + exactContextNeedsRuntimeLookup = exactType.IsCanonicalSubtype(CanonicalFormKind.Any); + } + if (directCall && targetMethod.IsAbstract) { ThrowHelper.ThrowBadImageFormatException(); From 29ba4aaea9321b42882a3a7b704d99d33f67e7a2 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 18 Feb 2026 17:27:38 +0100 Subject: [PATCH 7/8] Update crossgen2 getCallInfo too --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 79 +++++++++++++------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 42fb1148659daf..d5d940c6593f15 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2035,32 +2035,6 @@ private void ceeInfoGetCallInfo( // targetMethod = methodAfterConstraintResolution; - bool constrainedTypeNeedsRuntimeLookup = (constrainedType != null && constrainedType.IsCanonicalSubtype(CanonicalFormKind.Any)); - - if (targetMethod.HasInstantiation) - { - pResult->contextHandle = contextFromMethod(targetMethod); - pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || targetMethod.IsSharedByGenericInstantiations; - } - else - { - pResult->contextHandle = contextFromType(exactType); - pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || exactType.IsCanonicalSubtype(CanonicalFormKind.Any); - - // Use main method as the context as long as the methods are called on the same type - if (pResult->exactContextNeedsRuntimeLookup && - pResolvedToken.tokenContext == contextFromMethodBeingCompiled() && - constrainedType == null && - exactType == MethodBeingCompiled.OwningType) - { - var methodIL = HandleToObject(pResolvedToken.tokenScope); - var rawMethod = (MethodDesc)methodIL.GetMethodILScopeDefinition().GetObject((int)pResolvedToken.token); - if (IsTypeSpecForTypicalInstantiation(rawMethod.OwningType)) - { - pResult->contextHandle = contextFromMethodBeingCompiled(); - } - } - } // // Determine whether to perform direct call @@ -2137,6 +2111,59 @@ private void ceeInfoGetCallInfo( } } + // See if we can resolve to an async variant. Task-returning methods may + // not always have such a variant (for a T return where T is task, for + // example). + pResult->resolvedAsyncVariant = null; + if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_ALLOWASYNCVARIANT) != 0 && + originalMethod.GetTypicalMethodDefinition().Signature.ReturnsTaskOrValueTask() && + !originalMethod.OwningType.IsDelegate) + { + if (!directCall || targetMethod.IsAsync) + { + // Either a virtual call or a direct call where the async variant + // is user-defined. Switch to the async variant in these cases. + if (originalMethod == targetMethod) + { + originalMethod = targetMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(originalMethod); + } + else + { + originalMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(originalMethod); + targetMethod = _compilation.TypeSystemContext.GetAsyncVariantMethod(targetMethod); + } + + pResult->resolvedAsyncVariant = ObjectToHandle(originalMethod); + } + } + + bool constrainedTypeNeedsRuntimeLookup = (constrainedType != null && constrainedType.IsCanonicalSubtype(CanonicalFormKind.Any)); + + if (targetMethod.HasInstantiation) + { + pResult->contextHandle = contextFromMethod(targetMethod); + pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || targetMethod.IsSharedByGenericInstantiations; + } + else + { + pResult->contextHandle = contextFromType(exactType); + pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || exactType.IsCanonicalSubtype(CanonicalFormKind.Any); + + // Use main method as the context as long as the methods are called on the same type + if (pResult->exactContextNeedsRuntimeLookup && + pResolvedToken.tokenContext == contextFromMethodBeingCompiled() && + constrainedType == null && + exactType == MethodBeingCompiled.OwningType) + { + var methodIL = HandleToObject(pResolvedToken.tokenScope); + var rawMethod = (MethodDesc)methodIL.GetMethodILScopeDefinition().GetObject((int)pResolvedToken.token); + if (IsTypeSpecForTypicalInstantiation(rawMethod.OwningType)) + { + pResult->contextHandle = contextFromMethodBeingCompiled(); + } + } + } + methodToCall = targetMethod; bool isArrayConstructor = targetMethod.OwningType.IsArray && targetMethod.IsConstructor; MethodDesc canonMethod = (isArrayConstructor ? null : targetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)); From 88be40f133da4e84034248d20fb9ca136bf2c0e8 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 19 Feb 2026 11:47:07 +0100 Subject: [PATCH 8/8] Feedback --- .../tools/Common/JitInterface/CorInfoImpl.cs | 17 +++-------------- .../JitInterface/CorInfoImpl.RyuJit.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 2e8d771ec9e6ea..030872f114f692 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -1763,20 +1763,7 @@ private static object ResolveTokenInScope(MethodILScope methodIL, object typeOrM return result; } - private MethodDesc GetRuntimeDeterminedMethodForTokenWithTemplate(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc templateMethod) - { - object result = GetRuntimeDeterminedObjectForToken(ref pResolvedToken); - Debug.Assert(result is MethodDesc); - - if (templateMethod.IsAsyncVariant() && !((MethodDesc)result).IsAsyncVariant()) - { - result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); - } - - return (MethodDesc)result; - } - - private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken) + private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, bool isAsyncVariant = false) { // Since RyuJIT operates on canonical types (as opposed to runtime determined ones), but the // dependency analysis operates on runtime determined ones, we convert the resolved token @@ -1790,6 +1777,8 @@ private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pRe object result = GetRuntimeDeterminedObjectForToken(methodIL, typeOrMethodContext, pResolvedToken.token); if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_Newarr) result = ((TypeDesc)result).MakeArrayType(); + if (isAsyncVariant && !((MethodDesc)result).IsAsyncVariant()) + result = _compilation.TypeSystemContext.GetAsyncVariantMethod((MethodDesc)result); return result; } diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs index b2694fa3b1d1f6..4fd9c3385038ec 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs @@ -374,7 +374,7 @@ private void getReadyToRunDelegateCtorHelper(ref CORINFO_RESOLVED_TOKEN pTargetM MethodDesc expectedTargetMethod = HandleToObject(pTargetMethod.hMethod); TypeDesc delegateTypeDesc = HandleToObject(delegateType); - MethodDesc targetMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pTargetMethod, expectedTargetMethod); + MethodDesc targetMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pTargetMethod, expectedTargetMethod.IsAsyncVariant()); // If this was a constrained+ldftn sequence, we need to resolve the constraint TypeDesc constrainedType = null; @@ -1432,7 +1432,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO ReadyToRunHelperId lookupHelper; if (forceUseRuntimeLookup) { - MethodDesc runtimeDeterminedInterfaceMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); + MethodDesc runtimeDeterminedInterfaceMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null); targetOfLookup = new ConstrainedCallInfo(runtimeDeterminedConstrainedType, runtimeDeterminedInterfaceMethod); lookupHelper = ReadyToRunHelperId.ConstrainedDirectCall; @@ -1452,7 +1452,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO lookupMethod = targetMethod.GetMethodDefinition(); if (lookupMethod.HasInstantiation) { - var methodToGetInstantiation = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); + var methodToGetInstantiation = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null); lookupMethod = lookupMethod.MakeInstantiatedMethod(methodToGetInstantiation.Instantiation); } Debug.Assert(lookupMethod.GetCanonMethodTarget(CanonicalFormKind.Specific) == targetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)); @@ -1488,7 +1488,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->codePointerOrStubLookup.runtimeLookup.indirections = CORINFO.USEHELPER; pResult->codePointerOrStubLookup.lookupKind.runtimeLookupKind = GetGenericRuntimeLookupKind(HandleToObject(callerHandle)); pResult->codePointerOrStubLookup.lookupKind.runtimeLookupFlags = (ushort)ReadyToRunHelperId.MethodEntry; - pResult->codePointerOrStubLookup.lookupKind.runtimeLookupArgs = (void*)ObjectToHandle(GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method)); + pResult->codePointerOrStubLookup.lookupKind.runtimeLookupArgs = (void*)ObjectToHandle(GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null)); } else { @@ -1592,7 +1592,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->kind = CORINFO_CALL_KIND.CORINFO_CALL_CODE_POINTER; TypeDesc runtimeDeterminedConstrainedType = (TypeDesc)GetRuntimeDeterminedObjectForToken(ref *pConstrainedResolvedToken); - MethodDesc runtimeDeterminedInterfaceMethod = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method); + MethodDesc runtimeDeterminedInterfaceMethod = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null); var constrainedCallInfo = new ConstrainedCallInfo(runtimeDeterminedConstrainedType, runtimeDeterminedInterfaceMethod); var constrainedHelperId = ReadyToRunHelperId.ConstrainedDirectCall; @@ -1619,7 +1619,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO pResult->codePointerOrStubLookup.constLookup.accessType = InfoAccessType.IAT_VALUE; pResult->nullInstanceCheck = true; - MethodDesc targetOfLookup = _compilation.GetTargetOfGenericVirtualMethodCall((MethodDesc)GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method)); + MethodDesc targetOfLookup = _compilation.GetTargetOfGenericVirtualMethodCall((MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null)); _compilation.DetectGenericCycles( ((MethodILScope)HandleToObject((void*)pResolvedToken.tokenScope)).OwningMethod, @@ -1644,7 +1644,7 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO if (pResult->exactContextNeedsRuntimeLookup) { ComputeLookup(ref pResolvedToken, - GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, method), + (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, pResult->resolvedAsyncVariant != null), ReadyToRunHelperId.VirtualDispatchCell, HandleToObject(callerHandle), ref pResult->codePointerOrStubLookup); @@ -1776,7 +1776,7 @@ private void embedGenericHandle(ref CORINFO_RESOLVED_TOKEN pResolvedToken, bool helperId = ReadyToRunHelperId.MethodDictionary; } - target = GetRuntimeDeterminedMethodForTokenWithTemplate(ref pResolvedToken, md); + target = (MethodDesc)GetRuntimeDeterminedObjectForToken(ref pResolvedToken, md.IsAsyncVariant()); } else if (!fEmbedParent && pResolvedToken.hField != null) {