Adapt Tarjan generic cycle detector for use in Crossgen2 (#71426)
authorTomáš Rylek <trylek@microsoft.com>
Tue, 13 Jun 2023 00:49:39 +0000 (02:49 +0200)
committerGitHub <noreply@github.com>
Tue, 13 Jun 2023 00:49:39 +0000 (02:49 +0200)
This change modifies Crossgen2 to use the generic cycle detector
originally implemented for NativeAOT to trim infinite generic
expansion as observed in the LanguageExt public nuget package
and tracked by the issue

https://github.com/dotnet/runtime/issues/66079

For now the generic cycle detector is opt-in as it's relatively costly
in terms of compiler performance and it seems to be a relatively
niche case. We can follow up by optimizing the detector if it turns
out to be more prevalent or if we make the call to turn it on by
default, I have mentioned several options for that in the PR.

Thanks

Tomas

35 files changed:
src/coreclr/tools/Common/Compiler/GenericCycleDetection/GenericCycleDetection.projitems [new file with mode: 0644]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/Graph.Cycles.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/Graph.Cycles.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/Graph.Vertex.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/Graph.Vertex.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/Graph.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/Graph.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/GraphBuilder.ForEach.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/GraphBuilder.ForEach.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/GraphBuilder.MethodCall.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/GraphBuilder.MethodCall.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/GraphBuilder.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/GraphBuilder.cs with 100% similarity]
src/coreclr/tools/Common/Compiler/GenericCycleDetection/ModuleCycleInfo.cs [moved from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/LazyGenerics/ModuleCycleInfo.cs with 82% similarity]
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs
src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/AllMethodsOnTypeNode.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/DelayLoadHelperMethodImport.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodFixupSignature.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeFixupSignature.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilationBuilder.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs
src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs
src/coreclr/tools/aot/ILCompiler/Program.cs
src/coreclr/tools/aot/Mono.Linker.Tests/TestCasesRunner/ILCompilerDriver.cs
src/coreclr/tools/aot/crossgen2.sln
src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs
src/coreclr/tools/aot/crossgen2/Program.cs
src/coreclr/tools/aot/crossgen2/Properties/Resources.resx
src/tests/readytorun/GenericCycleDetection/Breadth1Test.cs [new file with mode: 0644]
src/tests/readytorun/GenericCycleDetection/Breadth1Test.csproj [new file with mode: 0644]
src/tests/readytorun/GenericCycleDetection/Depth1Test.cs [new file with mode: 0644]
src/tests/readytorun/GenericCycleDetection/Depth1Test.csproj [new file with mode: 0644]
src/tests/readytorun/GenericCycleDetection/Depth3Test.cs [new file with mode: 0644]
src/tests/readytorun/GenericCycleDetection/Depth3Test.csproj [new file with mode: 0644]

diff --git a/src/coreclr/tools/Common/Compiler/GenericCycleDetection/GenericCycleDetection.projitems b/src/coreclr/tools/Common/Compiler/GenericCycleDetection/GenericCycleDetection.projitems
new file mode 100644 (file)
index 0000000..e0cee3b
--- /dev/null
@@ -0,0 +1,13 @@
+<Project>
+
+  <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)Graph.cs" Link="Common\Compiler\GenericCycleDetection\Graph.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Graph.Cycles.cs" Link="Common\Compiler\GenericCycleDetection\Graph.Cycles.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Graph.Vertex.cs" Link="Common\Compiler\GenericCycleDetection\Graph.Vertex.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)GraphBuilder.cs" Link="Common\Compiler\GenericCycleDetection\GraphBuilder.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)GraphBuilder.ForEach.cs" Link="Common\Compiler\GenericCycleDetection\GraphBuilder.ForEach.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)GraphBuilder.MethodCall.cs" Link="Common\Compiler\GenericCycleDetection\GraphBuilder.MethodCall.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)ModuleCycleInfo.cs" Link="Common\Compiler\GenericCycleDetection\ModuleCycleInfo.cs" />
+  </ItemGroup>
+
+</Project>
@@ -8,7 +8,9 @@ using System.Collections.Concurrent;
 using Internal.TypeSystem;
 using Internal.TypeSystem.Ecma;
 
+#if !READYTORUN
 using ILLink.Shared;
+#endif
 
 using Debug = System.Diagnostics.Debug;
 
@@ -91,11 +93,13 @@ namespace ILCompiler
             // from the key, but since this is a key/value pair, might as well use the value too...
             private readonly ConcurrentDictionary<EntityPair, ModuleCycleInfo> _actualProblems = new ConcurrentDictionary<EntityPair, ModuleCycleInfo>();
 
-            private readonly int _cutoffPoint;
+            private readonly int _depthCutoff;
+            private readonly int _breadthCutoff;
 
-            public GenericCycleDetector(int cutoffPoint)
+            public GenericCycleDetector(int depthCutoff, int breadthCutoff)
             {
-                _cutoffPoint = cutoffPoint;
+                _depthCutoff = depthCutoff;
+                _breadthCutoff = breadthCutoff;
             }
 
             private bool IsDeepPossiblyCyclicInstantiation(TypeSystemEntity entity)
@@ -110,17 +114,31 @@ namespace ILCompiler
                 }
             }
 
-            private bool IsDeepPossiblyCyclicInstantiation(TypeDesc type, List<TypeDesc> seenTypes = null)
+            private bool IsDeepPossiblyCyclicInstantiation(TypeDesc type)
+            {
+                int breadthCounter = 0;
+                return IsDeepPossiblyCyclicInstantiation(type, ref breadthCounter, seenTypes: null);
+            }
+
+            private bool IsDeepPossiblyCyclicInstantiation(TypeDesc type, ref int breadthCounter, List<TypeDesc> seenTypes = null)
             {
                 switch (type.Category)
                 {
                     case TypeFlags.Array:
                     case TypeFlags.SzArray:
-                        return IsDeepPossiblyCyclicInstantiation(((ParameterizedType)type).ParameterType, seenTypes);
+                        return IsDeepPossiblyCyclicInstantiation(((ParameterizedType)type).ParameterType, ref breadthCounter, seenTypes);
                     default:
                         TypeDesc typeDef = type.GetTypeDefinition();
                         if (type != typeDef)
                         {
+                            if (FormsCycle(typeDef, out ModuleCycleInfo _))
+                            {
+                                if (_breadthCutoff >= 0 && ++breadthCounter >= _breadthCutoff)
+                                {
+                                    return true;
+                                }
+                            }
+
                             (seenTypes ??= new List<TypeDesc>()).Add(typeDef);
                             for (int i = 0; i < seenTypes.Count; i++)
                             {
@@ -133,14 +151,14 @@ namespace ILCompiler
                                         count++;
                                     }
 
-                                    if (count > _cutoffPoint)
+                                    if (count > _depthCutoff)
                                     {
                                         return true;
                                     }
                                 }
                             }
 
-                            bool result = IsDeepPossiblyCyclicInstantiation(type.Instantiation, seenTypes);
+                            bool result = IsDeepPossiblyCyclicInstantiation(type.Instantiation, ref breadthCounter, seenTypes);
                             seenTypes.RemoveAt(seenTypes.Count - 1);
                             return result;
                         }
