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
190 bool isPurposelessConstrainFunc(
191 SymbolTableCollection &symbolTables, FuncDefOp fn, DenseSet<Operation *> &activeFunctions
192 ) {
193 if (!fn.isStructConstrain()) {
194 return false;
195 }
196 // Calls to a constrain function are only removable when the callee cannot
197 // contain witness-generation state mutations such as global.write or
198 // ram.store. The WitnessGen verifier enforces that boundary unless the
199 // callee is explicitly marked allow_witness.
200 if (fn.hasAllowWitnessAttr()) {
201 return false;
202 }
203 if (!activeFunctions.insert(fn.getOperation()).second) {
204 // A recursive call path cannot be proven purposeless; retain the call
205 // conservatively.
206 return false;
207 }
208
209 bool res = true;
210 fn.walk([&](Operation *op) {
211 if (op == fn.getOperation()) {
212 return WalkResult::advance();
213 }
214 if (isa<EmitEqualityOp, EmitContainmentOp, AssertOp>(op)) {
215 res = false;
216 return WalkResult::interrupt();
217 } else if (auto callOp = dyn_cast<CallOp>(op)) {
218 if (!callsPurposelessConstrainFunc(symbolTables, callOp, activeFunctions)) {
219 res = false;
220 return WalkResult::interrupt();
221 }
222 return WalkResult::advance();
223 } else if (isMemoryEffectFree(op)) {
224 return WalkResult::advance();
225 }
226
227 // Removing a call to a constrain function is only safe when the callee has
228 // no unknown or mutating effects.
230 res = false;
231 return WalkResult::interrupt();
232 }
233 return WalkResult::advance();
234 });
235 activeFunctions.erase(fn.getOperation());
236 return res;
237 }
238
240 bool callsPurposelessConstrainFunc(
241 SymbolTableCollection &symbolTables, CallOp call, DenseSet<Operation *> &activeFunctions
242 ) {
243 auto callLookup = resolveCallable<FuncDefOp>(symbolTables, call);
244 return succeeded(callLookup) &&
245 isPurposelessConstrainFunc(symbolTables, callLookup->get(), activeFunctions);
246 }
247
248 void runOnFunc(SymbolTableCollection &symbolTables, CallableOpInterface callable) {
249 TranslationMap map;
250 SmallVector<Operation *> redundantOps;
251 DenseSet<OperationComparator> uniqueOps;
252 DominanceInfo domInfo(callable);
253
254 auto unnecessaryOpCheck = [&](Operation *op) -> bool {
255 if (auto emiteq = dyn_cast<EmitEqualityOp>(op);
256 emiteq && emiteq.getLhs() == emiteq.getRhs()) {
257 redundantOps.push_back(op);
258 return true;
259 }
260
261 if (auto callOp = dyn_cast<CallOp>(op)) {
262 DenseSet<Operation *> activeFunctions;
263 if (callsPurposelessConstrainFunc(symbolTables, callOp, activeFunctions)) {
264 redundantOps.push_back(op);
265 return true;
266 }
267 }
268 return false;
269 };
270
271 callable.walk([&](Operation *op) {
272 if (op == callable.getOperation()) {
273 return WalkResult::advance();
274 }
275
276 // Case 1: The operation itself is unnecessary.
277 if (unnecessaryOpCheck(op)) {
278 return WalkResult::advance();
279 }
280
281 // Case 2: An equivalent operation A has already been performed before
282 // the current operation B and A dominates B.
283 if (isDuplicateEliminationCandidate(op)) {
284 OperationComparator comp(op, map);
285 if (auto it = uniqueOps.find(comp);
286 it != uniqueOps.end() && domInfo.dominates(it->getOp(), op)) {
287 redundantOps.push_back(op);
288 for (unsigned opNum = 0; opNum < op->getNumResults(); opNum++) {
289 map[op->getResult(opNum)] = it->getOp()->getResult(opNum);
290 }
291 } else {
292 uniqueOps.insert(comp);
293 }
294 }
295 return WalkResult::advance();
296 });
297
298 DenseSet<Operation *> redundantOpSet;
299 for (Operation *op : redundantOps) {
300 redundantOpSet.insert(op);
301 }
302
303 SmallVector<Operation *> deadOpCandidates;
304 DenseSet<Operation *> queuedDeadOps;
305 auto enqueueDeadOpCandidate = [&](Value value) {
306 Operation *definingOp = value.getDefiningOp();
307 if (!definingOp || redundantOpSet.count(definingOp) ||
308 !queuedDeadOps.insert(definingOp).second) {
309 return;
310 }
311 deadOpCandidates.push_back(definingOp);
312 };
313
314 for (Operation *op : redundantOps) {
315 LLVM_DEBUG(llvm::dbgs() << "Removing op: " << *op << '\n');
316 for (Value result : op->getResults()) {
317 if (!result.use_empty()) {
318 auto it = map.find(result);
319 ensure(
320 it != map.end(), "failed to find a replacement value for redundant operation result"
321 );
322 LLVM_DEBUG(llvm::dbgs() << "Replacing " << it->first << " with " << it->second << '\n');
323 result.replaceAllUsesWith(it->second);
324 }
325 }
326
327 SmallVector<Value> operands(op->getOperands());
328 op->erase();
329 for (Value operand : operands) {
330 enqueueDeadOpCandidate(operand);
331 }
332 }
333
334 // Removing a redundant op may make its producers dead. Check whole
335 // operations so effects and every result are considered before erasure.
336 while (!deadOpCandidates.empty()) {
337 Operation *op = deadOpCandidates.pop_back_val();
338 queuedDeadOps.erase(op);
339 if (!isOpTriviallyDead(op)) {
340 continue;
341 }
342
343 SmallVector<Value> operands(op->getOperands());
344 LLVM_DEBUG(llvm::dbgs() << "Removing dead producer: " << *op << '\n');
345 op->erase();
346 for (Value operand : operands) {
347 enqueueDeadOpCandidate(operand);
348 }
349 }
350 }
351};
352
353} // 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:825
bool isStructConstrain()
Return true iff the function is within a StructDefOp and named FUNC_NAME_CONSTRAIN.
Definition Ops.h.inc:915
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)