LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
CallGraph.cpp
Go to the documentation of this file.
1//===-- CallGraph.cpp - LLZK-specific call graph implementation -*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2025 Veridise Inc.
6// SPDX-License-Identifier: Apache-2.0
7//
8// The contents of this file are adapted from llvm/lib/Analysis/CallGraph.cpp
9// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
10// See https://llvm.org/LICENSE.txt for license information.
11// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
12//
13//===----------------------------------------------------------------------===//
14
16
20
21#include <mlir/Analysis/CallGraph.h>
22#include <mlir/IR/Operation.h>
23#include <mlir/IR/SymbolTable.h>
24#include <mlir/Interfaces/CallInterfaces.h>
25
26#include <llvm/ADT/DepthFirstIterator.h>
27#include <llvm/ADT/SmallVector.h>
28#include <llvm/Support/ErrorHandling.h>
29
30namespace llzk {
31
32using namespace function;
33
34//===----------------------------------------------------------------------===//
35// CallGraphNode
36//===----------------------------------------------------------------------===//
37
39bool CallGraphNode::isExternal() const { return !callableRegion; }
40
43mlir::Region *CallGraphNode::getCallableRegion() const {
44 assert(!isExternal() && "the external node has no callable region");
45 return callableRegion;
46}
47
48mlir::CallableOpInterface CallGraphNode::getCalledFunction() const {
49 return llvm::dyn_cast<mlir::CallableOpInterface>(getCallableRegion()->getParentOp());
50}
51
54void CallGraphNode::addAbstractEdge(CallGraphNode *node) {
55 assert(isExternal() && "abstract edges are only valid on external nodes");
56 addEdge(node, Edge::Kind::Abstract);
57}
58
60void CallGraphNode::addCallEdge(CallGraphNode *node) { addEdge(node, Edge::Kind::Call); }
61
63void CallGraphNode::addChildEdge(CallGraphNode *child) { addEdge(child, Edge::Kind::Child); }
64
67 return llvm::any_of(edges, [](const Edge &edge) { return edge.isChild(); });
68}
69
71void CallGraphNode::addEdge(CallGraphNode *node, Edge::Kind kind) {
72 edges.insert({this, node, kind});
73}
74
75//===----------------------------------------------------------------------===//
76// CallGraph
77//===----------------------------------------------------------------------===//
78
81static void computeCallGraph(
82 mlir::Operation *op, CallGraph &cg, mlir::SymbolTableCollection &symbolTable,
83 CallGraphNode *parentNode, bool resolveCalls
84) {
85 if (mlir::CallOpInterface call = llvm::dyn_cast<mlir::CallOpInterface>(op)) {
86 // If there is no parent node, we ignore this operation. Even if this
87 // operation was a call, there would be no callgraph node to attribute it
88 // to.
89 if (resolveCalls && parentNode) {
90 parentNode->addCallEdge(cg.resolveCallable(call, symbolTable));
91 }
92 return;
93 }
94
95 // Compute the callgraph nodes and edges for each of the nested operations.
96 if (mlir::CallableOpInterface callable = llvm::dyn_cast<mlir::CallableOpInterface>(op)) {
97 if (auto *callableRegion = callable.getCallableRegion()) {
98 parentNode = cg.getOrAddNode(callableRegion, parentNode);
99 } else {
100 return;
101 }
102 }
103
104 for (mlir::Region &region : op->getRegions()) {
105 for (mlir::Operation &nested : region.getOps()) {
106 computeCallGraph(&nested, cg, symbolTable, parentNode, resolveCalls);
107 }
108 }
109}
110
111CallGraph::CallGraph(mlir::Operation *op)
112 : externalCallerNode(/*callable=*/nullptr), unknownCalleeNode(/*callable=*/nullptr) {
113 // Make two passes over the graph, one to compute the callables and one to
114 // resolve the calls. We split these up as we may have nested callable objects
115 // that need to be reserved before the calls.
116 mlir::SymbolTableCollection symbolTable;
117 computeCallGraph(
118 op, *this, symbolTable, /*parentNode=*/nullptr,
119 /*resolveCalls=*/false
120 );
121 computeCallGraph(
122 op, *this, symbolTable, /*parentNode=*/nullptr,
123 /*resolveCalls=*/true
124 );
125}
126
128CallGraphNode *CallGraph::getOrAddNode(mlir::Region *region, CallGraphNode *parentNode) {
129 assert(
130 region && llvm::isa<mlir::CallableOpInterface>(region->getParentOp()) &&
131 "expected parent operation to be callable"
132 );
133 std::unique_ptr<CallGraphNode> &node = nodes[region];
134 if (!node) {
135 node.reset(new CallGraphNode(region));
136
137 // Add this node to the given parent node if necessary.
138 if (parentNode) {
139 parentNode->addChildEdge(node.get());
140 } else {
141 // Otherwise, connect all callable nodes to the external node, this allows
142 // for conservatively including all callable nodes within the graph.
143 // FIXME This isn't correct, this is only necessary for callable nodes
144 // that *could* be called from external sources. This requires extending
145 // the interface for callables to check if they may be referenced
146 // externally.
147 externalCallerNode.addAbstractEdge(node.get());
148 }
149 }
150 return node.get();
151}
152
155CallGraphNode *CallGraph::lookupNode(mlir::Region *region) const {
156 const auto *it = nodes.find(region);
157 return it == nodes.end() ? nullptr : it->second.get();
158}
159
164 mlir::CallOpInterface call, mlir::SymbolTableCollection &symbolTable
165) const {
166 // `function.call` deliberately resolves from the LLZK root module rather
167 // than the nearest symbol table. Use the same lookup here so analyses and
168 // transformations agree when a nested symbol table shadows a root symbol.
169 if (auto funcCall = llvm::dyn_cast<CallOp>(call.getOperation())) {
170 auto res = funcCall.getCalleeTarget(symbolTable);
171 if (mlir::succeeded(res)) {
172 if (auto *node = lookupNode(res->get().getCallableRegion())) {
173 return node;
174 }
175 }
176 return getUnknownCalleeNode();
177 }
178
179 auto res = llzk::resolveCallable<mlir::CallableOpInterface>(symbolTable, call);
180 if (mlir::succeeded(res)) {
181 if (auto *node = lookupNode(res->get().getCallableRegion())) {
182 return node;
183 }
184 }
185 return getUnknownCalleeNode();
186}
187
190 // Erase any children of this node first.
191 if (node->hasChildren()) {
192 for (const CallGraphNode::Edge &edge : llvm::make_early_inc_range(*node)) {
193 if (edge.isChild()) {
194 eraseNode(edge.getTarget());
195 }
196 }
197 }
198 // Erase any edges to this node from any other nodes.
199 for (auto &it : nodes) {
200 it.second->edges.remove_if([node](const CallGraphNode::Edge &edge) {
201 return edge.getTarget() == node;
202 });
203 }
204 nodes.erase(node->getCallableRegion());
205}
206
207//===----------------------------------------------------------------------===//
208// Printing
209
211void CallGraph::dump() const { print(llvm::errs()); }
212void CallGraph::print(llvm::raw_ostream &os) const {
213 os << "// ---- CallGraph ----\n";
214
215 // Functor used to output the name for the given node.
216 auto emitNodeName = [&](const CallGraphNode *node) {
217 if (node == getExternalCallerNode()) {
218 os << "<External-Caller-Node>";
219 return;
220 }
221 if (node == getUnknownCalleeNode()) {
222 os << "<Unknown-Callee-Node>";
223 return;
224 }
225
226 auto *callableRegion = node->getCallableRegion();
227 auto *parentOp = callableRegion->getParentOp();
228 os << '\'' << callableRegion->getParentOp()->getName() << "' - Region #"
229 << callableRegion->getRegionNumber();
230 auto attrs = parentOp->getAttrDictionary();
231 if (!isNullOrEmpty(attrs)) {
232 os << " : " << attrs;
233 }
234 };
235
236 for (const auto &nodeIt : nodes) {
237 const CallGraphNode *node = nodeIt.second.get();
238
239 // Dump the header for this node.
240 os << "// - Node : ";
241 emitNodeName(node);
242 os << '\n';
243
244 // Emit each of the edges.
245 for (const auto &edge : *node) {
246 os << "// -- ";
247 if (edge.isCall()) {
248 os << "Call";
249 } else if (edge.isChild()) {
250 os << "Child";
251 }
252
253 os << "-Edge : ";
254 emitNodeName(edge.getTarget());
255 os << '\n';
256 }
257 os << "//\n";
258 }
259
260 os << "// -- SCCs --\n";
261
262 for (const auto &scc : make_range(llvm::scc_begin(this), llvm::scc_end(this))) {
263 os << "// - SCC : \n";
264 for (const auto &node : scc) {
265 os << "// -- Node :";
266 emitNodeName(node);
267 os << '\n';
268 }
269 os << '\n';
270 }
271
272 os << "// -------------------\n";
273}
274
275} // namespace llzk
This class represents a directed edge between two nodes in the callgraph.
Definition CallGraph.h:39
bool isChild() const
Returns true if this edge represents a Child edge.
Definition CallGraph.h:66
CallGraphNode * getTarget() const
Returns the target node for this edge.
Definition CallGraph.h:73
This is a simple port of the mlir::CallGraphNode with llzk::CallGraph as a friend class,...
Definition CallGraph.h:36
bool isExternal() const
Returns true if this node is an external node.
Definition CallGraph.cpp:39
mlir::Region * getCallableRegion() const
Returns the callable region this node represents.
Definition CallGraph.cpp:43
void addChildEdge(CallGraphNode *child)
Adds a reference edge to the given child node.
Definition CallGraph.cpp:63
bool hasChildren() const
Returns true if this node has any child edges.
Definition CallGraph.cpp:66
void addCallEdge(CallGraphNode *node)
Add an outgoing call edge from this node.
Definition CallGraph.cpp:60
mlir::CallableOpInterface getCalledFunction() const
Returns the called function that the callable region represents.
Definition CallGraph.cpp:48
void addAbstractEdge(CallGraphNode *node)
Adds an abstract reference edge to the given node.
Definition CallGraph.cpp:54
iterator end() const
Definition CallGraph.h:122
This is a port of mlir::CallGraph that has been adapted to use the custom symbol lookup helpers (see ...
Definition CallGraph.h:164
CallGraph(mlir::Operation *op)
CallGraphNode * getExternalCallerNode() const
Return the callgraph node representing an external caller.
Definition CallGraph.h:196
void dump() const
Dump the graph in a human readable format.
void print(llvm::raw_ostream &os) const
void eraseNode(CallGraphNode *node)
Erase the given node from the callgraph.
CallGraphNode * resolveCallable(mlir::CallOpInterface call, mlir::SymbolTableCollection &symbolTable) const
Resolve the callable for given callee to a node in the callgraph, or the external node if a valid nod...
CallGraphNode * lookupNode(mlir::Region *region) const
Lookup a call graph node for the given region, or nullptr if none is registered.
CallGraphNode * getUnknownCalleeNode() const
Return the callgraph node representing an indirect callee.
Definition CallGraph.h:201
CallGraphNode * getOrAddNode(mlir::Region *region, CallGraphNode *parentNode)
Get or add a call graph node for the given region.
bool isNullOrEmpty(mlir::ArrayAttr a)
mlir::FailureOr< SymbolLookupResult< T > > resolveCallable(mlir::SymbolTableCollection &symbolTable, mlir::CallOpInterface call)
Based on mlir::CallOpInterface::resolveCallable, but using LLZK lookup helpers.