Skip to content

Enable compilation of async thunks in composite mode#126904

Open
jtschuster wants to merge 80 commits intodotnet:mainfrom
jtschuster:composite-async-variants
Open

Enable compilation of async thunks in composite mode#126904
jtschuster wants to merge 80 commits intodotnet:mainfrom
jtschuster:composite-async-variants

Conversation

@jtschuster
Copy link
Copy Markdown
Member

@jtschuster jtschuster commented Apr 14, 2026

The issue with async thunks in composite mode was that we emit MutableModule tokens for the thunks that point to some types in the composite image. But using MutableModule tokens pointing to anything but CoreLib is explicitly blocked - without an IL module to determine the ALC, assembly load behavior might not be preserved. To work around this, I went back to the approach where we add missing tokens to the mutable module and compile the thunk as an ILStub. Since the thunks should only need to reference existing type refs/defs accessible from the composite image + some async helpers in CoreLib, this shouldn't end up adding any tokens to the manifest module that point anywhere but CoreLib.

The process of going through an IL method body and creating tokens in the MutableModule is handled by the ExternalReferenceTokenManager (open to renames).

This implementation initially caused some issues when emitting signatures for Generic methods on generic types. In resolving a module token for a method on an IL stub (CorInfoImpl.HandleToModuleToken()), we strip the instantiation info and get a token for the method definition. The comment in that method assumed this is fine because when emitting the signature, we add flags and generic instantiation info for the method. But when MethodWithToken..ctor() calculates the owning type, it assumes the ModuleToken is for the generic instantiated method on a generic instantiated type (but we just stripped off the instantiation). To account for that, the code now will tell the MethodWithToken to force the OwningType to be MethodDesc.OwningType and force it to emit the OwningType flag it in the Signature.

fixes #125337

jtschuster and others added 30 commits March 26, 2026 19:12
* Add crossgen CI analysis agentic workflow

* Use Opus
* Add crossgen CI analysis agentic workflow

* Use Opus

* Fix DIFC integrity filtering and improve issue quality in crossgen2 CI triage

- Add min-integrity: none to tools.github so the agent can read all
  dotnet/runtime issues regardless of author association (fixes false
  positive 'new' issues when existing issues were invisible)
- Require specific fully qualified test names and verbatim error output
  in created issues instead of summaries
- Add pull-requests: read permission to fix compilation warning
- Recompile lock.yml

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a new test suite at src/tests/readytorun/crossmoduleresolution/ that tests
R2R cross-module reference resolution across different assembly categories:

Assembly graph:
- A (main) - compilation target
- B (version bubble, --inputbubbleref)
- C (cross-module-inlineable only, --opt-cross-module)
- D (external/transitive dependency)
- E (type forwarder to D)

Test scenarios cover:
- TypeRef, MethodCall, FieldAccess across version bubble and cross-module-only
- Transitive dependencies (C → D)
- Nested types, type forwarders, mixed-origin generics
- Interface dispatch with cross-module interfaces

