Crossgen2 support for static virtual method resolution (take 2) (#87438)
authorTomáš Rylek <trylek@microsoft.com>
Tue, 8 Aug 2023 18:43:50 +0000 (20:43 +0200)
committerGitHub <noreply@github.com>
Tue, 8 Aug 2023 18:43:50 +0000 (20:43 +0200)
This change adds SVM resolution support to Crossgen2. We still resort to
runtime JIT in case we cannot resolve the SVM call at compile time (typically for
canonical generic methods); some of these cases are just due to current limitations
of the JIT interface and can be fixed in the future.

Thanks

Tomas

src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelegateCtorSignature.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapNode.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/Import.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodFixupSignature.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
src/coreclr/vm/methodtable.cpp
src/tests/Loader/classloader/StaticVirtualMethods/NegativeTestCases/MethodBodyOnUnrelatedType.ilproj
src/tests/Loader/classloader/StaticVirtualMethods/Regression/GitHub_70385.il
src/tests/readytorun/tests/main.cs
src/tests/readytorun/tests/test.cs

index b3dc1ff..47e7e6b 100644 (file)
@@ -1365,6 +1365,9 @@ namespace Internal.JitInterface
 
         // token comes from devirtualizing a method
         CORINFO_TOKENKIND_DevirtualizedMethod = 0x800 | CORINFO_TOKENKIND_Method,
+
+        // token comes from resolved static virtual method
+        CORINFO_TOKENKIND_ResolvedStaticVirtualMethod = 0x1000 | CORINFO_TOKENKIND_Method,
     };
 
     // These are error codes returned by CompileMethod
index c986357..66dbf74 100644 (file)
@@ -42,12 +42,20 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
             {
                 SignatureContext innerContext = builder.EmitFixup(factory, ReadyToRunFixupKind.DelegateCtor, _methodToken.Token.Module, factory.SignatureContext);
 
+                bool needsInstantiatingStub = _targetMethod.Method.HasInstantiation;
+                if (_targetMethod.Method.IsVirtual && _targetMethod.Method.Signature.IsStatic)
+                {
+                    // For static virtual methods, we always require an instantiating stub as the method may resolve to a canonical representation
+                    // at runtime without us being able to detect that at compile time.
+                    needsInstantiatingStub |= (_targetMethod.Method.OwningType.HasInstantiation || _methodToken.ConstrainedType != null);
+                }
+
                 builder.EmitMethodSignature(
                     _methodToken,
                     enforceDefEncoding: false,
                     enforceOwningType: false,
                     innerContext,
-                    isInstantiatingStub: _targetMethod.Method.HasInstantiation);
+                    isInstantiatingStub: needsInstantiatingStub);
 
                 builder.EmitTypeSignature(_delegateType, innerContext);
             }
index fbf41d5..1e54ff3 100644 (file)
@@ -88,12 +88,13 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 }
                 else
                 {
-                    bool isUnboxingStub = false;
+                    bool isStub = false;
                     if (methodNode is DelayLoadHelperImport methodImport)
                     {
-                        isUnboxingStub = ((MethodFixupSignature)methodImport.ImportSignature.Target).IsUnboxingStub;
+                        MethodFixupSignature signature = (MethodFixupSignature)methodImport.ImportSignature.Target;
+                        isStub = signature.IsUnboxingStub || signature.IsInstantiatingStub;
                     }
-                    builder.GetCallRefMap(methodNode.Method, isUnboxingStub);
+                    builder.GetCallRefMap(methodNode.Method, isStub);
                 }
                 if (methodIndex >= nextMethodIndex)
                 {
index 9e38401..3a5eb9f 100644 (file)
@@ -19,6 +19,8 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
 
         internal readonly MethodDesc CallingMethod;
 
+        public Signature Signature => ImportSignature.Target;
+
         public Import(ImportSectionNode tableNode, Signature importSignature, MethodDesc callingMethod = null)
         {
             Table = tableNode;
index 792430a..63bd020 100644 (file)
@@ -35,7 +35,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
             compilerContext.EnsureLoadableMethod(method.Method);
             compilerContext.EnsureLoadableType(_method.OwningType);
 
-            if (method.ConstrainedType != null)
+            if (method.ConstrainedType != null && !method.ConstrainedType.IsRuntimeDeterminedSubtype)
                 compilerContext.EnsureLoadableType(method.ConstrainedType);
         }
 
@@ -46,6 +46,10 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
 
         public bool IsUnboxingStub => _method.Unboxing;
 
+        public TypeDesc ConstrainedType => _method.ConstrainedType;
+
+        public bool NeedsInstantiationArg => _method.ConstrainedType?.IsCanonicalSubtype(CanonicalFormKind.Any) ?? false;
+
         protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory)
         {
             DependencyList list = base.ComputeNonRelocationBasedDependencies(factory);
index c57e731..cb00ffa 100644 (file)
@@ -280,6 +280,15 @@ namespace Internal.JitInterface
                                 currentType = currentType.BaseType;
                             }
 