@@ -148,11 +166,11 @@ namespace ILCompiler
                 }
             }
 
-            private bool IsDeepPossiblyCyclicInstantiation(Instantiation instantiation, List<TypeDesc> seenTypes = null)
+            private bool IsDeepPossiblyCyclicInstantiation(Instantiation instantiation, ref int breadthCounter, List<TypeDesc> seenTypes)
             {
                 foreach (TypeDesc arg in instantiation)
                 {
-                    if (IsDeepPossiblyCyclicInstantiation(arg, seenTypes))
+                    if (IsDeepPossiblyCyclicInstantiation(arg, ref breadthCounter, seenTypes))
                     {
                         return true;
                     }
@@ -163,13 +181,30 @@ namespace ILCompiler
 
             public bool IsDeepPossiblyCyclicInstantiation(MethodDesc method)
             {
-                return IsDeepPossiblyCyclicInstantiation(method.Instantiation) || IsDeepPossiblyCyclicInstantiation(method.OwningType);
+                int breadthCounter = 0;
+                return IsDeepPossiblyCyclicInstantiation(method.Instantiation, ref breadthCounter, seenTypes: null)
+                    || IsDeepPossiblyCyclicInstantiation(method.OwningType, ref breadthCounter, seenTypes: null);
+            }
+
+            private bool FormsCycle(TypeSystemEntity entity, out ModuleCycleInfo cycleInfo)
+            {
+                EcmaModule ownerModule = (entity as EcmaType)?.EcmaModule ?? (entity as EcmaMethod)?.Module;
+                if (ownerModule != null)
+                {
+                    cycleInfo = _hashtable.GetOrCreateValue(ownerModule);
+                    return cycleInfo.FormsCycle(entity);
+                }
+                else
+                {
+                    cycleInfo = null;
+                    return false;
+                }
             }
 
             public void DetectCycle(TypeSystemEntity owner, TypeSystemEntity referent)
             {
                 // This allows to disable cycle detection completely (typically for perf reasons as the algorithm is pretty slow)
-                if (_cutoffPoint < 0)
+                if (_depthCutoff < 0)
                     return;
 
                 // Not clear if generic recursion through fields is a thing
@@ -192,10 +227,7 @@ namespace ILCompiler
                     return;
                 }
 
-                EcmaModule ownerModule = (ownerDefinition as EcmaType)?.EcmaModule ?? ((EcmaMethod)ownerDefinition).Module;
-
-                ModuleCycleInfo cycleInfo = _hashtable.GetOrCreateValue(ownerModule);
-                if (cycleInfo.FormsCycle(ownerDefinition))
+                if (FormsCycle(ownerDefinition, out ModuleCycleInfo cycleInfo))
                 {
                     // Just the presence of a cycle is not a problem, but once we start getting too deep,
                     // we need to cut our losses.
@@ -217,6 +249,7 @@ namespace ILCompiler
                 }
             }
 
+#if !READYTORUN
             public void LogWarnings(Logger logger)
             {
                 // Might need to sort these if we care about warning determinism, but we probably don't.
@@ -252,6 +285,7 @@ namespace ILCompiler
                     logger.LogWarning(actualProblem.Key.Owner, DiagnosticId.GenericRecursionCycle, actualProblem.Key.Referent.GetDisplayName(), message);
                 }
             }
+#endif
         }
     }
 }
index b9c33d2..f831e74 100644 (file)
@@ -552,6 +552,17 @@ namespace Internal.JitInterface
                 {
                     if (computedNodes.Add(fixup))
                     {
+                        if (fixup is IMethodNode methodNode)
+                        {
+                            try
+                            {
+                                _compilation.NodeFactory.DetectGenericCycles(_methodCodeNode.Method, methodNode.Method);
+                            }
+                            catch (TypeLoadException)
+                            {
+                                throw new RequiresRuntimeJitException("Requires runtime JIT - potential generic cycle detected");
+                            }
+                        }
                         _methodCodeNode.Fixups.Add(fixup);
                     }
                 }
index a022eb5..87ee25d 100644 (file)
@@ -22,7 +22,9 @@ namespace ILCompiler
         // We want this to be high enough so that it doesn't cut off too early. But also not too
         // high because things that are recursive often end up expanding laterally as well
         // through various other generic code the deep code calls into.
