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
25#include "llzk/Util/Concepts.h"
28
29#include <mlir/Dialect/SCF/IR/SCF.h>
30#include <mlir/IR/BuiltinOps.h>
31
32#include <llvm/ADT/ArrayRef.h>
33#include <llvm/ADT/DenseMap.h>
34#include <llvm/ADT/DenseMapInfo.h>
35#include <llvm/ADT/DenseSet.h>
36#include <llvm/ADT/SmallVector.h>
37#include <llvm/Support/Debug.h>
38
39#include <deque>
40#include <memory>
41#include <optional>
42
43// Include the generated base pass class definitions.
44namespace llzk {
45#define GEN_PASS_DEF_REDUNDANTREADANDWRITEELIMINATIONPASS
47} // namespace llzk
48
49using namespace mlir;
50using namespace llzk;
51using namespace llzk::array;
52using namespace llzk::felt;
53using namespace llzk::function;
54using namespace llzk::component;
55
56#define DEBUG_TYPE "llzk-redundant-read-write-pass"
57
58namespace {
59
62class ReferenceID {
63public:
64 explicit ReferenceID(Value v) {
65 if (auto constVal = dyn_cast_if_present<FeltConstantOp>(v.getDefiningOp())) {
66 identifier = constVal.getValue();
67 } else if (auto constIdxVal = dyn_cast_if_present<arith::ConstantIndexOp>(v.getDefiningOp())) {
68 identifier = llvm::DynamicAPInt(constIdxVal.value());
69 } else {
70 identifier = v;
71 }
72 }
73 explicit ReferenceID(Attribute attr) : identifier(attr) {}
74 explicit ReferenceID(const llvm::DynamicAPInt &i) : identifier(i) {}
75 explicit ReferenceID(unsigned i) : identifier(llvm::DynamicAPInt(i)) {}
76
77 bool isValue() const { return std::holds_alternative<Value>(identifier); }
78 bool isAttribute() const { return std::holds_alternative<Attribute>(identifier); }
79 bool isConst() const { return std::holds_alternative<llvm::DynamicAPInt>(identifier); }
80
81 Value getValue() const {
82 ensure(isValue(), "does not hold Value");
83 return std::get<Value>(identifier);
84 }
85
86 Attribute getAttribute() const {
87 ensure(isAttribute(), "does not hold Attribute");
88 return std::get<Attribute>(identifier);
89 }
90
91 llvm::DynamicAPInt getConst() const {
92 ensure(isConst(), "does not hold const");
93 return std::get<llvm::DynamicAPInt>(identifier);
94 }
95
96 void print(raw_ostream &os) const {
97 if (const auto *v = std::get_if<Value>(&identifier)) {
98 if (auto opres = dyn_cast<OpResult>(*v)) {
99 os << '%' << opres.getResultNumber();
100 } else {
101 os << *v;
102 }
103 } else if (const auto *attr = std::get_if<Attribute>(&identifier)) {
104 os << *attr;
105 } else {
106 os << std::get<llvm::DynamicAPInt>(identifier);
107 }
108 }
109
110 friend bool operator==(const ReferenceID &lhs, const ReferenceID &rhs) {
111 return lhs.identifier == rhs.identifier;
112 }
113
114 friend raw_ostream &operator<<(raw_ostream &os, const ReferenceID &id) {
115 id.print(os);
116 return os;
117 }
118
119private:
124 std::variant<Attribute, llvm::DynamicAPInt, Value> identifier;
125};
126
127} // namespace
128
129namespace llvm {
130
132template <> struct DenseMapInfo<ReferenceID> {
133 static unsigned getHashValue(const ReferenceID &r) {
134 if (r.isValue()) {
135 return hash_value(r.getValue());
136 } else if (r.isAttribute()) {
137 return hash_value(r.getAttribute());
138 }
139 return llzk::hashDynamicAPInt(r.getConst());
140 }
141 static bool isEqual(const ReferenceID &lhs, const ReferenceID &rhs) { return lhs == rhs; }
142};
143
144} // namespace llvm
145
146namespace {
147
164class ReferenceNode {
165public:
166 struct AggregateSnapshotSource {
167 Value value;
168 Operation *write;
169 };
170
172 struct AggregateReadOrigin {
173 std::shared_ptr<ReferenceNode> storage;
174 uint64_t mutationEpoch;
175 Operation *read;
176 };
177
178 template <typename IdType> static std::shared_ptr<ReferenceNode> create(IdType id, Value v) {
179 ReferenceNode n(id, v);
180 // Need the move constructor version since constructor is private
181 return std::make_shared<ReferenceNode>(std::move(n));
182 }
183
186 std::shared_ptr<ReferenceNode> clone(bool withChildren = true) const {
187 ReferenceNode copy(identifier, storedValue);
188 copy.updateLastWrite(lastWrite);
189 copy.aggregateMutationEpoch = aggregateMutationEpoch;
190 if (withChildren) {
191 copy.dynamicChildCount = dynamicChildCount;
192 for (const auto &[id, child] : children) {
193 copy.children[id] = child->clone(withChildren);
194 }
195 }
196 return std::make_shared<ReferenceNode>(std::move(copy));
197 }
198
204 std::shared_ptr<ReferenceNode> cloneForValueCopy(Value copiedValue) const {
205 auto copy = clone();
206 copy->identifier = ReferenceID(copiedValue);
207 copy->storedValue = copiedValue;
208 copy->clearLastWritesInSubtree();
209 return copy;
210 }
211
212 template <typename IdType>
213 std::shared_ptr<ReferenceNode>
214 createChild(IdType id, Value storedVal, const std::shared_ptr<ReferenceNode> &valTree = nullptr) {
215 std::shared_ptr<ReferenceNode> child = create(id, storedVal);
216 child->setCurrentValue(storedVal, valTree);
217 if (child->identifier.isValue() && children.find(child->identifier) == children.end()) {
218 ++dynamicChildCount;
219 }
220 children[child->identifier] = child;
221 return child;
222 }
223
226 template <typename IdType> std::shared_ptr<ReferenceNode> getChild(IdType id) const {
227 auto it = children.find(ReferenceID(id));
228 if (it != children.end()) {
229 return it->second;
230 }
231 return nullptr;
232 }
233
237 template <typename IdType>
238 std::shared_ptr<ReferenceNode> getOrCreateChild(IdType id, Value storedVal = nullptr) {
239 auto it = children.find(ReferenceID(id));
240 if (it != children.end()) {
241 return it->second;
242 }
243 return createChild(id, storedVal);
244 }
245
248 Operation *updateLastWrite(Operation *writeOp) {
249 Operation *old = lastWrite;
250 lastWrite = writeOp;
251 return old;
252 }
253
254 void clearLastWrite() { lastWrite = nullptr; }
255
263 void clearLastWritesObservedBy(ArrayRef<ReferenceID> indices) {
264 if (indices.empty()) {
265 clearLastWritesInSubtree();
266 return;
267 }
268 clearLastWrite();
269
270 const ReferenceID &index = indices.front();
271 ArrayRef<ReferenceID> remaining = indices.drop_front();
272 if (!index.isConst()) {
273 for (const auto &[id, child] : children) {
274 if (!id.isAttribute()) {
275 child->clearLastWritesObservedBy(remaining);
276 }
277 }
278 return;
279 }
280
281 if (auto it = children.find(index); it != children.end()) {
282 it->second->clearLastWritesObservedBy(remaining);
283 }
284 for (const auto &[id, child] : children) {
285 if (id.isValue()) {
286 child->clearLastWritesObservedBy(remaining);
287 }
288 }
289 }
290
295 void clearLastWritesInSubtree() {
296 for (const auto &[_, child] : children) {
297 child->clearLastWritesInSubtree();
298 }
299 clearLastWrite();
300 }
301
302 void setCurrentValue(Value v, const std::shared_ptr<ReferenceNode> &valTree = nullptr) {
303 storedValue = v;
304 reusableAggregateRead.reset();
305 aggregateSnapshotSource.reset();
306 aggregateReadOrigin.reset();
307 if (valTree != nullptr) {
308 // Copy the source's known children along with its value.
309 children = valTree->children;
310 dynamicChildCount = valTree->dynamicChildCount;
311 } else {
312 // An untracked aggregate source supplies no facts about its contents.
313 // Its write replaces the destination as a whole, so retaining the
314 // destination's old children could forward values from the old snapshot.
315 invalidateChildren();
316 }
317 }
318
319 void invalidateChildren() {
320 children.clear();
321 dynamicChildCount = 0;
322 }
323
328 void invalidateDynamicChildren() {
329 if (dynamicChildCount == 0) {
330 return;
331 }
332 SmallVector<ReferenceID> invalidChildren;
333 for (const auto &[id, _] : children) {
334 if (id.isValue()) {
335 invalidChildren.push_back(id);
336 }
337 }
338 for (const ReferenceID &id : invalidChildren) {
339 children.erase(id);
340 }
341 dynamicChildCount = 0;
342 }
343
344 bool invalidateNonIntegerOffsetChildren() {
345 SmallVector<ReferenceID> invalidChildren;
346 size_t invalidDynamicChildCount = 0;
347 for (const auto &[id, _] : children) {
348 if (!id.isAttribute() || !isa<IntegerAttr>(id.getAttribute())) {
349 invalidChildren.push_back(id);
350 if (id.isValue()) {
351 ++invalidDynamicChildCount;
352 }
353 }
354 }
355 for (const ReferenceID &id : invalidChildren) {
356 children.erase(id);
357 }
358 dynamicChildCount -= invalidDynamicChildCount;
359 return !invalidChildren.empty();
360 }
361
362 bool isLeaf() const { return children.empty(); }
363
364 Value getStoredValue() const { return storedValue; }
365
366 bool hasStoredValue() const { return storedValue != nullptr; }
367
369 const std::optional<Value> &getReusableAggregateRead() const { return reusableAggregateRead; }
370
372 void setReusableAggregateRead(Value value) { reusableAggregateRead = value; }
373
375 const std::optional<AggregateSnapshotSource> &getAggregateSnapshotSource() const {
376 return aggregateSnapshotSource;
377 }
378
380 void setAggregateSnapshotSource(Value value, Operation *write) {
381 aggregateSnapshotSource = AggregateSnapshotSource {.value = value, .write = write};
382 }
383
385 void clearAggregateSnapshotSource() { aggregateSnapshotSource.reset(); }
386
388 void setAggregateReadOrigin(const std::shared_ptr<ReferenceNode> &storage, Operation *read) {
389 aggregateReadOrigin = AggregateReadOrigin {
390 .storage = storage,
391 .mutationEpoch = storage->aggregateMutationEpoch,
392 .read = read,
393 };
394 }
395
397 const std::optional<AggregateReadOrigin> &getAggregateReadOrigin() const {
398 return aggregateReadOrigin;
399 }
400
406 void markAggregateMutation() {
407 ++aggregateMutationEpoch;
408 reusableAggregateRead.reset();
409 }
410
411 uint64_t getAggregateMutationEpoch() const { return aggregateMutationEpoch; }
412
413 void print(raw_ostream &os, int indent = 0) const {
414 os.indent(indent) << '[' << identifier;
415 if (storedValue != nullptr) {
416 os << " => " << storedValue;
417 }
418 os << ']';
419 if (!children.empty()) {
420 os << "{\n";
421 for (const auto &[_, child] : children) {
422 child->print(os, indent + 4);
423 os << '\n';
424 }
425 os.indent(indent) << '}';
426 }
427 }
428
429 [[maybe_unused]]
430 friend raw_ostream &operator<<(raw_ostream &os, const ReferenceNode &r) {
431 r.print(os);
432 return os;
433 }
434
436 friend bool
437 topLevelEq(const std::shared_ptr<ReferenceNode> &lhs, const std::shared_ptr<ReferenceNode> &rhs) {
438 return lhs->identifier == rhs->identifier && lhs->storedValue == rhs->storedValue &&
439 lhs->lastWrite == rhs->lastWrite;
440 }
441
442 friend std::shared_ptr<ReferenceNode> greatestCommonSubtree(
443 const std::shared_ptr<ReferenceNode> &lhs, const std::shared_ptr<ReferenceNode> &rhs
444 ) {
445 if (!topLevelEq(lhs, rhs)) {
446 return nullptr;
447 }
448 auto res = lhs->clone(false); // childless clone
449 // Find common children and recurse
450 for (auto &[id, lhsChild] : lhs->children) {
451 if (auto it = rhs->children.find(id); it != rhs->children.end()) {
452 auto &rhsChild = it->second;
453 if (auto gcs = greatestCommonSubtree(lhsChild, rhsChild)) {
454 res->children[id] = gcs;
455 if (id.isValue()) {
456 ++res->dynamicChildCount;
457 }
458 }
459 }
460 }
461 return res;
462 }
463
464private:
465 ReferenceID identifier;
466 mlir::Value storedValue;
467 Operation *lastWrite;
468 // Candidates are deliberately not copied into cloned states or value
469 // snapshots. This keeps aggregate-read forwarding local to one allocation
470 // path and avoids forwarding across a value-copy boundary.
471 std::optional<Value> reusableAggregateRead;
472 std::optional<AggregateSnapshotSource> aggregateSnapshotSource;
473 std::optional<AggregateReadOrigin> aggregateReadOrigin;
474 DenseMap<ReferenceID, std::shared_ptr<ReferenceNode>> children;
475 // Number of direct dynamic children. Keep it synchronized with every children
476 // mutation so constant-only invalidation remains independent of child fanout.
477 size_t dynamicChildCount;
478 uint64_t aggregateMutationEpoch = 0;
479
480 template <typename IdType>
481 ReferenceNode(IdType id, Value initialVal)
482 : identifier(std::move(id)), storedValue(initialVal), lastWrite(nullptr), children(),
483 dynamicChildCount(0) {}
484};
485
486using ValueMap = DenseMap<mlir::Value, std::shared_ptr<ReferenceNode>>;
487
491bool requiresAggregateSnapshot(Type type) {
492 return isa<array::ArrayType, pod::PodType, component::StructType>(type);
493}
494
502bool isTransparentAggregateAliasCast(Operation *op) {
503 if (op == nullptr || op->getNumOperands() != 1 || op->getNumResults() != 1 ||
504 !requiresAggregateSnapshot(op->getOperand(0).getType()) ||
505 !requiresAggregateSnapshot(op->getResult(0).getType())) {
506 return false;
507 }
508 return isa<polymorphic::UnifiableCastOp, UnrealizedConversionCastOp>(op);
509}
510
512Value getAggregateAliasRoot(Value value) {
513 Operation *def = value.getDefiningOp();
514 while (isTransparentAggregateAliasCast(def)) {
515 value = def->getOperand(0);
516 def = value.getDefiningOp();
517 }
518 return value;
519}
520
523struct GlobalState {
524 std::optional<Value> scalar;
525 std::shared_ptr<ReferenceNode> aggregate;
526};
527
528using GlobalStateMap = DenseMap<SymbolRefAttr, GlobalState>;
529
532struct KnownState {
533 ValueMap values;
534 GlobalStateMap globals;
535 DenseMap<ReferenceID, Value> ram;
536 // Unlike `ram`, only exact translated address values justify store removal.
537 DenseMap<Value, Value> ramExact;
538};
539
542struct BlockWriteCandidates {
543 DenseMap<SymbolRefAttr, Operation *> globals;
544 DenseMap<Value, Operation *> ram;
545
546 void clear() {
547 globals.clear();
548 ram.clear();
549 }
550};
551
553ValueMap intersectValueMap(const ValueMap &lhs, const ValueMap &rhs) {
554 ValueMap res;
555 for (const auto &[id, lhsValTree] : lhs) {
556 if (!lhsValTree) {
557 continue;
558 }
559 if (auto it = rhs.find(id); it != rhs.end() && it->second) {
560 // A missing common subtree is the conservative no-common-fact state.
561 if (auto common = greatestCommonSubtree(lhsValTree, it->second)) {
562 res[id] = std::move(common);
563 }
564 }
565 }
566 return res;
567}
568
572GlobalStateMap intersectGlobals(const GlobalStateMap &lhs, const GlobalStateMap &rhs) {
573 GlobalStateMap res;
574 for (const auto &[id, lhsState] : lhs) {
575 if (!lhsState.scalar) {
576 continue;
577 }
578 if (auto it = rhs.find(id); it != rhs.end() && it->second.scalar == lhsState.scalar) {
579 res[id].scalar = lhsState.scalar;
580 }
581 }
582 return res;
583}
584
586template <typename KeyT>
587DenseMap<KeyT, Value>
588intersectValueLookup(const DenseMap<KeyT, Value> &lhs, const DenseMap<KeyT, Value> &rhs) {
589 DenseMap<KeyT, Value> res;
590 for (const auto &[id, lhsVal] : lhs) {
591 if (auto it = rhs.find(id); it != rhs.end() && it->second == lhsVal) {
592 res[id] = lhsVal;
593 }
594 }
595 return res;
597
599KnownState intersect(const KnownState &lhs, const KnownState &rhs) {
600 return {
601 intersectValueMap(lhs.values, rhs.values), intersectGlobals(lhs.globals, rhs.globals),
602 intersectValueLookup(lhs.ram, rhs.ram), intersectValueLookup(lhs.ramExact, rhs.ramExact)
603 };
608ValueMap cloneValueMap(const ValueMap &orig) {
609 ValueMap res;
610 for (const auto &[id, tree] : orig) {
611 res[id] = tree->clone();
612 }
613 return res;
614}
615
616GlobalStateMap cloneGlobalStateMap(const GlobalStateMap &orig) {
617 GlobalStateMap res;
618 for (const auto &[name, global] : orig) {
619 res[name].scalar = global.scalar;
620 if (global.aggregate) {
621 res[name].aggregate = global.aggregate->clone();
623 }
624 return res;
625}
626
627/// Deep copy the KnownState for exclusive branches/regions so tree updates do
628/// not mutate the incoming state.
629KnownState cloneKnownState(const KnownState &orig) {
630 return {cloneValueMap(orig.values), cloneGlobalStateMap(orig.globals), orig.ram, orig.ramExact};
631}
633class PassImpl : public llzk::impl::RedundantReadAndWriteEliminationPassBase<PassImpl> {
635 using Base::Base;
636
640 DenseSet<Value> aggregateWriteTargets;
641
647 void runOnOperation() override {
648 getOperation().walk([this](FuncDefOp fn) { runOnFunc(fn); });
649 }
650
653 void runOnFunc(FuncDefOp fn) {
654 // Nothing to do for body-less functions.
655 if (fn.getCallableRegion() == nullptr) {
656 return;
657 }
658
659 LLVM_DEBUG(llvm::dbgs() << "Running on " << fn.getName() << '\n');
660
661 aggregateWriteTargets.clear();
662 auto recordAggregateWriteTarget = [this](Value target) {
663 if (requiresAggregateSnapshot(target.getType())) {
664 aggregateWriteTargets.insert(getAggregateAliasRoot(target));
665 }
666 };
667 fn.walk([&](Operation *op) {
668 bool knownAggregateWrite = false;
669 if (auto memberWrite = dyn_cast<MemberWriteOp>(op)) {
670 recordAggregateWriteTarget(memberWrite.getComponent());
671 knownAggregateWrite = true;
672 } else if (
673 auto arrayAccess = llvm::dyn_cast<ArrayAccessOpInterface>(op);
674 arrayAccess && !arrayAccess.isRead()
675 ) {
676 recordAggregateWriteTarget(arrayAccess.getArrRef());
677 knownAggregateWrite = true;
678 } else if (auto podWrite = dyn_cast<pod::WritePodOp>(op)) {
679 recordAggregateWriteTarget(podWrite.getPodRef());
680 knownAggregateWrite = true;
681 } else if (isa<global::GlobalWriteOp, ram::StoreOp, CallOp>(op)) {
682 // These operations copy their source operand but do not mutate it.
683 knownAggregateWrite = true;
684 }
685 if (!knownAggregateWrite && hasUnknownOrNonReadEffect(op)) {
686 // An unmodeled effect may mutate any aggregate operand. Treating it
687 // as a write target prevents a later RAUW from merging two copies.
688 for (Value operand : op->getOperands()) {
689 recordAggregateWriteTarget(operand);
690 }
691 }
692 });
693
694 // Maps redundant value -> necessary value.
695 DenseMap<Value, Value> replacementMap;
696 // All values created by a new_* operation or from a read*/extract* operation.
697 SmallVector<Value> readVals;
698 // All writes that are either (1) overwritten by subsequent writes or (2)
699 // write a value that is already written.
700 SmallVector<Operation *> redundantWrites;
701
702 KnownState initState;
703 // Initialize the state to the function arguments.
704 for (auto arg : fn.getArguments()) {
705 initState.values[getAggregateAliasRoot(arg)] = ReferenceNode::create(arg, arg);
706 }
707 // Functions only have a single region
708 (void)runOnRegion(
709 *fn.getCallableRegion(), std::move(initState), replacementMap, readVals, redundantWrites
710 );
711
712 // Now that we have accumulated all necessary state, we perform the optimizations:
713 // - Replace all redundant values.
714 for (auto &[orig, replace] : replacementMap) {
715 LLVM_DEBUG(llvm::dbgs() << "replacing " << orig << " with " << replace << '\n');
716 orig.replaceAllUsesWith(replace);
717 // We save the deletion to the readVals loop to prevent double-free.
718 }
719 // -Remove redundant writes now that it is safe to do so.
720 for (auto *writeOp : redundantWrites) {
721 LLVM_DEBUG(llvm::dbgs() << "erase write: " << *writeOp << '\n');
722 writeOp->erase();
723 }
724 // - Now we do a pass over read values to see if any are now unused.
725 // We do this in reverse order to free up early reads if their users would
726 // be removed.
727 for (auto it = readVals.rbegin(); it != readVals.rend(); it++) {
728 Value readVal = *it;
729 if (readVal.use_empty()) {
730 LLVM_DEBUG(llvm::dbgs() << "erase read: " << readVal << '\n');
731 readVal.getDefiningOp()->erase();
732 }
733 }
734 }
735
736 KnownState runOnRegion(
737 Region &r, KnownState &&initState, DenseMap<Value, Value> &replacementMap,
738 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites
739 ) {
740 // maps block -> state at the end of the block
741 DenseMap<Block *, KnownState> endStates;
742 // The first block has no predecessors, so nullptr contains the init state
743 endStates[nullptr] = initState;
744 auto getBlockState = [&endStates](Block *blockPtr) {
745 auto it = endStates.find(blockPtr);
746 ensure(it != endStates.end(), "unknown end state means we have an unsupported backedge");
747 return cloneKnownState(it->second);
748 };
749 auto hasBlockState = [&endStates](Block *blockPtr) {
750 return endStates.find(blockPtr) != endStates.end();
751 };
752 std::deque<Block *> frontier;
753 DenseSet<Block *> queued;
754 DenseSet<Block *> processed;
755 auto enqueue = [&](Block *blockPtr) {
756 if (processed.find(blockPtr) == processed.end() && queued.insert(blockPtr).second) {
757 frontier.push_back(blockPtr);
758 }
759 };
760 enqueue(&r.front());
761
762 SmallVector<KnownState> terminalStates;
763 size_t deferralsWithoutProgress = 0;
764
765 while (!frontier.empty()) {
766 Block *currentBlock = frontier.front();
767 frontier.pop_front();
768 queued.erase(currentBlock);
769
770 // get predecessors
771 KnownState currentState;
772 auto it = currentBlock->pred_begin();
773 auto itEnd = currentBlock->pred_end();
774 if (it == itEnd) {
775 // get the state for the entry block.
776 currentState = getBlockState(nullptr);
777 } else {
778 bool ready = true;
779 for (auto predIt = it; predIt != itEnd; predIt++) {
780 ready &= hasBlockState(*predIt);
781 }
782 if (!ready) {
783 deferralsWithoutProgress++;
784 ensure(
785 deferralsWithoutProgress <= frontier.size(),
786 "unknown end state means we have an unsupported backedge"
787 );
788 enqueue(currentBlock);
789 continue;
790 }
791
792 currentState = getBlockState(*it);
793 // If we have multiple predecessors, we take a pessimistic view and
794 // set the state as only the intersection of all predecessor states
795 // (e.g., only the common state from an if branch).
796 for (it++; it != itEnd; it++) {
797 currentState = intersect(currentState, getBlockState(*it));
798 }
799 }
800
801 // Run this block, consuming currentState and producing the endState
802 deferralsWithoutProgress = 0;
803 auto endState = runOnBlock(
804 *currentBlock, std::move(currentState), replacementMap, readVals, redundantWrites
805 );
806
807 // Update the end states.
808 // Since we only support the scf dialect, we should never have any
809 // backedges, so we should never already have state for this block.
810 ensure(processed.find(currentBlock) == processed.end(), "backedge");
811 endStates[currentBlock] = std::move(endState);
812 processed.insert(currentBlock);
813
814 // add successors to frontier
815 if (currentBlock->hasNoSuccessors()) {
816 terminalStates.push_back(cloneKnownState(endStates[currentBlock]));
817 } else {
818 for (Block *succ : currentBlock->getSuccessors()) {
819 enqueue(succ);
820 }
821 }
822 }
823
824 // The final state is the intersection of all possible terminal states.
825 ensure(!terminalStates.empty(), "computed no states");
826 auto finalState = terminalStates.front();
827 for (const auto *it = terminalStates.begin() + 1; it != terminalStates.end(); it++) {
828 finalState = intersect(finalState, *it);
829 }
830 return finalState;
831 }
832
833 KnownState runOnBlock(
834 Block &b, KnownState &&state, DenseMap<Value, Value> &replacementMap,
835 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites
836 ) {
837 BlockWriteCandidates writeCandidates;
838 auto clearTreeWriteCandidates = [](KnownState &knownState) {
839 for (auto &[_, valueTree] : knownState.values) {
840 if (valueTree) {
841 valueTree->clearLastWritesInSubtree();
842 }
843 }
844 };
845
846 for (Operation &op : b) {
847 // Some operations have regions (e.g., scf.if). These regions must be
848 // traversed and the resulting state(s) are intersected for the final
849 // state of this operation.
850 if (!op.getRegions().empty()) {
851 KnownState parentState = cloneKnownState(state);
852 // Repeating regions (scf.for, scf.while) execute their body more than
853 // once. Pre-loop facts must not be used to declare a read inside the
854 // body redundant — the body may observe writes from a previous
855 // iteration.
856 KnownState regionEntryState = cloneKnownState(state);
857 // Tree-shaped reference last-write pointers are block-local deletion
858 // candidates. Do not let an exclusive region inherit a candidate from
859 // its parent, where one arm could otherwise queue it for function-wide
860 // erasure.
861 clearTreeWriteCandidates(regionEntryState);
862 if (isa<scf::ForOp, scf::WhileOp>(op)) {
863 regionEntryState.values.clear();
864 regionEntryState.globals.clear();
865 regionEntryState.ram.clear();
866 regionEntryState.ramExact.clear();
867 }
868 SmallVector<KnownState> regionStates;
869 for (Region &region : op.getRegions()) {
870 if (region.empty()) {
871 continue;
872 }
873 auto regionState = runOnRegion(
874 region, cloneKnownState(regionEntryState), replacementMap, readVals, redundantWrites
875 );
876 regionStates.push_back(regionState);
877 }
878 if (regionStates.empty()) {
879 // Region-bearing ops with no bodies still need their own effects handled.
880 runOperation(&op, state, replacementMap, readVals, redundantWrites, writeCandidates);
881 writeCandidates.clear();
882 continue;
883 }
884
885 KnownState finalState = regionStates.front();
886 for (const auto *it = regionStates.begin() + 1; it != regionStates.end(); it++) {
887 finalState = intersect(finalState, *it);
888 }
889 // A nested region may be conditional, zero-iteration, or otherwise not
890 // execute exactly once. Only propagate facts that remain true both
891 // before and after the region traversal. In particular, a one-armed
892 // scf.if must not make a write in its then-region appear unconditional.
893 finalState.values = intersectValueMap(parentState.values, finalState.values);
894 finalState.globals = intersectGlobals(parentState.globals, finalState.globals);
895 finalState.ram = intersectValueLookup(parentState.ram, finalState.ram);
896 finalState.ramExact = intersectValueLookup(parentState.ramExact, finalState.ramExact);
897 // Likewise, do not export a tree-shaped reference candidate from one
898 // region through the join. A later write must not treat an
899 // exclusive-arm write as a block-local predecessor.
900 clearTreeWriteCandidates(finalState);
901 state = std::move(finalState);
902 writeCandidates.clear();
903 continue;
904 }
905 runOperation(&op, state, replacementMap, readVals, redundantWrites, writeCandidates);
906 }
907 return std::move(state);
908 }
909
917 void runOperation(
918 Operation *op, KnownState &state, DenseMap<Value, Value> &replacementMap,
919 SmallVector<Value> &readVals, SmallVector<Operation *> &redundantWrites,
920 BlockWriteCandidates &writeCandidates
921 ) {
922 // Uses the replacement map to look up values to simplify later replacement.
923 // This avoids having a daisy chain of "replace B with A", "replace C with B",
924 // etc.
925 auto translate = [&replacementMap](Value v) {
926 if (auto it = replacementMap.find(v); it != replacementMap.end()) {
927 return it->second;
928 }
929 return v;
930 };
931
932 // Transparent aggregate casts preserve identity. Store one tree under the
933 // root value for the whole cast chain, rather than one entry per SSA alias.
934 // A cast operand may itself be replacement-pending, so repeat both
935 // normalizations until reaching the canonical state key.
936 auto canonicalStateKey = [&](Value v) {
937 while (true) {
938 Value normalized = getAggregateAliasRoot(translate(v));
939 if (normalized == v) {
940 return v;
941 }
942 v = normalized;
943 }
944 };
945
946 // Lookup the value tree in the current state or return nullptr.
947 auto tryGetValTree = [&state, &canonicalStateKey](Value v) -> std::shared_ptr<ReferenceNode> {
948 if (auto it = state.values.find(canonicalStateKey(v)); it != state.values.end()) {
949 return it->second;
950 }
951 return nullptr;
952 };
953
954 auto setValTree = [&state, &canonicalStateKey](Value v, std::shared_ptr<ReferenceNode> tree) {
955 state.values[canonicalStateKey(v)] = std::move(tree);
956 };
957 auto eraseValTree = [&state, &canonicalStateKey](Value v) {
958 state.values.erase(canonicalStateKey(v));
959 };
960
961 auto doStatefulRead =
962 [&]<typename KeyT>(Value resVal, DenseMap<KeyT, Value> &knownValues, const KeyT &key) {
963 readVals.push_back(resVal);
964 if (auto it = knownValues.find(key); it != knownValues.end()) {
965 replacementMap[resVal] = it->second;
966 return true;
967 } else {
968 knownValues[key] = resVal;
969 setValTree(resVal, ReferenceNode::create(resVal, resVal));
970 return false;
971 }
972 };
973
974 auto copiedValueTree = [&](Value value) {
975 if (auto tree = tryGetValTree(value); tree && requiresAggregateSnapshot(value.getType())) {
976 auto copy = tree->cloneForValueCopy(value);
977 // Copying observes all known source contents, so a later mutation of
978 // the source cannot erase a write that initialized the copy.
979 tree->clearLastWritesInSubtree();
980 return copy;
981 }
982 return tryGetValTree(value);
983 };
984
985 auto readAggregateSnapshot = [](const std::shared_ptr<ReferenceNode> &tree, Value result,
986 Operation *read) {
987 auto copy = tree->cloneForValueCopy(result);
988 copy->setAggregateReadOrigin(tree, read);
989 // The read observes all contents of this allocation, so preserve writes
990 // that initialized the independent value copy.
991 tree->clearLastWritesInSubtree();
992 return copy;
993 };
994
995 auto useMutatesAggregate = [](OpOperand &use) {
996 Value value = use.get();
997 Operation *user = use.getOwner();
998 if (auto memberWrite = dyn_cast<MemberWriteOp>(user)) {
999 return memberWrite.getComponent() == value;
1000 }
1001 if (auto arrayAccess = llvm::dyn_cast<ArrayAccessOpInterface>(user)) {
1002 return !arrayAccess.isRead() && arrayAccess.getArrRef() == value;
1003 }
1004 if (auto podWrite = dyn_cast<pod::WritePodOp>(user)) {
1005 return podWrite.getPodRef() == value;
1006 }
1007 if (isa<global::GlobalWriteOp, ram::StoreOp, CallOp>(user)) {
1008 // These operations consume a copied source value rather than mutate it.
1009 return false;
1010 }
1011 if (isa<constrain::ConstraintOpInterface>(user)) {
1012 // Constraints consume aggregate values but cannot mutate their storage.
1013 return false;
1014 }
1015 return hasUnknownOrNonReadEffect(user);
1016 };
1017
1018 // Search all aliases of `value` for a relevant mutation. Transparent
1019 // casts do not establish a value-copy boundary, so a mutation through a
1020 // cast result is also a mutation of the aggregate passed to that cast.
1021 // Start at the cast-chain root so this also finds a mutation through the
1022 // source when `value` itself is a cast result.
1023 auto mayMutateThroughAlias = [&](Value value, auto &&isRelevantUse) {
1024 SmallVector<Value> worklist = {getAggregateAliasRoot(value)};
1025 DenseSet<Value> visited;
1026 while (!worklist.empty()) {
1027 Value alias = worklist.pop_back_val();
1028 if (!visited.insert(alias).second) {
1029 continue;
1030 }
1031 for (OpOperand &use : alias.getUses()) {
1032 Operation *user = use.getOwner();
1033 if (isTransparentAggregateAliasCast(user)) {
1034 worklist.push_back(user->getResult(0));
1035 continue;
1036 }
1037 if (useMutatesAggregate(use) && isRelevantUse(user)) {
1038 return true;
1039 }
1040 }
1041 }
1042 return false;
1043 };
1044
1045 auto mayMutateAfter = [&](Value value, Operation *point) {
1046 return mayMutateThroughAlias(value, [point](Operation *user) {
1047 // Candidates are not propagated across control-flow state clones, so
1048 // a write outside this block is conservatively treated as later.
1049 return user->getBlock() != point->getBlock() || point->isBeforeInBlock(user);
1050 });
1051 };
1052
1053 auto mayMutateBetween = [&](Value value, Operation *before, Operation *after) {
1054 return mayMutateThroughAlias(value, [before, after](Operation *user) {
1055 return user->getBlock() != before->getBlock() || user->getBlock() != after->getBlock() ||
1056 (before->isBeforeInBlock(user) && user->isBeforeInBlock(after));
1057 });
1058 };
1059
1060 // A write records the aggregate source that was copied into an access. A
1061 // later aggregate read may reuse that source when it has not changed since
1062 // the copy and neither copy is mutated afterwards. The caller still
1063 // creates a snapshot first, so the read observes pending writes even when
1064 // its SSA result is subsequently replaced.
1065 auto tryForwardAggregateSnapshot = [&](const std::shared_ptr<ReferenceNode> &tree, Value result,
1066 Operation *read) -> std::shared_ptr<ReferenceNode> {
1067 const auto &snapshotSource = tree->getAggregateSnapshotSource();
1068 if (!snapshotSource || mayMutateBetween(snapshotSource->value, snapshotSource->write, read) ||
1069 mayMutateAfter(snapshotSource->value, read) || mayMutateAfter(result, read)) {
1070 return nullptr;
1071 }
1072 if (auto sourceTree = tryGetValTree(snapshotSource->value)) {
1073 replacementMap[result] = snapshotSource->value;
1074 return sourceTree;
1075 }
1076 return nullptr;
1077 };
1078
1079 // Reuse a prior aggregate read only when replacing the later copy with the
1080 // earlier copy preserves value-copy semantics. In particular, neither
1081 // copy may be mutated, and the candidate must not have been mutated before
1082 // this read was reached.
1083 auto tryReuseAggregateRead = [&](const std::shared_ptr<ReferenceNode> &tree, Value result,
1084 Operation *read) -> std::shared_ptr<ReferenceNode> {
1085 const auto &candidate = tree->getReusableAggregateRead();
1086 if (!candidate) {
1087 return nullptr;
1088 }
1089 Operation *candidateRead = candidate->getDefiningOp();
1090 if (candidateRead == nullptr || mayMutateBetween(*candidate, candidateRead, read) ||
1091 mayMutateAfter(*candidate, read) || mayMutateAfter(result, read)) {
1092 return nullptr;
1093 }
1094 if (auto candidateTree = tryGetValTree(*candidate)) {
1095 replacementMap[result] = *candidate;
1096 return candidateTree;
1097 }
1098 return nullptr;
1099 };
1100
1106 auto processAggregateRead = [&](const std::shared_ptr<ReferenceNode> &storage, Value result,
1107 Operation *read, auto &&tryReuse, auto &&rememberRead) {
1108 auto snapshot = readAggregateSnapshot(storage, result, read);
1109 if (auto sourceTree = tryForwardAggregateSnapshot(storage, result, read)) {
1110 return sourceTree;
1111 }
1112 if (auto reusableTree = tryReuse(storage, result, read)) {
1113 return reusableTree;
1114 }
1115 auto resultTree = std::move(snapshot);
1116 rememberRead(storage, result);
1117 return resultTree;
1118 };
1119
1120 auto tryStandardAggregateReuse = [&](const std::shared_ptr<ReferenceNode> &storage,
1121 Value result, Operation *read) {
1122 return tryReuseAggregateRead(storage, result, read);
1123 };
1124 auto rememberAggregateRead = [](const std::shared_ptr<ReferenceNode> &storage, Value result) {
1125 storage->setReusableAggregateRead(result);
1126 };
1127
1128 // Struct member reads use a function-wide mutation pre-scan as their
1129 // reuse policy. Keep that policy separate from the generic sequence above.
1130 auto isAggregateWriteTarget = [this](Value value) {
1131 return aggregateWriteTargets.contains(getAggregateAliasRoot(value));
1132 };
1133 auto tryMemberAggregateReuse = [&](const std::shared_ptr<ReferenceNode> &storage, Value result,
1134 Operation *) -> std::shared_ptr<ReferenceNode> {
1135 const auto &candidate = storage->getReusableAggregateRead();
1136 if (!candidate || isAggregateWriteTarget(*candidate) || isAggregateWriteTarget(result)) {
1137 return nullptr;
1138 }
1139 if (auto candidateTree = tryGetValTree(*candidate)) {
1140 replacementMap[result] = *candidate;
1141 return candidateTree;
1142 }
1143 return nullptr;
1144 };
1145 auto rememberMemberAggregateRead = [&](const std::shared_ptr<ReferenceNode> &storage,
1146 Value result) {
1147 if (!isAggregateWriteTarget(result)) {
1148 storage->setReusableAggregateRead(result);
1149 }
1150 };
1151
1152 // An aggregate write is redundant when it copies the same source snapshot
1153 // that is already stored at the destination and that source has not been
1154 // mutated since the earlier copy. The destination's state records writes
1155 // by exact access path, so any intervening potentially-aliasing write has
1156 // already invalidated or replaced this candidate.
1157 auto isRedundantAggregateWrite =
1158 [&](const std::optional<ReferenceNode::AggregateSnapshotSource> &previous, Value value,
1159 Operation *write) {
1160 return previous && previous->value == value &&
1161 !mayMutateBetween(value, previous->write, write);
1162 };
1163
1164 auto isRedundantTreeWrite =
1165 [&](const std::shared_ptr<ReferenceNode> &destination,
1166 const std::optional<ReferenceNode::AggregateSnapshotSource> &previous, Value value,
1167 Operation *write) {
1168 if (!requiresAggregateSnapshot(value.getType())) {
1169 return destination->getStoredValue() == value;
1170 }
1171 return isRedundantAggregateWrite(previous, value, write);
1172 };
1173
1177 auto commitTreeWrite = [&](const std::shared_ptr<ReferenceNode> &destination, Value value,
1178 Operation *write, const std::shared_ptr<ReferenceNode> &valueTree,
1179 ArrayRef<std::shared_ptr<ReferenceNode>> mutatedNodes) {
1180 for (const auto &node : mutatedNodes) {
1181 node->markAggregateMutation();
1182 }
1183 destination->clearAggregateSnapshotSource();
1184 if (Operation *lastWrite = destination->updateLastWrite(write)) {
1185 LLVM_DEBUG(
1186 llvm::dbgs() << write->getName().getStringRef() << ": replacing " << lastWrite
1187 << " with prior write " << *lastWrite << '\n'
1188 );
1189 redundantWrites.push_back(lastWrite);
1190 }
1191 destination->setCurrentValue(value, valueTree);
1192 if (requiresAggregateSnapshot(value.getType())) {
1193 destination->setAggregateSnapshotSource(value, write);
1194 }
1195 };
1196
1197 // Returns whether an operation between two aggregate accesses has an
1198 // unmodeled effect. Known writes are checked through per-access mutation
1199 // epochs instead, which lets a write to an unrelated array element or
1200 // record remain transparent to a read-back write.
1201 auto hasUnknownEffectBetween = [](Operation *before, Operation *after) {
1202 if (before == nullptr || after == nullptr || before->getBlock() != after->getBlock() ||
1203 !before->isBeforeInBlock(after)) {
1204 return true;
1205 }
1206 for (Operation *n = before->getNextNode(); n != after; n = n->getNextNode()) {
1207 if (isa<MemberWriteOp, pod::WritePodOp, global::GlobalWriteOp, ram::StoreOp, CallOp,
1208 constrain::ConstraintOpInterface>(n) ||
1209 llvm::dyn_cast<ArrayAccessOpInterface>(n)) {
1210 continue;
1211 }
1213 return true;
1214 }
1215 }
1216 return false;
1217 };
1218
1219 // A write that restores an unchanged snapshot to the same storage path is
1220 // redundant. The source snapshot remembers both its read location and the
1221 // location's mutation epoch. Writes on the same path, nested below it, or
1222 // through an alias advance that epoch; unrelated sibling writes do not.
1223 auto isRedundantAggregateReadBackWrite = [&](const std::shared_ptr<ReferenceNode> &destination,
1224 Value source, Operation *write) {
1225 auto sourceTree = tryGetValTree(source);
1226 if (sourceTree == nullptr) {
1227 return false;
1228 }
1229 const auto &origin = sourceTree->getAggregateReadOrigin();
1230 return origin && origin->storage == destination &&
1231 origin->mutationEpoch == destination->getAggregateMutationEpoch() &&
1232 !mayMutateBetween(source, origin->read, write) &&
1233 !hasUnknownEffectBetween(origin->read, write);
1234 };
1235
1236 // An omitted table offset denotes the current row.
1237 const IntegerAttr zeroTableOffset = IntegerAttr::get(IndexType::get(op->getContext()), 0);
1238 auto getMemberNode = [&](Value component, FlatSymbolRefAttr member) {
1239 std::shared_ptr<ReferenceNode> componentNode = tryGetValTree(translate(component));
1240 if (componentNode == nullptr) {
1241 return std::shared_ptr<ReferenceNode>();
1242 }
1243 return componentNode->getOrCreateChild(member);
1244 };
1245
1246 auto getMemberAccessNode = [&](MemberReadOp readm) {
1247 std::shared_ptr<ReferenceNode> access =
1248 getMemberNode(readm.getComponent(), readm.getMemberNameAttr());
1249 if (access == nullptr) {
1250 return access;
1251 }
1252 access = access->getOrCreateChild(readm.getTableOffset().value_or(zeroTableOffset));
1253 if (!readm.getMapOperands().empty()) {
1254 access = access->getOrCreateChild(readm.getMapOpGroupSizesAttr());
1255 access = access->getOrCreateChild(readm.getNumDimsPerMapAttr());
1256 }
1257 for (auto mapOperands : readm.getMapOperands()) {
1258 for (Value operand : mapOperands) {
1259 access = access->getOrCreateChild(translate(operand));
1260 }
1261 }
1262 return access;
1263 };
1264
1265 // Read a value from an array. This works on both readarr operations (which
1266 // return a scalar value) and extractarr operations (which return a subarray).
1267 auto doArrayReadLike = [&]<HasInterface<ArrayAccessOpInterface> OpClass>(OpClass readarr) {
1268 Value resVal = readarr.getResult();
1269 std::shared_ptr<ReferenceNode> currValTree = tryGetValTree(translate(readarr.getArrRef()));
1270 if (currValTree == nullptr) {
1271 setValTree(resVal, ReferenceNode::create(resVal, resVal));
1272 readVals.push_back(resVal);
1273 return;
1274 }
1275
1276 std::shared_ptr<ReferenceNode> rootValTree = currValTree;
1277 SmallVector<ReferenceID> indices;
1278 bool hasDynamicIndex = false;
1279 for (Value origIdx : readarr.getIndices()) {
1280 Value idxVal = translate(origIdx);
1281 ReferenceID indexId(idxVal);
1282 hasDynamicIndex |= !indexId.isConst();
1283 indices.push_back(indexId);
1284 currValTree = currValTree->getOrCreateChild(idxVal);
1285 }
1286
1287 if (requiresAggregateSnapshot(resVal.getType())) {
1288 if (hasDynamicIndex) {
1289 rootValTree->clearLastWritesObservedBy(indices);
1290 }
1291 setValTree(
1292 resVal, processAggregateRead(
1293 currValTree, resVal, readarr.getOperation(), tryStandardAggregateReuse,
1294 rememberAggregateRead
1295 )
1296 );
1297 readVals.push_back(resVal);
1298 return;
1299 }
1300
1301 if (!currValTree->hasStoredValue()) {
1302 currValTree->setCurrentValue(resVal);
1303 }
1304
1305 if (currValTree->getStoredValue() != resVal) {
1306 LLVM_DEBUG(
1307 llvm::dbgs() << readarr.getOperationName() << ": replace " << resVal << " with "
1308 << currValTree->getStoredValue() << '\n'
1309 );
1310 replacementMap[resVal] = currValTree->getStoredValue();
1311 } else {
1312 if (hasDynamicIndex) {
1313 rootValTree->clearLastWritesObservedBy(indices);
1314 }
1315 setValTree(resVal, currValTree);
1316 LLVM_DEBUG(
1317 llvm::dbgs() << readarr.getOperationName() << ": " << resVal << " => " << *currValTree
1318 << '\n'
1319 );
1320 }
1321
1322 readVals.push_back(resVal);
1323 };
1324
1325 // Handle array.write (scalar) and array.insert (subarray) with one or more
1326 // indices. Dynamic indices may alias any sibling; constant indices only
1327 // invalidate dynamic-index siblings.
1328 auto doArrayWriteLike = [&]<HasInterface<ArrayAccessOpInterface> OpClass>(OpClass writearr) {
1329 std::shared_ptr<ReferenceNode> currValTree = tryGetValTree(translate(writearr.getArrRef()));
1330 if (currValTree == nullptr) {
1331 return;
1332 }
1333 Value newVal = translate(writearr.getRvalue());
1334
1335 // Look up the destination without changing the access tree. This gives a
1336 // read-back write a chance to prove itself redundant before normal write
1337 // processing invalidates dynamic aliases or snapshot provenance.
1338 if (requiresAggregateSnapshot(newVal.getType())) {
1339 auto destination = currValTree;
1340 for (Value origIdx : writearr.getIndices()) {
1341 destination = destination->getChild(translate(origIdx));
1342 if (destination == nullptr) {
1343 break;
1344 }
1345 }
1346 if (destination != nullptr &&
1347 isRedundantAggregateReadBackWrite(destination, newVal, writearr.getOperation())) {
1348 redundantWrites.push_back(writearr.getOperation());
1349 return;
1350 }
1351 }
1352
1353 std::shared_ptr<ReferenceNode> valTree = copiedValueTree(newVal);
1354 SmallVector<std::shared_ptr<ReferenceNode>> mutatedNodes = {currValTree};
1355
1356 for (Value origIdx : writearr.getIndices()) {
1357 currValTree->clearAggregateSnapshotSource();
1358 Value idxVal = translate(origIdx);
1359 // A dynamic index may alias any sibling. A constant index only aliases
1360 // a dynamic sibling, so preserve unrelated constant-index facts.
1361 if (ReferenceID(idxVal).isConst()) {
1362 currValTree->invalidateDynamicChildren();
1363 } else {
1364 LLVM_DEBUG(llvm::dbgs() << writearr.getOperationName() << ": invalidate alias\n");
1365 currValTree->invalidateChildren();
1366 }
1367 currValTree = currValTree->getOrCreateChild(idxVal);
1368 mutatedNodes.push_back(currValTree);
1369 }
1370 std::optional<ReferenceNode::AggregateSnapshotSource> previousSnapshot =
1371 currValTree->getAggregateSnapshotSource();
1372
1373 // A subarray write copies its rvalue. The same aggregate SSA value can
1374 // therefore denote a different snapshot after the source is mutated.
1375 // SSA identity is only sufficient to prove a scalar write redundant.
1376 if (isRedundantTreeWrite(currValTree, previousSnapshot, newVal, writearr.getOperation())) {
1377 LLVM_DEBUG(
1378 llvm::dbgs() << writearr.getOperationName() << ": subsequent " << writearr
1379 << " is redundant\n"
1380 );
1381 redundantWrites.push_back(writearr);
1382 } else {
1383 // A write below an aggregate path invalidates read-back candidates for
1384 // that path and all of its ancestors, but not constant-index siblings.
1385 commitTreeWrite(currValTree, newVal, writearr.getOperation(), valTree, mutatedNodes);
1386 }
1387 };
1388
1389 // Global ops.
1390 if (auto readGlobal = dyn_cast<global::GlobalReadOp>(op)) {
1391 const auto name = readGlobal.getNameRef();
1392 Value result = readGlobal.getVal();
1393 readVals.push_back(result);
1394 if (requiresAggregateSnapshot(result.getType())) {
1395 if (auto it = state.globals.find(name); it != state.globals.end() && it->second.aggregate) {
1396 setValTree(
1397 result,
1398 processAggregateRead(
1399 it->second.aggregate, result, op, tryStandardAggregateReuse, rememberAggregateRead
1400 )
1401 );
1402 } else {
1403 auto globalState = ReferenceNode::create(result, result);
1404 setValTree(
1405 result, processAggregateRead(
1406 globalState, result, op, tryStandardAggregateReuse, rememberAggregateRead
1407 )
1408 );
1409 state.globals[name] = GlobalState {
1410 .scalar = std::nullopt,
1411 .aggregate = std::move(globalState),
1412 };
1413 }
1414 writeCandidates.globals.erase(name);
1415 } else if (
1416 auto it = state.globals.find(name); it != state.globals.end() && it->second.scalar
1417 ) {
1418 replacementMap[result] = *it->second.scalar;
1419 } else {
1420 state.globals[name] = GlobalState {.scalar = result, .aggregate = nullptr};
1421 setValTree(result, ReferenceNode::create(result, result));
1422 writeCandidates.globals.erase(name);
1423 }
1424 } else if (auto writeGlobal = dyn_cast<global::GlobalWriteOp>(op)) {
1425 const auto name = writeGlobal.getNameRef();
1426 Value value = translate(writeGlobal.getVal());
1427 if (requiresAggregateSnapshot(value.getType()) && [&] {
1428 auto known = state.globals.find(name);
1429 return known != state.globals.end() && known->second.aggregate &&
1430 isRedundantAggregateReadBackWrite(
1431 known->second.aggregate, value, writeGlobal.getOperation()
1432 );
1433 }()) {
1434 redundantWrites.push_back(writeGlobal.getOperation());
1435 } else if (!requiresAggregateSnapshot(value.getType()) && [&] {
1436 auto known = state.globals.find(name);
1437 return known != state.globals.end() && known->second.scalar == value;
1438 }()) {
1439 redundantWrites.push_back(writeGlobal.getOperation());
1440 } else if (requiresAggregateSnapshot(value.getType()) && [&] {
1441 auto known = state.globals.find(name);
1442 return known != state.globals.end() && known->second.aggregate &&
1443 isRedundantTreeWrite(
1444 known->second.aggregate, known->second.aggregate->getAggregateSnapshotSource(),
1445 value, writeGlobal.getOperation()
1446 );
1447 }()) {
1448 redundantWrites.push_back(writeGlobal.getOperation());
1449 } else {
1450 if (auto previous = writeCandidates.globals.find(name);
1451 previous != writeCandidates.globals.end()) {
1452 redundantWrites.push_back(previous->second);
1453 }
1454 if (requiresAggregateSnapshot(value.getType())) {
1455 if (auto valueTree = copiedValueTree(value)) {
1456 valueTree->setAggregateSnapshotSource(value, writeGlobal.getOperation());
1457 state.globals[name] = GlobalState {
1458 .scalar = std::nullopt,
1459 .aggregate = std::move(valueTree),
1460 };
1461 } else {
1462 state.globals.erase(name);
1463 }
1464 } else {
1465 state.globals[name] = GlobalState {.scalar = value, .aggregate = nullptr};
1466 }
1467 writeCandidates.globals[name] = writeGlobal.getOperation();
1468 }
1469 }
1470 // RAM ops
1471 else if (auto load = dyn_cast<ram::LoadOp>(op)) {
1472 Value address = translate(load.getAddr());
1473 if (!doStatefulRead(load.getVal(), state.ram, ReferenceID(address))) {
1474 writeCandidates.ram.clear();
1475 }
1476 state.ramExact[address] = translate(load.getVal());
1477 } else if (auto store = dyn_cast<ram::StoreOp>(op)) {
1478 Value address = translate(store.getAddr());
1479 Value value = translate(store.getVal());
1480 if (auto known = state.ramExact.find(address);
1481 known != state.ramExact.end() && known->second == value) {
1482 redundantWrites.push_back(store.getOperation());
1483 } else {
1484 if (auto previous = writeCandidates.ram.find(address);
1485 previous != writeCandidates.ram.end()) {
1486 redundantWrites.push_back(previous->second);
1487 }
1488 writeCandidates.ram[address] = store.getOperation();
1489 state.ram.clear();
1490 state.ramExact.clear();
1491 state.ram[ReferenceID(address)] = value;
1492 state.ramExact[address] = value;
1493 }
1494 }
1495 // struct ops
1496 else if (auto newStruct = dyn_cast<CreateStructOp>(op)) {
1497 // For new values, the "stored value" of the reference is the creation site.
1498 auto structVal = ReferenceNode::create(newStruct, newStruct);
1499 setValTree(newStruct, structVal);
1500 LLVM_DEBUG(llvm::dbgs() << newStruct.getOperationName() << ": " << *structVal << '\n');
1501 // adding this to readVals
1502 readVals.push_back(newStruct);
1503 } else if (auto readm = dyn_cast<MemberReadOp>(op)) {
1504 std::shared_ptr<ReferenceNode> access = getMemberAccessNode(readm);
1505 Value resVal = readm.getVal();
1506 if (access == nullptr) {
1507 setValTree(resVal, ReferenceNode::create(resVal, resVal));
1508 readVals.push_back(resVal);
1509 return;
1510 }
1511 if (requiresAggregateSnapshot(resVal.getType())) {
1512 setValTree(
1513 resVal, processAggregateRead(
1514 access, resVal, readm.getOperation(), tryMemberAggregateReuse,
1515 rememberMemberAggregateRead
1516 )
1517 );
1518 readVals.push_back(resVal);
1519 return;
1520 }
1521 if (!access->hasStoredValue()) {
1522 access->setCurrentValue(resVal);
1523 }
1524 if (access->getStoredValue() != resVal) {
1525 LLVM_DEBUG(
1526 llvm::dbgs() << readm.getOperationName() << ": adding replacement map entry { "
1527 << resVal << " => " << access->getStoredValue() << " }\n"
1528 );
1529 replacementMap[resVal] = access->getStoredValue();
1530 } else {
1531 setValTree(resVal, access);
1532 LLVM_DEBUG(llvm::dbgs() << readm.getOperationName() << ": " << *access << '\n');
1533 }
1534 readVals.push_back(resVal);
1535 } else if (auto writem = dyn_cast<MemberWriteOp>(op)) {
1536 auto componentTree = tryGetValTree(translate(writem.getComponent()));
1537 std::shared_ptr<ReferenceNode> member =
1538 getMemberNode(writem.getComponent(), writem.getMemberNameAttr());
1539 if (member == nullptr) {
1540 return;
1541 }
1542 Value writeVal = translate(writem.getVal());
1543 auto access = member->getOrCreateChild(zeroTableOffset);
1544 std::optional<ReferenceNode::AggregateSnapshotSource> previousSnapshot =
1545 access->getAggregateSnapshotSource();
1546 if (requiresAggregateSnapshot(writeVal.getType()) &&
1547 isRedundantAggregateReadBackWrite(access, writeVal, writem.getOperation())) {
1548 redundantWrites.push_back(writem);
1549 return;
1550 }
1551 if (requiresAggregateSnapshot(writeVal.getType()) &&
1552 isRedundantTreeWrite(access, previousSnapshot, writeVal, writem.getOperation())) {
1553 redundantWrites.push_back(writem);
1554 return;
1555 }
1556 if (componentTree) {
1557 componentTree->clearAggregateSnapshotSource();
1558 }
1559 // Symbolic and affine offsets may resolve to the current row. Constant
1560 // nonzero offsets stay distinct from a current-row member write.
1561 bool invalidatedMayAliasRead = member->invalidateNonIntegerOffsetChildren();
1562 auto valTree = copiedValueTree(writeVal);
1563
1564 if (invalidatedMayAliasRead) {
1565 access->clearLastWrite();
1566 }
1567 // Member writes copy aggregate values, so do not treat repeated source
1568 // SSA identity as repeated stored state.
1569 if (!requiresAggregateSnapshot(writeVal.getType()) &&
1570 isRedundantTreeWrite(access, previousSnapshot, writeVal, writem.getOperation())) {
1571 LLVM_DEBUG(
1572 llvm::dbgs() << writem.getOperationName() << ": recording redundant write " << writem
1573 << '\n'
1574 );
1575 redundantWrites.push_back(writem);
1576 } else {
1577 // A member write changes the current-row access and the enclosing
1578 // member/component snapshots, but leaves other members independent.
1579 SmallVector<std::shared_ptr<ReferenceNode>> mutatedNodes;
1580 if (componentTree) {
1581 mutatedNodes.push_back(componentTree);
1582 }
1583 mutatedNodes.push_back(member);
1584 mutatedNodes.push_back(access);
1585 commitTreeWrite(access, writeVal, writem.getOperation(), valTree, mutatedNodes);
1586 LLVM_DEBUG(
1587 llvm::dbgs() << writem.getOperationName() << ": " << *access << " set to " << writeVal
1588 << '\n'
1589 );
1590 }
1591 }
1592 // array ops
1593 else if (auto newArray = dyn_cast<CreateArrayOp>(op)) {
1594 auto arrayVal = ReferenceNode::create(newArray, newArray);
1595 setValTree(newArray, arrayVal);
1596
1597 // If we're given a constructor, we can instantiate elements using
1598 // constant indices.
1599 unsigned idx = 0;
1600 for (auto elem : newArray.getElements()) {
1601 Value elemVal = translate(elem);
1602 auto valTree = copiedValueTree(elemVal);
1603 auto elemChild = arrayVal->createChild(idx, elemVal, valTree);
1604 LLVM_DEBUG(
1605 llvm::dbgs() << newArray.getOperationName() << ": element " << idx << " initialized to "
1606 << *elemChild << '\n'
1607 );
1608 idx++;
1609 }
1610
1611 readVals.push_back(newArray);
1612 } else if (auto readarr = dyn_cast<ReadArrayOp>(op)) {
1613 doArrayReadLike(readarr);
1614 } else if (auto writearr = dyn_cast<WriteArrayOp>(op)) {
1615 doArrayWriteLike(writearr);
1616 } else if (auto extractarr = dyn_cast<ExtractArrayOp>(op)) {
1617 // Logic is essentially the same as readarr
1618 doArrayReadLike(extractarr);
1619 } else if (auto insertarr = dyn_cast<InsertArrayOp>(op)) {
1620 // Logic is essentially the same as writearr
1621 doArrayWriteLike(insertarr);
1622 } else if (auto newPod = dyn_cast<pod::NewPodOp>(op)) {
1623 Value podValue = newPod.getResult();
1624 auto podTree = ReferenceNode::create(podValue, podValue);
1625 setValTree(podValue, podTree);
1626 for (const auto &record : newPod.getInitializedRecordValues()) {
1627 Value value = translate(record.value);
1628 podTree->createChild(
1629 StringAttr::get(op->getContext(), record.name), value, copiedValueTree(value)
1630 );
1631 }
1632 readVals.push_back(podValue);
1633 } else if (auto readPod = dyn_cast<pod::ReadPodOp>(op)) {
1634 Value result = readPod.getResult();
1635 auto podTree = tryGetValTree(translate(readPod.getPodRef()));
1636 if (podTree == nullptr) {
1637 setValTree(result, ReferenceNode::create(result, result));
1638 } else {
1639 auto record = podTree->getOrCreateChild(readPod.getRecordNameAttr());
1640 if (requiresAggregateSnapshot(result.getType())) {
1641 setValTree(
1642 result, processAggregateRead(
1643 record, result, readPod.getOperation(), tryStandardAggregateReuse,
1644 rememberAggregateRead
1645 )
1646 );
1647 } else if (!record->hasStoredValue()) {
1648 record->setCurrentValue(result);
1649 setValTree(result, record);
1650 } else if (record->getStoredValue() != result) {
1651 replacementMap[result] = record->getStoredValue();
1652 } else {
1653 setValTree(result, record);
1654 }
1655 }
1656 readVals.push_back(result);
1657 } else if (auto writePod = dyn_cast<pod::WritePodOp>(op)) {
1658 auto podTree = tryGetValTree(translate(writePod.getPodRef()));
1659 if (podTree == nullptr) {
1660 return;
1661 }
1662 Value value = translate(writePod.getValue());
1663 auto record = podTree->getOrCreateChild(writePod.getRecordNameAttr());
1664 std::optional<ReferenceNode::AggregateSnapshotSource> previousSnapshot =
1665 record->getAggregateSnapshotSource();
1666 if (requiresAggregateSnapshot(value.getType()) &&
1667 isRedundantAggregateReadBackWrite(record, value, writePod.getOperation())) {
1668 redundantWrites.push_back(writePod.getOperation());
1669 return;
1670 }
1671 if (requiresAggregateSnapshot(value.getType()) &&
1672 isRedundantTreeWrite(record, previousSnapshot, value, writePod.getOperation())) {
1673 redundantWrites.push_back(writePod.getOperation());
1674 return;
1675 }
1676 podTree->clearAggregateSnapshotSource();
1677 // POD records also store aggregate snapshots rather than aliases.
1678 if (!requiresAggregateSnapshot(value.getType()) &&
1679 isRedundantTreeWrite(record, previousSnapshot, value, writePod.getOperation())) {
1680 redundantWrites.push_back(writePod.getOperation());
1681 } else {
1682 // Record writes affect this record and the enclosing POD, but not
1683 // unrelated records' copied aggregate snapshots.
1684 SmallVector<std::shared_ptr<ReferenceNode>> mutatedNodes = {podTree, record};
1685 commitTreeWrite(
1686 record, value, writePod.getOperation(), copiedValueTree(value), mutatedNodes
1687 );
1688 }
1689 } else if (hasUnknownOrNonReadEffect(op)) {
1690 // Calls and constraints consume aggregate operands by value. Other
1691 // unmodeled effects may mutate aggregate operands, so discard their
1692 // reference-tree state before a later read or write can reuse stale
1693 // contents. Other aggregate values remain valid because aggregate
1694 // copies have value-copy semantics.
1695 if (isa<CallOp, constrain::ConstraintOpInterface>(op)) {
1696 // Calls and constraints do not mutate their aggregate operands, but
1697 // they observe the complete value-copy snapshot. Retain the facts in
1698 // the tree for later read reuse while preventing a later write from
1699 // removing a write that this operation already observed.
1700 for (Value operand : op->getOperands()) {
1701 if (requiresAggregateSnapshot(operand.getType())) {
1702 if (auto valueTree = tryGetValTree(translate(operand))) {
1703 valueTree->clearLastWritesInSubtree();
1704 }
1705 }
1706 }
1707 } else {
1708 for (Value operand : op->getOperands()) {
1709 if (requiresAggregateSnapshot(operand.getType())) {
1710 // Every transparent aggregate-alias group has one canonical map
1711 // key, so invalidating one alias invalidates the whole group.
1712 eraseValTree(operand);
1713 }
1714 }
1715 }
1716 state.globals.clear();
1717 state.ram.clear();
1718 state.ramExact.clear();
1719 writeCandidates.clear();
1720 } else if (hasReadEffect(op)) {
1721 // A read does not invalidate known values, but it can observe a pending
1722 // write and therefore prevents removing that write as overwritten.
1723 writeCandidates.clear();
1724 }
1725 }
1726};
1727
1728} // namespace
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation source
Definition LICENSE.txt:28
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Definition LICENSE.txt:9
void print(llvm::raw_ostream &os) const
::mlir::Region * getCallableRegion()
Required by FunctionOpInterface.
Definition Ops.h.inc:1063
hash_code hashDynamicAPInt(const DynamicAPInt &value)
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)