+                            foreach (DefType interfaceType in methodTargetOwner.RuntimeInterfaces)
+                            {
+                                if (interfaceType == instantiatedOwningType ||
+                                    interfaceType.ConvertToCanonForm(CanonicalFormKind.Specific) == canonicalizedOwningType)
+                                {
+                                    return methodTargetOwner;
+                                }
+                            }
+
                             Debug.Assert(false);
                             throw new Exception();
                         }
@@ -931,7 +940,23 @@ namespace Internal.JitInterface
             TypeDesc delegateTypeDesc = HandleToObject(delegateType);
             MethodDesc targetMethodDesc = HandleToObject(pTargetMethod.hMethod);
             Debug.Assert(!targetMethodDesc.IsUnboxingThunk());
-            MethodWithToken targetMethod = new MethodWithToken(targetMethodDesc, HandleToModuleToken(ref pTargetMethod), constrainedType: null, unboxing: false, context: entityFromContext(pTargetMethod.tokenContext));
+
+            var typeOrMethodContext = (pTargetMethod.tokenContext == contextFromMethodBeingCompiled()) ?
+                MethodBeingCompiled : HandleToObject((void*)pTargetMethod.tokenContext);
+
+            TypeDesc constrainedType = null;
+            if (targetConstraint != 0)
+            {
+                MethodIL methodIL = _compilation.GetMethodIL(MethodBeingCompiled);
+                constrainedType = (TypeDesc)ResolveTokenInScope(methodIL, typeOrMethodContext, targetConstraint);
+            }
+
+            MethodWithToken targetMethod = new MethodWithToken(
+                targetMethodDesc,
+                HandleToModuleToken(ref pTargetMethod),
+                constrainedType: constrainedType,
+                unboxing: false,
+                context: typeOrMethodContext);
 
             pLookup.lookupKind.needsRuntimeLookup = false;
             pLookup.constLookup = CreateConstLookupToSymbol(_compilation.SymbolNodeFactory.DelegateCtor(delegateTypeDesc, targetMethod));
@@ -1330,7 +1355,15 @@ namespace Internal.JitInterface
                 }
             }
 
-            context = entityFromContext(pResolvedToken.tokenContext);
+            if (pResolvedToken.tokenType == CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod)
+            {
+                context = null;
+            }
+            else
+            {
+                context = entityFromContext(pResolvedToken.tokenContext);
+            }
+
             return HandleToModuleToken(ref pResolvedToken);
         }
 
@@ -1845,22 +1878,21 @@ namespace Internal.JitInterface
             }
 
             callerModule = ((EcmaMethod)callerMethod.GetTypicalMethodDefinition()).Module;
+            bool isCallVirt = (flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) != 0;
+            bool isLdftn = (flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_LDFTN) != 0;
+            bool isStaticVirtual = (originalMethod.Signature.IsStatic && originalMethod.IsVirtual);
 
             // Spec says that a callvirt lookup ignores static methods. Since static methods
             // can't have the exact same signature as instance methods, a lookup that found
             // a static method would have never found an instance method.
-            if (originalMethod.Signature.IsStatic && (flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) != 0)
+            if (originalMethod.Signature.IsStatic && isCallVirt)
             {
                 ThrowHelper.ThrowInvalidProgramException(ExceptionStringID.InvalidProgramCallVirtStatic, originalMethod);
             }
 
             exactType = type;
 
-            constrainedType = null;
-            if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) != 0 && pConstrainedResolvedToken != null)
-            {
-                constrainedType = HandleToObject(pConstrainedResolvedToken->hClass);
-            }
+            constrainedType = (pConstrainedResolvedToken != null ? HandleToObject(pConstrainedResolvedToken->hClass) : null);
 
             bool resolvedConstraint = false;
             bool forceUseRuntimeLookup = false;
@@ -1887,7 +1919,19 @@ namespace Internal.JitInterface
                     originalMethod = methodOnUnderlyingType;
                 }
 