-        public const int DefaultGenericCycleCutoffPoint = 4;
+        public const int DefaultGenericCycleDepthCutoff = 4;
+
+        public const int DefaultGenericCycleBreadthCutoff = 10;
 
         public SharedGenericsConfiguration GenericsConfig
         {
@@ -40,7 +42,9 @@ namespace ILCompiler
         private MetadataType _arrayOfTType;
         private MetadataType _attributeType;
 
-        public CompilerTypeSystemContext(TargetDetails details, SharedGenericsMode genericsMode, DelegateFeature delegateFeatures, int genericCycleCutoffPoint = DefaultGenericCycleCutoffPoint)
+        public CompilerTypeSystemContext(TargetDetails details, SharedGenericsMode genericsMode, DelegateFeature delegateFeatures,
+            int genericCycleDepthCutoff = DefaultGenericCycleDepthCutoff,
+            int genericCycleBreadthCutoff = DefaultGenericCycleBreadthCutoff)
             : base(details)
         {
             _genericsMode = genericsMode;
@@ -51,7 +55,7 @@ namespace ILCompiler
 
             _delegateInfoHashtable = new DelegateInfoHashtable(delegateFeatures);
 
-            _genericCycleDetector = new LazyGenericsSupport.GenericCycleDetector(genericCycleCutoffPoint);
+            _genericCycleDetector = new LazyGenericsSupport.GenericCycleDetector(genericCycleDepthCutoff, genericCycleBreadthCutoff);
 
             GenericsConfig = new SharedGenericsConfiguration();
         }
index e0bce1b..1822619 100644 (file)
     <Compile Include="Compiler\FeatureSwitchManager.cs" />
     <Compile Include="Compiler\GeneratingMetadataManager.cs" />
     <Compile Include="Compiler\GenericRootProvider.cs" />
-    <Compile Include="Compiler\LazyGenerics\ModuleCycleInfo.cs" />
-    <Compile Include="Compiler\LazyGenerics\Graph.cs" />
-    <Compile Include="Compiler\LazyGenerics\Graph.Cycles.cs" />
-    <Compile Include="Compiler\LazyGenerics\Graph.Vertex.cs" />
-    <Compile Include="Compiler\LazyGenerics\GraphBuilder.cs" />
-    <Compile Include="Compiler\LazyGenerics\GraphBuilder.ForEach.cs" />
-    <Compile Include="Compiler\LazyGenerics\GraphBuilder.MethodCall.cs" />
     <Compile Include="Compiler\HardwareIntrinsicHelpers.Aot.cs" />
     <Compile Include="Compiler\IInliningPolicy.cs" />
     <Compile Include="Compiler\Logging\NativeAotFatalErrorException.cs" />
   </ItemGroup>
 
   <Import Project="..\..\..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems" Label="Shared" />
+  <Import Project="..\..\Common\Compiler\GenericCycleDetection\GenericCycleDetection.projitems" />
+
 </Project>
index 1585861..a89a311 100644 (file)
@@ -40,7 +40,14 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 if (!method.IsGenericMethodDefinition &&
                     context.CompilationModuleGroup.ContainsMethodBody(method, false))
                 {
-                    dependencies.Add(context.CompiledMethodNode(method), $"Method on type {Type.ToString()}");
+                    try
+                    {
+                        context.DetectGenericCycles(Type, method);
+                        dependencies.Add(context.CompiledMethodNode(method), $"Method on type {Type.ToString()}");
+                    }
+                    catch (TypeSystemException)
+                    {
+                    }
                 }
             }
 
index d034ba5..eb996de 100644 (file)
@@ -48,8 +48,20 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 MethodDesc canonMethod = _method.Method.GetCanonMethodTarget(CanonicalFormKind.Specific);
                 if (factory.CompilationModuleGroup.ContainsMethodBody(canonMethod, false))
                 {
-                    ISymbolNode canonMethodNode = factory.CompiledMethodNode(canonMethod);
-                    yield return new DependencyListEntry(canonMethodNode, "Canonical method for instantiating stub");
+                    bool useDependency = true;
+                    try
+                    {
+                        factory.DetectGenericCycles(_method.Method, canonMethod);
+                    }
+                    catch (TypeSystemException)
+                    {
+                        useDependency = false;
+                    }
+                    if (useDependency)
+                    {
+                        ISymbolNode canonMethodNode = factory.CompiledMethodNode(canonMethod);
+                        yield return new DependencyListEntry(canonMethodNode, "Canonical method for instantiating stub");
+                    }
                 }
             }
         }
index e3a3d3a..792430a 100644 (file)
@@ -58,7 +58,14 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                 factory.CompilationModuleGroup.ContainsMethodBody(canonMethod, false))
             {
                 list = list ?? new DependencyAnalysisFramework.DependencyNodeCore<NodeFactory>.DependencyList();
-                list.Add(factory.CompiledMethodNode(canonMethod), "Virtual function dependency on cross module inlineable method");
+                try
+                {
+                    factory.DetectGenericCycles(_method.Method, canonMethod);
+                    list.Add(factory.CompiledMethodNode(canonMethod), "Virtual function dependency on cross module inlineable method");
+                }
+                catch (TypeSystemException)
+                {
+                }
             }
 
             return list;
index e379651..a32a1a1 100644 (file)
@@ -182,7 +182,14 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
                         {
                             case "MoveNext":
                             case ".cctor":
-                                dependencies.Add(factory.CompiledMethodNode(method), $"AsyncStateMachineBox Method on type {type.ToString()}");
+                                try
+                                {
+                                    factory.DetectGenericCycles(type, method);
+                                    dependencies.Add(factory.CompiledMethodNode(method), $"AsyncStateMachineBox Method on type {type.ToString()}");
+                                }
+                                catch (TypeSystemException)
+                                {
+                                }
                                 break;
                         }
                     }
index 3046ad9..2206340 100644 (file)
@@ -188,7 +188,9 @@ namespace ILCompiler.DependencyAnalysis
             ResourceData win32Resources,
             ReadyToRunFlags flags,
             NodeFactoryOptimizationFlags nodeFactoryOptimizationFlags,
-            ulong imageBase)
+            ulong imageBase,
+            int genericCycleDepthCutoff,
+            int genericCycleBreadthCutoff)
         {
             OptimizationFlags = nodeFactoryOptimizationFlags;
             TypeSystemContext = context;
@@ -216,6 +218,13 @@ namespace ILCompiler.DependencyAnalysis
             }
 
             CreateNodeCaches();
+
+            if (genericCycleBreadthCutoff >= 0 || genericCycleDepthCutoff >= 0)
+            {
+                _genericCycleDetector = new LazyGenericsSupport.GenericCycleDetector(
+                    depthCutoff: genericCycleDepthCutoff,
+                    breadthCutoff: genericCycleBreadthCutoff);
+            }
         }
 
         private void CreateNodeCaches()
@@ -389,6 +398,8 @@ namespace ILCompiler.DependencyAnalysis
 
         private NodeCache<ReadyToRunHelper, Import> _constructedHelpers;
 
+        private LazyGenericsSupport.GenericCycleDetector _genericCycleDetector;
+
         public Import GetReadyToRunHelperCell(ReadyToRunHelper helperId)
         {
             return _constructedHelpers.GetOrAdd(helperId);
@@ -1010,5 +1021,10 @@ namespace ILCompiler.DependencyAnalysis
         {
             return _copiedManagedResources.GetOrAdd(module);
         }
+
+        public void DetectGenericCycles(TypeSystemEntity caller, TypeSystemEntity callee)
+        {
+            _genericCycleDetector?.DetectCycle(caller, callee);
+        }
     }
 }
