LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
SymbolUseGraph.cpp
Go to the documentation of this file.
1//===-- SymbolUseGraph.cpp --------------------------------------*- 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//===----------------------------------------------------------------------===//
9
11
15#include "llzk/Util/Compare.h"
16#include "llzk/Util/Constants.h"
20
21#include <mlir/IR/BuiltinOps.h>
22
23#include <llvm/ADT/SmallPtrSet.h>
24#include <llvm/ADT/SmallSet.h>
25#include <llvm/Support/GraphWriter.h>
26
27using namespace mlir;
28
29namespace llzk {
30
31//===----------------------------------------------------------------------===//
32// SymbolUseGraphNode
33//===----------------------------------------------------------------------===//
34
35void SymbolUseGraphNode::addSuccessor(SymbolUseGraphNode *node) {
36 if (this->successors.insert(node)) {
37 node->predecessors.insert(this);
38 }
39}
40
41void SymbolUseGraphNode::removeSuccessor(SymbolUseGraphNode *node) {
42 if (this->successors.remove(node)) {
43 node->predecessors.remove(this);
44 }
45}
46
47FailureOr<SymbolLookupResultUntyped>
48SymbolUseGraphNode::lookupSymbol(SymbolTableCollection &tables, bool reportMissing) const {
49 if (!isRealNode()) {
50 return failure();
51 }
52 Operation *lookupFrom = getSymbolPathRoot().getOperation();
53 auto res = lookupSymbolIn(tables, getSymbolPath(), lookupFrom, lookupFrom, reportMissing);
54 if (succeeded(res) || !reportMissing) {
55 return res;
56 }
57 // This is likely an error in the use graph and not a case that should ever happen.
58 return lookupFrom->emitError().append(
59 "Could not find symbol referenced in UseGraph: ", getSymbolPath()
60 );
61}
62
63//===----------------------------------------------------------------------===//
64// SymbolUseGraph
65//===----------------------------------------------------------------------===//
66
67namespace {
68
69template <typename R>
70R getPathAndCall(SymbolOpInterface defOp, llvm::function_ref<R(ModuleOp, SymbolRefAttr)> callback) {
71 assert(defOp); // pre-condition
72
73 ModuleOp foundRoot;
74 FailureOr<SymbolRefAttr> path = llzk::getPathFromRoot(defOp, &foundRoot);
75 if (failed(path)) {
76 // This occurs if there is no root module with LANG_ATTR_NAME attribute
77 // or there is an unnamed module between the root module and the symbol.
78 auto diag = defOp.emitError("in SymbolUseGraph, failed to build symbol path");
79 diag.attachNote(defOp.getLoc()).append("for this SymbolOp");
80 diag.report();
81 return nullptr;
82 }
83 return callback(foundRoot, path.value());
84}
85
86} // namespace
87
88SymbolUseGraph::SymbolUseGraph(Operation *rootSymbolTableOp) {
89 assert(rootSymbolTableOp && rootSymbolTableOp->hasTrait<OpTrait::SymbolTable>());
90 buildGraph(rootSymbolTableOp);
91}
92
93void SymbolUseGraph::buildGraph(Operation *symbolTableOp) {
94 // Construction does not mutate IR. Share enclosing-symbol context across uses in the
95 // same regions, and resolve each owner's graph node only once. Both maps expire here.
96 DenseMap<Region *, SymbolOpInterface> regionOwners;
97 DenseMap<Operation *, SymbolUseGraphNode *> ownerNodes;
98 auto getOwner = [&regionOwners](Operation *user) {
99 if (auto symbol = llvm::dyn_cast<SymbolOpInterface>(user)) {
100 return symbol;
101 }
102 SmallVector<Region *> pending;
103 Region *region = user->getParentRegion();
104 SymbolOpInterface owner;
105 while (region) {
106 auto found = regionOwners.find(region);
107 if (found != regionOwners.end()) {
108 owner = found->second;
109 break;
110 }
111 pending.push_back(region);
112 Operation *parent = region->getParentOp();
113 owner = llvm::dyn_cast<SymbolOpInterface>(parent);
114 if (owner) {
115 break;
116 }
117 region = parent->getParentRegion();
118 }
119 for (Region *visited : pending) {
120 regionOwners.try_emplace(visited, owner);
121 }
122 return owner;
123 };
124 auto getUserNode = [this, &getOwner, &ownerNodes](Operation *user) {
125 SymbolOpInterface owner = getOwner(user);
126 auto found = ownerNodes.find(owner.getOperation());
127 if (found != ownerNodes.end()) {
128 return found->second;
129 }
130 auto *node = getPathAndCall<SymbolUseGraphNode *>(
131 owner, [this, owner](ModuleOp pathRoot, SymbolRefAttr path) {
132 auto *n = getOrAddNode(pathRoot, path, nullptr);
133 n->opsThatUseTheSymbol.insert(owner);
134 return n;
135 }
136 );
137 ownerNodes.try_emplace(owner.getOperation(), node);
138 return node;
139 };
140 auto walkFn = [this, &getUserNode](Operation *op, bool) {
141 assert(op->hasTrait<OpTrait::SymbolTable>());
142 FailureOr<ModuleOp> opRootModule = llzk::getRootModule(op);
143 if (failed(opRootModule)) {
144 return;
145 }
146
147 if (auto usesOpt = llzk::getSymbolUses(&op->getRegion(0))) {
148 // Create child node for each Symbol use, as successor of the user Symbol op.
149 for (SymbolTable::SymbolUse u : usesOpt.value()) {
150 bool isTemplateSymbol = false;
151 Operation *user = u.getUser();
152 SymbolRefAttr symRef = u.getSymbolRef();
153 // Normalize flat references to their defining scope when they name a struct
154 // member or a template symbol binding.
155 if (FlatSymbolRefAttr flatSymRef = llvm::dyn_cast<FlatSymbolRefAttr>(symRef)) {
156 if (auto fref = llvm::dyn_cast<component::MemberRefOpInterface>(user);
157 fref && fref.getMemberNameAttr() == flatSymRef) {
158 symRef = llzk::appendLeaf(fref.getStructType().getNameRef(), flatSymRef);
159 } else if (auto userTemplate = getSelfOrParentOfType<polymorphic::TemplateOp>(user)) {
160 StringAttr localName = flatSymRef.getAttr();
161 isTemplateSymbol =
162 userTemplate.hasConstNamed<polymorphic::TemplateSymbolBindingOpInterface>(
163 localName
164 );
165 if (isTemplateSymbol) {
166 // If 'flatSymRef' is defined in the SymbolTable for 'userTemplate' then it's
167 // a local symbol so prepend the full path of the template itself.
168 auto parentPath = llzk::getPathFromRoot(userTemplate);
169 assert(succeeded(parentPath));
170 symRef = llzk::appendLeaf(parentPath.value(), flatSymRef);
171 }
172 }
173 }
174 auto *node = this->getOrAddNode(opRootModule.value(), symRef, getUserNode(user));
175 node->isTemplateSymBinding = isTemplateSymbol;
176 node->opsThatUseTheSymbol.insert(user);
177 }
178 }
179 };
180 SymbolTable::walkSymbolTables(symbolTableOp, true, walkFn);
181
182 // Find all nodes with no successors and add the tail node as successor.
183 for (SymbolUseGraphNode *n : nodesIter()) {
184 if (!n->hasSuccessor()) {
185 n->addSuccessor(&tail);
186 }
187 }
188}
189
190SymbolUseGraphNode *SymbolUseGraph::getOrAddNode(
191 ModuleOp pathRoot, SymbolRefAttr path, SymbolUseGraphNode *predecessorNode
192) {
193 NodeMapKeyT key = std::make_pair(pathRoot, path);
194 std::unique_ptr<SymbolUseGraphNode> &nodeRef = nodes[key];
195 if (!nodeRef) {
196 nodeRef.reset(new SymbolUseGraphNode(pathRoot, path));
197 // When creating a new node, ensure it's attached to the graph, either as successor
198 // to the predecessor node (if given) else as successor to the root node.
199 if (predecessorNode) {
200 predecessorNode->addSuccessor(nodeRef.get());
201 } else {
202 root.addSuccessor(nodeRef.get());
203 }
204 } else if (predecessorNode) {
205 // When the node already exists and an additional predecessor node is given, add the node as a
206 // successor to the given predecessor node and detach from the 'root' (unless it's a self edge).
207 SymbolUseGraphNode *node = nodeRef.get();
208 predecessorNode->addSuccessor(node);
209 if (node != predecessorNode) {
210 root.removeSuccessor(node);
211 }
212 }
213 return nodeRef.get();
214}
215
216const SymbolUseGraphNode *SymbolUseGraph::lookupNode(ModuleOp pathRoot, SymbolRefAttr path) const {
217 NodeMapKeyT key = std::make_pair(pathRoot, path);
218 const auto *it = nodes.find(key);
219 return it == nodes.end() ? nullptr : it->second.get();
220}
221
222const SymbolUseGraphNode *SymbolUseGraph::lookupNode(SymbolOpInterface symbolDef) const {
223 return getPathAndCall<const SymbolUseGraphNode *>(symbolDef, [this](ModuleOp r, SymbolRefAttr p) {
224 return this->lookupNode(r, p);
225 });
226}
227
228//===----------------------------------------------------------------------===//
229// Printing
230//===----------------------------------------------------------------------===//
231
232std::string SymbolUseGraphNode::toString(bool showLocations) const {
233 return buildStringViaPrint(*this, showLocations);
234}
235
236namespace {
237
238inline void safeAppendPathRoot(llvm::raw_ostream &os, ModuleOp root) {
239 if (root) {
240 FailureOr<SymbolRefAttr> unambiguousRoot = getPathFromTopRoot(root);
241 if (succeeded(unambiguousRoot)) {
242 os << unambiguousRoot.value() << '\n';
243 } else {
244 os << "<<unknown path>>\n";
245 }
246 } else {
247 os << "<<NULL MODULE>>\n";
248 }
249}
250
251} // namespace
252
254 llvm::raw_ostream &os, bool showLocations, const std::string &locationLinePrefix
255) const {
256 os << '\'' << symbolPath << '\'';
257 if (isTemplateSymBinding) {
258 os << " (struct param)";
259 }
260 os << " with root module ";
261 safeAppendPathRoot(os, symbolPathRoot);
262 if (showLocations) {
263 // Print the user op locations (sorted for stable output). Printing only the location rather
264 // than the full Operation gives a short (single-line) format that's still useful for human
265 // debugging.
266 llvm::SmallSet<mlir::Location, 3, LocationComparator> locations;
267 for (Operation *user : getUserOps()) {
268 locations.insert(user->getLoc());
269 }
270 for (Location loc : locations) {
271 os << locationLinePrefix << loc << '\n';
272 }
273 }
274}
275
276void SymbolUseGraph::print(llvm::raw_ostream &os) const {
277 const SymbolUseGraphNode *rootPtr = &this->root;
278
279 // Tracks nodes that have been printed to ensure they are only printed once.
280 SmallPtrSet<SymbolUseGraphNode *, 16> done;
281
282 std::function<void(SymbolUseGraphNode *)> printNode = [rootPtr, &printNode, &done,
283 &os](SymbolUseGraphNode *node) {
284 // Skip if the node has been printed before
285 if (!done.insert(node).second) {
286 return;
287 }
288 // Print the current node
289 os << "// - Node : [" << node << "] ";
290 node->print(os, true, "// --- ");
291 // Print list of IDs for the predecessors (excluding root) and successors
292 os << "// --- Predecessors : [";
293 llvm::interleaveComma(
294 llvm::make_filter_range(
295 node->predecessorIter(), [rootPtr](SymbolUseGraphNode *n) { return n != rootPtr; }
296 ),
297 os
298 );
299 os << "]\n";
300 os << "// --- Successors : [";
301 llvm::interleaveComma(node->successorIter(), os);
302 os << "]\n";
303 // Recursively print the successors
304 for (SymbolUseGraphNode *c : node->successorIter()) {
305 printNode(c);
306 }
307 };
308
309 os << "// ---- SymbolUseGraph ----\n";
310 for (SymbolUseGraphNode *r : rootPtr->successorIter()) {
311 printNode(r);
312 }
313 os << "// ------------------------\n";
314 assert(done.size() == this->size() && "All nodes were not printed!");
315}
316
317void SymbolUseGraph::dumpToDotFile(std::string filename) const {
319 llvm::WriteGraph(this, "SymbolUseGraph", /*ShortNames*/ false, title, std::move(filename));
320}
321
322} // namespace llzk
mlir::FailureOr< SymbolLookupResultUntyped > lookupSymbol(mlir::SymbolTableCollection &tables, bool reportMissing=true) const
bool isRealNode() const
Return 'false' iff this node is an artificial node created for the graph head/tail.
std::string toString(bool showLocations=false) const
Print the node in a human readable format.
mlir::SymbolRefAttr getSymbolPath() const
The symbol path+name relative to the closest root ModuleOp.
const OpSet & getUserOps() const
The set of operations that use the symbol.
mlir::ModuleOp getSymbolPathRoot() const
Return the root ModuleOp for the path.
llvm::iterator_range< iterator > successorIter() const
Range over successor nodes.
void print(llvm::raw_ostream &os, bool showLocations=false, const std::string &locationLinePrefix="") const
void dumpToDotFile(std::string filename="") const
Dump the graph to file in dot graph format.
llvm::iterator_range< iterator > nodesIter() const
Range over all nodes in the graph.
void print(llvm::raw_ostream &os) const
const SymbolUseGraphNode * lookupNode(mlir::ModuleOp pathRoot, mlir::SymbolRefAttr path) const
Return the existing node for the symbol reference relative to the given module, else nullptr.
SymbolUseGraph(mlir::Operation *rootSymbolTableOp)
Build a use graph rooted at an operation with the SymbolTable trait.
std::optional< mlir::SymbolTable::UseRange > getSymbolUses(mlir::Operation *from)
Get an iterator range for all of the uses, for any symbol, that are nested within the given operation...
FailureOr< ModuleOp > getRootModule(Operation *from)
SymbolRefAttr appendLeaf(SymbolRefAttr orig, FlatSymbolRefAttr newLeaf)
std::string buildStringViaPrint(const T &base, Args &&...args)
Generate a string by calling base.print(llvm::raw_ostream &) on a stream backed by the returned strin...
mlir::FailureOr< SymbolLookupResultUntyped > lookupSymbolIn(mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr symbol, Within &&lookupWithin, mlir::Operation *origin, bool reportMissing=true)
FailureOr< SymbolRefAttr > getPathFromTopRoot(SymbolOpInterface to, ModuleOp *foundRoot)
OpClass getSelfOrParentOfType(mlir::Operation *op)
Return the closest surrounding parent/ancestor operation that is of type 'OpClass',...
Definition OpHelpers.h:40
FailureOr< SymbolRefAttr > getPathFromRoot(SymbolOpInterface to, ModuleOp *foundRoot)