-                MethodDesc directMethod = constrainedType.TryResolveConstraintMethodApprox(exactType, originalMethod, out forceUseRuntimeLookup);
+                MethodDesc directMethod;
+                if (isStaticVirtual)
+                {
+                    directMethod = constrainedType.ResolveVariantInterfaceMethodToStaticVirtualMethodOnType(originalMethod);
+                    if (directMethod != null && !_compilation.NodeFactory.CompilationModuleGroup.VersionsWithMethodBody(directMethod))
+                    {
+                        directMethod = null;
+                    }
+                }
+                else
+                {
+                    directMethod = constrainedType.TryResolveConstraintMethodApprox(exactType, originalMethod, out forceUseRuntimeLookup);
+                }
                 if (directMethod != null)
                 {
                     // Either
@@ -1909,6 +1953,15 @@ namespace Internal.JitInterface
                     pResult->thisTransform = CORINFO_THIS_TRANSFORM.CORINFO_NO_THIS_TRANSFORM;
 
                     exactType = constrainedType;
+                    if (isStaticVirtual)
+                    {
+                        pResolvedToken.tokenType = CorInfoTokenKind.CORINFO_TOKENKIND_ResolvedStaticVirtualMethod;
+                        constrainedType = null;
+                    }
+                }
+                else if (isStaticVirtual)
+                {
+                    pResult->thisTransform = CORINFO_THIS_TRANSFORM.CORINFO_NO_THIS_TRANSFORM;
                 }
                 else if (constrainedType.IsValueType)
                 {
@@ -1925,16 +1978,17 @@ namespace Internal.JitInterface
             //
 
             targetMethod = methodAfterConstraintResolution;
+            bool constrainedTypeNeedsRuntimeLookup = (constrainedType != null && constrainedType.IsCanonicalSubtype(CanonicalFormKind.Any));
 
             if (targetMethod.HasInstantiation)
             {
                 pResult->contextHandle = contextFromMethod(targetMethod);
-                pResult->exactContextNeedsRuntimeLookup = targetMethod.IsSharedByGenericInstantiations;
+                pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || targetMethod.IsSharedByGenericInstantiations;
             }
             else
             {
                 pResult->contextHandle = contextFromType(exactType);
-                pResult->exactContextNeedsRuntimeLookup = exactType.IsCanonicalSubtype(CanonicalFormKind.Any);
+                pResult->exactContextNeedsRuntimeLookup = constrainedTypeNeedsRuntimeLookup || exactType.IsCanonicalSubtype(CanonicalFormKind.Any);
 
                 // Use main method as the context as long as the methods are called on the same type
                 if (pResult->exactContextNeedsRuntimeLookup &&
@@ -1959,18 +2013,21 @@ namespace Internal.JitInterface
             bool resolvedCallVirt = false;
             bool callVirtCrossingVersionBubble = false;
 
-            if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_LDFTN) != 0)
+            if (isStaticVirtual && !resolvedConstraint)
+            {
+                // Don't use direct calls for static virtual method calls unresolved at compile time
+            }
+            else if (isLdftn)
             {
                 PrepareForUseAsAFunctionPointer(targetMethod);
                 directCall = true;
             }
-            else
-            if (targetMethod.Signature.IsStatic)
+            else if (targetMethod.Signature.IsStatic)
             {
                 // Static methods are always direct calls
                 directCall = true;
             }
-            else if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) == 0 || resolvedConstraint)
+            else if (!isCallVirt || resolvedConstraint)
             {
                 directCall = true;
             }
@@ -2029,16 +2086,41 @@ namespace Internal.JitInterface
 
             if (directCall)
             {
+                bool isVirtualBehaviorUnresolved = (isCallVirt && !resolvedCallVirt || isStaticVirtual && !resolvedConstraint);
+
                 // Direct calls to abstract methods are not allowed
                 if (targetMethod.IsAbstract &&
                     // Compensate for always treating delegates as direct calls above
-                    !(((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_LDFTN) != 0) && ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) != 0) && !resolvedCallVirt))
+                    !(isLdftn && isVirtualBehaviorUnresolved))
                 {
                     ThrowHelper.ThrowInvalidProgramException(ExceptionStringID.InvalidProgramCallAbstractMethod, targetMethod);
                 }
 
                 bool allowInstParam = (flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_ALLOWINSTPARAM) != 0;
 
