//===- CheckUses.cpp - Expensive transform value validity checks ----------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This file defines a pass that performs expensive opt-in checks for Transform // dialect values being potentially used after they have been consumed. // //===----------------------------------------------------------------------===// #include "mlir/Dialect/Transform/Transforms/Passes.h" #include "mlir/Dialect/Transform/Interfaces/TransformInterfaces.h" #include "mlir/Interfaces/SideEffectInterfaces.h" #include "mlir/Pass/Pass.h" #include "llvm/ADT/SetOperations.h" namespace mlir { namespace transform { #define GEN_PASS_DEF_CHECKUSESPASS #include "mlir/Dialect/Transform/Transforms/Passes.h.inc" } // namespace transform } // namespace mlir using namespace mlir; namespace { /// Returns a reference to a cached set of blocks that are reachable from the /// given block via edges computed by the `getNextNodes` function. For example, /// if `getNextNodes` returns successors of a block, this will return the set of /// reachable blocks; if it returns predecessors of a block, this will return /// the set of blocks from which the given block can be reached. The block is /// considered reachable form itself only if there is a cycle. template const llvm::SmallPtrSet & getReachableImpl(Block *block, FnTy getNextNodes, DenseMap> &cache) { auto [it, inserted] = cache.try_emplace(block); if (!inserted) return it->getSecond(); llvm::SmallPtrSet &reachable = it->second; SmallVector worklist; worklist.push_back(block); while (!worklist.empty()) { Block *current = worklist.pop_back_val(); for (Block *predecessor : getNextNodes(current)) { // The block is reachable from its transitive predecessors. Only add // them to the worklist if they weren't already visited. if (reachable.insert(predecessor).second) worklist.push_back(predecessor); } } return reachable; } /// An analysis that identifies whether a value allocated by a Transform op may /// be used by another such op after it may have been freed by a third op on /// some control flow path. This is conceptually similar to a data flow /// analysis, but relies on side effects related to particular values that /// currently cannot be modeled by the MLIR data flow analysis framework (also, /// the lattice element would be rather expensive as it would need to include /// live and/or freed values for each operation). /// /// This analysis is conservatively pessimisic: it will consider that a value /// may be freed if it is freed on any possible control flow path between its /// allocation and a relevant use, even if the control never actually flows /// through the operation that frees the value. It also does not differentiate /// between may- (freed on at least one control flow path) and must-free (freed /// on all possible control flow paths) because it would require expensive graph /// algorithms. /// /// It is intended as an additional non-blocking verification or debugging aid /// for ops in the Transform dialect. It leverages the requirement for Transform /// dialect ops to implement the MemoryEffectsOpInterface, and expects the /// values in the Transform IR to have an allocation effect on the /// TransformMappingResource when defined. class TransformOpMemFreeAnalysis { public: MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TransformOpMemFreeAnalysis) /// Computes the analysis for Transform ops nested in the given operation. explicit TransformOpMemFreeAnalysis(Operation *root) { root->walk([&](Operation *op) { if (isa(op)) { collectFreedValues(op); return WalkResult::skip(); } return WalkResult::advance(); }); } /// A list of operations that may be deleting a value. Non-empty list /// contextually converts to boolean "true" value. class PotentialDeleters { public: /// Creates an empty list that corresponds to the value being live. static PotentialDeleters live() { return PotentialDeleters({}); } /// Creates a list from the operations that may be deleting the value. static PotentialDeleters maybeFreed(ArrayRef deleters) { return PotentialDeleters(deleters); } /// Converts to "true" if there are operations that may be deleting the /// value. explicit operator bool() const { return !deleters.empty(); } /// Concatenates the lists of operations that may be deleting the value. The /// value is known to be live if the reuslting list is still empty. PotentialDeleters &operator|=(const PotentialDeleters &other) { llvm::append_range(deleters, other.deleters); return *this; } /// Returns the list of ops that may be deleting the value. ArrayRef getOps() const { return deleters; } private: /// Constructs the list from the given operations. explicit PotentialDeleters(ArrayRef ops) { llvm::append_range(deleters, ops); } /// The list of operations that may be deleting the value. SmallVector deleters; }; /// Returns the list of operations that may be deleting the operand value on /// any control flow path between the definition of the value and its use as /// the given operand. For the purposes of this analysis, the value is /// considered to be allocated at its definition point and never re-allocated. PotentialDeleters isUseLive(OpOperand &operand) { const llvm::SmallPtrSet &deleters = freedBy[operand.get()]; if (deleters.empty()) return live(); #ifndef NDEBUG // Check that the definition point actually allocates the value. If the // definition is a block argument, it may be just forwarding the operand of // the parent op without doing a new allocation, allow that. We currently // don't have the capability to analyze region-based control flow here. // // TODO: when this ported to the dataflow analysis infra, we should have // proper support for region-based control flow. Operation *valueSource = isa(operand.get()) ? operand.get().getDefiningOp() : operand.get().getParentBlock()->getParentOp(); auto iface = cast(valueSource); SmallVector instances; iface.getEffectsOnResource(transform::TransformMappingResource::get(), instances); assert((isa(operand.get()) || hasEffect(instances, operand.get())) && "expected the op defining the value to have an allocation effect " "on it"); #endif // Collect ancestors of the use operation. Block *defBlock = operand.get().getParentBlock(); SmallVector ancestors; Operation *ancestor = operand.getOwner(); do { ancestors.push_back(ancestor); if (ancestor->getParentRegion() == defBlock->getParent()) break; ancestor = ancestor->getParentOp(); } while (true); std::reverse(ancestors.begin(), ancestors.end()); // Consider the control flow from the definition point of the value to its // use point. If the use is located in some nested region, consider the path // from the entry block of the region to the use. for (Operation *ancestor : ancestors) { // The block should be considered partially if it is the block that // contains the definition (allocation) of the value being used, and the // value is defined in the middle of the block, i.e., is not a block // argument. bool isOutermost = ancestor == ancestors.front(); bool isFromBlockPartial = isOutermost && isa(operand.get()); // Check if the value may be freed by operations between its definition // (allocation) point in its block and the terminator of the block or the // ancestor of the use if it is located in the same block. This is only // done for partial blocks here, full blocks will be considered below // similarly to other blocks. if (isFromBlockPartial) { bool defUseSameBlock = ancestor->getBlock() == defBlock; // Consider all ops from the def to its block terminator, except the // when the use is in the same block, in which case only consider the // ops until the user. if (PotentialDeleters potentialDeleters = isFreedInBlockAfter( operand.get().getDefiningOp(), operand.get(), defUseSameBlock ? ancestor : nullptr)) return potentialDeleters; } // Check if the value may be freed by opeations preceding the ancestor in // its block. Skip the check for partial blocks that contain both the // definition and the use point, as this has been already checked above. if (!isFromBlockPartial || ancestor->getBlock() != defBlock) { if (PotentialDeleters potentialDeleters = isFreedInBlockBefore(ancestor, operand.get())) return potentialDeleters; } // Check if the value may be freed by operations in any of the blocks // between the definition point (in the outermost region) or the entry // block of the region (in other regions) and the operand or its ancestor // in the region. This includes the entire "form" block if (1) the block // has not been considered as partial above and (2) the block can be // reached again through some control-flow loop. This includes the entire // "to" block if it can be reached form itself through some control-flow // cycle, regardless of whether it has been visited before. Block *ancestorBlock = ancestor->getBlock(); Block *from = isOutermost ? defBlock : &ancestorBlock->getParent()->front(); if (PotentialDeleters potentialDeleters = isMaybeFreedOnPaths(from, ancestorBlock, operand.get(), /*alwaysIncludeFrom=*/!isFromBlockPartial)) return potentialDeleters; } return live(); } private: /// Make PotentialDeleters constructors available with shorter names. static PotentialDeleters maybeFreed(ArrayRef deleters) { return PotentialDeleters::maybeFreed(deleters); } static PotentialDeleters live() { return PotentialDeleters::live(); } /// Returns the list of operations that may be deleting the given value betwen /// the first and last operations, non-inclusive. `getNext` indicates the /// direction of the traversal. PotentialDeleters isFreedBetween(Value value, Operation *first, Operation *last, llvm::function_ref getNext) const { auto it = freedBy.find(value); if (it == freedBy.end()) return live(); const llvm::SmallPtrSet &deleters = it->getSecond(); for (Operation *op = getNext(first); op != last; op = getNext(op)) { if (deleters.contains(op)) return maybeFreed(op); } return live(); } /// Returns the list of operations that may be deleting the given value /// between `root` and `before` values. `root` is expected to be in the same /// block as `before` and precede it. If `before` is null, consider all /// operations until the end of the block including the terminator. PotentialDeleters isFreedInBlockAfter(Operation *root, Value value, Operation *before = nullptr) const { return isFreedBetween(value, root, before, [](Operation *op) { return op->getNextNode(); }); } /// Returns the list of operations that may be deleting the given value /// between the entry of the block and the `root` operation. PotentialDeleters isFreedInBlockBefore(Operation *root, Value value) const { return isFreedBetween(value, root, nullptr, [](Operation *op) { return op->getPrevNode(); }); } /// Returns the list of operations that may be deleting the given value on /// any of the control flow paths between the "form" and the "to" block. The /// operations from any block visited on any control flow path are /// consdiered. The "from" block is considered if there is a control flow /// cycle going through it, i.e., if there is a possibility that all /// operations in this block are visited or if the `alwaysIncludeFrom` flag is /// set. The "to" block is considered only if there is a control flow cycle /// going through it. PotentialDeleters isMaybeFreedOnPaths(Block *from, Block *to, Value value, bool alwaysIncludeFrom) { // Find all blocks that lie on any path between "from" and "to", i.e., the // intersection of blocks reachable from "from" and blocks from which "to" // is rechable. const llvm::SmallPtrSet &sources = getReachableFrom(to); if (!sources.contains(from)) return live(); llvm::SmallPtrSet reachable(getReachable(from)); llvm::set_intersect(reachable, sources); // If requested, include the "from" block that may not be present in the set // of visited blocks when there is no cycle going through it. if (alwaysIncludeFrom) reachable.insert(from); // Join potential deleters from all blocks as we don't know here which of // the paths through the control flow is taken. PotentialDeleters potentialDeleters = live(); for (Block *block : reachable) { for (Operation &op : *block) { if (freedBy[value].count(&op)) potentialDeleters |= maybeFreed(&op); } } return potentialDeleters; } /// Popualtes `reachable` with the set of blocks that are rechable from the /// given block. A block is considered reachable from itself if there is a /// cycle in the control-flow graph that invovles the block. const llvm::SmallPtrSet &getReachable(Block *block) { return getReachableImpl( block, [](Block *b) { return b->getSuccessors(); }, reachableCache); } /// Populates `sources` with the set of blocks from which the given block is /// reachable. const llvm::SmallPtrSet &getReachableFrom(Block *block) { return getReachableImpl( block, [](Block *b) { return b->getPredecessors(); }, reachableFromCache); } /// Returns true of `instances` contains an effect of `EffectTy` on `value`. template static bool hasEffect(ArrayRef instances, Value value) { return llvm::any_of(instances, [&](const MemoryEffects::EffectInstance &instance) { return instance.getValue() == value && isa(instance.getEffect()); }); } /// Records the values that are being freed by an operation or any of its /// children in `freedBy`. void collectFreedValues(Operation *root) { SmallVector instances; root->walk([&](Operation *child) { if (isa(child)) return; // TODO: extend this to conservatively handle operations with undeclared // side effects as maybe freeing the operands. auto iface = cast(child); instances.clear(); iface.getEffectsOnResource(transform::TransformMappingResource::get(), instances); for (Value operand : child->getOperands()) { if (hasEffect(instances, operand)) { // All parents of the operation that frees a value should be // considered as potentially freeing the value as well. // // TODO: differentiate between must-free/may-free as well as between // this op having the effect and children having the effect. This may // require some analysis of all control flow paths through the nested // regions as well as a mechanism to separate proper side effects from // those obtained by nesting. Operation *parent = child; do { freedBy[operand].insert(parent); if (parent == root) break; parent = parent->getParentOp(); } while (true); } } }); } /// The mapping from a value to operations that have a Free memory effect on /// the TransformMappingResource and associated with this value, or to /// Transform operations transitively containing such operations. DenseMap> freedBy; /// Caches for sets of reachable blocks. DenseMap> reachableCache; DenseMap> reachableFromCache; }; //// A simple pass that warns about any use of a value by a transform operation // that may be using the value after it has been freed. class CheckUsesPass : public transform::impl::CheckUsesPassBase { public: void runOnOperation() override { auto &analysis = getAnalysis(); getOperation()->walk([&](Operation *child) { for (OpOperand &operand : child->getOpOperands()) { TransformOpMemFreeAnalysis::PotentialDeleters deleters = analysis.isUseLive(operand); if (!deleters) continue; InFlightDiagnostic diag = child->emitWarning() << "operand #" << operand.getOperandNumber() << " may be used after free"; diag.attachNote(operand.get().getLoc()) << "allocated here"; for (Operation *d : deleters.getOps()) { diag.attachNote(d->getLoc()) << "freed here"; } } }); } }; } // namespace