1 // Copyright (c) 2020 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "source/fuzz/call_graph.h"
16 
17 #include <queue>
18 
19 namespace spvtools {
20 namespace fuzz {
21 
CallGraph(opt::IRContext * context)22 CallGraph::CallGraph(opt::IRContext* context) {
23   // Initialize function in-degree, call graph edges and corresponding maximum
24   // loop nesting depth to 0, empty and 0 respectively.
25   for (auto& function : *context->module()) {
26     function_in_degree_[function.result_id()] = 0;
27     call_graph_edges_[function.result_id()] = std::set<uint32_t>();
28     function_max_loop_nesting_depth_[function.result_id()] = 0;
29   }
30 
31   // Record the maximum loop nesting depth for each edge, by keeping a map from
32   // pairs of function ids, where (A, B) represents a function call from A to B,
33   // to the corresponding maximum depth.
34   std::map<std::pair<uint32_t, uint32_t>, uint32_t> call_to_max_depth;
35 
36   // Compute |function_in_degree_|, |call_graph_edges_| and |call_to_max_depth|.
37   BuildGraphAndGetDepthOfFunctionCalls(context, &call_to_max_depth);
38 
39   // Compute |functions_in_topological_order_|.
40   ComputeTopologicalOrderOfFunctions();
41 
42   // Compute |function_max_loop_nesting_depth_|.
43   ComputeInterproceduralFunctionCallDepths(call_to_max_depth);
44 }
45 
BuildGraphAndGetDepthOfFunctionCalls(opt::IRContext * context,std::map<std::pair<uint32_t,uint32_t>,uint32_t> * call_to_max_depth)46 void CallGraph::BuildGraphAndGetDepthOfFunctionCalls(
47     opt::IRContext* context,
48     std::map<std::pair<uint32_t, uint32_t>, uint32_t>* call_to_max_depth) {
49   // Consider every function.
50   for (auto& function : *context->module()) {
51     // Avoid considering the same callee of this function multiple times by
52     // recording known callees.
53     std::set<uint32_t> known_callees;
54     // Consider every function call instruction in every block.
55     for (auto& block : function) {
56       for (auto& instruction : block) {
57         if (instruction.opcode() != SpvOpFunctionCall) {
58           continue;
59         }
60         // Get the id of the function being called.
61         uint32_t callee = instruction.GetSingleWordInOperand(0);
62 
63         // Get the loop nesting depth of this function call.
64         uint32_t loop_nesting_depth =
65             context->GetStructuredCFGAnalysis()->LoopNestingDepth(block.id());
66         // If inside a loop header, consider the function call nested inside the
67         // loop headed by the block.
68         if (block.IsLoopHeader()) {
69           loop_nesting_depth++;
70         }
71 
72         // Update the map if we have not seen this pair (caller, callee)
73         // before or if this function call is from a greater depth.
74         if (!known_callees.count(callee) ||
75             call_to_max_depth->at({function.result_id(), callee}) <
76                 loop_nesting_depth) {
77           call_to_max_depth->insert(
78               {{function.result_id(), callee}, loop_nesting_depth});
79         }
80 
81         if (known_callees.count(callee)) {
82           // We have already considered a call to this function - ignore it.
83           continue;
84         }
85         // Increase the callee's in-degree and add an edge to the call graph.
86         function_in_degree_[callee]++;
87         call_graph_edges_[function.result_id()].insert(callee);
88         // Mark the callee as 'known'.
89         known_callees.insert(callee);
90       }
91     }
92   }
93 }
94 
ComputeTopologicalOrderOfFunctions()95 void CallGraph::ComputeTopologicalOrderOfFunctions() {
96   // This is an implementation of Kahn’s algorithm for topological sorting.
97 
98   // Initialise |functions_in_topological_order_|.
99   functions_in_topological_order_.clear();
100 
101   // Get a copy of the initial in-degrees of all functions.  The algorithm
102   // involves decrementing these values, hence why we work on a copy.
103   std::map<uint32_t, uint32_t> function_in_degree = GetFunctionInDegree();
104 
105   // Populate a queue with all those function ids with in-degree zero.
106   std::queue<uint32_t> queue;
107   for (auto& entry : function_in_degree) {
108     if (entry.second == 0) {
109       queue.push(entry.first);
110     }
111   }
112 
113   // Pop ids from the queue, adding them to the sorted order and decreasing the
114   // in-degrees of their successors.  A successor who's in-degree becomes zero
115   // gets added to the queue.
116   while (!queue.empty()) {
117     auto next = queue.front();
118     queue.pop();
119     functions_in_topological_order_.push_back(next);
120     for (auto successor : GetDirectCallees(next)) {
121       assert(function_in_degree.at(successor) > 0 &&
122              "The in-degree cannot be zero if the function is a successor.");
123       function_in_degree[successor] = function_in_degree.at(successor) - 1;
124       if (function_in_degree.at(successor) == 0) {
125         queue.push(successor);
126       }
127     }
128   }
129 
130   assert(functions_in_topological_order_.size() == function_in_degree.size() &&
131          "Every function should appear in the sort.");
132 
133   return;
134 }
135 
ComputeInterproceduralFunctionCallDepths(const std::map<std::pair<uint32_t,uint32_t>,uint32_t> & call_to_max_depth)136 void CallGraph::ComputeInterproceduralFunctionCallDepths(
137     const std::map<std::pair<uint32_t, uint32_t>, uint32_t>&
138         call_to_max_depth) {
139   // Find the maximum loop nesting depth that each function can be
140   // called from, by considering them in topological order.
141   for (uint32_t function_id : functions_in_topological_order_) {
142     const auto& callees = call_graph_edges_[function_id];
143 
144     // For each callee, update its maximum loop nesting depth, if a call from
145     // |function_id| increases it.
146     for (uint32_t callee : callees) {
147       uint32_t max_depth_from_this_function =
148           function_max_loop_nesting_depth_[function_id] +
149           call_to_max_depth.at({function_id, callee});
150       if (function_max_loop_nesting_depth_[callee] <
151           max_depth_from_this_function) {
152         function_max_loop_nesting_depth_[callee] = max_depth_from_this_function;
153       }
154     }
155   }
156 }
157 
PushDirectCallees(uint32_t function_id,std::queue<uint32_t> * queue) const158 void CallGraph::PushDirectCallees(uint32_t function_id,
159                                   std::queue<uint32_t>* queue) const {
160   for (auto callee : GetDirectCallees(function_id)) {
161     queue->push(callee);
162   }
163 }
164 
GetIndirectCallees(uint32_t function_id) const165 std::set<uint32_t> CallGraph::GetIndirectCallees(uint32_t function_id) const {
166   std::set<uint32_t> result;
167   std::queue<uint32_t> queue;
168   PushDirectCallees(function_id, &queue);
169 
170   while (!queue.empty()) {
171     auto next = queue.front();
172     queue.pop();
173     if (result.count(next)) {
174       continue;
175     }
176     result.insert(next);
177     PushDirectCallees(next, &queue);
178   }
179   return result;
180 }
181 
182 }  // namespace fuzz
183 }  // namespace spvtools
184