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