Skip to content

crossgen2: allow struct/unmanaged-constrained generics over primitives in the version bubble - #135906

Merged
lewing merged 3 commits into
dotnet:mainfrom
lewing:lewing-r2r-struct-constrained-generics
Oct 19, 2026
Merged

lewing merged 3 commits into
dotnet:mainfrom
lewing:lewing-r2r-struct-constrained-generics

Conversation

@lewing

@lewing lewing commented Oct 15, 2026

Copy link
Copy Markdown
Member

Problem

crossgen2 neither precompiles nor inlines a generic method or type with a where T : struct or where T : unmanaged constraint when it is instantiated over a CoreLib primitive (for example S<char> or GS<int>.Get) from code outside CoreLib's version bubble. The same generic without the constraint compiles and inlines fine. When inlining fails, the JIT dump shows:

INLINER: ... result 'failed this callee' reason 'cannot get method info' for 'V:CallS(char):int' calling 'V:S[char](char):int'

Root cause

ReadyToRunCompilationModuleGroupBase.ComputeInstantiationVersionsWithCode counts a primitive type argument as part of the generic's version bubble only when the generic parameter has no type constraints at all. C# writes where T : struct and where T : unmanaged into metadata as a System.ValueType type constraint (the unmanaged modreq is not visible in the type system). That makes the constraint list non-empty, so VersionsWithMethodBody returns false, and two things follow:

  • the instantiation is not compiled into the defining module, and
  • CorInfoImpl.getMethodInfo → CanInline returns false, which the JIT reports as "cannot get method info".

The rule dates from #1889. Its design doc already says primitives are in the bubble "as long as the generic is not constrained on an interface or class". System.ValueType is just how C# encodes struct: every primitive satisfies it, and it exposes nothing an unconstrained T can't already use. The runtime needs no change. ReadyToRunInfo::ComputeAlternateGenericLocationForR2RCode skips primitive arguments, so these instantiations are looked up in the defining module, the same path the unconstrained case already uses.

Fix

When the type argument is a primitive, ignore System.ValueType constraints. Any other constraint, interface or class, still disqualifies the instantiation, as before. The object/string branch is unchanged.

Other changes in this PR:

  • R2R reader: ILCompiler.Reflection.ReadyToRun's CrossModuleInliningInfoSection now names local inlinees that have no compiled body of their own, such as AggressiveInlining helpers, using the image metadata. Previously they showed as <MethodDef 0x…>. The new test's inlining assertion needs this, and it also improves R2RDump output.
  • Docs: docs/design/features/crossgen2-compilation-structure-enhancements.md now states the rule.
  • Test: new R2RTestSuites.StructConstrainedPrimitiveInstantiations ([Fact], fixture TestCases/PrimitiveInstantiations/StructConstrainedGenerics.cs). It asserts that StructConstrained<int>, UnmanagedConstrained<int> and StructConstrainedBox<int>.GetValueHashCode are compiled, and that the struct- and unmanaged-constrained AggressiveInlining helpers are inlined.

Impact

  • Affected:

    • native per-assembly R2R without --opt-cross-module;
    • app-only composite without --opt-cross-module or --inputbubble.

    These instantiations are jitted at startup instead of using R2R code, and they miss R2R inlining.

  • Not affected, so this change is a no-op there:

    • browser-wasm defaults: per-assembly plus --opt-cross-module:*, used for both app publish and the shared framework;
    • composites that include the framework: browser composite, WASI and Apple mobile.

Interface constraints still disqualify primitive instantiations by design. The separate generic-math issue (calls to static virtual methods such as INumber<T> operators that can't be resolved across the version bubble) is tracked in #134559 and is not addressed here.

Related: #135899 (the same crossgen2 area, found while working on it).

Validation

All runs on osx-arm64, checked:

  • ./build.sh clr+libs -lc Release -c checked: succeeded with 0 warnings and 0 errors.
  • ./build.sh -s clr.r2rtests -lc Release -c checked -test: 51 total, 36 passed, 0 failed, 15 skipped (target-specific tests).
  • I ran the same command with only the crossgen2 change reverted. StructConstrainedPrimitiveInstantiations failed with Expected compiled method 'StructConstrainedGenerics.StructConstrained<int>' not found, and passed again once the fix was restored.
  • I also ran a standalone repro through the built crossgen2 for both --targetos:osx --targetarch:arm64 -O and --targetos:browser --targetarch:wasm --obj-format:wasm -O, with default args.
    • SN<int>, MN<int> and GS<int>.Get are now compiled.
    • CallS(char) and CallM(char) now have the same code size as the unconstrained CallU(char), which shows the call is inlined.
    • IN<int>, which has an IEquatable<T> constraint, is still not compiled, as intended.

Not run: the CI browser-wasm R2R test leg (CLR_R2R_Tests_browser_wasm), which needs a browser CoreCLR build. The fix doesn't depend on the target, and the new test is a plain [Fact], so that leg will run it.

Note

This PR description and the change were generated with GitHub Copilot.

…s in the version bubble

ComputeInstantiationVersionsWithCode treated a primitive instantiation
argument as versioning with the generic only when the generic parameter had
no type constraints at all. C# encodes `where T : struct` and
`where T : unmanaged` as a System.ValueType type constraint, so such
instantiations (e.g. S<char>) were neither precompiled nor inlined
("cannot get method info") without --inputbubble/--opt-cross-module.
Ignore System.ValueType constraints for primitive arguments; interface and
class constraints still disqualify.

Also teach CrossModuleInliningInfoSection to name local inlinees that have
no compiled body from the image metadata, and add an R2R test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f5ac7e42-b3b6-4fe3-bf0b-f40970e8fae0
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/crossgen-contrib
See info in area-owners.md if you want to be subscribed.

…strained-generics

# Conflicts:
#	src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
@lewing
lewing marked this pull request as ready for review October 16, 2026 20:19
@lewing
lewing merged commit 08ee9c0 into dotnet:main Oct 19, 2026
101 of 104 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants