Skip to content

Commit

Permalink
Handle MakeGeneric APIs in dataflow (#99037)
Browse files Browse the repository at this point in the history
Contributes to #81204.

With this, we can handle `typeof(Foo<>).MakeGenericType(typeof(T))` (and `MakeGenericMethod`), including in shared generic code.

The dataflow analysis change should be straightforward (we just look if it's a known array, with all known elements). I scoped this down to "exactly one array" and "exactly one possible value in the element" because the objective is constraint bridging code. We could extend to supporting multiple values, but I don't see a need.

If we know what the values are, we elide generating a warning.

The support for this in the rest of the compiler is a bit annoying due to generics. We only run dataflow on uninstantiated code. So we have a dependency node representing the dataflow analysis. This node obviously cannot know what are all the possible instantiations we saw, so it cannot directly report things about instantiated generic code. Three possible options to solve this:

* Re-run dataflow analysis on instantiated code. I didn't feel great about doing that.
* Introduce a new node `SpecializedDataflowAnalyzedMethodNode` that would be injected as a conditional dependency of all scanned/compiled method bodies, conditioned on `DataflowAnalyzedMethodNode` being present. This would work, but to obtain a `DataflowAnalyzedMethod` we need the `MethodIL` of the definition (not the `MethodDesc`) and that started to look sketchy.
* Use dynamic dependencies. We only use these for GVM analysis right now. They give a node the ability to receive callbacks when new nodes are added to the graph. These nodes need to declare they are "interesting" to dynamic dependency analysis. It's not great, but it works.

I went with option 3.

I'm not making this resolve the above issue. We'd ideally want to implement this in the analyzer too. I had a quick look at that and it looks like we need to write a brand new `DiagnosticAnalyzer` similar to the `DynamicallyAccessedMembersAnalyzer`.
  • Loading branch information
MichalStrehovsky authored Mar 6, 2024
1 parent 5f52977 commit 78452bf
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ public static TypeDesc ReplaceTypesInConstructionOfType(this TypeDesc type, Type
if (type.HasInstantiation)
{
TypeDesc[] newInstantiation = null;
Debug.Assert(type is InstantiatedType);
int instantiationIndex = 0;
for (; instantiationIndex < type.Instantiation.Length; instantiationIndex++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class ReflectionMarker
public NodeFactory Factory { get; }
public FlowAnnotations Annotations { get; }
public DependencyList Dependencies { get => _dependencies; }
public List<INodeWithRuntimeDeterminedDependencies> RuntimeDeterminedDependencies { get; } = new List<INodeWithRuntimeDeterminedDependencies>();

internal enum AccessKind
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata;
using ILCompiler.DependencyAnalysis;
using ILCompiler.DependencyAnalysisFramework;
using ILCompiler.Logging;
using ILLink.Shared;
using ILLink.Shared.TrimAnalysis;
Expand Down Expand Up @@ -121,12 +124,13 @@ protected override void Scan(MethodIL methodBody, ref InterproceduralState inter
}
}

public static DependencyList ScanAndProcessReturnValue(NodeFactory factory, FlowAnnotations annotations, Logger logger, MethodIL methodBody)
public static DependencyList ScanAndProcessReturnValue(NodeFactory factory, FlowAnnotations annotations, Logger logger, MethodIL methodBody, out List<INodeWithRuntimeDeterminedDependencies> runtimeDependencies)
{
var scanner = new ReflectionMethodBodyScanner(factory, annotations, logger, new MessageOrigin(methodBody.OwningMethod));

scanner.InterproceduralScan(methodBody);

runtimeDependencies = scanner._reflectionMarker.RuntimeDeterminedDependencies;
return scanner._reflectionMarker.Dependencies;
}

Expand Down Expand Up @@ -357,8 +361,6 @@ public static bool HandleCall(
case IntrinsicId.Type_GetConstructor:
case IntrinsicId.MethodBase_GetMethodFromHandle:
case IntrinsicId.MethodBase_get_MethodHandle:
case IntrinsicId.Type_MakeGenericType:
case IntrinsicId.MethodInfo_MakeGenericMethod:
case IntrinsicId.Expression_Call:
case IntrinsicId.Expression_New:
case IntrinsicId.Type_GetType:
Expand All @@ -371,18 +373,124 @@ public static bool HandleCall(
case IntrinsicId.AppDomain_CreateInstanceFromAndUnwrap:
case IntrinsicId.Assembly_CreateInstance:
{
bool result = handleCallAction.Invoke(calledMethod, instanceValue, argumentValues, intrinsicId, out methodReturnValue);
return handleCallAction.Invoke(calledMethod, instanceValue, argumentValues, intrinsicId, out methodReturnValue);
}

// Special case some intrinsics for AOT handling (on top of the trimming handling done in the HandleCallAction)
switch (intrinsicId)
case IntrinsicId.Type_MakeGenericType:
{
bool triggersWarning = false;

if (!instanceValue.IsEmpty() && !argumentValues[0].IsEmpty())
{
case IntrinsicId.Type_MakeGenericType:
case IntrinsicId.MethodInfo_MakeGenericMethod:
CheckAndReportRequires(diagnosticContext, calledMethod, DiagnosticUtilities.RequiresDynamicCodeAttribute);
break;
foreach (var value in instanceValue.AsEnumerable())
{
if (value is SystemTypeValue typeValue)
{
TypeDesc typeInstantiated = typeValue.RepresentedType.Type;
if (!typeInstantiated.IsGenericDefinition)
{
// Nothing to do, will fail at runtime
}
else if (TryGetMakeGenericInstantiation(callingMethodDefinition, argumentValues[0], out Instantiation inst, out bool isExact))
{
if (inst.Length == typeInstantiated.Instantiation.Length)
{
typeInstantiated = ((MetadataType)typeInstantiated).MakeInstantiatedType(inst);

if (isExact)
{
reflectionMarker.MarkType(diagnosticContext.Origin, typeInstantiated, "MakeGenericType");
}
else
{
reflectionMarker.RuntimeDeterminedDependencies.Add(new MakeGenericTypeSite(typeInstantiated));
}
}
}
else
{
triggersWarning = true;
}

}
else if (value == NullValue.Instance)
{
// Nothing to do
}
else
{
// We don't know what type the `MakeGenericType` was called on
triggersWarning = true;
}
}
}

if (triggersWarning)
{
CheckAndReportRequires(diagnosticContext, calledMethod, DiagnosticUtilities.RequiresDynamicCodeAttribute);
}

// This intrinsic is relevant to both trimming and AOT - call into trimming logic as well.
return handleCallAction.Invoke(calledMethod, instanceValue, argumentValues, intrinsicId, out methodReturnValue);
}

case IntrinsicId.MethodInfo_MakeGenericMethod:
{
bool triggersWarning = false;

if (!instanceValue.IsEmpty())
{
foreach (var methodValue in instanceValue.AsEnumerable())
{
if (methodValue is SystemReflectionMethodBaseValue methodBaseValue)
{
MethodDesc methodInstantiated = methodBaseValue.RepresentedMethod.Method;
if (!methodInstantiated.IsGenericMethodDefinition)
{
// Nothing to do, will fail at runtime
}
else if (!methodInstantiated.OwningType.IsGenericDefinition
&& TryGetMakeGenericInstantiation(callingMethodDefinition, argumentValues[0], out Instantiation inst, out bool isExact))
{
if (inst.Length == methodInstantiated.Instantiation.Length)
{
methodInstantiated = methodInstantiated.MakeInstantiatedMethod(inst);

if (isExact)
{
reflectionMarker.MarkMethod(diagnosticContext.Origin, methodInstantiated, "MakeGenericMethod");
}
else
{
reflectionMarker.RuntimeDeterminedDependencies.Add(new MakeGenericMethodSite(methodInstantiated));
}
}
}
else
{
// If the owning type is a generic definition, we can't help much.
triggersWarning = true;
}
}
else if (methodValue == NullValue.Instance)
{
// Nothing to do
}
else
{
// We don't know what method the `MakeGenericMethod` was called on
triggersWarning = true;
}
}
}

return result;
if (triggersWarning)
{
CheckAndReportRequires(diagnosticContext, calledMethod, DiagnosticUtilities.RequiresDynamicCodeAttribute);
}

// This intrinsic is relevant to both trimming and AOT - call into trimming logic as well.
return handleCallAction.Invoke(calledMethod, instanceValue, argumentValues, intrinsicId, out methodReturnValue);
}

case IntrinsicId.None:
Expand Down Expand Up @@ -686,6 +794,105 @@ void AddReturnValue(MultiValue value)
}
}

private static bool TryGetMakeGenericInstantiation(
MethodDesc contextMethod,
in MultiValue genericParametersArray,
out Instantiation inst,
out bool isExact)
{
// We support calling MakeGeneric APIs with a very concrete instantiation array.
// Only the form of `new Type[] { typeof(Foo), typeof(T), typeof(Foo<T>) }` is supported.

inst = default;
isExact = true;
Debug.Assert(contextMethod.GetTypicalMethodDefinition() == contextMethod);

var typesValue = genericParametersArray.AsSingleValue();
if (typesValue is NullValue)
{
// This will fail at runtime but no warning needed
inst = Instantiation.Empty;
return true;
}

// Is this an array we model?
if (typesValue is not ArrayValue array)
{
return false;
}

int? size = array.Size.AsConstInt();
if (size == null)
{
return false;
}

TypeDesc[]? sigInst = null;
TypeDesc[]? defInst = null;

ArrayBuilder<TypeDesc> result = default;
for (int i = 0; i < size.Value; i++)
{
// Go over each element of the array. If the value is unknown, bail.
if (!array.TryGetValueByIndex(i, out MultiValue value))
{
return false;
}

var singleValue = value.AsSingleValue();

TypeDesc? type = singleValue switch
{
SystemTypeValue systemType => systemType.RepresentedType.Type,
GenericParameterValue genericParamType => genericParamType.GenericParameter.GenericParameter,
NullableSystemTypeValue nullableSystemType => nullableSystemType.NullableType.Type,
_ => null
};

if (type is null)
{
return false;
}

// type is now some type.
// Because dataflow analysis oddly operates on method bodies instantiated over
// generic parameters (as opposed to instantiated over signature variables)
// We need to swap generic parameters (T, U,...) for signature variables (!0, !!1,...).
// We need to do this for both generic parameters of the owning type, and generic
// parameters of the owning method.
if (type.ContainsSignatureVariables(treatGenericParameterLikeSignatureVariable: true))
{
if (sigInst == null)
{
TypeDesc contextType = contextMethod.OwningType;
sigInst = new TypeDesc[contextType.Instantiation.Length + contextMethod.Instantiation.Length];
defInst = new TypeDesc[contextType.Instantiation.Length + contextMethod.Instantiation.Length];
TypeSystemContext context = type.Context;
for (int j = 0; j < contextType.Instantiation.Length; j++)
{
sigInst[j] = context.GetSignatureVariable(j, method: false);
defInst[j] = contextType.Instantiation[j];
}
for (int j = 0; j < contextMethod.Instantiation.Length; j++)
{
sigInst[j + contextType.Instantiation.Length] = context.GetSignatureVariable(j, method: true);
defInst[j + contextType.Instantiation.Length] = contextMethod.Instantiation[j];
}
}

isExact = false;

// defInst is [T, U, V], sigInst is `[!0, !!0, !!1]`.
type = type.ReplaceTypesInConstructionOfType(defInst, sigInst);
}

result.Add(type);
}

inst = new Instantiation(result.ToArray());
return true;
}

private static bool IsAotUnsafeDelegate(TypeDesc parameterType)
{
TypeSystemContext context = parameterType.Context;
Expand Down Expand Up @@ -846,5 +1053,33 @@ private static bool IsPInvokeDangerous(MethodDesc calledMethod, out bool comDang

return aotUnsafeDelegate || comDangerousMethod;
}

private sealed class MakeGenericMethodSite : INodeWithRuntimeDeterminedDependencies
{
private readonly MethodDesc _method;

public MakeGenericMethodSite(MethodDesc method) => _method = method;

public IEnumerable<DependencyNodeCore<NodeFactory>.DependencyListEntry> InstantiateDependencies(NodeFactory factory, Instantiation typeInstantiation, Instantiation methodInstantiation)
{
var list = new DependencyList();
RootingHelpers.TryGetDependenciesForReflectedMethod(ref list, factory, _method.InstantiateSignature(typeInstantiation, methodInstantiation), "MakeGenericMethod");
return list;
}
}

private sealed class MakeGenericTypeSite : INodeWithRuntimeDeterminedDependencies
{
private readonly TypeDesc _type;

public MakeGenericTypeSite(TypeDesc type) => _type = type;

public IEnumerable<DependencyNodeCore<NodeFactory>.DependencyListEntry> InstantiateDependencies(NodeFactory factory, Instantiation typeInstantiation, Instantiation methodInstantiation)
{
var list = new DependencyList();
RootingHelpers.TryGetDependenciesForReflectedType(ref list, factory, _type.InstantiateSignature(typeInstantiation, methodInstantiation), "MakeGenericType");
return list;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace ILCompiler.DependencyAnalysis
public class DataflowAnalyzedMethodNode : DependencyNodeCore<NodeFactory>
{
private readonly MethodIL _methodIL;
private List<INodeWithRuntimeDeterminedDependencies> _runtimeDependencies;

public DataflowAnalyzedMethodNode(MethodIL methodIL)
{
Expand All @@ -32,26 +33,51 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
var mdManager = (UsageBasedMetadataManager)factory.MetadataManager;
try
{
return Dataflow.ReflectionMethodBodyScanner.ScanAndProcessReturnValue(factory, mdManager.FlowAnnotations, mdManager.Logger, _methodIL);
return Dataflow.ReflectionMethodBodyScanner.ScanAndProcessReturnValue(factory, mdManager.FlowAnnotations, mdManager.Logger, _methodIL, out _runtimeDependencies);
}
catch (TypeSystemException)
{
// Something wrong with the input - missing references, etc.
// The method body likely won't compile either, so we don't care.
_runtimeDependencies = new List<INodeWithRuntimeDeterminedDependencies>();
return Array.Empty<DependencyListEntry>();
}
}

public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory factory)
{
MethodDesc analyzedMethod = _methodIL.OwningMethod;

// Look for any generic specialization of this method. If any are found, specialize any dataflow dependencies.
for (int i = firstNode; i < markedNodes.Count; i++)
{
if (markedNodes[i] is not IMethodBodyNode methodBody)
continue;

MethodDesc method = methodBody.Method;
if (method.GetTypicalMethodDefinition() != analyzedMethod)
continue;

// Instantiate all runtime dependencies for the found generic specialization
foreach (var n in _runtimeDependencies)
{
foreach (var d in n.InstantiateDependencies(factory, method.OwningType.Instantiation, method.Instantiation))
{
yield return new CombinedDependencyListEntry(d.Node, null, d.Reason);
}
}
}
}

protected override string GetName(NodeFactory factory)
{
return "Dataflow analysis for " + _methodIL.OwningMethod.ToString();
}

public override bool InterestingForDynamicDependencyAnalysis => false;
public override bool HasDynamicDependencies => false;
public override bool HasDynamicDependencies => _runtimeDependencies.Count > 0;
public override bool HasConditionalStaticDependencies => false;
public override bool StaticDependenciesAreComputed => true;
public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory context) => null;
public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null;
}
}
Loading

0 comments on commit 78452bf

Please sign in to comment.