Skip to content

Enable compilation of async thunks in composite mode#126904

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

Enable compilation of async thunks in composite mode#126904
jtschuster wants to merge 73 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>
jtschuster and others added 3 commits April 20, 2026 14:00
Composite mode does produce a CrossModuleInlineInfo section when an
inlineable method comes from an assembly outside the version bubble
(passed as a Reference with --opt-cross-module). This complements
CompositeDoesNotProduceCrossModuleInliningInfo, which covers the
case where all inlinees are composite inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Composite mode and the emission of Async methods are fairly orthogonal and don't need tests just validation the existence of async method.
Copilot AI review requested due to automatic review settings April 20, 2026 22:33
The new forceOwningTypeNotDerivedFromToken constructor parameter
affects signature emission via OwningTypeNotDerivedFromToken, but
equality previously only compared Field and Token. Since
FieldWithToken is used as a cache key (e.g., NodeCache<FieldWithToken,
Import>), instances differing only by this flag could incorrectly
share cached signatures. Mirrors the existing MethodWithToken.Equals
behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jtschuster and others added 2 commits April 20, 2026 15:39
The MethodDesc branch already forwards strippedInstantiation as
forceOwningTypeFromMethodDesc; mirror that for the FieldDesc branch
so that faux-IL stripped-instantiation tokens correctly mark
OwningTypeNotDerivedFromToken rather than discarding the flag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The FieldDesc branch of ComputeRuntimeLookupForSharedGenericToken is
not expected to encounter stripped instantiations from faux IL. Add a
Debug.Assert to catch any future regression while keeping the
forwarding to FieldWithToken as a defensive fallback for retail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 18 out of 18 changed files in this pull request and generated 2 comments.

Comment on lines +1400 to +1431
ModuleToken _HandleToModuleToken(ref CORINFO_RESOLVED_TOKEN pResolvedToken, MethodDesc methodDesc, out TypeSystemEntity context, ref TypeDesc constrainedType, out bool strippedInstantiation)
{
if ((CorTokenType)(unchecked((uint)pResolvedToken.token) & 0xFF000000u) == CorTokenType.mdtMethodDef &&
methodDesc?.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod)
if (methodDesc != null && (_compilation.NodeFactory.CompilationModuleGroup.VersionsWithMethodBody(methodDesc)
|| (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_DevirtualizedMethod)
|| methodDesc.IsPInvoke))
{
mdToken token = (mdToken)MetadataTokens.GetToken(ecmaMethod.Handle);
if ((CorTokenType)(unchecked((uint)pResolvedToken.token) & 0xFF000000u) == CorTokenType.mdtMethodDef &&
methodDesc?.GetTypicalMethodDefinition() is EcmaMethod ecmaMethod)
{
mdToken token = (mdToken)MetadataTokens.GetToken(ecmaMethod.Handle);

// This is used for de-virtualization of non-generic virtual methods, and should be treated
// as a the methodDesc parameter defining the exact OwningType, not doing resolution through the token.
context = null;
constrainedType = null;
// This is used for de-virtualization of non-generic virtual methods, and should be treated
// as a the methodDesc parameter defining the exact OwningType, not doing resolution through the token.
context = null;
constrainedType = null;
strippedInstantiation = false;

return new ModuleToken(ecmaMethod.Module, token);
return new ModuleToken(ecmaMethod.Module, token);
}
}
}

if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod)
{
context = null;
}
else
{
context = entityFromContext(pResolvedToken.tokenContext);
}
if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod)
{
context = null;
}
else
{
context = entityFromContext(pResolvedToken.tokenContext);
}

return HandleToModuleToken(ref pResolvedToken);
return HandleToModuleToken(ref pResolvedToken, out strippedInstantiation);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

GitHub doesn't show this well, but this just moved to a local method since it's not used anywhere else.