+                // If the target method is resolved via constrained static virtual dispatch
+                // And it requires an instParam, we do not have the generic dictionary infrastructure
+                // to load the correct generic context arg via EmbedGenericHandle.
+                // Instead, force the call to go down the CORINFO_CALL_CODE_POINTER code path
+                // which should have somewhat inferior performance. This should only actually happen in the case
+                // of shared generic code calling a shared generic implementation method, which should be rare.
+                //
+                // An alternative design would be to add a new generic dictionary entry kind to hold the MethodDesc
+                // of the constrained target instead, and use that in some circumstances; however, implementation of 
+                // that design requires refactoring variuos parts of the JIT interface as well as
+                // TryResolveConstraintMethodApprox. In particular we would need to be abled to embed a constrained lookup
+                // via EmbedGenericHandle, as well as decide in TryResolveConstraintMethodApprox if the call can be made
+                // via a single use of CORINFO_CALL_CODE_POINTER, or would be better done with a CORINFO_CALL + embedded
+                // constrained generic handle, or if there is a case where we would want to use both a CORINFO_CALL and
+                // embedded constrained generic handle. Given the current expected high performance use case of this feature
+                // which is generic numerics which will always resolve to exact valuetypes, it is not expected that
+                // the complexity involved would be worth the risk. Other scenarios are not expected to be as performance
+                // sensitive.
+                if (isStaticVirtual && pResult->exactContextNeedsRuntimeLookup)
+                {
+                    allowInstParam = false;
+                }
+
                 if (!allowInstParam && canonMethod != null && canonMethod.RequiresInstArg())
                 {
                     useInstantiatingStub = true;
@@ -2085,11 +2167,16 @@ namespace Internal.JitInterface
                     {
                         pResult->kind = CORINFO_CALL_KIND.CORINFO_CALL_CODE_POINTER;
 
-                        // For reference types, the constrained type does not affect method resolution
-                        DictionaryEntryKind entryKind = (constrainedType != null && constrainedType.IsValueType
+                        // For reference types, the constrained type does not affect instance virtual method resolution
+                        DictionaryEntryKind entryKind = (constrainedType != null && (constrainedType.IsValueType || !isCallVirt)
                             ? DictionaryEntryKind.ConstrainedMethodEntrySlot
                             : DictionaryEntryKind.MethodEntrySlot);
 
+                        if (isStaticVirtual && exactType.HasInstantiation)
+                        {
+                            useInstantiatingStub = true;
+                        }
+
                         ComputeRuntimeLookupForSharedGenericToken(entryKind, ref pResolvedToken, pConstrainedResolvedToken, originalMethod, ref pResult->codePointerOrStubLookup);
                     }
                 }
@@ -2111,6 +2198,33 @@ namespace Internal.JitInterface
                 }
                 pResult->nullInstanceCheck = resolvedCallVirt;
             }
+            else if (isStaticVirtual)
+            {
+                pResult->kind = CORINFO_CALL_KIND.CORINFO_CALL;
+                pResult->nullInstanceCheck = false;
+
+                // Always use an instantiating stub for unresolved constrained SVM calls as we cannot
+                // always tell at compile time that a given SVM resolves to a method on a generic base
+                // class and not requesting the instantiating stub makes the runtime transform the
+                // owning type to its canonical equivalent that would need different codegen
+                // (supplying the instantiation argument).
+                if (!resolvedConstraint)
+                {
+                    if (pResult->exactContextNeedsRuntimeLookup)
+                    {
+                        throw new RequiresRuntimeJitException("EmbedGenericHandle currently doesn't support propagation of RUNTIME_LOOKUP or pConstrainedResolvedToken from ComputeRuntimeLookupForSharedGenericToken");
+                        // ComputeRuntimeLookupForSharedGenericToken(DictionaryEntryKind.DispatchStubAddrSlot, ref pResolvedToken, pConstrainedResolvedToken, originalMethod, ref pResult->codePointerOrStubLookup);
+                        // useInstantiatingStub = false;
+                    }
+                    else
+                    {
+                        throw new RequiresRuntimeJitException("CanInline currently doesn't support propagation of constrained type so that we cannot reliably tell whether a SVM call can be inlined");
+                        // Even if we decided to support SVMs unresolved at compile time, we'd still need to force the use of instantiating stub
+                        // as we can't tell in advance whether the method will be runtime-resolved to a canonical representation.
+                        // useInstantiatingStub = true;
+                    }
+                }
+            }
             // All virtual calls which take method instantiations must
             // currently be implemented by an indirect call via a runtime-lookup
             // function pointer
