Skip to content

Commit df5b4ed

Browse files
committed
Implement call graph discovery of nested functions
1 parent 2576ee4 commit df5b4ed

File tree

6 files changed

+118
-148
lines changed

6 files changed

+118
-148
lines changed

src/linker/Linker.Steps/MarkStep.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -559,28 +559,6 @@ bool QueueIsEmpty ()
559559

560560
protected virtual void EnqueueMethod (MethodDefinition method, in DependencyInfo reason, in MarkScopeStack.Scope scope)
561561
{
562-
if (GeneratedNames.IsGeneratedMemberName (method.Name)) {
563-
// We only expect a generated method to represent a lambda or local function.
564-
// TODO: what about implicit Main? Async main?
565-
Debug.Assert (GeneratedNames.TryParseLambdaMethodName (method.Name, out _) || GeneratedNames.TryParseLocalFunctionMethodName (method.Name, out _, out _));
566-
// We rely on the callers of compiler-generated methods being marked first... which might not always be true,
567-
// if they are marked for a reason other than a discovered call to them.
568-
// Might need to postpone processing of such methods. But for now just check the assumption.
569-
570-
// Forget the reason assert. There are too many.
571-
// Debug.Assert (reason.Kind is DependencyKind.DirectCall or DependencyKind.VirtualCall or DependencyKind.Newobj or DependencyKind.Ldftn
572-
// // Could be a directcall to a generic LocalFunction... :/
573-
// or DependencyKind.ElementMethod
574-
// // Or could by dynamically accessed... hopefully not the first time?
575-
// or DependencyKind.DynamicallyAccessedMember);
576-
// We might mark a generic instantiation, which gets resolved to the definition, and the source
577-
// is the generic instance. Which doesn't satisfy the above.
578-
if (reason.Kind is not (DependencyKind.ElementMethod or DependencyKind.MethodOnGenericInstance)) {
579-
Debug.Assert (Annotations.IsMarked ( (IMetadataTokenProvider) reason.Source));
580-
MethodDefinition caller = (MethodDefinition) reason.Source;
581-
Context.CompilerGeneratedState.TrackCallToLambdaOrLocalFunction (caller, method);
582-
}
583-
}
584562
_methods.Enqueue ((method, reason, scope));
585563
}
586564

