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