@@ -2250,13 +2364,6 @@ namespace Internal.JitInterface
 
         private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESOLVED_TOKEN* pConstrainedResolvedToken, CORINFO_METHOD_STRUCT_* callerHandle, CORINFO_CALLINFO_FLAGS flags, CORINFO_CALL_INFO* pResult)
         {
-            if ((flags & CORINFO_CALLINFO_FLAGS.CORINFO_CALLINFO_CALLVIRT) == 0 && pConstrainedResolvedToken != null)
-            {
-                // Defer constrained call / ldftn instructions used for static virtual methods
-                // to runtime resolution.
-                throw new RequiresRuntimeJitException("SVM");
-            }
-
             MethodDesc methodToCall;
             MethodDesc targetMethod;
             TypeDesc constrainedType;
index e84f8b0..df47319 100644 (file)
@@ -9242,7 +9242,7 @@ MethodTable::TryResolveConstraintMethodApprox(
     {
         _ASSERTE(!thInterfaceType.IsTypeDesc());
         _ASSERTE(thInterfaceType.IsInterface());
-        BOOL uniqueResolution;
+        BOOL uniqueResolution = TRUE;
 
         ResolveVirtualStaticMethodFlags flags = ResolveVirtualStaticMethodFlags::AllowVariantMatches
                                               | ResolveVirtualStaticMethodFlags::InstantiateResultOverFinalMethodDesc;
@@ -9255,7 +9255,8 @@ MethodTable::TryResolveConstraintMethodApprox(
             thInterfaceType.GetMethodTable(),
             pInterfaceMD,
             flags,
-            &uniqueResolution);
+            (pfForceUseRuntimeLookup != NULL ? &uniqueResolution : NULL));
+
         if (result == NULL || !uniqueResolution)
         {
             _ASSERTE(pfForceUseRuntimeLookup != NULL);
index 19680b9..397521a 100644 (file)
@@ -1,6 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk.IL">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
+
+    <!-- Crossgen2 currently doesn't support this negative check - that should be fine as runtime behavior is undefined in the presence of invalid IL. -->
+    <CrossGenTest>false</CrossGenTest>
   </PropertyGroup>
   <PropertyGroup>
     <DebugType>Full</DebugType>
index 3116af1..12e585a 100644 (file)
@@ -15,7 +15,7 @@
   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
   .ver 7:0:0:0
 }
-.assembly RecursiveGeneric
+.assembly GitHub_70385
 {
   .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
   .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
@@ -27,7 +27,7 @@
   .hash algorithm 0x00008004
   .ver 0:0:0:0
 }
-.module RecursiveGeneric.dll
+.module GitHub_70385.dll
 // MVID: {10541B0F-16D6-4F9A-B0EB-E793F524F163}
 .imagebase 0x00400000
 .file alignment 0x00000200
index 0431486..8201652 100644 (file)
@@ -441,6 +441,19 @@ class Program
         Assert.AreEqual(ILInliningTest.TestDifferentIntValue(), actualMethodCallResult);
     }
 
+    private class CallDefaultVsExactStaticVirtual<T> where T : IDefaultVsExactStaticVirtual
+    {
+        public static string CallMethodOnGenericType() => T.Method();
+    }
+
+    [MethodImplAttribute(MethodImplOptions.NoInlining)]
+    static void TestDefaultVsExactStaticVirtualMethodImplementation()
+    {
+        Assert.AreEqual(CallDefaultVsExactStaticVirtual<DefaultVsExactStaticVirtualClass>.CallMethodOnGenericType(), "DefaultVsExactStaticVirtualMethod");
+        // Naively one would expect that the following should do, however Roslyn fails to compile it claiming that the type DVESVC doesn't contain 'Method':
+        // Assert.AreEqual(DefaultVsExactStaticVirtualClass.Method(), "DefaultVsExactStaticVirtualMethod");
+    }
+
     static void RunAllTests()
     {
         Console.WriteLine("TestVirtualMethodCalls");
@@ -527,6 +540,10 @@ class Program
 
         Console.WriteLine("TestILBodyChange");
         TestILBodyChange();
+        
+        Console.WriteLine("TestDefaultVsExactStaticVirtualMethodImplementation");
+        TestDefaultVsExactStaticVirtualMethodImplementation();
+        
         ILInliningVersioningTest<LocallyDefinedStructure>.RunAllTests(typeof(Program).Assembly);
     }
 
index 73e4dfb..a24576f 100644 (file)
@@ -438,6 +438,24 @@ static class OpenClosedDelegateExtensionTest
     }
 }
 
+public interface IDefaultVsExactStaticVirtual
+{
+    static virtual string Method() =>
+#if V2
+        "Error - IDefaultVsExactStaticVirtual.Method shouldn't be used in V2"
+#else
+        "DefaultVsExactStaticVirtualMethod"
+#endif
+    ;
+}
+
+public class DefaultVsExactStaticVirtualClass : IDefaultVsExactStaticVirtual
+{
+#if V2
+    static string IDefaultVsExactStaticVirtual.Method() => "DefaultVsExactStaticVirtualMethod";
+#endif
+}
+
 // Test dependent versioning details
 public class ILInliningVersioningTest<T>
 {