LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKRedundantReadAndWriteEliminationPass.cpp
Go to the documentation of this file.
1//===-- LLZKRedundantReadAndWriteEliminationPass.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
21#include "llzk/Util/Concepts.h"
24
25#include <mlir/Dialect/SCF/IR/SCF.h>
26#include <mlir/IR/BuiltinOps.h>
27
28#include <llvm/ADT/DenseMap.h>
29#include <llvm/ADT/DenseMapInfo.h>
30#include <llvm/ADT/SmallVector.h>
31#include <llvm/Support/Debug.h>
32
33#include <deque>
34#include <memory>
35
36// Include the generated base pass class definitions.
37namespace llzk {
38#define GEN_PASS_DEF_REDUNDANTREADANDWRITEELIMINATIONPASS
40} // namespace llzk
41
42using namespace mlir;
43using namespace llzk;
44using namespace llzk::array;
45using namespace llzk::felt;
46using namespace llzk::function;
47using namespace llzk::component;
48
49#define DEBUG_TYPE "llzk-redundant-read-write-pass"
50
51namespace {
52
55class ReferenceID {
56public:
57 explicit ReferenceID(Value v) {
58 // reserved special pointer values for DenseMapInfo
59 if (v == llvm::DenseMapInfo<Value>::getEmptyKey() ||
60 v == llvm::DenseMapInfo<Value>::getTombstoneKey()) {
61 identifier = v;
62 } else if (auto constVal = dyn_cast_if_present<FeltConstantOp>(v.getDefiningOp())) {
63 identifier = constVal.getValue();
64 } else if (auto constIdxVal = dyn_cast_if_present<arith::ConstantIndexOp>(v.getDefiningOp())) {
65 identifier = llvm::cast<IntegerAttr>(constIdxVal.getValue()).getValue();
66 } else {
67 identifier = v;
68 }
69 }
70 explicit ReferenceID(Attribute attr) : identifier(attr) {}
71 explicit ReferenceID(const APInt &i) : identifier(i) {}
72 explicit ReferenceID(unsigned i) : identifier(APInt(64, i)) {}
73
74 bool isValue() const { return std::holds_alternative<Value>(identifier); }
75 bool isAttribute() const { return std::holds_alternative<Attribute>(identifier); }
76 bool isConst() const { return std::holds_alternative<APInt>(identifier); }
77
78 Value getValue() const {
79 ensure(isValue(), "does not hold Value");
80 return std::get<Value>(identifier);
81 }
82
83 Attribute getAttribute() const {
84 ensure(isAttribute(), "does not hold Attribute");
85 return std::get<Attribute>(identifier);
86 }
87
88 APInt getConst() const {
89 ensure(isConst(), "does not hold const");
90 return std::get<APInt>(identifier);
91 }
92
93 void print(raw_ostream &os) const {
94 if (const auto *v = std::get_if<Value>(&identifier)) {
95 if (auto opres = dyn_cast<OpResult>(*v)) {
96 os << '%' << opres.getResultNumber();
97 } else {
98 os << *v;
99 }
100 } else if (const auto *attr = std::get_if<Attribute>(&identifier)) {
101 os << *attr;
102 } else {
103 os << std::get<APInt>(identifier);
104 }
105 }
106
107 friend bool operator==(const ReferenceID &lhs, const ReferenceID &rhs) {
108 return lhs.identifier == rhs.identifier;
109 }
110
111 friend raw_ostream &operator<<(raw_ostream &os, const ReferenceID &id) {
112 id.print(os);
113 return os;
114 }
115
116private:
121 std::variant<Attribute, APInt, Value> identifier;
122};
123
124} // namespace
125
126namespace llvm {
127
129template <> struct DenseMapInfo<ReferenceID> {
130 static ReferenceID getEmptyKey() { return ReferenceID(DenseMapInfo<Value>::getEmptyKey()); }
131 static inline ReferenceID getTombstoneKey() {
132 return ReferenceID(DenseMapInfo<Value>::getTombstoneKey());
133 }
134 static unsigned getHashValue(const ReferenceID &r) {
135 if (r.isValue()) {
136 return hash_value(r.getValue());
137 } else if (r.isAttribute()) {
138 return hash_value(r.getAttribute());
139 }
140 return hash_value(r.getConst());
141 }
142 static bool isEqual(const ReferenceID &lhs, const ReferenceID &rhs) { return lhs == rhs; }
143};
144
145} // namespace llvm
146
147namespace {
148
168class ReferenceNode {
169public:
170 template <typename IdType> static std::shared_ptr<ReferenceNode> create(IdType id, Value v) {
171 ReferenceNode n(id, v);
172 // Need the move constructor version since constructor is private
173 return std::make_shared<ReferenceNode>(std::move(n));
174 }
175
178 std::shared_ptr<ReferenceNode> clone(bool withChildren = true) const {
179 ReferenceNode copy(identifier, storedValue);
180 copy.updateLastWrite(lastWrite);
181 if (withChildren) {
182 for (const auto &[id, child] : children) {
183 copy.children[id] = child->clone(withChildren);
184 }
185 }
186 return std::make_shared<ReferenceNode>(std::move(copy));
187 }
188
189 template <typename IdType>
190 std::shared_ptr<ReferenceNode>
191 createChild(IdType id, Value storedVal, const std::shared_ptr<ReferenceNode> &valTree = nullptr) {
192 std::shared_ptr<ReferenceNode> child = create(id, storedVal);
193 child->setCurrentValue(storedVal, valTree);
194 children[child->identifier] = child;
195 return child;
196 }
197
200 template <typename IdType> std::shared_ptr<ReferenceNode> getChild(IdType id) const {
201 auto it = children.find(ReferenceID(id));
202 if (it != children.end()) {
203 return it->second;
204 }
205 return nullptr;
206 }
207
211 template <typename IdType>
212 std::shared_ptr<ReferenceNode> getOrCreateChild(IdType id, Value storedVal = nullptr) {
213 auto it = children.find(ReferenceID(id));
214 if (it != children.end()) {
215 return it->second;
216 }
217 return createChild(id, storedVal);
218 }
219
222 Operation *updateLastWrite(Operation *writeOp) {
223 Operation *old = lastWrite;
224 lastWrite = writeOp;
225 return old;
226 }
227
228 void clearLastWrite() { lastWrite = nullptr; }
229
230 void setCurrentValue(Value v, const std::shared_ptr<ReferenceNode> &valTree = nullptr) {
231 storedValue = v;
232 if (valTree != nullptr) {
233 // Overwrite our current set of children with new children, since we overwrote
234 // the stored value.
235 children = valTree->children;
236 }
237 }
238
239 void invalidateChildren() { children.clear(); }
240
241 bool invalidateNonIntegerOffsetChildren() {
242 SmallVector<ReferenceID> invalidChildren;
243 for (const auto &[id, _] : children) {
244 if (!id.isAttribute() || !isa<IntegerAttr>(id.getAttribute())) {
245 invalidChildren.push_back(id);
246 }
247 }
248 for (const ReferenceID &id : invalidChildren) {
249 children.erase(id);
250 }
251 return !invalidChildren.empty();
252 }
253
254 bool isLeaf() const { return children.empty(); }
255
256 Value getStoredValue() const { return storedValue; }
257
258 bool hasStoredValue() const { return storedValue != nullptr; }
259
260 void print(raw_ostream &os, int indent = 0) const {
261 os.indent(indent) << '[' << identifier;
262 if (storedValue != nullptr) {
263 os << " => " << storedValue;
264 }
265 os << ']';
266 if (!children.empty()) {
267 os << "{\n";
268 for (const auto &[_, child] : children) {
269 child->print(os, indent + 4);
270 os << '\n';
271 }
272 os.indent(indent) << '}';
273 }
274 }
275
276 [[maybe_unused]]
277 friend raw_ostream &operator<<(raw_ostream &os, const ReferenceNode &r) {
278 r.print(os);
279 return os;
280 }
281
283 friend bool
284 topLevelEq(const std::shared_ptr<ReferenceNode> &lhs, const std::shared_ptr<ReferenceNode> &rhs) {
285 return lhs->identifier == rhs->identifier && lhs->storedValue == rhs->storedValue &&
286 lhs->lastWrite == rhs->lastWrite;
287 }
288
289 friend std::shared_ptr<ReferenceNode> greatestCommonSubtree(
290 const std::shared_ptr<ReferenceNode> &lhs, const std::shared_ptr<ReferenceNode> &rhs
291 ) {
292 if (!topLevelEq(lhs, rhs)) {
293 return nullptr;
294 }
295 auto res = lhs->clone(false); // childless clone
296 // Find common children and recurse
297 for (auto &[id, lhsChild] : lhs->children) {
298 if (auto it = rhs->children.find(id); it != rhs->children.end()) {
299 auto &rhsChild = it->second;
300 if (auto gcs = greatestCommonSubtree(lhsChild, rhsChild)) {
301 res->children[id] = gcs;
302 }
303 }
304 }
305 return res;
306 }
307
308private:
309 ReferenceID identifier;
310 mlir::Value storedValue;
311 Operation *lastWrite;
312 DenseMap<ReferenceID, std::shared_ptr<ReferenceNode>> children;
313
314 template <typename IdType>
315 ReferenceNode(IdType id, Value initialVal)
316 : identifier(std::move(id)), storedValue(initialVal), lastWrite(nullptr), children() {}
317};
318
319using ValueMap = DenseMap<mlir::Value, std::shared_ptr<ReferenceNode>>;
320
323struct KnownState {
324 ValueMap values;
325 DenseMap<SymbolRefAttr, Value> globals;
326 DenseMap<ReferenceID, Value> ram;
327 // Unlike `ram`, only exact translated address values justify store removal.
328 DenseMap<Value, Value> ramExact;
329};
330
333struct BlockWriteCandidates {
334 DenseMap<SymbolRefAttr, Operation *> globals;
335 DenseMap<Value, Operation *> ram;
336
337 void clear() {
338 globals.clear();
339 ram.clear();
340 }
341};
342
344ValueMap intersectValueMap(const ValueMap &lhs, const ValueMap &rhs) {
345 ValueMap res;
346 for (const auto &[id, lhsValTree] : lhs) {
347 if (auto it = rhs.find(id); it != rhs.end()) {
348 const auto &rhsValTree = it->second;
349 res[id] = greatestCommonSubtree(lhsValTree, rhsValTree);
350 }
351 }
352 return res;
353}
354
356template <typename KeyT>
357DenseMap<KeyT, Value>
358intersectValueLookup(const DenseMap<KeyT, Value> &lhs, const DenseMap<KeyT, Value> &rhs) {
359 DenseMap<KeyT, Value> res;
360 for (const auto &[id, lhsVal] : lhs) {
361 if (auto it = rhs.find(id); it != rhs.end() && it->second == lhsVal) {
362 res[id] = lhsVal;
363 }
364 }
365 return res;
366}
367
369KnownState intersect(const KnownState &lhs, const KnownState &rhs) {
370 return {
371 intersectValueMap(lhs.values, rhs.values), intersectValueLookup(lhs.globals, rhs.globals),
372 intersectValueLookup(lhs.ram, rhs.ram), intersectValueLookup(lhs.ramExact, rhs.ramExact)
373 };
374}
375
378ValueMap cloneValueMap(const ValueMap &orig) {
379 ValueMap res;
380 for (const auto &[id, tree] : orig) {
381 res[id] = tree->clone();
382 }
383 return res;
384}
385
388KnownState cloneKnownState(const KnownState &orig) {
389 return {cloneValueMap(orig.values), orig.globals, orig.ram, orig.ramExact};
390}
391
392class PassImpl : public llzk::impl::RedundantReadAndWriteEliminationPassBase<PassImpl> {
393 using Base = RedundantReadAndWriteEliminationPassBase<PassImpl>;
394 using Base::Base;
395
401 void runOnOperation() override {
402 getOperation().walk([&](FuncDefOp fn) { runOnFunc(fn); });
403 }
404
407 void runOnFunc(FuncDefOp fn) {
408 // Nothing to do for body-less functions.
409 if (fn.getCallableRegion() == nullptr) {
410 return;
411 }
412
413 LLVM_DEBUG(llvm::dbgs() << "Running on " << fn.getName() << '\n');
414
415 // Maps redundant value -> necessary value.
416 DenseMap<Value, Value> replacementMap;
417 // All values created by a new_* operation or from a read*/extract* operation.
418 SmallVector<Value> readVals;
419 // All writes that are either (1) overwritten by subsequent writes or (2)
420 // write a value that is already written.
421 SmallVector<Operation *> redundantWrites;
422
423 KnownState initState;
424 // Initialize the state to the function arguments.
425 for (auto arg : fn.getArguments()) {
426 initState.values[arg] = ReferenceNode::create(arg, arg);
427 }
428 // Functions only have a single region
429 (void)runOnRegion(
430 *fn.getCallableRegion(), std::move(initState), replacementMap, readVals, redundantWrites
431 );
432
433 // Now that we have accumulated all necessary state, we perform the optimizations:
434 // - Replace all redundant values.
435 for (auto &[orig, replace] : replacementMap) {
436 LLVM_DEBUG(llvm::dbgs() << "replacing " << orig << " with " << replace << '\n');
437 orig.replaceAllUsesWith(replace);
438 // We save the deletion to the readVals loop to prevent double-free.
439 }
440 // -Remove redundant writes now that it is safe to do so.
441 for (auto *writeOp : redundantWrites) {
442 LLVM_DEBUG(llvm::dbgs() << "erase write: " << *writeOp << '\n');
443 writeOp->erase();
444 }
445 // - Now we do a pass over read values to see if any are now unused.
446 // We do this in reverse order to free up early reads if their users would
447 // be removed.
448 for (auto it = readVals.rbegin(); it != readVals.rend(); it++) {
449 Value readVal = *it;
450 if (readVal.use_empty()) {
451 LLVM_DEBUG(llvm::dbgs() << "erase read: " << readVal << '\n');
452 readVal.getDefiningOp()->erase();
453 }
454 }
455 }
456
457 KnownState runOnRegion(
458 Region &r, KnownState &&initState, DenseMap<Value, Value> &replacementMap,
459 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites
460 ) {
461 // maps block -> state at the end of the block
462 DenseMap<Block *, KnownState> endStates;
463 // The first block has no predecessors, so nullptr contains the init state
464 endStates[nullptr] = initState;
465 auto getBlockState = [&endStates](Block *blockPtr) {
466 auto it = endStates.find(blockPtr);
467 ensure(it != endStates.end(), "unknown end state means we have an unsupported backedge");
468 return cloneKnownState(it->second);
469 };
470 auto hasBlockState = [&endStates](Block *blockPtr) {
471 return endStates.find(blockPtr) != endStates.end();
472 };
473 std::deque<Block *> frontier;
474 DenseSet<Block *> queued;
475 DenseSet<Block *> processed;
476 auto enqueue = [&](Block *blockPtr) {
477 if (processed.find(blockPtr) == processed.end() && queued.insert(blockPtr).second) {
478 frontier.push_back(blockPtr);
479 }
480 };
481 enqueue(&r.front());
482
483 SmallVector<KnownState> terminalStates;
484 size_t deferralsWithoutProgress = 0;
485
486 while (!frontier.empty()) {
487 Block *currentBlock = frontier.front();
488 frontier.pop_front();
489 queued.erase(currentBlock);
490
491 // get predecessors
492 KnownState currentState;
493 auto it = currentBlock->pred_begin();
494 auto itEnd = currentBlock->pred_end();
495 if (it == itEnd) {
496 // get the state for the entry block.
497 currentState = getBlockState(nullptr);
498 } else {
499 bool ready = true;
500 for (auto predIt = it; predIt != itEnd; predIt++) {
501 ready &= hasBlockState(*predIt);
502 }
503 if (!ready) {
504 deferralsWithoutProgress++;
505 ensure(
506 deferralsWithoutProgress <= frontier.size(),
507 "unknown end state means we have an unsupported backedge"
508 );
509 enqueue(currentBlock);
510 continue;
511 }
512
513 currentState = getBlockState(*it);
514 // If we have multiple predecessors, we take a pessimistic view and
515 // set the state as only the intersection of all predecessor states
516 // (e.g., only the common state from an if branch).
517 for (it++; it != itEnd; it++) {
518 currentState = intersect(currentState, getBlockState(*it));
519 }
520 }
521
522 // Run this block, consuming currentState and producing the endState
523 deferralsWithoutProgress = 0;
524 auto endState = runOnBlock(
525 *currentBlock, std::move(currentState), replacementMap, readVals, redundantWrites
526 );
527
528 // Update the end states.
529 // Since we only support the scf dialect, we should never have any
530 // backedges, so we should never already have state for this block.
531 ensure(processed.find(currentBlock) == processed.end(), "backedge");
532 endStates[currentBlock] = std::move(endState);
533 processed.insert(currentBlock);
534
535 // add successors to frontier
536 if (currentBlock->hasNoSuccessors()) {
537 terminalStates.push_back(cloneKnownState(endStates[currentBlock]));
538 } else {
539 for (Block *succ : currentBlock->getSuccessors()) {
540 enqueue(succ);
541 }
542 }
543 }
544
545 // The final state is the intersection of all possible terminal states.
546 ensure(!terminalStates.empty(), "computed no states");
547 auto finalState = terminalStates.front();
548 for (const auto *it = terminalStates.begin() + 1; it != terminalStates.end(); it++) {
549 finalState = intersect(finalState, *it);
550 }
551 return finalState;
552 }
553
554 KnownState runOnBlock(
555 Block &b, KnownState &&state, DenseMap<Value, Value> &replacementMap,
556 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites
557 ) {
558 BlockWriteCandidates writeCandidates;
559 for (Operation &op : b) {
560 // Some operations have regions (e.g., scf.if). These regions must be
561 // traversed and the resulting state(s) are intersected for the final
562 // state of this operation.
563 if (!op.getRegions().empty()) {
564 KnownState parentState = cloneKnownState(state);
565 // Repeating regions (scf.for, scf.while) execute their body
566 // more than once. Pre-loop global/RAM facts must not be used
567 // to declare a read inside the body redundant — the body may
568 // observe writes from a previous iteration.
569 KnownState regionEntryState = cloneKnownState(state);
570 if (isa<scf::ForOp, scf::WhileOp>(op)) {
571 regionEntryState.globals.clear();
572 regionEntryState.ram.clear();
573 regionEntryState.ramExact.clear();
574 }
575 SmallVector<KnownState> regionStates;
576 for (Region &region : op.getRegions()) {
577 if (region.empty()) {
578 continue;
579 }
580 auto regionState = runOnRegion(
581 region, cloneKnownState(regionEntryState), replacementMap, readVals, redundantWrites
582 );
583 regionStates.push_back(regionState);
584 }
585 if (regionStates.empty()) {
586 // Region-bearing ops with no bodies still need their own effects handled.
587 runOperation(&op, state, replacementMap, readVals, redundantWrites, writeCandidates);
588 writeCandidates.clear();
589 continue;
590 }
591
592 KnownState finalState = regionStates.front();
593 for (const auto *it = regionStates.begin() + 1; it != regionStates.end(); it++) {
594 finalState = intersect(finalState, *it);
595 }
596 // A nested region may be conditional, zero-iteration, or otherwise not
597 // execute exactly once. Keep prior struct/array behavior, but only
598 // propagate global/RAM facts that remain true both before and after the
599 // region traversal.
600 finalState.globals = intersectValueLookup(parentState.globals, finalState.globals);
601 finalState.ram = intersectValueLookup(parentState.ram, finalState.ram);
602 finalState.ramExact = intersectValueLookup(parentState.ramExact, finalState.ramExact);
603 state = std::move(finalState);
604 writeCandidates.clear();
605 continue;
606 }
607 runOperation(&op, state, replacementMap, readVals, redundantWrites, writeCandidates);
609 return std::move(state);
610 }
616 /// @param replacementMap A mutable map of original -> replacement values
617 /// @param readVals A mutable list of all read values
618 /// @param redundantWrites A mutable list of all writes that are considered redundant
619 void runOperation(
620 Operation *op, KnownState &state, DenseMap<Value, Value> &replacementMap,
621 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites,
622 BlockWriteCandidates &writeCandidates
623 ) {
624 // Uses the replacement map to look up values to simplify later replacement.
625 // This avoids having a daisy chain of "replace B with A", "replace C with B",
626 // etc.
627 auto translate = [&replacementMap](Value v) {
628 if (auto it = replacementMap.find(v); it != replacementMap.end()) {
629 return it->second;
630 }
631 return v;
632 };
633
634 // Lookup the value tree in the current state or return nullptr.
635 auto tryGetValTree = [&state](Value v) -> std::shared_ptr<ReferenceNode> {
636 if (auto it = state.values.find(v); it != state.values.end()) {
637 return it->second;
638 }
639 return nullptr;
640 };
641
642 auto doStatefulRead =
643 [&]<typename KeyT>(Value resVal, DenseMap<KeyT, Value> &knownValues, const KeyT &key) {
644 if (auto it = knownValues.find(key); it != knownValues.end()) {
645 replacementMap[resVal] = it->second;
646 readVals.push_back(resVal);
647 return true;
648 } else {
649 knownValues[key] = resVal;
650 state.values[resVal] = ReferenceNode::create(resVal, resVal);
651 }
652 readVals.push_back(resVal);
653 return false;
654 };
655
656 // An omitted table offset denotes the current row.
657 const IntegerAttr zeroTableOffset = IntegerAttr::get(IndexType::get(op->getContext()), 0);
658 auto getMemberNode = [&](Value component, FlatSymbolRefAttr member) {
659 std::shared_ptr<ReferenceNode> componentNode = tryGetValTree(translate(component));
660 if (componentNode == nullptr) {
661 return std::shared_ptr<ReferenceNode>();
663 return componentNode->getOrCreateChild(member);
664 };
665 auto getMemberAccessNode = [&](MemberReadOp readm) {
666 std::shared_ptr<ReferenceNode> access =
667 getMemberNode(readm.getComponent(), readm.getMemberNameAttr());
668 if (access == nullptr) {
669 return access;
670 }
671 access = access->getOrCreateChild(readm.getTableOffset().value_or(zeroTableOffset));
672 if (!readm.getMapOperands().empty()) {
673 access = access->getOrCreateChild(readm.getMapOpGroupSizesAttr());
674 access = access->getOrCreateChild(readm.getNumDimsPerMapAttr());
675 }
676 for (auto mapOperands : readm.getMapOperands()) {
677 for (Value operand : mapOperands) {
678 access = access->getOrCreateChild(translate(operand));
679 }
680 }
681 return access;
682 };
683
684 // Read a value from an array. This works on both readarr operations (which
685 // return a scalar value) and extractarr operations (which return a subarray).
686 auto doArrayReadLike = [&]<HasInterface<ArrayAccessOpInterface> OpClass>(OpClass readarr) {
687 Value resVal = readarr.getResult();
688 std::shared_ptr<ReferenceNode> currValTree = tryGetValTree(translate(readarr.getArrRef()));
689 if (currValTree == nullptr) {
690 state.values[resVal] = ReferenceNode::create(resVal, resVal);
691 readVals.push_back(resVal);
692 return;
693 }
694
695 for (Value origIdx : readarr.getIndices()) {
696 Value idxVal = translate(origIdx);
697 currValTree = currValTree->getOrCreateChild(idxVal);
698 }
699
700 if (!currValTree->hasStoredValue()) {
701 currValTree->setCurrentValue(resVal);
702 }
703
704 if (currValTree->getStoredValue() != resVal) {
705 LLVM_DEBUG(
706 llvm::dbgs() << readarr.getOperationName() << ": replace " << resVal << " with "
707 << currValTree->getStoredValue() << '\n'
708 );
709 replacementMap[resVal] = currValTree->getStoredValue();
710 } else {
711 state.values[resVal] = currValTree;
712 LLVM_DEBUG(
713 llvm::dbgs() << readarr.getOperationName() << ": " << resVal << " => " << *currValTree
714 << '\n'
715 );
716 }
717
718 readVals.push_back(resVal);
719 };
720
721 // Write a scalar value (for writearr) or a subarray value (for insertarr)
722 // to an array. The unique part of this operation relative to others is that
723 // we may receive a variable index (i.e., not a constant). In this case, we
724 // invalidate adjacent subtree state because the variable index may alias
725 // another element.
726 auto doArrayWriteLike = [&]<HasInterface<ArrayAccessOpInterface> OpClass>(OpClass writearr) {
727 std::shared_ptr<ReferenceNode> currValTree = tryGetValTree(translate(writearr.getArrRef()));
728 if (currValTree == nullptr) {
729 return;
730 }
731 Value newVal = translate(writearr.getRvalue());
732 std::shared_ptr<ReferenceNode> valTree = tryGetValTree(newVal);
733
734 for (Value origIdx : writearr.getIndices()) {
735 Value idxVal = translate(origIdx);
736 // This write will invalidate all children, since it may reference
737 // any number of them.
738 if (ReferenceID(idxVal).isValue()) {
739 LLVM_DEBUG(llvm::dbgs() << writearr.getOperationName() << ": invalidate alias\n");
740 currValTree->invalidateChildren();
741 }
742 currValTree = currValTree->getOrCreateChild(idxVal);
743 }
744
745 if (currValTree->getStoredValue() == newVal) {
746 LLVM_DEBUG(
747 llvm::dbgs() << writearr.getOperationName() << ": subsequent " << writearr
748 << " is redundant\n"
749 );
750 redundantWrites.push_back(writearr);
751 } else {
752 if (Operation *lastWrite = currValTree->updateLastWrite(writearr)) {
753 LLVM_DEBUG(
754 llvm::dbgs() << writearr.getOperationName() << "writearr: replacing " << lastWrite
755 << " with prior write " << *lastWrite << '\n'
756 );
757 redundantWrites.push_back(lastWrite);
758 }
759 currValTree->setCurrentValue(newVal, valTree);
760 }
761 };
762
763 // global ops
764 if (auto readGlobal = dyn_cast<global::GlobalReadOp>(op)) {
765 const auto name = readGlobal.getNameRef();
766 if (!doStatefulRead(readGlobal.getVal(), state.globals, name)) {
767 writeCandidates.globals.erase(name);
768 }
769 } else if (auto writeGlobal = dyn_cast<global::GlobalWriteOp>(op)) {
770 const auto name = writeGlobal.getNameRef();
771 Value value = translate(writeGlobal.getVal());
772 if (auto known = state.globals.find(name);
773 known != state.globals.end() && known->second == value) {
774 redundantWrites.push_back(writeGlobal.getOperation());
775 } else {
776 if (auto previous = writeCandidates.globals.find(name);
777 previous != writeCandidates.globals.end()) {
778 redundantWrites.push_back(previous->second);
779 }
780 state.globals[name] = value;
781 writeCandidates.globals[name] = writeGlobal.getOperation();
782 }
783 }
784 // RAM ops
785 else if (auto load = dyn_cast<ram::LoadOp>(op)) {
786 Value address = translate(load.getAddr());
787 if (!doStatefulRead(load.getVal(), state.ram, ReferenceID(address))) {
788 writeCandidates.ram.clear();
789 }
790 state.ramExact[address] = translate(load.getVal());
791 } else if (auto store = dyn_cast<ram::StoreOp>(op)) {
792 Value address = translate(store.getAddr());
793 Value value = translate(store.getVal());
794 if (auto known = state.ramExact.find(address);
795 known != state.ramExact.end() && known->second == value) {
796 redundantWrites.push_back(store.getOperation());
797 } else {
798 if (auto previous = writeCandidates.ram.find(address);
799 previous != writeCandidates.ram.end()) {
800 redundantWrites.push_back(previous->second);
801 }
802 writeCandidates.ram[address] = store.getOperation();
803 state.ram.clear();
804 state.ramExact.clear();
805 state.ram[ReferenceID(address)] = value;
806 state.ramExact[address] = value;
807 }
808 }
809 // struct ops
810 else if (auto newStruct = dyn_cast<CreateStructOp>(op)) {
811 // For new values, the "stored value" of the reference is the creation site.
812 auto structVal = ReferenceNode::create(newStruct, newStruct);
813 state.values[newStruct] = structVal;
814 LLVM_DEBUG(
815 llvm::dbgs() << newStruct.getOperationName() << ": " << *state.values[newStruct] << '\n'
816 );
817 // adding this to readVals
818 readVals.push_back(newStruct);
819 } else if (auto readm = dyn_cast<MemberReadOp>(op)) {
820 std::shared_ptr<ReferenceNode> access = getMemberAccessNode(readm);
821 Value resVal = readm.getVal();
822 if (access == nullptr) {
823 state.values[resVal] = ReferenceNode::create(resVal, resVal);
824 readVals.push_back(resVal);
825 return;
826 }
827 if (!access->hasStoredValue()) {
828 access->setCurrentValue(resVal);
829 }
830 if (access->getStoredValue() != resVal) {
831 LLVM_DEBUG(
832 llvm::dbgs() << readm.getOperationName() << ": adding replacement map entry { "
833 << resVal << " => " << access->getStoredValue() << " }\n"
834 );
835 replacementMap[resVal] = access->getStoredValue();
836 } else {
837 state.values[resVal] = access;
838 LLVM_DEBUG(llvm::dbgs() << readm.getOperationName() << ": " << *access << '\n');
839 }
840 readVals.push_back(resVal);
841 } else if (auto writem = dyn_cast<MemberWriteOp>(op)) {
842 std::shared_ptr<ReferenceNode> member =
843 getMemberNode(writem.getComponent(), writem.getMemberNameAttr());
844 if (member == nullptr) {
845 return;
846 }
847 // Symbolic and affine offsets may resolve to the current row. Constant
848 // nonzero offsets stay distinct from a current-row member write.
849 bool invalidatedMayAliasRead = member->invalidateNonIntegerOffsetChildren();
850 Value writeVal = translate(writem.getVal());
851 auto valTree = tryGetValTree(writeVal);
852
853 auto access = member->getOrCreateChild(zeroTableOffset);
854 if (invalidatedMayAliasRead) {
855 access->clearLastWrite();
856 }
857 if (access->getStoredValue() == writeVal) {
858 LLVM_DEBUG(
859 llvm::dbgs() << writem.getOperationName() << ": recording redundant write " << writem
860 << '\n'
861 );
862 redundantWrites.push_back(writem);
863 } else {
864 if (auto *lastWrite = access->updateLastWrite(writem)) {
865 LLVM_DEBUG(
866 llvm::dbgs() << writem.getOperationName() << ": recording overwritten write "
867 << *lastWrite << '\n'
868 );
869 redundantWrites.push_back(lastWrite);
870 }
871 access->setCurrentValue(writeVal, valTree);
872 LLVM_DEBUG(
873 llvm::dbgs() << writem.getOperationName() << ": " << *access << " set to " << writeVal
874 << '\n'
875 );
876 }
877 }
878 // array ops
879 else if (auto newArray = dyn_cast<CreateArrayOp>(op)) {
880 auto arrayVal = ReferenceNode::create(newArray, newArray);
881 state.values[newArray] = arrayVal;
882
883 // If we're given a constructor, we can instantiate elements using
884 // constant indices.
885 unsigned idx = 0;
886 for (auto elem : newArray.getElements()) {
887 Value elemVal = translate(elem);
888 auto valTree = tryGetValTree(elemVal);
889 auto elemChild = arrayVal->createChild(idx, elemVal, valTree);
890 LLVM_DEBUG(
891 llvm::dbgs() << newArray.getOperationName() << ": element " << idx << " initialized to "
892 << *elemChild << '\n'
893 );
894 idx++;
895 }
896
897 readVals.push_back(newArray);
898 } else if (auto readarr = dyn_cast<ReadArrayOp>(op)) {
899 doArrayReadLike(readarr);
900 } else if (auto writearr = dyn_cast<WriteArrayOp>(op)) {
901 doArrayWriteLike(writearr);
902 } else if (auto extractarr = dyn_cast<ExtractArrayOp>(op)) {
903 // Logic is essentially the same as readarr
904 doArrayReadLike(extractarr);
905 } else if (auto insertarr = dyn_cast<InsertArrayOp>(op)) {
906 // Logic is essentially the same as writearr
907 doArrayWriteLike(insertarr);
908 } else if (hasUnknownOrNonReadEffect(op)) {
909 state.globals.clear();
910 state.ram.clear();
911 state.ramExact.clear();
912 writeCandidates.clear();
913 } else if (hasReadEffect(op)) {
914 // A read does not invalidate known values, but it can observe a pending
915 // write and therefore prevents removing that write as overwritten.
916 writeCandidates.clear();
917 }
918 }
919};
920
921} // namespace
void print(llvm::raw_ostream &os) const
::mlir::Region * getCallableRegion()
Required by FunctionOpInterface.
Definition Ops.h.inc:866
void ensure(bool condition, const llvm::Twine &errMsg)
Interval operator<<(const Interval &lhs, const Interval &rhs)
bool hasReadEffect(mlir::Operation *op)
Returns true when op has a memory read effect.
bool hasUnknownOrNonReadEffect(mlir::Operation *op)
Returns true when op may have an unknown effect or any effect other than memory read.
mlir::Operation * create(MlirOpBuilder cBuilder, MlirLocation cLocation, Args &&...args)
Creates a new operation using an ODS build method.
Definition Builder.h:41
static bool isEqual(const ReferenceID &lhs, const ReferenceID &rhs)