src/linker/Linker/CallGraph.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Mono.Cecil;
6+
7+
namespace Mono.Linker
8+
{
9+
class CallGraph
10+
{
11+
readonly Dictionary<MethodDefinition, HashSet<MethodDefinition>> callGraph;
12+
13+
public CallGraph () => callGraph = new Dictionary<MethodDefinition, HashSet<MethodDefinition>> ();
14+
15+
public void TrackCall (MethodDefinition fromMethod, MethodDefinition toMethod)
16+
{
17+
if (!callGraph.TryGetValue (fromMethod, out HashSet<MethodDefinition>? toMethods)) {
18+
toMethods = new HashSet<MethodDefinition> ();
19+
callGraph.Add (fromMethod, toMethods);
20+
}
21+
toMethods.Add (toMethod);
22+
}
23+
24+
public IEnumerable<MethodDefinition> GetReachableMethods (MethodDefinition start)
25+
{
26+
Queue<MethodDefinition> queue = new ();
27+
HashSet<MethodDefinition> visited = new ();
28+
visited.Add (start);
29+
queue.Enqueue (start);
30+
while (queue.TryDequeue (out MethodDefinition? method)) {
31+
if (!callGraph.TryGetValue (method, out HashSet<MethodDefinition>? neighbors))
32+
continue;
33+
34+
foreach (var neighbor in neighbors) {
35+
if (visited.Add (neighbor)) {
36+
queue.Enqueue (neighbor);
37+
yield return neighbor;
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
using System.Diagnostics.CodeAnalysis;
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
23

34
namespace Mono.Linker
45
{
5-
class GeneratedNames
6+
class CompilerGeneratedNames
67
{
78
internal static bool IsGeneratedMemberName (string memberName)
89
{
@@ -19,52 +20,36 @@ internal static bool IsLambdaDisplayClass (string className)
1920
return className.StartsWith ("<>c");
2021
}
2122

23+
internal static bool IsLambdaOrLocalFunction (string methodName) => IsLambdaMethod (methodName) || IsLocalFunction (methodName);
24+
2225
// Lambda methods have generated names like "<UserMethod>c__0_1" where "UserMethod" is the name
2326
// of the original user code that contains the lambda method declaration.
2427
// Note: this might not be the immediately containing method, if the containing method is
2528
// a lambda or local function. This is the name of the user method.
26-
internal static bool TryParseLambdaMethodName (string methodName, [NotNullWhen (true)] out string? userMethodName)
29+
internal static bool IsLambdaMethod (string methodName)
2730
{
28-
userMethodName = null;
2931
if (!IsGeneratedMemberName (methodName))
3032
return false;
3133

3234
int i = methodName.IndexOf ('>', 1);
3335
if (i == -1)
3436
return false;
35-
if (methodName[i + 1] != 'b')
36-
return false;
3737

3838
// Ignore the method ordinal/generation and lambda ordinal/generation.
39-
userMethodName = methodName.Substring (1, i - 1);
40-
return true;
39+
return methodName[i + 1] == 'b';
4140
}
4241

43-
internal static bool TryParseLocalFunctionMethodName (string methodName, [NotNullWhen (true)] out string? userMethodName, [NotNullWhen (true)] out string? localFunctionName)
42+
internal static bool IsLocalFunction (string methodName)
4443
{
45-
userMethodName = null;
46-
localFunctionName = null;
4744
if (!IsGeneratedMemberName (methodName))
4845
return false;
4946

5047
int i = methodName.IndexOf ('>', 1);
5148
if (i == -1)
5249
return false;
53-
if (methodName[i + 1] != 'g')
54-
return false;
5550

5651
// Ignore the method ordinal/generation and local function ordinal/generation.
57-
userMethodName = methodName[1..i];
58-
i += 2;
59-
if (methodName[i++] != '_' || methodName[i++] != '_')
60-
return false;
61-
62-
int j = methodName.IndexOf ('|', i);
63-
if (j == -1)
64-
return false;
65-
66-
localFunctionName = methodName[i..j];
67-
return true;
52+
return methodName[i + 1] == 'g';
6853
}
6954
}
7055
}

src/linker/Linker/CompilerGeneratedState.cs

Lines changed: 57 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using ILLink.Shared;
88
using Mono.Cecil;
9+
using Mono.Cecil.Cil;
910

1011
namespace Mono.Linker
1112
{
@@ -14,8 +15,7 @@ public class CompilerGeneratedState
1415
{
1516
readonly LinkContext _context;
1617
readonly Dictionary<TypeDefinition, MethodDefinition> _compilerGeneratedTypeToUserCodeMethod;
17-
// TODO: fix accessibility
18-
internal readonly Dictionary<MethodDefinition, MethodDefinition> _compilerGeneratedMethodToUserCodeMethod;
18+
readonly Dictionary<MethodDefinition, MethodDefinition> _compilerGeneratedMethodToUserCodeMethod;
1919
readonly HashSet<TypeDefinition> _typesWithPopulatedCache;
2020

2121
public CompilerGeneratedState (LinkContext context)
@@ -27,119 +27,44 @@ public CompilerGeneratedState (LinkContext context)
2727
}
2828

2929
static bool HasRoslynCompilerGeneratedName (TypeDefinition type) =>
30-
GeneratedNames.IsGeneratedMemberName (type.Name) || (type.DeclaringType != null && HasRoslynCompilerGeneratedName (type.DeclaringType));
30+
CompilerGeneratedNames.IsGeneratedMemberName (type.Name) || (type.DeclaringType != null && HasRoslynCompilerGeneratedName (type.DeclaringType));
3131

3232

33-
public void TrackCallToLambdaOrLocalFunction (MethodDefinition caller, MethodDefinition lambdaOrLocalFunction)
34-
{
35-
// The declaring type check makes sure we don't treat MoveNext as a normal method. It should be treated as compiler-generated,
36-
// mapping to the state machine user method. TODO: check that this doesn't cause problems for global type, etc.
37-
bool callerIsStateMachineMethod = GeneratedNames.IsGeneratedMemberName (caller.DeclaringType.Name) && !GeneratedNames.IsLambdaDisplayClass (caller.DeclaringType.Name);
38-
if (callerIsStateMachineMethod)
39-
return;
40-
41-
bool callerIsLambdaOrLocal = GeneratedNames.IsGeneratedMemberName (caller.Name) && !callerIsStateMachineMethod;
42-
43-
if (!callerIsLambdaOrLocal) {
44-
// Caller is a normal method...
45-
bool added = _compilerGeneratedMethodToUserCodeMethod.TryAdd (lambdaOrLocalFunction, caller);
46-
// There should only be one non-compiler-generated caller of a lambda or local function.
47-
Debug.Assert (added || _compilerGeneratedMethodToUserCodeMethod[lambdaOrLocalFunction] == caller);
48-
return;
49-
}
50-
51-
Debug.Assert (GeneratedNames.TryParseLambdaMethodName (caller.Name, out _) || GeneratedNames.TryParseLocalFunctionMethodName (caller.Name, out _, out _));
52-
// Caller is a lambda or local function. This means the lambda or local function is contained within the scope of the caller's user-defined method.
53-
54-
if (_compilerGeneratedMethodToUserCodeMethod.TryGetValue (caller, out MethodDefinition? userCodeMethod)) {
55-
// This lambda/localfn is in the same user code as the caller.
56-
bool added = _compilerGeneratedMethodToUserCodeMethod.TryAdd (lambdaOrLocalFunction, userCodeMethod);
57-
Debug.Assert (added || _compilerGeneratedMethodToUserCodeMethod[lambdaOrLocalFunction] == caller);
58-
} else {
59-
// Haven't tracked any calls to the caller yet.
60-
throw new System.Exception ("Not yet handled! Need to postpone marking of such methods until we can identify a caller, or bail out.");
61-
}
62-
}
63-
6433
void PopulateCacheForType (TypeDefinition type)
6534
{
6635
// Avoid repeat scans of the same type
6736
if (!_typesWithPopulatedCache.Add (type))
6837
return;
6938

70-
Dictionary<string, List<MethodDefinition>>? lambdaMethods = null;
71-
Dictionary<string, List<MethodDefinition>>? localFunctions = null;
39+
var callGraph = new CallGraph ();
40+
var callingMethods = new HashSet<MethodDefinition> ();
7241

73-
foreach (TypeDefinition nested in type.NestedTypes) {
74-
if (!GeneratedNames.IsLambdaDisplayClass (nested.Name))
75-
continue;
76-
77-
// Lambdas and local functions may be generated into a display class which holds
78-
// the closure environment. Lambdas are always generated into such a class.
79-
80-
// Local functions typically get emitted outside of the
81-
// display class (which is a struct in this case), but when any of the captured state
82-
// is used by a state machine local function, the local function is emitted into a
83-
// display class holding that captured state.
84-
lambdaMethods ??= new Dictionary<string, List<MethodDefinition>> ();
85-
localFunctions ??= new Dictionary<string, List<MethodDefinition>> ();
86-
87-
foreach (var lambdaMethod in nested.Methods) {
88-
if (!GeneratedNames.TryParseLambdaMethodName (lambdaMethod.Name, out string? userMethodName))
89-
continue;
90-
if (!lambdaMethods.TryGetValue (userMethodName, out List<MethodDefinition>? lambdaMethodsForName)) {
91-
lambdaMethodsForName = new List<MethodDefinition> ();
92-
lambdaMethods.Add (userMethodName, lambdaMethodsForName);
93-
}
94-
lambdaMethodsForName.Add (lambdaMethod);
42+
void ProcessMethod (MethodDefinition method)
43+
{
44+
if (!CompilerGeneratedNames.IsLambdaOrLocalFunction (method.Name)) {
45+
// If it's not a nested function, track as an entry point to the call graph.
46+
var added = callingMethods.Add (method);
47+
Debug.Assert (added);
9548
}
9649

97-
foreach (var localFunction in nested.Methods) {
98-
if (!GeneratedNames.TryParseLocalFunctionMethodName (localFunction.Name, out string? userMethodName, out string? localFunctionName))
50+
// Discover calls to lambdas or local functions.
51+
foreach (var instruction in method.Body.Instructions) {
52+
if (instruction.OpCode.OperandType != OperandType.InlineMethod)
9953
continue;
100-
if (!localFunctions.TryGetValue (userMethodName, out List<MethodDefinition>? localFunctionsForName)) {
101-
localFunctionsForName = new List<MethodDefinition> ();
102-
localFunctions.Add (userMethodName, localFunctionsForName);
103-
}
104-
localFunctionsForName.Add (localFunction);
105-
}
106-
}
107-
10854

109-
foreach (MethodDefinition localFunction in type.Methods) {
110-
if (!GeneratedNames.TryParseLocalFunctionMethodName (localFunction.Name, out string? userMethodName, out string? localFunctionName))
111-
continue;
112-
113-
// Local functions may be generated into the same type as its declaring method,
114-
// alongside a displayclass which holds the captured state.
115-
// Or it may not have a displayclass, if there is no captured state.
116-
117-
localFunctions ??= new Dictionary<string, List<MethodDefinition>> ();
118-
119-
if (!localFunctions.TryGetValue (userMethodName, out List<MethodDefinition>? localFunctionsForName)) {
120-
localFunctionsForName = new List<MethodDefinition> ();
121-
localFunctions.Add (userMethodName, localFunctionsForName);
122-
}
123-
localFunctionsForName.Add (localFunction);
124-
}
125-
126-
foreach (MethodDefinition method in type.Methods) {
127-
// TODO: combine into one thing?
55+
MethodDefinition? lambdaOrLocalFunction = _context.TryResolve ((MethodReference) instruction.Operand);
56+
if (lambdaOrLocalFunction == null)
57+
continue;
12858

129-
if (lambdaMethods?.TryGetValue (method.Name, out List<MethodDefinition>? lambdaMethodsForName) == true) {
130-
foreach (var lambdaMethod in lambdaMethodsForName)
131-
// TODO: change back to add
132-
_compilerGeneratedMethodToUserCodeMethod.TryAdd (lambdaMethod, method);
133-
}
59+
if (!CompilerGeneratedNames.IsLambdaOrLocalFunction (lambdaOrLocalFunction.Name))
60+
continue;
13461

135-
if (localFunctions?.TryGetValue (method.Name, out List<MethodDefinition>? localFunctionsForName) == true) {
136-
foreach (var localFunction in localFunctionsForName)
137-
// TODO: change back to add
138-
_compilerGeneratedMethodToUserCodeMethod.TryAdd (localFunction, method);
62+
callGraph.TrackCall (method, lambdaOrLocalFunction);
13963
}
14064

65+
// Discover state machine methods.
14166
if (!method.HasCustomAttributes)
142-
continue;
67+
return;
14368

14469
foreach (var attribute in method.CustomAttributes) {
14570
if (attribute.AttributeType.Namespace != "System.Runtime.CompilerServices")
@@ -162,6 +87,38 @@ void PopulateCacheForType (TypeDefinition type)
16287
}
16388
}
16489
}
90+
91+
// Look for state machine methods, and methods which call local functions.
92+
foreach (MethodDefinition method in type.Methods)
93+
ProcessMethod (method);
94+
95+
// Also scan compiler-generated state machine methods (in case they have calls to nested functions),
96+
// and nested functions inside compiler-generated closures (in case they call other nested functions).
97+
foreach (var nestedType in type.NestedTypes) {
98+
if (!CompilerGeneratedNames.IsGeneratedMemberName (nestedType.Name))
99+
continue;
100+
101+
// TODO: state machine types shouldn't contain state machine methods. Assert this?
102+
foreach (var method in nestedType.Methods)
103+
ProcessMethod (method);
104+
}
105+
106+
// Now we've discovered the call graphs for calls to nested functions.
107+
// Use this to map back from nested functions to the declaring user methods.
108+
109+
// Note: This maps all nested functions back to the user code, not to the immediately
110+
// declaring local function. The IL doesn't contain enough information in general for
111+
// us to determine the nesting of local functions and lambdas.
112+
113+
// Note: this only discovers nested functions which are referenced from the user
114+
// code or its referenced nested functions. There is no reliable way to determine from
115+
// IL which user code an unused nested function belongs to.
116+
foreach (var userDefinedMethod in callingMethods) {
117+
foreach (var nestedFunction in callGraph.GetReachableMethods (userDefinedMethod)) {
118+
Debug.Assert (CompilerGeneratedNames.IsLambdaOrLocalFunction (nestedFunction.Name));
119+
_compilerGeneratedMethodToUserCodeMethod.Add (nestedFunction, userDefinedMethod);
120+
}
121+
}
165122
}
166123

167124
static TypeDefinition? GetFirstConstructorArgumentAsType (CustomAttribute attribute)
@@ -172,6 +129,9 @@ void PopulateCacheForType (TypeDefinition type)
172129
return attribute.ConstructorArguments[0].Value as TypeDefinition;
173130
}
174131

132+
// For state machine types/members, maps back to the state machine method.
133+
// For local functions and lambdas, maps back to the owning method in user code (not the declaring
134+
// lambda or local function, because the IL doesn't contain enough information to figure this out).
175135
public bool TryGetOwningMethodForCompilerGeneratedMember (IMemberDefinition sourceMember, [NotNullWhen (true)] out MethodDefinition? owningMethod)
176136
{
177137
owningMethod = null;

test/Mono.Linker.Tests.Cases/RequiresCapability/RequiresInCompilerGeneratedCode.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,8 @@ static void Outer ()
914914
// code for the lambda contained in Outer(int i).
915915
}
916916

917-
static void Outer(int i) {
917+
static void Outer (int i)
918+
{
918919
LocalFunction ();
919920

920921
[ExpectedWarning ("IL2026", "--MethodWithRequires--")]
@@ -1221,7 +1222,8 @@ static void Outer ()
12211222
// code for the lambda contained in Outer(int i).
12221223
}
12231224

1224-
static void Outer(int i) {
1225+
static void Outer (int i)
1226+
{
12251227
var lambda =
12261228
[ExpectedWarning ("IL2026", "--MethodWithRequires--")]
12271229
[ExpectedWarning ("IL3002", ProducedBy = ProducedBy.Analyzer)]

0 commit comments

Comments
 (0)