Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,54 @@ static void Validate(ReadyToRunReader reader)
}
}

/// <summary>
/// Composite + runtime-async caller awaiting a NON-runtime-async virtual callee that the JIT
/// devirtualizes to a sealed receiver. Resolving the callee's async-variant thunk must unwrap it
/// to the underlying EcmaMethod.
/// </summary>
[Fact]
public void CompositeAsyncDevirtNonAsyncCallee()
{
// Compiled WITHOUT runtime-async so the awaited virtuals get synthesized async-variant thunks.
var nonAsyncCalleeLib = new CompiledAssembly
{
AssemblyName = "AsyncDevirtNonAsyncCalleeLib",
SourceResourceNames = ["RuntimeAsync/Dependencies/AsyncDevirtNonAsyncCalleeLib.cs"],
};
var main = new CompiledAssembly
{
AssemblyName = "CompositeAsyncDevirtNonAsyncCalleeMain",
SourceResourceNames = ["RuntimeAsync/CompositeAsyncDevirtNonAsyncCalleeMain.cs"],
Comment thread
jtschuster marked this conversation as resolved.
Features = { RuntimeAsyncFeature },
References = [nonAsyncCalleeLib],
};

new R2RTestRunner(_output).Run(new R2RTestCase(
nameof(CompositeAsyncDevirtNonAsyncCallee),
[
new(nameof(CompositeAsyncDevirtNonAsyncCallee),
[
new CrossgenAssembly(nonAsyncCalleeLib),
new CrossgenAssembly(main),
])
{
Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize],
Validate = Validate,
},
]));

static void Validate(ReadyToRunReader reader)
{
string diag;
Assert.True(R2RAssert.HasManifestRef(reader, "AsyncDevirtNonAsyncCalleeLib", out diag), diag);

Assert.True(R2RAssert.HasAsyncVariant(reader, "WriterBase.CompleteValueTaskAsync(", out diag), diag);
Assert.True(R2RAssert.HasAsyncVariant(reader, "WriterBase.CompleteTaskAsync(", out diag), diag);
Assert.True(R2RAssert.HasAsyncVariant(reader, "AwaitInheritedValueTask(", out diag), diag);
Assert.True(R2RAssert.HasAsyncVariant(reader, "AwaitInheritedTask(", out diag), diag);
}
}

/// <summary>
/// Composite with 3 assemblies in A→B→C transitive chain.
/// Validates manifest refs for all three and transitive inlining.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Runtime-async caller for the composite devirt regression test. Awaits non-runtime-async virtuals
// (in AsyncDevirtNonAsyncCalleeLib) that the JIT devirtualizes to the sealed receiver, requesting the
// callee's synthesized async-variant thunk.
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public static class CompositeAsyncDevirtNonAsyncCallee
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task AwaitInheritedValueTask(Holder h)
{
await h.Writer.CompleteValueTaskAsync();
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task AwaitInheritedTask(Holder h)
{
await h.Writer.CompleteTaskAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Helper for the composite runtime-async devirt regression test. Compiled WITHOUT runtime-async, so
// WriterBase's virtuals are classic methods that get an "async variant" thunk when a runtime-async
// caller in another module awaits them. ConcreteWriter is sealed and doesn't override, and Holder
// exposes it base-typed, so the caller late-devirtualizes to the inherited base method.
using System.Threading.Tasks;

public class WriterBase
{
public virtual ValueTask CompleteValueTaskAsync() => default;

public virtual Task CompleteTaskAsync() => Task.CompletedTask;
}

public sealed class ConcreteWriter : WriterBase
{
}

public sealed class Holder
{
private readonly ConcreteWriter _writer = new ConcreteWriter();

public WriterBase Writer => _writer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1427,8 +1427,12 @@ ModuleToken _HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, Meth
|| (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_DevirtualizedMethod)
|| methodDesc.IsPInvoke))
{
// Unwrap synthetic MethodDesc wrappers (e.g. async-variant thunks) to the
// underlying metadata method before resolving its token. For a devirtualized
// callee, resolveVirtualMethod guarantees a real methoddef token in that
// method's own EcmaModule, so token and module are sourced consistently here.
if ((CorTokenType)(unchecked((uint)pResolvedToken.token) & 0xFF000000u) == CorTokenType.mdtMethodDef &&
methodDesc?.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod)
methodDesc?.GetPrimaryMethodDesc().GetTypicalMethodDefinition() is EcmaMethod ecmaMethod)
{
mdToken token = (mdToken)MetadataTokens.GetToken(ecmaMethod.Handle);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xunit;

// A runtime-async caller awaits a devirtualized call to a NON-runtime-async virtual method that a
// sealed receiver inherits without overriding. Resolving the callee's synthesized async-variant thunk
// must unwrap it to the underlying method instead of indexing the thunk's small token table with the
// callee's real methoddef token; otherwise composite ReadyToRun compilation corrupts token resolution
// and crossgen2 aborts.
public class Async2DevirtualizeInheritedNonAsync
{
public class WriterBase
{
// Non-runtime-async virtuals with bodies: awaited from a runtime-async caller via a thunk.
[RuntimeAsyncMethodGeneration(false)]
public virtual ValueTask CompleteValueTaskAsync() => default;

[RuntimeAsyncMethodGeneration(false)]
public virtual Task CompleteTaskAsync() => Task.CompletedTask;
}

// Sealed and does NOT override: late devirtualization resolves to the inherited base methods.
public sealed class ConcreteWriter : WriterBase
{
}

public sealed class Holder
{
private readonly ConcreteWriter _writer = new ConcreteWriter();

// Base-typed accessor over the sealed concrete type, so the exact receiver is only known via
// late devirtualization.
public WriterBase Writer => _writer;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task AwaitInheritedValueTask(Holder h)
{
await h.Writer.CompleteValueTaskAsync();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task AwaitInheritedTask(Holder h)
{
await h.Writer.CompleteTaskAsync();
}

[Fact]
public static void TestEntryPoint()
{
var h = new Holder();
AwaitInheritedValueTask(h).GetAwaiter().GetResult();
AwaitInheritedTask(h).GetAwaiter().GetResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
</ItemGroup>
</Project>
Loading