Repository navigation
Conversation
|
Azure Pipelines: 16 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/tests/async/covariant-return/covariant-returns.cs:404
- PR metadata says "Fixes #124238", but the added coverage is currently marked [ActiveIssue] (and this PR doesn't include a runtime/product fix), so it doesn't actually fix the issue yet. Consider updating the PR title/description to reflect that this is a repro/coverage-only change until the runtime fix is included and the [ActiveIssue] attributes can be removed.
[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/124238")]
public static void TestCustomTaskOverride()
{
|
@copilot lets try fixing the runtime behavior
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/coreclr/vm/methodtablebuilder.cpp:2666
- PR description says the runtime fix is out of scope for this PR, but this change adds a runtime behavior change in MethodTableBuilder to synthesize async variants for covariant Task overrides. Please update the PR description/checklist (or split into separate PRs) so scope and CI expectations match the actual contents.
// Task and Task<T> are not sealed, thus a covariant override may return a type that derives from
// Task/Task<T>, while not being Task/Task<T> itself. Such a method is not Task-returning on its own,
// but the method that it overrides may well be. Since the overridden method has an Async variant,
// the override must have one as well, or it would not be able to override it.
//
src/coreclr/vm/methodtablebuilder.cpp:2704
- TryGetCovariantOverrideAsyncVariantReturnType bails out for MethodImpl declarations that are MemberRefs with a TypeSpec parent (instantiated generic type). That means covariant overrides of Task/Task-returning methods on closed generic base types won’t get an async variant and will likely keep the incorrect dispatch behavior this change is trying to address.
// The signature of a member of an instantiated generic type may refer to the generic
// parameters of that type. Such references do not have the same meaning in the scope of
// the overriding method, so we cannot reuse the signature. That case is not supported yet.
mdToken tkParent;
if (FAILED(pMDInternalImport->GetParentToken(tkDecl, &tkParent)) ||
(TypeFromToken(tkParent) == mdtTypeSpec))
{
return false;
}
|
@copilot is the failure on "windows-x64 Checked NativeAOT" related to this change? |
Yes. In build Note This reply was generated with GitHub Copilot. |
|
@copilot exclude the aot failing scenario on aot |
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
|
@jkotas - anything else that we need to address in this PR? |
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
|
@copilot please fix build failures |
The current build failures are unrelated to this PR. All four failing pipelines (1650332–1650335) report CS0115 in The fix is already open in #136497. No code changes or CI reruns made here; this PR needs the upstream fix before the merged CI tree can build. |
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Loader-time generic signature substitution and synthesized virtual async methods require specialist review and checked-runtime/ReadyToRun validation beyond what static inspection can establish.
No open issues 🎉
2 resolved since last review
Review effort: Balanced
|
@jkotas - seems no more issues that copilot could find. |
|
@copilot check the test failures. at least one seems related to this PR |
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Found PR-related failures and addressed them in
Validated Linux x64 Checked: baseline and async builds passed; all 149 runner entries passed in both ReadyToRun and non-ReadyToRun modes, including both isolated regressions. Review and secret scan passed. CodeQL was invoked but skipped C# analysis because the database was too large. The TLS failure is unrelated and already matched to #136560. The browser-WASM R2R crash is consistent with the reproduced compiler failure, but I cannot confirm its exact cause: the Helix console/dump storage hostname fails DNS resolution here. Browser-WASM execution was not validated locally. |
pcshrosbree
left a comment
There was a problem hiding this comment.
Two suggestions: drop TryGetDeclaringTypeInstantiation, whose non-empty result I only saw for types that then fail to load, and update three comments that still give the dropped reason for the thunk. I built 78f088d and its merge base 152ad96 locally (Release, linux-x64) and ran the cases below on both.
| // Returns false if the declaring type could not be reached without loading types | ||
| // (i.e. it is in another module) or if the case is not supported. | ||
| // | ||
| static bool TryGetDeclaringTypeInstantiation( |
There was a problem hiding this comment.
I think this helper could go. Its result is non-empty only when the declaration is a MethodDef on a generic type, and in my runs on this PR's build no type with such a declaration loaded.
A MethodImpl whose declaration is a MethodDef on a generic base failed type load when the loader looked up the declaration, whether or not the return was covariant. I assembled this with ilasm 10.0.0 (the class's constructor is left out here):
.class public auto ansi beforefieldinit E`1<T> extends class GBase`1<!T>
{
.method public hidebysig newslot virtual instance class MyTask`1<!T> M(!T t) cil managed
{
.override method instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> GBase`1::M(!0)
ldarg.1
newobj instance void class MyTask`1<!T>::.ctor(!0)
ret
}
}Creating an E<string> fails with Method override 'M' on type 'E`1' ... cannot find a method to replace. The same happens when the override returns Task<T>, and for a covariant override two generic extends hops below GBase<T>. The same class with .override ... class GBase`1<!T>::M(!0), a MemberRef on a TypeSpec, loads and the call reaches the override. So does a MethodDef declaration on a non-generic base. The merge base gives the same five results.
With an fprintf added locally to the helper, it returned a non-empty instantiation only for the one-hop and two-hop cases, and both types then failed to load. Across src/tests/async (149 passed, including this PR's two new tests), it ran 13 times, never walked a generic base, and always returned an empty instantiation.
In the three C# shapes I compiled with the 10.0.402 SDK, which mirror this PR's tests, the declaration for a generic base is a MemberRef on a TypeSpec. For example, MultiHopDerived<U> gets GBase<List<!0>[]>::M1. So the multi-hop test that was added for this loop goes through the MemberRef branch of TryGetCovariantOverrideAsyncVariantReturnType instead.
So the MethodDef branch only needs an empty instantiation, since a non-generic declaring type has no !n to substitute.
| // A special thunk for an override that covariantly returns a type derived from Task/Task<T>. | ||
| // Such a method does not formally return Task/Task<T>, so its IL cannot be compiled as an | ||
| // async version. The thunk calls the ordinary variant and awaits the returned Task instead. |
There was a problem hiding this comment.
Could this comment give the reason from the EmitCovariantForwardingThunk comment instead: a thunk means only covariant overrides need the extra variant? The same applies to methodtablebuilder.cpp at lines 3784-3785 and 3913-3915. All three still give the reason that the PR description and that comment dropped after the earlier discussion here.

Fixes covariant returns in runtime async scenarios where an override returns a type derived from
Task/Task<T>rather thanTask/Task<T>itself.Problem
TaskandTask<T>are not sealed, so a covariant override may return, say,MyTask : Task. Such a method is not task-returning as far as the runtime is concerned, so it got no Async variant and could not override the Async variant of the base method. As a result, when the call was made in a way that could become a runtime async call (e.g.await b.M1()), the base implementation ran instead of the override. The same code compiled without theruntime-asyncfeature dispatched correctly.Fix
methodtablebuilder.cpp— when enumerating class methods, detect a virtualMethodImplthat requires covariant return type checking and whose decl is task-returning. Such a method is now treated as task-returning and gets exactly one Async variant, whose return type is the element type of the overridden method (voidforTask,TforTask<T>). Type variables of the declaring type inTare substituted from theTypeSpecinstantiation of the decl, so generic types/methods, closed and composed base instantiations are handled.New
AsyncMethodFlags::CovariantForwardingThunk— this variant is always a thunk.The thunk's purpose is to switch to a different virtual slot and continue dispatching. The
MyTask- returning method may be further overriden, thus the thunk needs to containCALLVIRTto the non-async variant.The forwarding thunk was chosen over async version so that only the covarainly overriding methods may need another variant and we can continue mostly ignoring non-Task returning methods for async purposes (only covariant overrides have a special case and check if base returns a Task).
asyncthunks.cpp—EmitCovariantForwardingThunkemits the Async variant asCALLVIRTof the ordinary variant followed byAsyncHelpers.TransparentAwait/TransparentAwait<T>on the returned task.FindDeclMethodOnClassInHierarchy— useGetParallelMethodDescinstead ofGetAsyncVariant(); the latter may create anInstantiatedMethodDescand load types, which is not allowed while building a MethodTable (fires a contract violation on checked builds). Only the slot is needed.readytoruninfo.cpp— like return-dropping thunks, covariant forwarding thunks are VM-synthesized and share the token/signature shape of a regular async variant, so R2R lookup is skipped for them and the IL is generated transiently in the prestub.cDAC / data contract — the new flag is added to
AsyncMethodFlagsin the contract docs and readers, and the thunk is treated as diagnostics-hidden.Tests
src/tests/async/covariant-return/covariant-returns.csadds coverage for:MyTask/MyTask<int>, awaited directly and observed asTask/Task<T>objects,[RuntimeAsyncMethodGeneration(false)],Task<List<T>>/Task<T[]>element types, struct element types, closed (GBase<int>) and composed (GBase<List<U>>) base instantiations.Not in scope
NativeAOT / crossgen2 (AOT compilation of these thunks) is not fixed here. The test project is disabled for
TestBuildMode == nativeaot; at runtime, R2R lookup for these thunks is bypassed in favor of transient IL. Full AOT support is a follow-up.Mono does not support runtime async; the test project is disabled there.
Fixes [RuntimeAsync] Handle covariant returns scenarios in combination with task-returning methods. #124238