index 09b786b..e975df9 100644 (file)
@@ -106,6 +106,8 @@ namespace ILCompiler
                 }
             }
 
+            _nodeFactory.DetectGenericCycles(caller, callee);
+
             return NodeFactory.CompilationModuleGroup.CanInline(caller, callee);
         }
 
@@ -426,7 +428,9 @@ namespace ILCompiler
                 win32Resources: new Win32Resources.ResourceData(inputModule),
                 flags,
                 _nodeFactory.OptimizationFlags,
-                _nodeFactory.ImageBase);
+                _nodeFactory.ImageBase,
+                genericCycleDepthCutoff: -1, // We don't need generic cycle detection when rewriting component assemblies
+                genericCycleBreadthCutoff: -1); // as we're not actually compiling anything
 
             IComparer<DependencyNodeCore<NodeFactory>> comparer = new SortableDependencyNode.ObjectNodeComparer(CompilerComparer.Instance);
             DependencyAnalyzerBase<NodeFactory> componentGraph = new DependencyAnalyzer<NoLogStrategy<NodeFactory>, NodeFactory>(componentFactory, comparer);
index 573eacc..ae40f3e 100644 (file)
@@ -42,6 +42,8 @@ namespace ILCompiler
         private CompositeImageSettings _compositeImageSettings;
         private ulong _imageBase;
         private NodeFactoryOptimizationFlags _nodeFactoryOptimizationFlags = new NodeFactoryOptimizationFlags();
+        private int _genericCycleDetectionDepthCutoff = -1;
+        private int _genericCycleDetectionBreadthCutoff = -1;
 
         private string _jitPath;
         private string _outputFile;
@@ -210,6 +212,13 @@ namespace ILCompiler
             return this;
         }
 
+        public ReadyToRunCodegenCompilationBuilder UseGenericCycleDetection(int depthCutoff, int breadthCutoff)
+        {
+            _genericCycleDetectionDepthCutoff = depthCutoff;
+            _genericCycleDetectionBreadthCutoff = breadthCutoff;
+            return this;
+        }
+
         public override ICompilation ToCompilation()
         {
             // TODO: only copy COR headers for single-assembly build and for composite build with embedded MSIL
@@ -254,7 +263,9 @@ namespace ILCompiler
                 win32Resources,
                 flags,
                 _nodeFactoryOptimizationFlags,
-                _imageBase
+                _imageBase,
+                genericCycleDepthCutoff: _genericCycleDetectionDepthCutoff,
+                genericCycleBreadthCutoff: _genericCycleDetectionBreadthCutoff
                 );
 
             factory.CompositeImageSettings = _compositeImageSettings;
index 27d0a97..265301f 100644 (file)
@@ -418,6 +418,7 @@ namespace ILCompiler
                 if (CorInfoImpl.ShouldCodeNotBeCompiledIntoFinalImage(_instructionSetSupport, calleeMethod))
                     canInline = false;
             }
+
             return canInline;
         }
 
index c6d40d4..3fc37c6 100644 (file)
@@ -3,7 +3,6 @@
 
 using System;
 using System.Collections.Generic;
-
 using Internal.TypeSystem;
 
 using Debug = System.Diagnostics.Debug;
