LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKRedundantOperationEliminationPass.cpp
Go to the documentation of this file.
1//===-- LLZKRedundantOperationEliminationPass.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
23
24#include <mlir/Dialect/Arith/IR/Arith.h>
25#include <mlir/IR/BuiltinOps.h>
26#include <mlir/IR/Dominance.h>
27#include <mlir/IR/OperationSupport.h>
28
29#include <llvm/ADT/DenseMap.h>
30#include <llvm/ADT/DenseSet.h>
31#include <llvm/ADT/Hashing.h>
32#include <llvm/ADT/PostOrderIterator.h>
33#include <llvm/ADT/SmallVector.h>
34
35#include <utility>
36
37// Include the generated base pass class definitions.
38namespace llzk {
39#define GEN_PASS_DEF_REDUNDANTOPERATIONELIMINATIONPASS
41} // namespace llzk
42
43using namespace mlir;
44using namespace llzk;
45using namespace llzk::boolean;
46using namespace llzk::component;
47using namespace llzk::constrain;
48using namespace llzk::function;
49
50#define DEBUG_TYPE "llzk-duplicate-op-elim"
51
52namespace {
53
54// Maps original -> replacement value
55using TranslationMap = DenseMap<Value, Value>;
56
57static bool isDuplicateEliminationCandidate(Operation *op) {
58 if (isa<NonDetOp>(op) || op->hasTrait<OpTrait::IsTerminator>() || op->getNumRegions() != 0 ||
59 op->getNumSuccessors() != 0) {
60 return false;
61 }
62
63 return isa<ConstraintOpInterface>(op) || isMemoryEffectFree(op);
64}
65
69class OperationComparator {
70public:
71 explicit OperationComparator(Operation *o) : op(o), operands(o->getOperands()) {}
72
73 OperationComparator(Operation *o, const TranslationMap &m) : op(o) {
74 for (Value operand : op->getOperands()) {
75 if (auto it = m.find(operand); it != m.end()) {
76 operands.push_back(it->second);
77 } else {
78 operands.push_back(operand);
79 }
80 }
81 }
82
83 Operation *getOp() const { return op; }
84
85 const SmallVector<Value> &getOperands() const { return operands; }
86
87 bool isCommutative() const { return op->hasTrait<OpTrait::IsCommutative>(); }
88
89 friend bool operator==(const OperationComparator &lhs, const OperationComparator &rhs) {
90 if (!OperationEquivalence::isEquivalentTo(
91 lhs.op, rhs.op, OperationEquivalence::ignoreValueEquivalence,
92 /*markEquivalent=*/nullptr, OperationEquivalence::IgnoreLocations
93 )) {
94 return false;
95 }
96
97 // Preserve the pass's existing commutative matching for binary operations.
98 // For a future n-ary commutative op, exact operand order remains conservative.
99 if (lhs.isCommutative() && lhs.operands.size() == 2) {
100 return (lhs.operands[0] == rhs.operands[0] && lhs.operands[1] == rhs.operands[1]) ||
101 (lhs.operands[0] == rhs.operands[1] && lhs.operands[1] == rhs.operands[0]);
102 }
103
104 return lhs.operands == rhs.operands;
105 }
106
107private:
108 Operation *op;
109 SmallVector<Value> operands;
110};
111
112} // namespace
113
114namespace llvm {
115
116template <> struct DenseMapInfo<OperationComparator> {
117 static unsigned getHashValue(const OperationComparator &oc) {
118 hash_code opHash = mlir::OperationEquivalence::computeHash(
119 oc.getOp(), mlir::OperationEquivalence::ignoreHashValue,
120 mlir::OperationEquivalence::ignoreHashValue, mlir::OperationEquivalence::IgnoreLocations
121 );
122
123 ArrayRef<Value> operands = oc.getOperands();
124 hash_code operandHash;
125 if (oc.isCommutative() && operands.size() == 2) {
126 size_t lhsHash = hash_value(operands[0]);
127 size_t rhsHash = hash_value(operands[1]);
128 if (rhsHash < lhsHash) {
129 std::swap(lhsHash, rhsHash);
130 }
131 operandHash = hash_combine(lhsHash, rhsHash);
132 } else {
133 operandHash = hash_combine_range(operands.begin(), operands.end());
134 }
135
136 return hash_combine(opHash, operandHash);
137 }
138 static bool isEqual(const OperationComparator &lhs, const OperationComparator &rhs) {
139 return lhs == rhs;
140 }
141};
142
143} // namespace llvm
144
145namespace {
146
147class PassImpl : public llzk::impl::RedundantOperationEliminationPassBase<PassImpl> {
148 using Base = RedundantOperationEliminationPassBase<PassImpl>;
149 using Base::Base;
150
151 void runOnOperation() override {
152 SymbolTableCollection symbolTables;
153 // Traverse functions from the bottom of the call graph up.
154 // This way, we may create empty constrain functions to which we can eliminate
155 // calls.
156 auto &cga = getAnalysis<CallGraphAnalysis>();
157 const llzk::CallGraph *callGraph = &cga.getCallGraph();
158 for (const llzk::CallGraphNode *node : llvm::post_order(callGraph)) {
159 if (!node->isExternal()) {
160 runOnFunc(symbolTables, node->getCalledFunction());
161 }
162 }
163 }
164
169 bool isPurposelessConstrainFunc(
170 SymbolTableCollection &symbolTables, FuncDefOp fn, DenseSet<Operation *> &activeFunctions
171 ) {
172 if (!fn.isStructConstrain()) {
173 return false;
174 }
175 // Calls to a constrain function are only removable when the callee cannot
176 // contain witness-generation state mutations such as global.write or
177 // ram.store. The WitnessGen verifier enforces that boundary unless the
178 // callee is explicitly marked allow_witness.
179 if (fn.hasAllowWitnessAttr()) {
180 return false;
181 }
182 if (!activeFunctions.insert(fn.getOperation()).second) {
183 // A recursive call path cannot be proven purposeless; retain the call
184 // conservatively.
185 return false;
186 }
187
188 bool res = true;
189 fn.walk([&](Operation *op) {
190 if (op == fn.getOperation()) {
191 return WalkResult::advance();
192 }
193 if (isa<EmitEqualityOp, EmitContainmentOp, AssertOp>(op)) {
194 res = false;
195 return WalkResult::interrupt();
196 } else if (auto callOp = dyn_cast<CallOp>(op)) {
197 if (!callsPurposelessConstrainFunc(symbolTables, callOp, activeFunctions)) {
198 res = false;
199 return WalkResult::interrupt();
200 }
201 return WalkResult::advance();
202 } else if (isMemoryEffectFree(op)) {
203 return WalkResult::advance();
204 }
205
206 // Removing a call to a constrain function is only safe when the callee has
207 // no unknown or mutating effects.
209 res = false;
210 return WalkResult::interrupt();
211 }
212 return WalkResult::advance();
213 });
214 activeFunctions.erase(fn.getOperation());
215 return res;
216 }
217
219 bool callsPurposelessConstrainFunc(
220 SymbolTableCollection &symbolTables, CallOp call, DenseSet<Operation *> &activeFunctions
221 ) {
222 auto callLookup = resolveCallable<FuncDefOp>(symbolTables, call);
223 return succeeded(callLookup) &&
224 isPurposelessConstrainFunc(symbolTables, callLookup->get(), activeFunctions);
225 }
226
227 void runOnFunc(SymbolTableCollection &symbolTables, CallableOpInterface callable) {
228 TranslationMap map;
229 SmallVector<Operation *> redundantOps;
230 DenseSet<OperationComparator> uniqueOps;
231 DominanceInfo domInfo(callable);
232
233 auto unnecessaryOpCheck = [&](Operation *op) -> bool {
234 if (auto emiteq = dyn_cast<EmitEqualityOp>(op);
235 emiteq && emiteq.getLhs() == emiteq.getRhs()) {
236 redundantOps.push_back(op);
237 return true;
238 }
239
240 if (auto callOp = dyn_cast<CallOp>(op)) {
241 DenseSet<Operation *> activeFunctions;
242 if (callsPurposelessConstrainFunc(symbolTables, callOp, activeFunctions)) {
243 redundantOps.push_back(op);
244 return true;
245 }
246 }
247 return false;
248 };
249
250 callable.walk([&](Operation *op) {
251 if (op == callable.getOperation()) {
252 return WalkResult::advance();
253 }
254
255 // Case 1: The operation itself is unnecessary.
256 if (unnecessaryOpCheck(op)) {
257 return WalkResult::advance();
258 }
259
260 // Case 2: An equivalent operation A has already been performed before
261 // the current operation B and A dominates B.
262 if (isDuplicateEliminationCandidate(op)) {
263 OperationComparator comp(op, map);
264 if (auto it = uniqueOps.find(comp);
265 it != uniqueOps.end() && domInfo.dominates(it->getOp(), op)) {
266 redundantOps.push_back(op);
267 for (unsigned opNum = 0; opNum < op->getNumResults(); opNum++) {
268 map[op->getResult(opNum)] = it->getOp()->getResult(opNum);
269 }
270 } else {
271 uniqueOps.insert(comp);
272 }
273 }
274 return WalkResult::advance();
275 });
276
277 DenseSet<Operation *> redundantOpSet;
278 for (Operation *op : redundantOps) {
279 redundantOpSet.insert(op);
280 }
281
282 SmallVector<Operation *> deadOpCandidates;
283 DenseSet<Operation *> queuedDeadOps;
284 auto enqueueDeadOpCandidate = [&](Value value) {
285 Operation *definingOp = value.getDefiningOp();
286 if (!definingOp || redundantOpSet.count(definingOp) ||
287 !queuedDeadOps.insert(definingOp).second) {
288 return;
289 }
290 deadOpCandidates.push_back(definingOp);
291 };
292
293 for (Operation *op : redundantOps) {
294 LLVM_DEBUG(llvm::dbgs() << "Removing op: " << *op << '\n');
295 for (Value result : op->getResults()) {
296 if (!result.use_empty()) {
297 auto it = map.find(result);
298 ensure(
299 it != map.end(), "failed to find a replacement value for redundant operation result"
300 );
301 LLVM_DEBUG(llvm::dbgs() << "Replacing " << it->first << " with " << it->second << '\n');
302 result.replaceAllUsesWith(it->second);
303 }
304 }
305
306 SmallVector<Value> operands(op->getOperands());
307 op->erase();
308 for (Value operand : operands) {
309 enqueueDeadOpCandidate(operand);
310 }
311 }
312
313 // Removing a redundant op may make its producers dead. Check whole
314 // operations so effects and every result are considered before erasure.
315 while (!deadOpCandidates.empty()) {
316 Operation *op = deadOpCandidates.pop_back_val();
317 queuedDeadOps.erase(op);
318 if (!isOpTriviallyDead(op)) {
319 continue;
320 }
321
322 SmallVector<Value> operands(op->getOperands());
323 LLVM_DEBUG(llvm::dbgs() << "Removing dead producer: " << *op << '\n');
324 op->erase();
325 for (Value operand : operands) {
326 enqueueDeadOpCandidate(operand);
327 }
328 }
329 }
330};
331
332} // namespace
bool hasAllowWitnessAttr()
Return true iff the function def has the allow_witness attribute.
Definition Ops.h.inc:1009
bool isStructConstrain()
Return true iff the function is within a StructDefOp and named FUNC_NAME_CONSTRAIN.
Definition Ops.h.inc:1099
void ensure(bool condition, const llvm::Twine &errMsg)
std::unordered_map< SourceRef, SourceRefLatticeValue, SourceRef::Hash > TranslationMap
mlir::FailureOr< SymbolLookupResult< T > > resolveCallable(mlir::SymbolTableCollection &symbolTable, mlir::CallOpInterface call)
Based on mlir::CallOpInterface::resolveCallable, but using LLZK lookup helpers.
bool hasUnknownOrNonReadEffect(mlir::Operation *op)
Returns true when op may have an unknown effect or any effect other than memory read.
static unsigned getHashValue(const OperationComparator &oc)
static bool isEqual(const OperationComparator &lhs, const OperationComparator &rhs)