From 39bc7fb2349a76c21d46ae055ec4adaa3c3ee402 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 16:39:06 -0500 Subject: [PATCH 1/3] Fold box+isinst of value types to non-interface types in R2R crossgen2's compareTypesForCast reported every MustNot as May in ReadyToRun, so the JIT could not fold negative `box T; isinst X` checks and kept a box allocation and cast helper call for each one (e.g. `value is byte or sbyte` in MemoryExtensions.IndexOfAnyInRange). A boxed value type is sealed, and the only non-interface types it can be cast to are itself and its fixed base types, so a negative result to a non-interface, non-System.Enum target can't change across versions. Keep MustNot in that case (excluding Nullable and type-equivalent types). Fixes #135892 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57ce813-cc00-4e28-824c-2eecc4d8fb9f --- .../tools/Common/JitInterface/CorInfoImpl.cs | 13 ++++- .../TestCases/CastFolding/BoxIsInstFolding.cs | 45 ++++++++++++++++ .../TestCases/R2RTestSuites.cs | 51 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 992d5c750c485b..fd641c1deb90d7 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3179,8 +3179,17 @@ private TypeCompareState compareTypesForCast(CORINFO_CLASS_STRUCT_* fromClass, C // In R2R it is a breaking change for a previously positive // cast to become negative, but not for a previously negative // cast to become positive. So in R2R a negative result is - // always reported back as May. - if (result == TypeCompareState.MustNot) + // reported back as May, unless it can't change across versions. + // + // A value type is sealed, so the only non-interface types it can be + // cast to are itself and its base types (ValueType, Object, and Enum + // for enums). New interfaces may be added in a later version, and + // System.Enum is excluded in case a struct becomes an enum, so those + // targets stay May. + if (result == TypeCompareState.MustNot && + !(fromType.IsValueType && !fromType.IsNullable && + !toType.IsInterface && !toType.IsWellKnownType(WellKnownType.Enum) && + !fromType.HasTypeEquivalence && !toType.HasTypeEquivalence)) { result = TypeCompareState.May; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs new file mode 100644 index 00000000000000..0c83091029393f --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs @@ -0,0 +1,45 @@ +// 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; + +public interface INotImplemented +{ +} + +public struct SourceStruct +{ + public int Value; +} + +public struct TargetStruct +{ + public int Value; +} + +public struct ControlStruct +{ + public int Value; +} + +public static class BoxIsInstFolding +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Classify(T value) + { + if (value is byte or sbyte) + return 1; + if (value is short or ushort or char) + return 2; + return 3; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int ClassifyChar(char c) => Classify(c); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool StructIsOtherStruct(SourceStruct s) => (object)s is TargetStruct; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool StructIsInterface(ControlStruct s) => (object)s is INotImplemented; +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 55728597715585..e91a21337af3e0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -145,6 +145,57 @@ static void Validate(ReadyToRunReader reader) } } + [Fact] + public void BoxIsInstOfValueTypeFolds() + { + var boxIsInstFolding = new CompiledAssembly + { + AssemblyName = nameof(BoxIsInstOfValueTypeFolds), + SourceResourceNames = ["CastFolding/BoxIsInstFolding.cs"], + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(BoxIsInstOfValueTypeFolds), + [ + new(nameof(BoxIsInstOfValueTypeFolds), [new CrossgenAssembly(boxIsInstFolding)]) + { + Options = [Crossgen2Option.Optimize], + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + // Helper cells are bound lazily and aren't listed in per-method fixups, so check the import sections. + var formattingOptions = new SignatureFormattingOptions(); + var isInstImports = new List(); + var newObjectImports = new List(); + foreach (ReadyToRunImportSection section in reader.ImportSections) + { + foreach (ReadyToRunImportSection.ImportSectionEntry entry in section.Entries) + { + if (entry.Signature?.FixupKind == ReadyToRunFixupKind.IsInstanceOf) + isInstImports.Add(entry.Signature.ToString(formattingOptions)); + else if (entry.Signature?.FixupKind == ReadyToRunFixupKind.NewObject) + newObjectImports.Add(entry.Signature.ToString(formattingOptions)); + } + } + + string diagnostic = + $"IsInstanceOf: [{string.Join(", ", isInstImports)}]; NewObject: [{string.Join(", ", newObjectImports)}]"; + + // A cast to an interface may start succeeding in a later version, so it must not be folded. + Assert.True(isInstImports.Exists(s => s.StartsWith("INotImplemented ", StringComparison.Ordinal)), diagnostic); + Assert.True(newObjectImports.Exists(s => s.StartsWith("ControlStruct ", StringComparison.Ordinal)), diagnostic); + + // Casts of a boxed value type to a different non-interface type fail in every version, + // so both the box and the cast helper are folded away. The shared canonical body of + // Classify still casts (but doesn't box), so its IsInstanceOf imports remain. + Assert.True(newObjectImports.Count == 1, diagnostic); + Assert.False(isInstImports.Exists(s => s.StartsWith("TargetStruct ", StringComparison.Ordinal)), diagnostic); + } + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))] public void WasmWebcilModule() { From 403230c3134348b5abdbb33611afe0b8ff6fa0f1 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 16:47:27 -0500 Subject: [PATCH 2/3] Make R2R box/isinst test target-independent and tighten comments Depending on the target, box and isinst reference their types through NEW_OBJECT/IS_INSTANCE_OF imports or TYPE_HANDLE fixups (e.g. wasm only has the latter), so check for the type name in any import. Use non-generic cases so a shared canonical body doesn't add imports for the same types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57ce813-cc00-4e28-824c-2eecc4d8fb9f --- .../tools/Common/JitInterface/CorInfoImpl.cs | 10 ++---- .../TestCases/CastFolding/BoxIsInstFolding.cs | 12 +------ .../TestCases/R2RTestSuites.cs | 33 +++++++++---------- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index fd641c1deb90d7..018d1c9d95da85 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3179,13 +3179,9 @@ private TypeCompareState compareTypesForCast(CORINFO_CLASS_STRUCT_* fromClass, C // In R2R it is a breaking change for a previously positive // cast to become negative, but not for a previously negative // cast to become positive. So in R2R a negative result is - // reported back as May, unless it can't change across versions. - // - // A value type is sealed, so the only non-interface types it can be - // cast to are itself and its base types (ValueType, Object, and Enum - // for enums). New interfaces may be added in a later version, and - // System.Enum is excluded in case a struct becomes an enum, so those - // targets stay May. + // reported back as May, unless it can't change across versions: + // a value type can gain interfaces (or become an enum), but its + // other cast targets are fixed. if (result == TypeCompareState.MustNot && !(fromType.IsValueType && !fromType.IsNullable && !toType.IsInterface && !toType.IsWellKnownType(WellKnownType.Enum) && diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs index 0c83091029393f..4896a4c96a1223 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs @@ -24,18 +24,8 @@ public struct ControlStruct public static class BoxIsInstFolding { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Classify(T value) - { - if (value is byte or sbyte) - return 1; - if (value is short or ushort or char) - return 2; - return 3; - } - [MethodImpl(MethodImplOptions.NoInlining)] - public static int ClassifyChar(char c) => Classify(c); + public static bool CharIsByte(char c) => (object)c is byte; [MethodImpl(MethodImplOptions.NoInlining)] public static bool StructIsOtherStruct(SourceStruct s) => (object)s is TargetStruct; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index e91a21337af3e0..17ecf2b2c93548 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -166,33 +166,32 @@ public void BoxIsInstOfValueTypeFolds() static void Validate(ReadyToRunReader reader) { - // Helper cells are bound lazily and aren't listed in per-method fixups, so check the import sections. + // Box and isinst reference their types through NEW_OBJECT/IS_INSTANCE_OF imports + // or TYPE_HANDLE fixups depending on the target, so look for the type in any import. var formattingOptions = new SignatureFormattingOptions(); - var isInstImports = new List(); - var newObjectImports = new List(); + var importedTypes = new HashSet(); foreach (ReadyToRunImportSection section in reader.ImportSections) { foreach (ReadyToRunImportSection.ImportSectionEntry entry in section.Entries) { - if (entry.Signature?.FixupKind == ReadyToRunFixupKind.IsInstanceOf) - isInstImports.Add(entry.Signature.ToString(formattingOptions)); - else if (entry.Signature?.FixupKind == ReadyToRunFixupKind.NewObject) - newObjectImports.Add(entry.Signature.ToString(formattingOptions)); + string? signature = entry.Signature?.ToString(formattingOptions); + int kindStart = signature?.LastIndexOf(" (", StringComparison.Ordinal) ?? -1; + if (kindStart > 0) + importedTypes.Add(signature![..kindStart]); } } - string diagnostic = - $"IsInstanceOf: [{string.Join(", ", isInstImports)}]; NewObject: [{string.Join(", ", newObjectImports)}]"; + string diagnostic = $"Imported types: [{string.Join(", ", importedTypes)}]"; - // A cast to an interface may start succeeding in a later version, so it must not be folded. - Assert.True(isInstImports.Exists(s => s.StartsWith("INotImplemented ", StringComparison.Ordinal)), diagnostic); - Assert.True(newObjectImports.Exists(s => s.StartsWith("ControlStruct ", StringComparison.Ordinal)), diagnostic); + // An interface cast may succeed in a later version, so it isn't folded. + Assert.True(importedTypes.Contains("ControlStruct"), diagnostic); + Assert.True(importedTypes.Contains("INotImplemented"), diagnostic); - // Casts of a boxed value type to a different non-interface type fail in every version, - // so both the box and the cast helper are folded away. The shared canonical body of - // Classify still casts (but doesn't box), so its IsInstanceOf imports remain. - Assert.True(newObjectImports.Count == 1, diagnostic); - Assert.False(isInstImports.Exists(s => s.StartsWith("TargetStruct ", StringComparison.Ordinal)), diagnostic); + // Value type casts to other non-interface types fold, along with their boxes. + Assert.False(importedTypes.Contains("char"), diagnostic); + Assert.False(importedTypes.Contains("byte"), diagnostic); + Assert.False(importedTypes.Contains("SourceStruct"), diagnostic); + Assert.False(importedTypes.Contains("TargetStruct"), diagnostic); } } From de835bf428ce216e8c51913d189590a8237d0725 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 18:20:55 -0500 Subject: [PATCH 3/3] Keep MustNot for casts whose types all version with the R2R code Address review feedback: - Drop the Nullable source exclusion. No object's exact type is Nullable (the box path passes getTypeForBox), and the VM's compareTypesForCast doesn't exclude it. - Also keep MustNot when toType and every type in fromType's base chain and interface list version with the compilation (e.g. CoreLib, composite and input-bubble builds). Arrays, variant targets and COM types stay May. Move the policy into CorInfoImpl.ReadyToRun.cs. Cover both per-assembly and input-bubble builds in the R2R test, and fix the test harness's InputBubble option, which passed --input-bubble instead of --inputbubble. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57ce813-cc00-4e28-824c-2eecc4d8fb9f --- .../tools/Common/JitInterface/CorInfoImpl.cs | 9 +- .../TestCases/CastFolding/BoxIsInstFolding.cs | 35 ------ .../TestCases/CastFolding/CastFolding.cs | 117 ++++++++++++++++++ .../TestCases/R2RTestSuites.cs | 44 +++++-- .../TestCasesRunner/R2RDriver.cs | 2 +- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 38 ++++++ 6 files changed, 189 insertions(+), 56 deletions(-) delete mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/CastFolding.cs diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 018d1c9d95da85..fc33e3eeb2c8b4 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3179,13 +3179,8 @@ private TypeCompareState compareTypesForCast(CORINFO_CLASS_STRUCT_* fromClass, C // In R2R it is a breaking change for a previously positive // cast to become negative, but not for a previously negative // cast to become positive. So in R2R a negative result is - // reported back as May, unless it can't change across versions: - // a value type can gain interfaces (or become an enum), but its - // other cast targets are fixed. - if (result == TypeCompareState.MustNot && - !(fromType.IsValueType && !fromType.IsNullable && - !toType.IsInterface && !toType.IsWellKnownType(WellKnownType.Enum) && - !fromType.HasTypeEquivalence && !toType.HasTypeEquivalence)) + // reported back as May, unless it can't change across versions. + if (result == TypeCompareState.MustNot && !IsNegativeCastResultVersionResilient(fromType, toType)) { result = TypeCompareState.May; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs deleted file mode 100644 index 4896a4c96a1223..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/BoxIsInstFolding.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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; - -public interface INotImplemented -{ -} - -public struct SourceStruct -{ - public int Value; -} - -public struct TargetStruct -{ - public int Value; -} - -public struct ControlStruct -{ - public int Value; -} - -public static class BoxIsInstFolding -{ - [MethodImpl(MethodImplOptions.NoInlining)] - public static bool CharIsByte(char c) => (object)c is byte; - - [MethodImpl(MethodImplOptions.NoInlining)] - public static bool StructIsOtherStruct(SourceStruct s) => (object)s is TargetStruct; - - [MethodImpl(MethodImplOptions.NoInlining)] - public static bool StructIsInterface(ControlStruct s) => (object)s is INotImplemented; -} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/CastFolding.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/CastFolding.cs new file mode 100644 index 00000000000000..4999c441c37b51 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/CastFolding/CastFolding.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +public interface INotImplemented +{ +} + +public interface INotImplementedByClass +{ +} + +public interface INotImplementedByCom +{ +} + +public interface IVariant +{ +} + +public interface ISource +{ +} + +public interface ITarget +{ +} + +public interface ISourceForOutsideTarget +{ +} + +public interface IDerivedFromOutside : IDisposable +{ +} + +public interface ITargetOfDerived +{ +} + +public interface IGenericSource +{ +} + +public interface ITargetOfGeneric +{ +} + +public struct SourceStruct +{ + public int Value; +} + +public struct TargetStruct +{ + public int Value; +} + +public struct ControlStruct +{ + public int Value; +} + +public sealed class SealedClass +{ +} + +public sealed class VariantImplementation : IVariant +{ +} + +[ComImport] +[Guid("8F4E2A61-5C9B-4D3E-A7F0-1B2C3D4E5F60")] +public sealed class ComClass +{ +} + +public static class CastFolding +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool CharIsByte(char c) => (object)c is byte; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool StructIsOtherStruct(SourceStruct s) => (object)s is TargetStruct; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool StructIsInterface(ControlStruct s) => (object)s is INotImplemented; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool SealedClassIsInterface(SealedClass c) => (object)c is INotImplementedByClass; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool VariantCast(VariantImplementation v) => (object)v is IVariant; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool ComClassIsInterface(ComClass c) => (object)c is INotImplementedByCom; + + // Each guard gets a signal of its own: an unfolded IsAssignableFrom or IsAssignableTo leaves an import + // for its call, while the type handle imports for typeof remain even when the cast folds. + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool InterfaceIsInterface() => typeof(ITarget).IsAssignableFrom(typeof(ISource)); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool InterfaceIsOutsideInterface() => typeof(IComparable).IsAssignableFrom(typeof(ISourceForOutsideTarget)); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool DerivedFromOutsideIsInterface() => typeof(IDerivedFromOutside).IsAssignableTo(typeof(ITargetOfDerived)); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool SharedGenericIsInterface() where T : class => typeof(ITargetOfGeneric).IsAssignableFrom(typeof(IGenericSource)); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool CallSharedGeneric() => SharedGenericIsInterface(); +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 17ecf2b2c93548..a60f16717524d7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -145,26 +145,31 @@ static void Validate(ReadyToRunReader reader) } } - [Fact] - public void BoxIsInstOfValueTypeFolds() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CastFoldingRespectsVersionBubble(bool inputBubble) { - var boxIsInstFolding = new CompiledAssembly + var castFolding = new CompiledAssembly { - AssemblyName = nameof(BoxIsInstOfValueTypeFolds), - SourceResourceNames = ["CastFolding/BoxIsInstFolding.cs"], + AssemblyName = nameof(CastFoldingRespectsVersionBubble), + SourceResourceNames = ["CastFolding/CastFolding.cs"], }; + // With --input-bubble and no explicit bubble references, the framework is in the version bubble. + List options = inputBubble ? [Crossgen2Option.InputBubble, Crossgen2Option.Optimize] : [Crossgen2Option.Optimize]; + new R2RTestRunner(_output).Run(new R2RTestCase( - nameof(BoxIsInstOfValueTypeFolds), + nameof(CastFoldingRespectsVersionBubble), [ - new(nameof(BoxIsInstOfValueTypeFolds), [new CrossgenAssembly(boxIsInstFolding)]) + new(nameof(CastFoldingRespectsVersionBubble), [new CrossgenAssembly(castFolding)]) { - Options = [Crossgen2Option.Optimize], + Options = options, Validate = Validate, }, ])); - static void Validate(ReadyToRunReader reader) + void Validate(ReadyToRunReader reader) { // Box and isinst reference their types through NEW_OBJECT/IS_INSTANCE_OF imports // or TYPE_HANDLE fixups depending on the target, so look for the type in any import. @@ -183,15 +188,28 @@ static void Validate(ReadyToRunReader reader) string diagnostic = $"Imported types: [{string.Join(", ", importedTypes)}]"; - // An interface cast may succeed in a later version, so it isn't folded. - Assert.True(importedTypes.Contains("ControlStruct"), diagnostic); - Assert.True(importedTypes.Contains("INotImplemented"), diagnostic); - // Value type casts to other non-interface types fold, along with their boxes. Assert.False(importedTypes.Contains("char"), diagnostic); Assert.False(importedTypes.Contains("byte"), diagnostic); Assert.False(importedTypes.Contains("SourceStruct"), diagnostic); Assert.False(importedTypes.Contains("TargetStruct"), diagnostic); + + // Casts to interfaces fold only when every type in the hierarchy is in the version bubble, + // which includes System.Object and System.ValueType only with the framework in the bubble. + Assert.True(importedTypes.Contains("ControlStruct") != inputBubble, diagnostic); + Assert.True(importedTypes.Contains("INotImplemented") != inputBubble, diagnostic); + Assert.True(importedTypes.Contains("INotImplementedByClass") != inputBubble, diagnostic); + + // Interfaces have no base type, so typeof(...) assignability checks between them fold whenever + // the target and every inherited interface are in the version bubble, including for shared + // generics. Per assembly, IsAssignableFrom stays only for the IComparable target and + // IsAssignableTo only for the interface that inherits IDisposable. + Assert.True(importedTypes.Any(s => s.Contains("System.Type.IsAssignableFrom(", StringComparison.Ordinal)) != inputBubble, diagnostic); + Assert.True(importedTypes.Any(s => s.Contains("System.Type.IsAssignableTo(", StringComparison.Ordinal)) != inputBubble, diagnostic); + + // Variant and COM casts are never folded. + Assert.True(importedTypes.Contains("IVariant`1"), diagnostic); + Assert.True(importedTypes.Contains("INotImplementedByCom"), diagnostic); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs index 07152a76ac6d05..470855bd16e09a 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RDriver.cs @@ -56,7 +56,7 @@ internal static class Crossgen2OptionsExtensions public static string ToArg(this Crossgen2Option kind) => kind switch { Crossgen2Option.Composite => $"--composite", - Crossgen2Option.InputBubble => $"--input-bubble", + Crossgen2Option.InputBubble => $"--inputbubble", Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting", Crossgen2Option.Optimize => $"--optimize", Crossgen2Option.StripILBodies => $"--strip-il-bodies", diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 53d7e67d740a45..1675b9a79d411f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -3952,6 +3952,44 @@ void ValidateSafetyOfUsingTypeEquivalenceOfType(TypeDesc type) } } + /// + /// Returns true if a cast from an object of exact type to + /// that fails now also fails in every later version of its dependencies. + /// + private bool IsNegativeCastResultVersionResilient(TypeDesc fromType, TypeDesc toType) + { + if (fromType.HasTypeEquivalence || toType.HasTypeEquivalence) + return false; + + // A value type can gain interfaces (or become an enum), but its other cast targets are fixed. + if (fromType.IsValueType && !toType.IsInterface && !toType.IsWellKnownType(WellKnownType.Enum)) + return true; + + // Otherwise every type that decides the cast must version with this code. Array and variant casts + // also depend on element types and generic arguments, and COM objects can be cast to interfaces + // through QueryInterface. + if (fromType is not DefType || toType is not DefType || toType.HasVariance) + return false; + + CompilationModuleGroup moduleGroup = _compilation.CompilationModuleGroup; + if (!moduleGroup.VersionsWithType(toType)) + return false; + + for (TypeDesc type = fromType; type is not null; type = type.BaseType) + { + if (type.IsComImport || !moduleGroup.VersionsWithType(type)) + return false; + } + + foreach (DefType iface in fromType.RuntimeInterfaces) + { + if (!moduleGroup.VersionsWithType(iface)) + return false; + } + + return true; + } + private bool notifyMethodInfoUsage(CORINFO_METHOD_STRUCT_* ftn) { MethodDesc method = HandleToObject(ftn);