Skip to content

Move async variant resolution into getCallInfo#124550

Open
jakobbotsch wants to merge 8 commits intodotnet:mainfrom
jakobbotsch:fix-static-virtuals
Open

Move async variant resolution into getCallInfo#124550
jakobbotsch wants to merge 8 commits intodotnet:mainfrom
jakobbotsch:fix-static-virtuals

Conversation

@jakobbotsch
Copy link
Member

@jakobbotsch jakobbotsch commented Feb 18, 2026

  • Remove await CORINFO_TOKENKINDs
  • Add CORINFO_CALLINFO_ALLOWASYNCVARIANT flag and pass this for getCallInfo
  • Teach getCallInfo to resolve async variants
  • Reorder some code in getCallInfo so that we can know if an await is direct or not at the time where we resolve async variants

Another benefit is that we now automatically take IsEffectivelySealed into account in NAOT.

Fix #124545

Copilot AI review requested due to automatic review settings February 18, 2026 13:39
@github-actions github-actions bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Feb 18, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors async variant resolution by moving it from resolveToken into getCallInfo. This change fixes issue #124545 where static virtual calls were incorrectly going through task-returning thunks instead of calling the async implementation directly.

Changes:

  • Removes CORINFO_TOKENKIND_Await and CORINFO_TOKENKIND_AwaitVirtual token kinds from the JIT/EE interface
  • Adds CORINFO_CALLINFO_ALLOWASYNCVARIANT flag to request async variant resolution during getCallInfo
  • Moves async variant resolution logic from token resolution to call info resolution, ensuring it happens after direct call determination
  • Updates JIT code to pass the new flag when async patterns are detected
  • Replaces the combine helper function with proper C++ operators for flag manipulation

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/coreclr/inc/corinfo.h Removes Await token kinds, adds ALLOWASYNCVARIANT flag, adds resolvedAsyncVariant field to CORINFO_CALL_INFO
src/coreclr/inc/jiteeversionguid.h Updates JIT/EE interface GUID for the interface change
src/coreclr/vm/jitinterface.cpp Removes async variant resolution from resolveToken, adds it to getCallInfo after directCall determination
src/coreclr/jit/importer.cpp Updates call import logic to use new flag instead of special token kinds
src/coreclr/jit/importercalls.cpp Removes obsolete comment about CEE_CALLI async handling
src/coreclr/jit/morph.cpp Updates flag combining to use new operators
src/coreclr/jit/ee_il_dll.hpp Replaces combine function with proper C++ operators for CORINFO_CALLINFO_FLAGS

Copilot AI review requested due to automatic review settings February 18, 2026 15:01
Comment on lines 1766 to 1777
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;
}
Copy link
Member Author

@jakobbotsch jakobbotsch Feb 18, 2026

Choose a reason for hiding this comment

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

Not so sure about this. I am open to suggestions of alternatives. This is the replacement of the CorInfoTokenKind.CORINFO_TOKENKIND_Await that used to be in GetRuntimeDeterminedObjectForToken.

Copy link
Member

Choose a reason for hiding this comment

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

How about private object GetRuntimeDeterminedObjectForToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, bool isAsyncVariant = false) (add optional parameter to the existing method)?

The value of the bool is easy to pass in getCallInfo (we can just remember it) and elsewhere we populate it from whatever was considered the template.

Copy link
Member Author

Choose a reason for hiding this comment

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

Implemented that. Can you take another look?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment on lines +5179 to +5183
if ((flags & CORINFO_CALLINFO_ALLOWASYNCVARIANT) && pMD->ReturnsTaskOrValueTask())
{
if (!directCall || pTargetMD->IsAsyncThunkMethod())
{
// Either a virtual call or a direct call where the async variant
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

getCallInfo now resolves async variants when CORINFO_CALLINFO_ALLOWASYNCVARIANT is set, but (unlike the NativeAOT implementation) it doesn’t exclude delegate types. For Delegate.Invoke calls, resolving to an async variant can defeat the existing wrapperDelegateInvoke fast-path and may introduce unnecessary thunking. Consider adding a guard like !pMD->GetMethodTable()->IsDelegate() (or equivalent) before calling GetAsyncVariant.

Copilot uses AI. Check for mistakes.
@jakobbotsch
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch
Copy link
Member Author

Green CI including nativeaot-outerloop! But crossgen2 handling is missing so will push that.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Copy link
Member

@MichalStrehovsky MichalStrehovsky left a comment

Choose a reason for hiding this comment

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

The managed part LGTM otherwise!

result = ((TypeDesc)result).MakeArrayType();

if (pResolvedToken.tokenType is CorInfoTokenKind.CORINFO_TOKENKIND_Await or CorInfoTokenKind.CORINFO_TOKENKIND_AwaitVirtual)
if (isAsyncVariant && !((MethodDesc)result).IsAsyncVariant())
Copy link
Member

Choose a reason for hiding this comment

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

Is this part necessary?

Suggested change
if (isAsyncVariant && !((MethodDesc)result).IsAsyncVariant())
if (isAsyncVariant)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it is needed since the token may represent async variants directly for internal compiler-generated IL (like the thunks), so the result may already be the async variant for those cases. GetAsyncVariant asserts since they aren't task-returning in those cases.
(We won't hit that from the getCallInfo callers, but we can hit it from uses in other functions)

Comment on lines -6920 to -6924
else if (opcode == CEE_CALLI)
{
// Used for unboxing/instantiating stubs
JITDUMP("Call is an async calli\n");
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated cleanup, this is no longer needed.

@jakobbotsch
Copy link
Member Author

I think @VSadov is OOF. @jkotas can you take a look at the VM bits?
@davidwrighton can you take a look at the interpreter part? Also, do the async tests run with interpreter in CI or how do I test that?
cc @EgorBo for the JIT parts.

Copy link
Member

@EgorBo EgorBo left a comment

Choose a reason for hiding this comment

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

JIT lgtm

// 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;
Copy link
Member

@jkotas jkotas Feb 19, 2026

Choose a reason for hiding this comment

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

Why is this a separate CORINFO_METHOD_HANDLE field and not just returned in hMethod?

The idea with getCallInfo is that it tells the JIT what to call exactly. I assume that if the VM tells the JIT to call async variant, the JIT is better to call it. Is that correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

hMethod does return the async variant of the target method, and that's what the JIT calls, but that differs from the async variant of the token's method in cases of sharing or due to constrained calls. The JIT takes this CORINFO_METHOD_HANDLE and updates hMethod of the token with it since the token itself is passed back into the EE in various cases. That makes token match what was happening before when it was part of resolveToken (specifically so that the EE sees the same hMethod in that token).

I am not 100% sure what the EE sides end up using hMethod of the token for in those callbacks and whether this could be simplified or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose the primary one is that we call embedGenericHandle and pass it the token to compute the runtime lookup for an instantiation parameter, here:

instParam = impTokenToHandle(pResolvedToken, &runtimeLookup, true /*mustRestoreHandle*/);
if (instParam == nullptr)
{
assert(compDonotInline());
return TYP_UNDEF;
}

This works now because the JIT updates the token's hMethod with callInfo.resolvedAsyncVariant when it notices that the async variant was resolved.

Perhaps a cleaner way would be if getCallInfo directly returned the runtime lookup instead of this embed roundtrip. The current token vs callInfo.hMethod mismatch leads to other problems here, I guess this comment is referencing the same thing:

// If the target method is resolved via constrained static virtual dispatch
// And it requires an instParam, we do not have the generic dictionary infrastructure
// to load the correct generic context arg via EmbedGenericHandle.
// Instead, force the call to go down the CORINFO_CALL_CODE_POINTER code path
// which should have somewhat inferior performance. This should only actually happen in the case
// of shared generic code calling a shared generic implementation method, which should be rare.
//
// An alternative design would be to add a new generic dictionary entry kind to hold the MethodDesc
// of the constrained target instead, and use that in some circumstances; however, implementation of
// that design requires refactoring variuos parts of the JIT interface as well as
// TryResolveConstraintMethodApprox. In particular we would need to be abled to embed a constrained lookup
// via EmbedGenericHandle, as well as decide in TryResolveConstraintMethodApprox if the call can be made
// via a single use of CORINFO_CALL_CODE_POINTER, or would be better done with a CORINFO_CALL + embedded
// constrained generic handle, or if there is a case where we would want to use both a CORINFO_CALL and
// embedded constrained generic handle. Given the current expected high performance use case of this feature
// which is generic numerics which will always resolve to exact valuetypes, it is not expected that
// the complexity involved would be worth the risk. Other scenarios are not expected to be as performance
// sensitive.

Copy link
Member

Choose a reason for hiding this comment

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

How about we introduce CORINFO_TOKENKIND_Async, then delete CORINFO_METHOD_HANDLE resolvedAsyncVariant;, and keep using hMethod, but also set the async tokenType on the EE side whenever we return the async variant.

That way we'll keep clearly marking the token as "async variant token". It is weird that the EE side would change tokenType, but also JIT side changing hMethod is not exactly clean.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would work but it will be a new contract for getCallInfo (now has another output, requires SPMI changes).

It's probably a bit better but I am leaning towards making it so that we do not need the token anymore for non-trivial things after getCallInfo has been called by making it return the runtime lookup. It seems it is the only way to make things self-consistent.

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

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Static virtual calls go through task-returning thunks always

4 participants

Comments