@jtschuster jtschuster marked this pull request as ready for review April 20, 2026 22:57
Copilot AI review requested due to automatic review settings April 20, 2026 22:57
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 18 out of 18 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs:166

  • CreateCrossModuleInlineableTokensForILBody now always wraps EcmaMethodIL.Create((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()). When method is an AsyncMethodVariant (the caller comment in CorInfoImpl.reportInliningDecision explicitly notes this can happen), this produces a wrapped IL whose OwningMethod is the underlying EcmaMethod, not the AsyncMethodVariant key you store it under. This can break token resolution and compilation because MethodIL.OwningMethod no longer matches the method being compiled. Consider restoring support for wrapping AsyncEcmaMethodIL/explicit owning-method (as the previous overload did) so the wrapped IL preserves OwningMethod == method for async variants.
        public void CreateCrossModuleInlineableTokensForILBody(MethodDesc method)
        {
            // This method accepts only method definitions, but accepts non-primary method definitions.
            // That is, it must not be generic, but it may represent an AsyncVariant
            Debug.Assert(method.IsTypicalMethodDefinition);
            Debug.Assert(_manifestMutableModule != null);
            var wrappedMethodIL = new ManifestModuleWrappedMethodIL();

            Debug.Assert(!_compilationModuleGroup.VersionsWithMethodBody(method) &&
                    _compilationModuleGroup.CrossModuleInlineable(method));

            if (!wrappedMethodIL.Initialize(_manifestMutableModule, EcmaMethodIL.Create((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition())))
            {
                // If we could not initialize the wrapped method IL, we should store a null.
                // That will result in the IL code for the method being unavailable for use in
                // the compilation, which is version safe.
                wrappedMethodIL = null;
            }

            _manifestModuleWrappedMethods.Add(method, wrappedMethodIL);
            IncrementVersion();
        }

…TokensAreAvailable

ExternalReferenceTokenManager.EnsureDefTokensAreAvailable now manages
this flag itself when referencesAreForAsyncMethod is true, so the
manual try/finally in EnsureAsyncThunkTokensAreAvailable is redundant.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 22, 2026 21:48
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 17 out of 17 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs:110

  • FieldWithToken.AppendMangledName does not incorporate OwningTypeNotDerivedFromToken (or the owning type override). Since OwningTypeNotDerivedFromToken changes the emitted field signature (READYTORUN_FIELD_SIG_OwnerType), two distinct FieldWithToken instances could end up producing identical symbol names, risking node/symbol collisions when both variants are present. Consider appending a stable marker (and owning type, if needed) to the mangled name when OwningTypeNotDerivedFromToken is true, similar to MethodWithToken.AppendMangledName.
        public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
        {
            sb.Append(nameMangler.GetMangledFieldName(Field));
            sb.Append("; "u8);
            sb.Append(Token.ToString());
        }

OwningTypeNotDerivedFromToken = true;
}
}

Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

MethodWithToken/FieldWithToken now use OwningTypeNotDerivedFromToken to control READYTORUN_*_SIG_OwnerType emission. However, the GenericLookupKey cache uses RuntimeDeterminedTypeHelper.Equals/GetHashCode for MethodWithToken/FieldWithToken, which currently ignores OwningTypeNotDerivedFromToken. This can cause generic lookup helper imports to be reused across signatures that differ only by the owner-type-override flag, leading to incorrect fixup signatures. Update the RuntimeDeterminedTypeHelper comparisons/hashes (or GenericLookupKey equality) to include the owning-type-override flag (and any other fields that affect signature encoding).

Suggested change
public bool Equals(MethodWithToken other)
{
return EqualityComparer<MethodDesc>.Default.Equals(Method, other.Method)
&& Token.Equals(other.Token)
&& EqualityComparer<TypeDesc>.Default.Equals(ConstrainedType, other.ConstrainedType)
&& Unboxing == other.Unboxing
&& OwningTypeNotDerivedFromToken == other.OwningTypeNotDerivedFromToken
&& EqualityComparer<TypeDesc>.Default.Equals(OwningType, other.OwningType);
}
public override bool Equals(object obj)
{
return obj is MethodWithToken other && Equals(other);
}
public override int GetHashCode()
{
HashCode hash = new HashCode();
hash.Add(Method);
hash.Add(Token);
hash.Add(ConstrainedType);
hash.Add(Unboxing);
hash.Add(OwningTypeNotDerivedFromToken);
hash.Add(OwningType);
return hash.ToHashCode();
}
public static bool operator ==(MethodWithToken left, MethodWithToken right)
{
return left.Equals(right);
}
public static bool operator !=(MethodWithToken left, MethodWithToken right)
{
return !left.Equals(right);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@davidwrighton davidwrighton left a comment

Choose a reason for hiding this comment

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

I like it! I'm assuming you've run the various crossgen2 outerloops, as I can't actually validate these sorts of changes by hand in a reliable manner. I have one question around changing the behavior of the FieldWithToken and MethodToken comparisons, but otherwise, it looks reasonable.

else
return -1;
}
result = OwningTypeNotDerivedFromToken.CompareTo(other.OwningTypeNotDerivedFromToken);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These details around the OwningTypeNotDerivedFromToken are interesting. I expect that adding these comparisons to the logic will increase the size of R2R images by not sharing some cases which could be sharing. Could you do an experiment where instead of adding a compare based on OwningTypeNotDerivedFromToken, you keep the forceOwningTypeFromMethodDesc flag on the MethodToken, and add that to the comparison instead? You'd also need to tweak these asserts, but it might be a size win.

break;
}
}
if (forceOwningTypeNotDerivedFromToken)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the same concerns around adding the OwningTypeNotDerivedFromToken to the comparisons here as I do for MethodToken.

@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).

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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Crossgen2 does not emit runtime-async thunks in composite mode

4 participants