Two test modes via separate .csproj files:
- main_crossmodule: --opt-cross-module:assemblyC (MutableModule #:N resolution)
- main_bubble: --inputbubbleref:assemblyB.dll (MODULE_ZAPSIG encoding)

Also adds CrossModuleResolutionTestPlan.md documenting all 5 planned phases
including composite mode, ALC, and programmatic R2R validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add async Task<T> and async Task variants of each cross-module
inlineable method in assemblyC, with corresponding test methods in
main.cs. Enable runtime-async compilation via Features=runtime-async=on
and --opt-async-methods crossgen2 flag in both test projects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add r2rvalidate tool using ILCompiler.Reflection.ReadyToRun to
programmatically verify R2R compilation artifacts:
- Assembly references (MSIL + manifest metadata AssemblyRef tables)
- CHECK_IL_BODY fixups proving cross-module inlining occurred
- RuntimeFunction counts confirming async thunk generation (3+)

Integrate validator into test precommands for both main_crossmodule
and main_bubble test variants. Validator runs after crossgen2 and
before the actual test execution.

For non-composite images, reads MSIL AssemblyRefs via
GetGlobalMetadata().MetadataReader (not ManifestReferenceAssemblies
which only holds extra manifest-level refs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace ~160 lines of inline bash/batch in each csproj with a shared
crossmoduleresolution.targets file that generates scripts from MSBuild
items and properties.

Each csproj now declaratively specifies:
- Crossgen2Step items (dependency assemblies with extra args)
- Crossgen2MainRef items (references for main assembly)
- R2RExpect* items (validation expectations)
- Crossgen2CommonArgs/MainExtraArgs properties

The targets file generates:
- A __crossgen2() helper function (bash) / :__cg2_invoke subroutine (batch)
- IL_DLLS copy loop from Crossgen2Step + main assembly names
- Per-step crossgen2 calls via @() item transforms
- Main assembly compilation with extra args and --map
- R2R validation invocation with args from R2RExpect* items

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace inline bash/batch precommands with a C# RunCrossgen tool
invoked at MSBuild build time via corerun. The targets file now uses
an AfterTargets=Build Exec target that runs runcrossgen.dll to:
- Copy IL assemblies before crossgen2 overwrites them
- Run crossgen2 on each dependency in declared order
- Run crossgen2 on the main assembly with refs and flags
- Run r2rvalidate for R2R image validation

This moves all crossgen2 work from test execution time to build time,
making the generated test scripts clean (no precommands).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set OutputPath to $(CORE_ROOT)/runcrossgen/ so the tool lives alongside
crossgen2 and corerun. The targets file now references it from CORE_ROOT
instead of navigating relative to the test output directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set RuntimeIdentifier=$(NETCoreSdkRuntimeIdentifier) since runcrossgen
runs at build time on the build machine, not at test time on the target.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Roslyn-based test framework for crossgen2 R2R validation:
- Compiles test assemblies with Roslyn at test time
- Invokes crossgen2 out-of-process via dotnet exec
- Validates R2R output using ILCompiler.Reflection.ReadyToRun
- 4 test cases: basic inlining, transitive refs, async, composite
- Integrated into crossgen2.slnx and clr.toolstests subset

Status: crossgen2 invocation works, fixup validation WIP.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use FixupKind enum + ToString(SignatureFormattingOptions) instead of
  ToString() which returned the class name for TodoSignature fixups
- Add System.Private.CoreLib from native/ dir to crossgen2 references
- Use dotnet exec instead of corerun to invoke crossgen2
- Remove opt-async-methods plumbing (unrelated to runtime-async)
- Rename AsyncMethodThunks to AsyncCrossModuleInlining with correct
  expectations (CHECK_IL_BODY fixups, not RuntimeFunction counts)
- Remove unused CoreRun property from TestPaths
- Add KEEP_R2R_TESTS env var to preserve temp dirs for debugging

All 4 tests pass: BasicCrossModuleInlining, TransitiveReferences,
AsyncCrossModuleInlining, CompositeBasic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 5 new tests covering key runtime-async crossgen2 PRs:

- RuntimeAsyncMethodEmission (dotnet#124203): Validates [ASYNC] variant
  entries for Task/ValueTask-returning methods
- RuntimeAsyncContinuationLayout (dotnet#123643): Validates ContinuationLayout
  and ResumptionStubEntryPoint fixups for methods with GC refs across awaits
- RuntimeAsyncDevirtualize (dotnet#125420): Validates async virtual method
  devirtualization produces [ASYNC] entries for sealed/interface dispatch
- RuntimeAsyncNoYield (dotnet#124203): Validates async methods without await
  still produce [ASYNC] variants
- RuntimeAsyncCrossModule (dotnet#121679): Validates MutableModule async
  references work with cross-module inlining of runtime-async methods

Infrastructure changes:
- Add Roslyn feature flag support (runtime-async=on) to R2RTestCaseCompiler
- Add R2RExpectations for async variants, resumption stubs,
  continuation layouts, and arbitrary fixup kinds
- Add MainExtraSourceResourceNames to R2RTestCase for shared source files
- Add null guard for method.Fixups in CheckFixupKinds

All 9 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dd typed Crossgen2Option

- Rename DependencyInfo -> CompiledAssembly with IsCrossgenInput property
- Remove AdditionalReferences: compile dependencies in listed order, each
  referencing all previously compiled assemblies
- Add Crossgen2OptionKind enum and Crossgen2Option record replacing raw
  string CLI args
- Remove unused CrossgenOptions from CompiledAssembly
- Remove dead ReadEmbeddedSource locals in async tests
- Fix Crossgen2Dir fallback to check for crossgen2.dll file instead of
  just directory existence (empty Debug dir was preventing Checked fallback)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…back

- R2RTestCase now takes an Action<ReadyToRunReader> Validate callback
  instead of an R2RExpectations data object
- Move CompositeMode, Crossgen2Options, Features to R2RTestCase directly
- Convert R2RResultChecker to R2RAssert static helper class with methods:
  HasManifestRef, HasInlinedMethod, HasAsyncVariant, HasResumptionStub,
  HasContinuationLayout, HasResumptionStubFixup, HasFixupKind
- Delete unused Expectations/R2RExpectationAttributes.cs (attribute-based
  design replaced by inline test code)
- Each test now owns its assertions directly, making them more readable
  and easier to extend with custom checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix --opt-cross-module arg emission: use space-separated args without
  .dll suffix instead of colon-joined format
- Add -composite suffix to composite output FilePath to prevent component
  stubs from overwriting the composite image
- Add IsComposite property to CrossgenCompilation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
InliningInfoSection2:
- Add InliningEntry struct, GetEntries(), GetInliningPairs()
- Method name resolution via local method map and OpenReferenceAssembly

R2RResultChecker:
- HasInlinedMethod checks both CrossModuleInlineInfo and all per-assembly
  InliningInfo2 sections (needed for composite images)
- Add HasContinuationLayout(reader, methodName) overload
- Add HasResumptionStubFixup(reader, methodName) overload
- Add HasFixupKindOnMethod generic helper
- GetAllInliningInfo2Sections iterates global + all assembly headers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New tests covering the intersection of cross-module inlining,
runtime-async, and composite mode:
- CompositeCrossModuleInlining, CompositeTransitive
- CompositeAsync, CompositeAsyncCrossModuleInlining,
  CompositeAsyncTransitive (skipped: async not in composite yet)
- AsyncCrossModuleContinuation, AsyncCrossModuleTransitive
- MultiStepCompositeConsumer, CompositeAsyncDevirt, RuntimeAsyncNoYield

Fix existing tests: BasicCrossModuleInlining (InlineableLib as
Reference), TransitiveReferences (CrossModuleOptimization on lib).
Use method-targeted HasContinuationLayout/HasResumptionStubFixup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add C# source files for CompositeAsync, MultiStepConsumer,
AsyncCrossModuleContinuation, AsyncTransitiveMain, CompositeAsyncDevirt
test cases and their dependencies.

Add CrossModuleInliningInfoSection parser for ILCompiler.Reflection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sertion

In composite mode, crossgen2 skips the DebuggableAttribute-based
optimization detection (Program.cs:588), defaulting to
OptimizationMode.None which sets CORJIT_FLAG_DEBUG_CODE and disables
all inlining. Add Crossgen2Option.Optimize to all composite
compilations.

Add HasInlinedMethod assertion to CompositeCrossModuleInlining test
to verify cross-assembly inlining actually occurs in composite mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CrossModuleInliningInfoSection: Fix cross-module inliner index parsing.
  The writer emits absolute ILBody indices (baseIndex is never updated),
  and the runtime reads them as absolute. The parser was incorrectly
  accumulating them as deltas, which would break for inlinees with 2+
  cross-module inliners.

- R2RDriver: Read stdout/stderr concurrently via ReadToEndAsync to avoid
  the classic redirected-stdio deadlock when crossgen2 fills one pipe
  buffer while we block reading the other.

- R2RResultChecker: Load PE images into memory via File.ReadAllBytes
  instead of holding FileStream handles open. IAssemblyMetadata has no
  disposal contract and ReadyToRunReader caches instances indefinitely,
  so stream-backed implementations leak file handles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a test that exercises the cross-module INLINER parsing path in
CrossModuleInliningInfoSection, where multiple cross-module inliner
entries exist for the same inlinee.

The test uses two generic types (GenericWrapperA<T>, GenericWrapperB<T>)
from a --opt-cross-module reference library, each with a virtual method
that inlines the same Utility.GetValue(). Value type instantiations
(LocalStruct) are required because CrossModuleCompileable discovery uses
CanonicalFormKind.Specific, which preserves value types (unlike reference
types that canonicalize to __Canon, losing alternate location info).

This produces two distinct cross-module inliner MethodDefs for the same
inlinee, validating that the parser correctly reads absolute (not
delta-accumulated) inliner indices.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename HasMultipleCrossModuleInliners to HasCrossModuleInliners and
accept expected inliner method names instead of just a count. This
ensures the absolute-encoded ILBody import indices actually resolve
to the correct methods (GenericWrapperA, GenericWrapperB).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These are local session artifacts that should not be in the repository.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These tests were superseded by the ILCompiler.ReadyToRun.Tests test suite
which compiles and validates R2R images programmatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- R2RResultChecker: fix broken see-cref (R2RTestCase.Validate -> CrossgenCompilation.Validate)
- R2RTestRunner: fix comment on BuildReferencePaths (runtime pack only, not compiled assemblies)
- CrossModuleInliningInfoSection: fix version to v6.2 per readytorun.h
- AsyncMethods.cs: describe actual test behavior (cross-module inlining, not RuntimeFunction layout)
- BasicAsyncEmission.cs: remove false claim about resumption stubs
- AsyncDevirtualize.cs: describe actual validation ([ASYNC] variants, not devirtualized calls)
- AsyncCrossModule.cs: describe actual validation (manifest refs + [ASYNC] variants)
- AsyncCrossModuleContinuation.cs: remove false claim about ContinuationLayout fixups
- R2RTestSuites: fix 'and' -> 'or' for inlining info sections, fix A+B -> A ref, clarify devirt test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the two gates that prevented async method variants (runtime-async)
from being compiled in composite R2R mode:

1. ReadyToRunILProvider.NeedsCrossModuleInlineableTokens: Removed the
   '!VersionsWithModule(SystemModule)' condition for async stubs. Async
   thunks and resumption stubs use compiler-generated IL with synthetic
   tokens created by ILEmitter.NewToken() that ALWAYS need rewriting
   into the MutableModule via ManifestModuleWrappedMethodIL, regardless
   of whether CoreLib is in the version bubble.

2. CorInfoImpl.ShouldSkipCompilation: Removed the blanket composite-mode
   skip for IsCompilerGeneratedILBodyForAsync() methods. This was a
   workaround for the missing wrapping in (1) — with the root cause
   fixed, async stubs compile correctly in composite mode.

Fix two test issues:
- CompositeAsyncCrossModuleInlining: Validate sync inlining (GetValueSync)
  instead of async inlining (await doesn't produce traditional inlining).
- MultiStepCompositeAndNonCompositeAsync: Fix assembly reference mismatch
  by adding a proper MultiStepAsyncConsumer source that calls AsyncCompositeLib.

All 20 R2R tests pass including the 5 previously-skipped composite async tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidwrighton davidwrighton self-requested a review April 23, 2026 21:43
@davidwrighton
Copy link
Copy Markdown
Member

@jtschuster Also, I was perusing code, and noticed that AddResumptionStubFixup directly adds the fixup to the methodCodeNode. Is there a reason it doesn't use AddPrecodeFixup which caches the adds so that they only happen once, and only if the method actually succeeds its compile?

@jtschuster
Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr crossgen2

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Copy Markdown
Member Author

I'm having trouble reproducing the failures seen in crossgen CI.

@jtschuster
Copy link
Copy Markdown
Member Author

@jtschuster Also, I was perusing code, and noticed that AddResumptionStubFixup directly adds the fixup to the methodCodeNode. Is there a reason it doesn't use AddPrecodeFixup which caches the adds so that they only happen once, and only if the method actually succeeds its compile?

I don't recall a reason for that, I'll see if that can work. Maybe it could be in another PR, though.

@jtschuster
Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr crossgen2

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr crossgen2-composite

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Copy Markdown
Member Author

@jtschuster Also, I was perusing code, and noticed that AddResumptionStubFixup directly adds the fixup to the methodCodeNode. Is there a reason it doesn't use AddPrecodeFixup which caches the adds so that they only happen once, and only if the method actually succeeds its compile?

I don't recall a reason for that, I'll see if that can work. Maybe it could be in another PR, though.

Created #127523 for that work. Looks like there was a deduping going on already, but it seems more correct to use the AddPrecodeFixup.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 16:21
Copy link
Copy Markdown
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 21 out of 21 changed files in this pull request and generated 4 comments.

Comment on lines +102 to 105
return Field == fieldWithToken.Field
&& Token.Equals(fieldWithToken.Token)
&& _forceOwningTypeNotDerivedFromToken == fieldWithToken._forceOwningTypeNotDerivedFromToken;
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

FieldWithToken.Equals now includes _forceOwningTypeNotDerivedFromToken, but GetHashCode() still only combines Field and Token. This violates the equality/hash contract and can lead to incorrect behavior in hashed collections/caches (misses, duplicates). Update GetHashCode() to incorporate _forceOwningTypeNotDerivedFromToken (or stop comparing it in Equals if it shouldn't participate in identity).

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +350
bool equals = Method == methodWithToken.Method
&& Token.Equals(methodWithToken.Token)
&& OwningType == methodWithToken.OwningType
&& ConstrainedType == methodWithToken.ConstrainedType
&& Unboxing == methodWithToken.Unboxing
&& _forceOwningTypeNotDerivedFromToken == methodWithToken._forceOwningTypeNotDerivedFromToken;
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

MethodWithToken.Equals now includes _forceOwningTypeNotDerivedFromToken, but GetHashCode() does not. This breaks the equality/hash contract and can cause incorrect behavior in NodeCache/Dictionary lookups where MethodWithToken is used as a key. Include _forceOwningTypeNotDerivedFromToken in GetHashCode() (or remove it from Equals/CompareTo if it shouldn't affect key identity).

Copilot uses AI. Check for mistakes.
{
ModuleToken methodModuleToken = HandleToModuleToken(ref pResolvedToken);
ModuleToken methodModuleToken = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation);
Debug.Assert(!strippedInstantiation);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

HandleToModuleToken(..., out strippedInstantiation) can legitimately set strippedInstantiation=true for faux MethodIL (e.g., compiler-generated IL stubs) when it strips method instantiation (see CorInfoImpl.ReadyToRun.cs where it compares primary vs typical method definition). Since recordToken explicitly allows IMethodTokensAreUseableInCompilation, Debug.Assert(!strippedInstantiation) here can fire when compiling those stubs (including async thunk scenarios this PR enables). Consider removing this assert or adjusting the logic to tolerate stripped instantiation (and rely on the MethodDesc/type context when building signatures).

Suggested change
Debug.Assert(!strippedInstantiation);
// Faux/stub MethodIL can legitimately strip method instantiation when producing a module token.
// Record the token anyway and rely on the resolved MethodDesc for signature-based operations.
_ = strippedInstantiation;

Copilot uses AI. Check for mistakes.
{
helperArg = new FieldWithToken(fieldDesc, HandleToModuleToken(ref pResolvedToken));
ModuleToken fieldToken = HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation);
Debug.Assert(!strippedInstantiation);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

HandleToModuleToken(..., out strippedInstantiation) can return strippedInstantiation=true for fields in faux MethodIL (it strips to the typical field definition). The Debug.Assert(!strippedInstantiation) here is therefore not generally valid and could trip when a compiler-generated IL stub triggers a shared-generic field lookup. Either remove/relax the assert, or plumb the flag through consistently (similar to MethodWithToken) so signature emission can compensate when the owning type is not encoded by the token.

Suggested change
Debug.Assert(!strippedInstantiation);

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

/azp run runtime-coreclr crossgen2

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr crossgen2-composite

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Comment thread src/coreclr/tools/Common/TypeSystem/MetadataEmitter/TypeSystemMetadataEmitter.cs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-crossgen2-coreclr only use for closed issues

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Crossgen2 does not emit runtime-async thunks in composite mode

5 participants