@@ -32,13 +31,31 @@ namespace ILCompiler
 
     public partial class ReadyToRunCompilerContext : CompilerTypeSystemContext
     {
+        // Depth cutoff specifies the number of repetitions of a particular generic type within a type instantiation
+        // to trigger marking the type as potentially cyclic. Considering a generic type CyclicType`1<T> marked as
+        // cyclic by the initial module analysis, for instance CyclicType`1<CyclicType`1<CyclicType`1<__Canon>>> has "depth 3"
+        // so it will be cut off by specifying anything less than or equal to three.
+        public const int DefaultGenericCycleDepthCutoff = 4;
+
+        // Breadth cutoff specifies the minimum total number of generic types identified as potentially cyclic
+        // that must appear within a type instantiation to mark it as potentially cyclic. Considering generic types
+        // CyclicA`1, CyclicB`1 and CyclicC`1 marked as cyclic by the initial module analysis, a hypothetical type
+        // SomeType`3<CyclicA`1<__Canon>, List`1<CyclicB`1<__Canon>>, IEnumerable`1<HashSet`1<CyclicC`1<__Canon>>>>
+        // will have "breadth 3" and will be cut off by specifying anything less than or equal to three.
+        public const int DefaultGenericCycleBreadthCutoff = 2;
+
         private ReadyToRunMetadataFieldLayoutAlgorithm _r2rFieldLayoutAlgorithm;
         private SystemObjectFieldLayoutAlgorithm _systemObjectFieldLayoutAlgorithm;
         private VectorOfTFieldLayoutAlgorithm _vectorOfTFieldLayoutAlgorithm;
         private VectorFieldLayoutAlgorithm _vectorFieldLayoutAlgorithm;
         private Int128FieldLayoutAlgorithm _int128FieldLayoutAlgorithm;
 
-        public ReadyToRunCompilerContext(TargetDetails details, SharedGenericsMode genericsMode, bool bubbleIncludesCorelib, InstructionSetSupport instructionSetSupport, CompilerTypeSystemContext oldTypeSystemContext = null)
+        public ReadyToRunCompilerContext(
+            TargetDetails details,
+            SharedGenericsMode genericsMode,
+            bool bubbleIncludesCorelib,
+            InstructionSetSupport instructionSetSupport,
+            CompilerTypeSystemContext oldTypeSystemContext)
             : base(details, genericsMode)
         {
             InstructionSetSupport = instructionSetSupport;
index 3bb4455..5ca0605 100644 (file)
@@ -30,6 +30,8 @@
     </PackageReference>
   </ItemGroup>
 
+  <Import Project="..\..\Common\Compiler\GenericCycleDetection\GenericCycleDetection.projitems" />
+
   <ItemGroup>
     <Compile Include="..\..\Common\Internal\Runtime\CorConstants.cs" Link="Common\CorConstants.cs" />
     <Compile Include="..\..\Common\Internal\Runtime\ReadyToRunConstants.cs" Link="Common\ReadyToRunConstants.cs" />
index 3d46a44..fd6868b 100644 (file)
@@ -871,12 +871,14 @@ namespace Internal.JitInterface
                         if (helperId == ReadyToRunHelperId.MethodEntry && pGenericLookupKind.runtimeLookupArgs != null)
                         {
                             constrainedType = (TypeDesc)GetRuntimeDeterminedObjectForToken(ref *(CORINFO_RESOLVED_TOKEN*)pGenericLookupKind.runtimeLookupArgs);
+                            _compilation.NodeFactory.DetectGenericCycles(MethodBeingCompiled, constrainedType);
                         }
                         object helperArg = GetRuntimeDeterminedObjectForToken(ref pResolvedToken);
                         if (helperArg is MethodDesc methodDesc)
                         {
                             var methodIL = HandleToObject(pResolvedToken.tokenScope);
                             MethodDesc sharedMethod = methodIL.OwningMethod.GetSharedRuntimeFormMethodTarget();
+                            _compilation.NodeFactory.DetectGenericCycles(MethodBeingCompiled, sharedMethod);
                             helperArg = new MethodWithToken(methodDesc, HandleToModuleToken(ref pResolvedToken), constrainedType, unboxing: false, context: sharedMethod);
                         }
                         else if (helperArg is FieldDesc fieldDesc)
@@ -2260,6 +2262,11 @@ namespace Internal.JitInterface
                 out callerModule,
                 out useInstantiatingStub);
 
+            if (callerMethod.HasInstantiation || callerMethod.OwningType.HasInstantiation)
+            {
+                _compilation.NodeFactory.DetectGenericCycles(callerMethod, methodToCall);
+            }
+
             if (pResult->thisTransform == CORINFO_THIS_TRANSFORM.CORINFO_BOX_THIS)
             {
                 // READYTORUN: FUTURE: Optionally create boxing stub at runtime
index 574bfb7..743192f 100644 (file)
@@ -138,8 +138,10 @@ namespace ILCompiler
             new(new[] { "--directpinvoke" }, Array.Empty<string>, "PInvoke to call directly");
         public Option<string[]> DirectPInvokeLists { get; } =
             new(new[] { "--directpinvokelist" }, Array.Empty<string>, "File with list of PInvokes to call directly");
-        public Option<int> MaxGenericCycle { get; } =
-            new(new[] { "--maxgenericcycle" }, () => CompilerTypeSystemContext.DefaultGenericCycleCutoffPoint, "Max depth of generic cycle");
+        public Option<int> MaxGenericCycleDepth { get; } =
+            new(new[] { "--maxgenericcycle" }, () => CompilerTypeSystemContext.DefaultGenericCycleDepthCutoff, "Max depth of generic cycle");
+        public Option<int> MaxGenericCycleBreadth { get; } =
+            new(new[] { "--maxgenericcyclebreadth" }, () => CompilerTypeSystemContext.DefaultGenericCycleBreadthCutoff, "Max breadth of generic cycle expansion");
         public Option<string[]> RootedAssemblies { get; } =
             new(new[] { "--root" }, Array.Empty<string>, "Fully generate given assembly");
         public Option<IEnumerable<string>> ConditionallyRootedAssemblies { get; } =
@@ -225,7 +227,8 @@ namespace ILCompiler
             AddOption(SingleWarnDisabledAssemblies);
             AddOption(DirectPInvokes);
             AddOption(DirectPInvokeLists);
-            AddOption(MaxGenericCycle);
+            AddOption(MaxGenericCycleDepth);
+            AddOption(MaxGenericCycleBreadth);
             AddOption(RootedAssemblies);
             AddOption(ConditionallyRootedAssemblies);
             AddOption(TrimmedAssemblies);
index 9f0af5d..5c54516 100644 (file)
@@ -92,7 +92,9 @@ namespace ILCompiler
             var targetAbi = TargetAbi.NativeAot;
             var targetDetails = new TargetDetails(targetArchitecture, targetOS, targetAbi, simdVectorLength);
             CompilerTypeSystemContext typeSystemContext =
-                new CompilerTypeSystemContext(targetDetails, genericsMode, supportsReflection ? DelegateFeature.All : 0, Get(_command.MaxGenericCycle));
+                new CompilerTypeSystemContext(targetDetails, genericsMode, supportsReflection ? DelegateFeature.All : 0,
+                    genericCycleDepthCutoff: Get(_command.MaxGenericCycleDepth),
+                    genericCycleBreadthCutoff: Get(_command.MaxGenericCycleBreadth));
 
             //
             // TODO: To support our pre-compiled test tree, allow input files that aren't managed assemblies since
index 16d2e42..7542979 100644 (file)
@@ -25,7 +25,7 @@ namespace Mono.Linker.Tests.TestCasesRunner
                        ComputeDefaultOptions (out var targetOS, out var targetArchitecture);
                        var targetDetails = new TargetDetails (targetArchitecture, targetOS, TargetAbi.NativeAot);
                        CompilerTypeSystemContext typeSystemContext =
-                               new CompilerTypeSystemContext (targetDetails, SharedGenericsMode.CanonicalReferenceTypes, DelegateFeature.All, genericCycleCutoffPoint: -1);
+                               new CompilerTypeSystemContext (targetDetails, SharedGenericsMode.CanonicalReferenceTypes, DelegateFeature.All, genericCycleDepthCutoff: -1);
 
                        typeSystemContext.InputFilePaths = options.InputFilePaths;
                        typeSystemContext.ReferenceFilePaths = options.ReferenceFilePaths;
index ee4d3a0..1f7ac88 100644 (file)
@@ -87,10 +87,10 @@ Global
                {3EACD929-4725-4173-A845-734936BBDF87}.Debug|x86.Build.0 = Debug|Any CPU
                {3EACD929-4725-4173-A845-734936BBDF87}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {3EACD929-4725-4173-A845-734936BBDF87}.Release|Any CPU.Build.0 = Release|Any CPU
-               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x64.ActiveCfg = Release|Any CPU
-               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x64.Build.0 = Release|Any CPU
-               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x86.ActiveCfg = Release|Any CPU
-               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x86.Build.0 = Release|Any CPU
+               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x64.ActiveCfg = Release|x64
+               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x64.Build.0 = Release|x64
+               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x86.ActiveCfg = Release|x86
+               {3EACD929-4725-4173-A845-734936BBDF87}.Release|x86.Build.0 = Release|x86
                {0BB34BA1-1B3A-445C-9C04-0D710D1983F0}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
                {0BB34BA1-1B3A-445C-9C04-0D710D1983F0}.Checked|Any CPU.Build.0 = Debug|Any CPU
                {0BB34BA1-1B3A-445C-9C04-0D710D1983F0}.Checked|x64.ActiveCfg = Debug|x64
index 85d6b16..259975b 100644 (file)
@@ -77,6 +77,12 @@ namespace ILCompiler
             new(new[] { "--resilient" }, SR.ResilientOption);
         public Option<string> ImageBase { get; } =
             new(new[] { "--imagebase" }, SR.ImageBase);
+        public Option<bool> EnableGenericCycleDetection { get; } =
+            new(new[] { "--enable-generic-cycle-detection" }, SR.EnableGenericCycleDetection);
+        public Option<int> GenericCycleDepthCutoff { get; } =
+            new(new[] { "--maxgenericcycle" }, () => ReadyToRunCompilerContext.DefaultGenericCycleDepthCutoff, SR.GenericCycleDepthCutoff);
+        public Option<int> GenericCycleBreadthCutoff { get; } =
+            new(new[] { "--maxgenericcyclebreadth" }, () => ReadyToRunCompilerContext.DefaultGenericCycleBreadthCutoff, SR.GenericCycleBreadthCutoff);
         public Option<TargetArchitecture> TargetArchitecture { get; } =
             new(new[] { "--targetarch" }, result =>
             {
@@ -225,6 +231,9 @@ namespace ILCompiler
             AddOption(SupportIbc);
             AddOption(Resilient);
             AddOption(ImageBase);
+            AddOption(EnableGenericCycleDetection);
+            AddOption(GenericCycleDepthCutoff);
+            AddOption(GenericCycleBreadthCutoff);
             AddOption(TargetArchitecture);
             AddOption(TargetOS);
             AddOption(JitPath);
index 1546def..4b522a3 100644 (file)
@@ -127,7 +127,9 @@ namespace ILCompiler
             //
             // Initialize type system context
             //
-            _typeSystemContext = new ReadyToRunCompilerContext(targetDetails, genericsMode, versionBubbleIncludesCoreLib, instructionSetSupport);
+            _typeSystemContext = new ReadyToRunCompilerContext(targetDetails, genericsMode, versionBubbleIncludesCoreLib,
+                instructionSetSupport,
+                oldTypeSystemContext: null);
 
             string compositeRootPath = Get(_command.CompositeRootPath);
 
@@ -269,7 +271,9 @@ namespace ILCompiler
                     {
                         bool singleCompilationVersionBubbleIncludesCoreLib = versionBubbleIncludesCoreLib || (String.Compare(inputFile.Key, "System.Private.CoreLib", StringComparison.OrdinalIgnoreCase) == 0);
 
-                        typeSystemContext = new ReadyToRunCompilerContext(targetDetails, genericsMode, singleCompilationVersionBubbleIncludesCoreLib, _typeSystemContext.InstructionSetSupport, _typeSystemContext);
+                        typeSystemContext = new ReadyToRunCompilerContext(targetDetails, genericsMode, singleCompilationVersionBubbleIncludesCoreLib,
+                            _typeSystemContext.InstructionSetSupport,
+                            _typeSystemContext);
                         typeSystemContext.InputFilePaths = singleCompilationInputFilePaths;
                         typeSystemContext.ReferenceFilePaths = referenceFilePaths;
                         typeSystemContext.SetSystemModule((EcmaModule)typeSystemContext.GetModuleForSimpleName(systemModuleName));
@@ -617,6 +621,13 @@ namespace ILCompiler
                         .UseCompilationRoots(compilationRoots)
                         .UseOptimizationMode(optimizationMode);
 
+                    if (Get(_command.EnableGenericCycleDetection))
+                    {
+                        builder.UseGenericCycleDetection(
+                            depthCutoff: Get(_command.GenericCycleDepthCutoff),
+                            breadthCutoff: Get(_command.GenericCycleBreadthCutoff));
+                    }
+
                     builder.UsePrintReproInstructions(CreateReproArgumentString);
 
                     compilation = builder.ToCompilation();
index a737ea6..584ae25 100644 (file)
@@ -1,17 +1,17 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <root>
-  <!--
-    Microsoft ResX Schema
-
+  <!-- 
+    Microsoft ResX Schema 
+    
     Version 2.0
-
-    The primary goals of this format is to allow a simple XML format
-    that is mostly human readable. The generation and parsing of the
-    various data types are done through the TypeConverter classes
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
     associated with the data types.
-
+    
     Example:
-
+    
     ... ado.net/XML headers & schema ...
     <resheader name="resmimetype">text/microsoft-resx</resheader>
     <resheader name="version">2.0</resheader>
         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
         <comment>This is a comment</comment>
     </data>
-
-    There are any number of "resheader" rows that contain simple
+                
+    There are any number of "resheader" rows that contain simple 
     name/value pairs.
-
-    Each data row contains a name, and value. The row also contains a
-    type or mimetype. Type corresponds to a .NET class that support
-    text/value conversion through the TypeConverter architecture.
-    Classes that don't support this are serialized and stored with the
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
     mimetype set.
-
-    The mimetype is used for serialized objects, and tells the
-    ResXResourceReader how to depersist the object. This is currently not
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
     extensible. For a given mimetype the value must be set accordingly:
-
-    Note - application/x-microsoft.net.object.binary.base64 is the format
-    that the ResXResourceWriter will generate, however the reader can
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
     read any of the formats listed below.
-
+    
     mimetype: application/x-microsoft.net.object.binary.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
             : and then encoded with base64 encoding.
-
+    
     mimetype: application/x-microsoft.net.object.soap.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
             : and then encoded with base64 encoding.
 
     mimetype: application/x-microsoft.net.object.bytearray.base64
-    value   : The object must be serialized into a byte array
+    value   : The object must be serialized into a byte array 
             : using a System.ComponentModel.TypeConverter
             : and then encoded with base64 encoding.
     -->
   <data name="Crossgen2BannerText" xml:space="preserve">
     <value>.NET Crossgen2 Compiler</value>
   </data>
-</root>
+  <data name="EnableGenericCycleDetection" xml:space="preserve">
+    <value>Perform generic cycle detection during compilation (incurs longer compilation time)</value>
+  </data>
+  <data name="GenericCycleBreadthCutoff" xml:space="preserve">
+    <value>Total number of occurrences of potentially cyclic generic types within a generic type to cut off</value>
+  </data>
+  <data name="GenericCycleDepthCutoff" xml:space="preserve">
+    <value>Number of nested occurrences of a potentially cyclic generic type to cut off</value>
+  </data>
+</root>
\ No newline at end of file
diff --git a/src/tests/readytorun/GenericCycleDetection/Breadth1Test.cs b/src/tests/readytorun/GenericCycleDetection/Breadth1Test.cs
new file mode 100644 (file)
index 0000000..08a7763
--- /dev/null
@@ -0,0 +1,203 @@
+// 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 Xunit;
+
+internal class Program
+{
+    // This test exercises the "breadth cutoff" parameter of generic cycle detector.
+    // It mimics generic representation of expressions.
+    private struct Expression<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            if (--seed <= 0) return 10;
+            return seed switch
+            {
+                1 => Assignment<U, T>.Construct(seed),
+                2 => Assignment<Expression<U, T>, T>.Construct(seed),
+                3 => Assignment<U, Expression<U, T>>.Construct(seed),
+                _ => Assignment<Expression<U, T>, Expression<T, U>>.Construct(seed)
+            };
+        }
+    }
+    
+    private struct Assignment<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Conditional<U, T>.Construct(seed),
+                2 => Conditional<Assignment<U, T>, T>.Construct(seed),
+                3 => Conditional<U, Assignment<U, T>>.Construct(seed),
+                _ => Conditional<Assignment<U, T>, Assignment<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct Conditional<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => LogicalOr<U, T>.Construct(seed),
+                2 => LogicalOr<Conditional<U, T>, T>.Construct(seed),
+                3 => LogicalOr<U, Conditional<U, T>>.Construct(seed),
+                _ => LogicalOr<Conditional<U, T>, Conditional<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct LogicalOr<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => LogicalAnd<U, T>.Construct(seed),
+                2 => LogicalAnd<LogicalOr<U, T>, T>.Construct(seed),
+                3 => LogicalAnd<U, LogicalOr<U, T>>.Construct(seed),
+                _ => LogicalAnd<LogicalOr<U, T>, LogicalOr<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct LogicalAnd<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => BitwiseOr<U, T>.Construct(seed),
+                2 => BitwiseOr<LogicalAnd<U, T>, T>.Construct(seed),
+                3 => BitwiseOr<U, LogicalAnd<U, T>>.Construct(seed),
+                _ => BitwiseOr<LogicalAnd<U, T>, LogicalAnd<T, U>>.Construct(seed),
+            };
+        }
+    }
+
+    private struct BitwiseOr<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => BitwiseAnd<U, T>.Construct(seed),
+                2 => BitwiseAnd<BitwiseOr<U, T>, T>.Construct(seed),
+                3 => BitwiseAnd<U, BitwiseOr<U, T>>.Construct(seed),
+                _ => BitwiseAnd<BitwiseOr<U, T>, BitwiseOr<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct BitwiseAnd<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Equality<U, T>.Construct(seed),
+                2 => Equality<BitwiseAnd<U, T>, T>.Construct(seed),
+                3 => Equality<U, BitwiseAnd<U, T>>.Construct(seed),
+                _ => Equality<BitwiseAnd<U, T>, BitwiseAnd<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct Equality<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Comparison<U, T>.Construct(seed),
+                2 => Comparison<Equality<U, T>, T>.Construct(seed),
+                3 => Comparison<U, Equality<U, T>>.Construct(seed),
+                _ => Comparison<Equality<U, T>, Equality<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct Comparison<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => BitwiseShift<U, T>.Construct(seed),
+                2 => BitwiseShift<Comparison<U, T>, T>.Construct(seed),
+                3 => BitwiseShift<U, Comparison<U, T>>.Construct(seed),
+                _ => BitwiseShift<Comparison<U, T>, Comparison<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct BitwiseShift<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Addition<U, T>.Construct(seed),
+                2 => Addition<BitwiseShift<U, T>, T>.Construct(seed),
+                3 => Addition<U, BitwiseShift<U, T>>.Construct(seed),
+                _ => Addition<BitwiseShift<U, T>, BitwiseShift<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct Addition<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Multiplication<U, T>.Construct(seed),
+                2 => Multiplication<Addition<U, T>, T>.Construct(seed),
+                3 => Multiplication<U, Addition<U, T>>.Construct(seed),
+                _ => Multiplication<Addition<U, T>, Addition<T, U>>.Construct(seed)
+            };
+        }
+    }
+
+    private struct Multiplication<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Nested<U, T>.Construct(seed),
+                2 => Nested<Multiplication<U, T>, T>.Construct(seed),
+                3 => Nested<U, Multiplication<U, T>>.Construct(seed),
+                _ => Nested<Multiplication<U, T>, Multiplication<T, U>>.Construct(seed)
+            };
+        }
+    }
+    
+    private struct Nested<T, U>
+    {
+        public static int Construct(int seed)
+        {
+            return seed switch
+            {
+                1 => Expression<U, T>.Construct(seed),
+                2 => Expression<Nested<U, T>, T>.Construct(seed),
+                3 => Expression<U, Nested<U, T>>.Construct(seed),
+                _ => Expression<Nested<U, T>, Nested<T, U>>.Construct(seed)
+            };
+        }
+    }
+    
+    [Fact]
+    public static int BreadthTest()
+    {
+        return Expression<long, int>.Construct(2) * Expression<float, double>.Construct(2);
+    }
+    
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private static int ReturnTwoAndDontTellJIT() => 2;
+}
diff --git a/src/tests/readytorun/GenericCycleDetection/Breadth1Test.csproj b/src/tests/readytorun/GenericCycleDetection/Breadth1Test.csproj
new file mode 100644 (file)
index 0000000..31b2280
--- /dev/null
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="Current">
+  <PropertyGroup>
+    <OutputType>exe</OutputType>
+    <CLRTestTargetUnsupported Condition="'$(RuntimeFlavor)' != 'coreclr'">true</CLRTestTargetUnsupported>
+    <!-- This is an explicit crossgen test -->
+    <AlwaysUseCrossGen2>true</AlwaysUseCrossGen2>
+    <!-- Without this flag Crossgen2 crashes after several minutes with arithmetic overflow -->
+    <CrossGen2TestExtraArguments>--enable-generic-cycle-detection --maxgenericcycle:1 --maxgenericcyclebreadth:1</CrossGen2TestExtraArguments>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="$(MSBuildProjectName).cs" />
+  </ItemGroup>
+</Project>
diff --git a/src/tests/readytorun/GenericCycleDetection/Depth1Test.cs b/src/tests/readytorun/GenericCycleDetection/Depth1Test.cs
new file mode 100644 (file)
index 0000000..2e20735
--- /dev/null
@@ -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;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+internal class Program
+{
+    // This test only works when the "depth cutoff" parameter of generic cycle detector
+    // is reduced to 1, otherwise the combinatorical explosion overflows the code generator.
+    private struct Breadth<T1, T2, T3, T4, T5, T6>
+    {
+        public static long TypeNestedFactorial(int count)
+        {
+            if (count <= 1)
+            {
+                return 1;
+            }
+            long result = 0;
+            if (result < count) result = Breadth<T2, T3, T4, T5, T6, Oper1<T1>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Breadth<T2, T3, T4, T5, T6, Oper2<T1>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Breadth<T2, T3, T4, T5, T6, Oper3<T1>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Breadth<T2, T3, T4, T5, T6, Oper4<T1>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Breadth<T2, T3, T4, T5, T6, Oper5<T1>>.TypeNestedFactorial(count - 1);
+            return count * result;
+        }
+    }
+    
+    private struct Oper1<T> {}
+    private struct Oper2<T> {}
+    private struct Oper3<T> {}
+    private struct Oper4<T> {}
+    private struct Oper5<T> {}
+    
+    [Fact]
+    public static int BreadthTest()
+    {
+        const long Factorial20 = 20L * 19L * 18L * 17L * 16L * 15L * 14L * 13L * 12L * 11L * 10L * 9L * 8L * 7L * 6L * 5L * 4L * 3L * 2L;
+        return Breadth<byte, char, int, long, float, double>.TypeNestedFactorial(ReturnTwentyAndDontTellJIT()) == Factorial20 ? 100 : 101;
+    }
+    
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private static int ReturnTwentyAndDontTellJIT() => 20;
+}
diff --git a/src/tests/readytorun/GenericCycleDetection/Depth1Test.csproj b/src/tests/readytorun/GenericCycleDetection/Depth1Test.csproj
new file mode 100644 (file)
index 0000000..2c0617a
--- /dev/null
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="Current">
+  <PropertyGroup>
+    <OutputType>exe</OutputType>
+    <CLRTestTargetUnsupported Condition="'$(RuntimeFlavor)' != 'coreclr'">true</CLRTestTargetUnsupported>
+    <!-- This is an explicit crossgen test -->
+    <AlwaysUseCrossGen2>true</AlwaysUseCrossGen2>
+    <!-- Without this flag Crossgen2 crashes after several minutes with arithmetic overflow -->
+    <CrossGen2TestExtraArguments>--enable-generic-cycle-detection --maxgenericcycle:1 --maxgenericcyclebreadth:-1</CrossGen2TestExtraArguments>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="$(MSBuildProjectName).cs" />
+  </ItemGroup>
+</Project>
diff --git a/src/tests/readytorun/GenericCycleDetection/Depth3Test.cs b/src/tests/readytorun/GenericCycleDetection/Depth3Test.cs
new file mode 100644 (file)
index 0000000..2edaef5
--- /dev/null
@@ -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;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+internal class Program
+{
+    // This test exercises the "depth cutoff" parameter of generic cycle detector.
+    // It is a simple algorithm generating deep generic types of the form
+    // Depth1`1<Depth1`1<Depth1`1<Depth1`1<T>>>>
+    private struct Depth1<T>
+    {
+        public static long TypeNestedFactorial(int count)
+        {
+            if (count <= 1)
+            {
+                return 1;
+            }
+            long result = 0;
+            if (result < count) result = Depth1<Depth1<T>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Depth1<Depth2<T>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Depth1<Depth3<T>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Depth1<Depth4<T>>.TypeNestedFactorial(count - 1);
+            if (result < count) result = Depth1<Depth5<T>>.TypeNestedFactorial(count - 1);
+            return count * result;
+        }
+    }
+    
+    private struct Depth2<T> {}
+    private struct Depth3<T> {}
+    private struct Depth4<T> {}
+    private struct Depth5<T> {}
+    
+    [Fact]
+    public static int DepthTest()
+    {
+        const long Factorial20 = 20L * 19L * 18L * 17L * 16L * 15L * 14L * 13L * 12L * 11L * 10L * 9L * 8L * 7L * 6L * 5L * 4L * 3L * 2L;
+        return Depth1<long>.TypeNestedFactorial(ReturnTwentyAndDontTellJIT()) == Factorial20 ? 100 : 101;
+    }
+    
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private static int ReturnTwentyAndDontTellJIT() => 20;
+}
diff --git a/src/tests/readytorun/GenericCycleDetection/Depth3Test.csproj b/src/tests/readytorun/GenericCycleDetection/Depth3Test.csproj
new file mode 100644 (file)
index 0000000..9f6f947
--- /dev/null
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="Current">
+  <PropertyGroup>
+    <OutputType>exe</OutputType>
+    <CLRTestTargetUnsupported Condition="'$(RuntimeFlavor)' != 'coreclr'">true</CLRTestTargetUnsupported>
+    <!-- This is an explicit crossgen test -->
+    <AlwaysUseCrossGen2>true</AlwaysUseCrossGen2>
+    <!-- Without this flag Crossgen2 crashes after several minutes with arithmetic overflow -->
+    <CrossGen2TestExtraArguments>--enable-generic-cycle-detection --maxgenericcycle:3 --maxgenericcyclebreadth:-1</CrossGen2TestExtraArguments>
+    <!-- This test OOMs Crossgen2 when running in 32-bit address space -->
+    <CLRTestTargetUnsupported Condition="'$(TargetBits)' == '32'">true</CLRTestTargetUnsupported>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="$(MSBuildProjectName).cs" />
+  </ItemGroup>
+</Project>