diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index 992d5c750c485b..fc33e3eeb2c8b4 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3179,8 +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 - // always reported back as May. - if (result == TypeCompareState.MustNot) + // 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/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 55728597715585..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,6 +145,74 @@ static void Validate(ReadyToRunReader reader) } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CastFoldingRespectsVersionBubble(bool inputBubble) + { + var castFolding = new CompiledAssembly + { + 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(CastFoldingRespectsVersionBubble), + [ + new(nameof(CastFoldingRespectsVersionBubble), [new CrossgenAssembly(castFolding)]) + { + Options = options, + Validate = Validate, + }, + ])); + + 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. + var formattingOptions = new SignatureFormattingOptions(); + var importedTypes = new HashSet(); + foreach (ReadyToRunImportSection section in reader.ImportSections) + { + foreach (ReadyToRunImportSection.ImportSectionEntry entry in section.Entries) + { + string? signature = entry.Signature?.ToString(formattingOptions); + int kindStart = signature?.LastIndexOf(" (", StringComparison.Ordinal) ?? -1; + if (kindStart > 0) + importedTypes.Add(signature![..kindStart]); + } + } + + string diagnostic = $"Imported types: [{string.Join(", ", importedTypes)}]"; + + // 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); + } + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))] public void WasmWebcilModule() { 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);