LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKUnusedDeclarationEliminationPass.cpp
Go to the documentation of this file.
1//===-- LLZKUnusedDeclarationEliminationPass.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//===----------------------------------------------------------------------===//
13//===----------------------------------------------------------------------===//
14
19
20#include <mlir/IR/BuiltinOps.h>
21#include <mlir/IR/SymbolTable.h>
22
23#include <llvm/ADT/SmallVector.h>
24#include <llvm/Support/Debug.h>
25
26// Include the generated base pass class definitions.
27namespace llzk {
28#define GEN_PASS_DEF_UNUSEDDECLARATIONELIMINATIONPASS
30} // namespace llzk
31
32using namespace mlir;
33using namespace llzk;
34using namespace llzk::component;
35
36#define DEBUG_TYPE "llzk-unused-declaration-elim"
37
38namespace {
39
40class PassImpl : public llzk::impl::UnusedDeclarationEliminationPassBase<PassImpl> {
41 using Base = UnusedDeclarationEliminationPassBase<PassImpl>;
42 using Base::Base;
43
46 struct PassContext {
47 DenseMap<SymbolRefAttr, StructDefOp> symbolToStruct;
48 DenseMap<StructDefOp, SymbolRefAttr> structToSymbol;
49
50 const SymbolRefAttr &getSymbol(StructDefOp s) const { return structToSymbol.at(s); }
51 StructDefOp getStruct(const SymbolRefAttr &sym) const { return symbolToStruct.at(sym); }
52
53 static PassContext populate(ModuleOp modOp) {
54 PassContext ctx;
55
56 modOp.walk<WalkOrder::PreOrder>([&ctx](StructDefOp structDef) {
57 auto structSymbolRes = getPathFromTopRoot(structDef);
58 ensure(succeeded(structSymbolRes), "failed to lookup struct symbol");
59 SymbolRefAttr structSym = *structSymbolRes;
60 ctx.symbolToStruct[structSym] = structDef;
61 ctx.structToSymbol[structDef] = structSym;
62 });
63 return ctx;
64 }
65 };
66
67 void runOnOperation() override {
68 PassContext ctx = PassContext::populate(getOperation());
69 // First, remove unused members. This may allow more structs to be removed,
70 // if their final remaining uses are as types for unused members.
71 if (failed(removeUnusedMembers(ctx))) {
72 signalPassFailure();
73 return;
74 }
75
76 // Last, remove unused structs if configured
77 if (removeStructs) {
78 removeUnusedStructs(ctx);
79 removeEmptyModules();
80 }
81 }
82
87 LogicalResult removeUnusedMembers(PassContext &ctx) {
88 ModuleOp modOp = getOperation();
89
90 SymbolTableCollection symbolTables;
91
92 // Candidate members are tracked by resolved definitions instead of by the
93 // symbol text used at each read/write site, which may be relative.
94 DenseSet<MemberDefOp> members;
95 for (const auto &entry : ctx.structToSymbol) {
96 StructDefOp structDef = entry.first;
97 bool notMain = !structDef.isMainComponent();
98 structDef.walk([notMain, &members](MemberDefOp member) {
99 // We don't consider public members in the Main component for removal, as these are output
100 // values and removing them would result in modifying the overall circuit interface.
101 if (notMain || !member.hasPublicAttr()) {
102 members.insert(member);
103 }
104 });
105 }
106
107 // Remove all members that are read.
108 WalkResult readWalk = modOp.walk([&](MemberReadOp readm) {
109 FailureOr<SymbolLookupResult<MemberDefOp>> memberDef = readm.getMemberDefOp(symbolTables);
110 if (failed(memberDef)) {
111 return WalkResult::interrupt();
112 }
113
114 members.erase(memberDef->get());
115 return WalkResult::advance();
116 });
117 if (readWalk.wasInterrupted()) {
118 return failure();
119 }
120
121 // Remove all writes that reference the remaining members, as these writes
122 // are now known to only update write-only members.
123 WalkResult writeWalk = modOp.walk([&](MemberWriteOp writem) {
124 FailureOr<SymbolLookupResult<MemberDefOp>> memberDef = writem.getMemberDefOp(symbolTables);
125 if (failed(memberDef)) {
126 return WalkResult::interrupt();
127 }
128
129 MemberDefOp writtenMember = memberDef->get();
130 if (members.contains(writtenMember)) {
131 // We need not check the users of a writem, since it produces no results.
132 LLVM_DEBUG(
133 llvm::dbgs() << "Removing write " << writem << " to write-only member " << writtenMember
134 << '\n'
135 );
136 writem.erase();
137 }
138
139 return WalkResult::advance();
140 });
141 if (writeWalk.wasInterrupted()) {
142 return failure();
143 }
144
145 // Finally, erase the remaining members.
146 for (MemberDefOp memberDef : members) {
147 LLVM_DEBUG(llvm::dbgs() << "Removing member " << memberDef << '\n');
148 memberDef->erase();
149 }
150
151 return success();
152 }
153
158 void removeUnusedStructs(PassContext &ctx) {
159 DenseMap<StructDefOp, DenseSet<StructDefOp>> uses;
160 DenseMap<StructDefOp, DenseSet<StructDefOp>> usedBy;
161
162 // initialize both maps with empty sets so we can identify unused structs
163 for (auto &[structDef, _] : ctx.structToSymbol) {
164 uses[structDef] = {};
165 usedBy[structDef] = {};
166 }
167
168 getOperation().walk([&](Operation *op) {
169 auto structParent = op->getParentOfType<StructDefOp>();
170 if (structParent == nullptr) {
171 return WalkResult::advance();
172 }
173
174 auto tryAddUse = [&](Type ty) {
175 if (auto structTy = dyn_cast<StructType>(ty)) {
176 // This name ref is required to be fully qualified
177 SymbolRefAttr sym = structTy.getNameRef();
178 StructDefOp refStruct = ctx.getStruct(sym);
179 if (refStruct != structParent) {
180 uses[structParent].insert(refStruct);
181 usedBy[refStruct].insert(structParent);
182 }
183 }
184 };
185
186 // LLZK requires fully-qualified references to struct symbols. So, we
187 // simply need to look for the struct symbol within this op's symbol uses.
188
189 // Check operands
190 for (Value operand : op->getOperands()) {
191 tryAddUse(operand.getType());
192 }
193
194 // Check results
195 for (Value result : op->getResults()) {
196 tryAddUse(result.getType());
197 }
198
199 // Check block arguments
200 for (Region &region : op->getRegions()) {
201 for (Block &block : region) {
202 for (BlockArgument arg : block.getArguments()) {
203 tryAddUse(arg.getType());
204 }
205 }
206 }
207
208 // Check attributes
209 for (const auto &namedAttr : op->getAttrs()) {
210 namedAttr.getValue().walk([&tryAddUse](TypeAttr typeAttr) {
211 tryAddUse(typeAttr.getValue());
212 });
213 }
214
215 return WalkResult::advance();
216 });
217
218 SmallVector<StructDefOp> unusedStructs;
219
220 auto updateUnusedStructs = [&usedBy, &unusedStructs]() {
221 for (auto &[structDef, users] : usedBy) {
222 if (users.empty() && !structDef.isMainComponent()) {
223 unusedStructs.push_back(structDef);
224 }
225 }
226 };
227
228 updateUnusedStructs();
229
230 while (!unusedStructs.empty()) {
231 StructDefOp unusedStruct = unusedStructs.back();
232 unusedStructs.pop_back();
233
234 // See what structs are being used by this unused struct
235 for (auto usedStruct : uses[unusedStruct]) {
236 // The usedStruct is no longer used by the unusedStruct
237 usedBy[usedStruct].erase(unusedStruct);
238 }
239
240 // Remove the unused struct from both maps and the IR
241 usedBy.erase(unusedStruct);
242 uses.erase(unusedStruct);
243 unusedStruct->erase();
244
245 // Check to see if we've created any more unused structs after we process
246 // all existing known unused structs (to avoid double processing).
247 if (unusedStructs.empty()) {
248 updateUnusedStructs();
249 }
250 }
251 }
252
254 void removeEmptyModules() {
255 SmallVector<ModuleOp> emptyModules;
256
257 ModuleOp rootModOp = getOperation();
258 rootModOp.walk<WalkOrder::PostOrder>([&](ModuleOp modOp) {
259 if (modOp == rootModOp) {
260 return;
261 }
262 Region &region = modOp.getBodyRegion();
263 if (region.empty() || region.front().empty()) { // module has `SingleBlock` trait
264 emptyModules.push_back(modOp);
265 }
266 });
267
268 for (ModuleOp modOp : emptyModules) {
269 LLVM_DEBUG(llvm::dbgs() << "Removing empty module " << modOp.getName() << '\n');
270 modOp->erase();
271 }
272 }
273};
274
275} // namespace
This file defines methods symbol lookup across LLZK operations and included files.
bool hasPublicAttr()
Returns whether this member is a public output.
Definition Ops.h.inc:463
inline ::mlir::FailureOr< SymbolLookupResult< MemberDefOp > > getMemberDefOp(::mlir::SymbolTableCollection &tables)
Gets the definition for the member referenced in this op.
Definition Ops.h.inc:802
inline ::mlir::FailureOr< SymbolLookupResult< MemberDefOp > > getMemberDefOp(::mlir::SymbolTableCollection &tables)
Gets the definition for the member referenced in this op.
Definition Ops.h.inc:1030
bool isMainComponent()
Return true iff this struct.def is the main struct. See llzk::MAIN_ATTR_NAME.
Definition Ops.cpp:478
void ensure(bool condition, const llvm::Twine &errMsg)
FailureOr< SymbolRefAttr > getPathFromTopRoot(SymbolOpInterface to, ModuleOp *foundRoot)