//===- DenseAnalysis.h - Dense data-flow analysis -------------------------===// // // 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 implements dense data-flow analysis using the data-flow analysis // framework. The analysis is forward and conditional and uses the results of // dead code analysis to prune dead code during the analysis. // //===----------------------------------------------------------------------===// #ifndef MLIR_ANALYSIS_DENSEDATAFLOWANALYSIS_H #define MLIR_ANALYSIS_DENSEDATAFLOWANALYSIS_H #include "mlir/Analysis/DataFlowFramework.h" #include "mlir/IR/SymbolTable.h" #include "mlir/Interfaces/CallInterfaces.h" #include "mlir/Interfaces/ControlFlowInterfaces.h" namespace mlir { namespace dataflow { //===----------------------------------------------------------------------===// // CallControlFlowAction //===----------------------------------------------------------------------===// /// Indicates whether the control enters, exits, or skips over the callee (in /// the case of external functions). enum class CallControlFlowAction { EnterCallee, ExitCallee, ExternalCallee }; //===----------------------------------------------------------------------===// // AbstractDenseLattice //===----------------------------------------------------------------------===// /// This class represents a dense lattice. A dense lattice is attached to /// program point to represent the program state at the program point. /// lattice is propagated through the IR by dense data-flow analysis. class AbstractDenseLattice : public AnalysisState { public: /// A dense lattice can only be created for operations and blocks. using AnalysisState::AnalysisState; /// Join the lattice across control-flow or callgraph edges. virtual ChangeResult join(const AbstractDenseLattice &rhs) { return ChangeResult::NoChange; } virtual ChangeResult meet(const AbstractDenseLattice &rhs) { return ChangeResult::NoChange; } }; //===----------------------------------------------------------------------===// // AbstractDenseForwardDataFlowAnalysis //===----------------------------------------------------------------------===// /// Base class for dense forward data-flow analyses. Dense data-flow analysis /// attaches a lattice to program points and implements a transfer function from /// the lattice before each operation to the lattice after. The lattice contains /// information about the state of the program at that program point. /// /// Visit a program point in forward dense data-flow analysis will invoke the /// transfer function of the operation preceding the program point iterator. /// Visit a program point at the begining of block will visit the block itself. class AbstractDenseForwardDataFlowAnalysis : public DataFlowAnalysis { public: using DataFlowAnalysis::DataFlowAnalysis; /// Initialize the analysis by visiting every program point whose execution /// may modify the program state; that is, every operation and block. LogicalResult initialize(Operation *top) override; /// Initialize lattice anchor equivalence class from the provided top-level /// operation. /// /// This function will union lattice anchor to same equivalent class if the /// analysis can determine the lattice content of lattice anchor is /// necessarily identical under the corrensponding lattice type. virtual void initializeEquivalentLatticeAnchor(Operation *top) override; /// Visit a program point that modifies the state of the program. If the /// program point is at the beginning of a block, then the state is propagated /// from control-flow predecessors or callsites. If the operation before /// program point iterator is a call operation or region control-flow /// operation, then the state after the execution of the operation is set by /// control-flow or the callgraph. Otherwise, this function invokes the /// operation transfer function before the program point iterator. LogicalResult visit(ProgramPoint *point) override; protected: /// Propagate the dense lattice before the execution of an operation to the /// lattice after its execution. virtual LogicalResult visitOperationImpl(Operation *op, const AbstractDenseLattice &before, AbstractDenseLattice *after) = 0; /// Get the dense lattice on the given lattice anchor. virtual AbstractDenseLattice *getLattice(LatticeAnchor anchor) = 0; /// Get the dense lattice on the given lattice anchor and add dependent as its /// dependency. That is, every time the lattice after anchor is updated, the /// dependent program point must be visited, and the newly triggered visit /// might update the lattice on dependent. virtual const AbstractDenseLattice *getLatticeFor(ProgramPoint *dependent, LatticeAnchor anchor) = 0; /// Set the dense lattice at control flow entry point and propagate an update /// if it changed. virtual void setToEntryState(AbstractDenseLattice *lattice) = 0; /// Join a lattice with another and propagate an update if it changed. void join(AbstractDenseLattice *lhs, const AbstractDenseLattice &rhs) { propagateIfChanged(lhs, lhs->join(rhs)); } /// Visit an operation. If this is a call operation or region control-flow /// operation, then the state after the execution of the operation is set by /// control-flow or the callgraph. Otherwise, this function invokes the /// operation transfer function. virtual LogicalResult processOperation(Operation *op); /// Visit an operation. If this analysis can confirm that lattice content /// of lattice anchors around operation are necessarily identical, join /// them into the same equivalent class. virtual void buildOperationEquivalentLatticeAnchor(Operation *op) {} /// Visit a block and propagate the dense lattice forward along the control /// flow edge from predecessor to block. `point` corresponds to the program /// point before `block`. The default implementation merges in the state from /// the predecessor's terminator. virtual void visitBlockTransfer(Block *block, ProgramPoint *point, Block *predecessor, const AbstractDenseLattice &before, AbstractDenseLattice *after) { // Merge in the state from the predecessor's terminator. join(after, before); } /// Propagate the dense lattice forward along the control flow edge from /// `regionFrom` to `regionTo` regions of the `branch` operation. `nullopt` /// values correspond to control flow branches originating at or targeting the /// `branch` operation itself. Default implementation just joins the states, /// meaning that operations implementing `RegionBranchOpInterface` don't have /// any effect on the lattice that isn't already expressed by the interface /// itself. virtual void visitRegionBranchControlFlowTransfer( RegionBranchOpInterface branch, std::optional regionFrom, std::optional regionTo, const AbstractDenseLattice &before, AbstractDenseLattice *after) { join(after, before); } /// Propagate the dense lattice forward along the call control flow edge, /// which can be either entering or exiting the callee. Default implementation /// for enter and exit callee actions just meets the states, meaning that /// operations implementing `CallOpInterface` don't have any effect on the /// lattice that isn't already expressed by the interface itself. Default /// implementation for the external callee action additionally sets the /// "after" lattice to the entry state. virtual void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const AbstractDenseLattice &before, AbstractDenseLattice *after) { join(after, before); // Note that `setToEntryState` may be a "partial fixpoint" for some // lattices, e.g., lattices that are lists of maps of other lattices will // only set fixpoint for "known" lattices. if (action == CallControlFlowAction::ExternalCallee) setToEntryState(after); } /// Visit a program point within a region branch operation with predecessors /// in it. This can either be an entry block of one of the regions of the /// parent operation itself. void visitRegionBranchOperation(ProgramPoint *point, RegionBranchOpInterface branch, AbstractDenseLattice *after); private: /// Visit a block. The state at the start of the block is propagated from /// control-flow predecessors or callsites. void visitBlock(Block *block); /// Visit an operation for which the data flow is described by the /// `CallOpInterface`. void visitCallOperation(CallOpInterface call, const AbstractDenseLattice &before, AbstractDenseLattice *after); }; //===----------------------------------------------------------------------===// // DenseForwardDataFlowAnalysis //===----------------------------------------------------------------------===// /// A dense forward data-flow analysis for propagating lattices before and /// after the execution of every operation across the IR by implementing /// transfer functions for operations. /// /// `LatticeT` is expected to be a subclass of `AbstractDenseLattice`. template class DenseForwardDataFlowAnalysis : public AbstractDenseForwardDataFlowAnalysis { static_assert( std::is_base_of::value, "analysis state class expected to subclass AbstractDenseLattice"); public: using AbstractDenseForwardDataFlowAnalysis:: AbstractDenseForwardDataFlowAnalysis; /// Visit an operation with the dense lattice before its execution. This /// function is expected to set the dense lattice after its execution and /// trigger change propagation in case of change. virtual LogicalResult visitOperation(Operation *op, const LatticeT &before, LatticeT *after) = 0; /// Hook for customizing the behavior of lattice propagation along the call /// control flow edges. Two types of (forward) propagation are possible here: /// - `action == CallControlFlowAction::Enter` indicates that: /// - `before` is the state before the call operation; /// - `after` is the state at the beginning of the callee entry block; /// - `action == CallControlFlowAction::Exit` indicates that: /// - `before` is the state at the end of a callee exit block; /// - `after` is the state after the call operation. /// By default, the `after` state is simply joined with the `before` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. Specifically, if the `call` op may affect /// the lattice prior to entering the callee, the custom behavior can be added /// for `action == CallControlFlowAction::Enter`. If the `call` op may affect /// the lattice post exiting the callee, the custom behavior can be added for /// `action == CallControlFlowAction::Exit`. virtual void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const LatticeT &before, LatticeT *after) { AbstractDenseForwardDataFlowAnalysis::visitCallControlFlowTransfer( call, action, before, after); } /// Hook for customizing the behavior of lattice propagation along the control /// flow edges between regions and their parent op. The control flows from /// `regionFrom` to `regionTo`, both of which may be `nullopt` to indicate the /// parent op. The lattice is propagated forward along this edge. The lattices /// are as follows: /// - `before:` /// - if `regionFrom` is a region, this is the lattice at the end of the /// block that exits the region; note that for multi-exit regions, the /// lattices are equal at the end of all exiting blocks, but they are /// associated with different program points. /// - otherwise, this is the lattice before the parent op. /// - `after`: /// - if `regionTo` is a region, this is the lattice at the beginning of /// the entry block of that region; /// - otherwise, this is the lattice after the parent op. /// By default, the `after` state is simply joined with the `before` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. Specifically, if the `branch` op may affect /// the lattice before entering any region, the custom behavior can be added /// for `regionFrom == nullopt`. If the `branch` op may affect the lattice /// after all terminated, the custom behavior can be added for `regionTo == /// nullptr`. The behavior can be further refined for specific pairs of "from" /// and "to" regions. virtual void visitRegionBranchControlFlowTransfer( RegionBranchOpInterface branch, std::optional regionFrom, std::optional regionTo, const LatticeT &before, LatticeT *after) { AbstractDenseForwardDataFlowAnalysis::visitRegionBranchControlFlowTransfer( branch, regionFrom, regionTo, before, after); } /// Hook for customizing the behavior of lattice propagation along the control /// flow edges between blocks. The control flows from `predecessor` to /// `block`. The lattice is propagated forward along this edge. The lattices /// are as follows: /// - `before` is the lattice at the end of the predecessor block; /// - `after` is the lattice at the beginning of the block. /// By default, the `after` state is simply joined with the `before` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. virtual void visitBlockTransfer(Block *block, ProgramPoint *point, Block *predecessor, const LatticeT &before, LatticeT *after) { AbstractDenseForwardDataFlowAnalysis::visitBlockTransfer( block, point, predecessor, before, after); } protected: /// Get the dense lattice on this lattice anchor. LatticeT *getLattice(LatticeAnchor anchor) override { return getOrCreate(anchor); } /// Get the dense lattice on the given lattice anchor and add dependent as its /// dependency. That is, every time the lattice after anchor is updated, the /// dependent program point must be visited, and the newly triggered visit /// might update the lattice on dependent. const AbstractDenseLattice *getLatticeFor(ProgramPoint *dependent, LatticeAnchor anchor) override { return getOrCreateFor(dependent, anchor); } /// Set the dense lattice at control flow entry point and propagate an update /// if it changed. virtual void setToEntryState(LatticeT *lattice) = 0; void setToEntryState(AbstractDenseLattice *lattice) override { setToEntryState(static_cast(lattice)); } /// Type-erased wrappers that convert the abstract dense lattice to a derived /// lattice and invoke the virtual hooks operating on the derived lattice. LogicalResult visitOperationImpl(Operation *op, const AbstractDenseLattice &before, AbstractDenseLattice *after) final { return visitOperation(op, static_cast(before), static_cast(after)); } void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const AbstractDenseLattice &before, AbstractDenseLattice *after) final { visitCallControlFlowTransfer(call, action, static_cast(before), static_cast(after)); } void visitRegionBranchControlFlowTransfer(RegionBranchOpInterface branch, std::optional regionFrom, std::optional regionTo, const AbstractDenseLattice &before, AbstractDenseLattice *after) final { visitRegionBranchControlFlowTransfer(branch, regionFrom, regionTo, static_cast(before), static_cast(after)); } void visitBlockTransfer(Block *block, ProgramPoint *point, Block *predecessor, const AbstractDenseLattice &before, AbstractDenseLattice *after) final { visitBlockTransfer(block, point, predecessor, static_cast(before), static_cast(after)); } }; //===----------------------------------------------------------------------===// // AbstractDenseBackwardDataFlowAnalysis //===----------------------------------------------------------------------===// /// Base class for dense backward dataflow analyses. Such analyses attach a /// lattice to program point and implement a transfer function from the lattice /// after the operation to the lattice before it, thus propagating backward. /// /// Visit a program point in dense backward data-flow analysis will invoke the /// transfer function of the operation following the program point iterator. /// Visit a program point at the end of block will visit the block itself. class AbstractDenseBackwardDataFlowAnalysis : public DataFlowAnalysis { public: /// Construct the analysis in the given solver. Takes a symbol table /// collection that is used to cache symbol resolution in interprocedural part /// of the analysis. The symbol table need not be prefilled. AbstractDenseBackwardDataFlowAnalysis(DataFlowSolver &solver, SymbolTableCollection &symbolTable) : DataFlowAnalysis(solver), symbolTable(symbolTable) {} /// Initialize the analysis by visiting every program point whose execution /// may modify the program state; that is, every operation and block. LogicalResult initialize(Operation *top) override; /// Initialize lattice anchor equivalence class from the provided top-level /// operation. /// /// This function will union lattice anchor to same equivalent class if the /// analysis can determine the lattice content of lattice anchor is /// necessarily identical under the corrensponding lattice type. virtual void initializeEquivalentLatticeAnchor(Operation *top) override; /// Visit a program point that modifies the state of the program. The state is /// propagated along control flow directions for branch-, region- and /// call-based control flow using the respective interfaces. For other /// operations, the state is propagated using the transfer function /// (visitOperation). /// /// Note: the transfer function is currently *not* invoked before operations /// with region or call interface, but *is* invoked before block terminators. LogicalResult visit(ProgramPoint *point) override; protected: /// Propagate the dense lattice after the execution of an operation to the /// lattice before its execution. virtual LogicalResult visitOperationImpl(Operation *op, const AbstractDenseLattice &after, AbstractDenseLattice *before) = 0; /// Get the dense lattice before the execution of the lattice anchor. That is, /// before the execution of the given operation or after the execution of the /// block. virtual AbstractDenseLattice *getLattice(LatticeAnchor anchor) = 0; /// Get the dense lattice on the given lattice anchor and add dependent as its /// dependency. That is, every time the lattice after anchor is updated, the /// dependent program point must be visited, and the newly triggered visit /// might update the lattice before dependent. virtual const AbstractDenseLattice *getLatticeFor(ProgramPoint *dependent, LatticeAnchor anchor) = 0; /// Set the dense lattice before at the control flow exit point and propagate /// the update if it changed. virtual void setToExitState(AbstractDenseLattice *lattice) = 0; /// Meet a lattice with another lattice and propagate an update if it changed. void meet(AbstractDenseLattice *lhs, const AbstractDenseLattice &rhs) { propagateIfChanged(lhs, lhs->meet(rhs)); } /// Visit an operation. Dispatches to specialized methods for call or region /// control-flow operations. Otherwise, this function invokes the operation /// transfer function. virtual LogicalResult processOperation(Operation *op); /// Visit an operation. If this analysis can confirm that lattice content /// of lattice anchors around operation are necessarily identical, join /// them into the same equivalent class. virtual void buildOperationEquivalentLatticeAnchor(Operation *op) {} /// Visit a block and propagate the dense lattice backward along the control /// flow edge from successor to block. `point` corresponds to the program /// point after `block`. The default implementation merges in the state from /// the successor's first operation or the block itself when empty. virtual void visitBlockTransfer(Block *block, ProgramPoint *point, Block *successor, const AbstractDenseLattice &after, AbstractDenseLattice *before) { meet(before, after); } /// Propagate the dense lattice backwards along the control flow edge from /// `regionFrom` to `regionTo` regions of the `branch` operation. `nullopt` /// values correspond to control flow branches originating at or targeting the /// `branch` operation itself. Default implementation just meets the states, /// meaning that operations implementing `RegionBranchOpInterface` don't have /// any effect on the lattice that isn't already expressed by the interface /// itself. virtual void visitRegionBranchControlFlowTransfer( RegionBranchOpInterface branch, RegionBranchPoint regionFrom, RegionSuccessor regionTo, const AbstractDenseLattice &after, AbstractDenseLattice *before) { meet(before, after); } /// Propagate the dense lattice backwards along the call control flow edge, /// which can be either entering or exiting the callee. Default implementation /// for enter and exit callee action just meets the states, meaning that /// operations implementing `CallOpInterface` don't have any effect on the /// lattice that isn't already expressed by the interface itself. Default /// implementation for external callee action additional sets the result to /// the exit (fixpoint) state. virtual void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const AbstractDenseLattice &after, AbstractDenseLattice *before) { meet(before, after); // Note that `setToExitState` may be a "partial fixpoint" for some lattices, // e.g., lattices that are lists of maps of other lattices will only // set fixpoint for "known" lattices. if (action == CallControlFlowAction::ExternalCallee) setToExitState(before); } private: /// Visit a block. The state and the end of the block is propagated from /// control-flow successors of the block or callsites. void visitBlock(Block *block); /// Visit a program point within a region branch operation with successors /// (from which the state is propagated) in or after it. `regionNo` indicates /// the region that contains the successor, `nullopt` indicating the successor /// of the branch operation itself. void visitRegionBranchOperation(ProgramPoint *point, RegionBranchOpInterface branch, RegionBranchPoint branchPoint, AbstractDenseLattice *before); /// Visit an operation for which the data flow is described by the /// `CallOpInterface`. Performs inter-procedural data flow as follows: /// /// - find the callable (resolve via the symbol table), /// - get the entry block of the callable region, /// - take the state before the first operation if present or at block end /// otherwise, /// - meet that state with the state before the call-like op, or use the /// custom logic if overridden by concrete analyses. void visitCallOperation(CallOpInterface call, const AbstractDenseLattice &after, AbstractDenseLattice *before); /// Symbol table for call-level control flow. SymbolTableCollection &symbolTable; }; //===----------------------------------------------------------------------===// // DenseBackwardDataFlowAnalysis //===----------------------------------------------------------------------===// /// A dense backward dataflow analysis propagating lattices after and before the /// execution of every operation across the IR by implementing transfer /// functions for operations. /// /// `LatticeT` is expected to be a subclass of `AbstractDenseLattice`. template class DenseBackwardDataFlowAnalysis : public AbstractDenseBackwardDataFlowAnalysis { static_assert(std::is_base_of_v, "analysis state expected to subclass AbstractDenseLattice"); public: using AbstractDenseBackwardDataFlowAnalysis:: AbstractDenseBackwardDataFlowAnalysis; /// Transfer function. Visits an operation with the dense lattice after its /// execution. This function is expected to set the dense lattice before its /// execution and trigger propagation in case of change. virtual LogicalResult visitOperation(Operation *op, const LatticeT &after, LatticeT *before) = 0; /// Hook for customizing the behavior of lattice propagation along the call /// control flow edges. Two types of (back) propagation are possible here: /// - `action == CallControlFlowAction::Enter` indicates that: /// - `after` is the state at the top of the callee entry block; /// - `before` is the state before the call operation; /// - `action == CallControlFlowAction::Exit` indicates that: /// - `after` is the state after the call operation; /// - `before` is the state of exit blocks of the callee. /// By default, the `before` state is simply met with the `after` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. Specifically, if the `call` op may affect /// the lattice prior to entering the callee, the custom behavior can be added /// for `action == CallControlFlowAction::Enter`. If the `call` op may affect /// the lattice post exiting the callee, the custom behavior can be added for /// `action == CallControlFlowAction::Exit`. virtual void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const LatticeT &after, LatticeT *before) { AbstractDenseBackwardDataFlowAnalysis::visitCallControlFlowTransfer( call, action, after, before); } /// Hook for customizing the behavior of lattice propagation along the control /// flow edges between regions and their parent op. The control flows from /// `regionFrom` to `regionTo`, both of which may be `nullopt` to indicate the /// parent op. The lattice is propagated back along this edge. The lattices /// are as follows: /// - `after`: /// - if `regionTo` is a region, this is the lattice at the beginning of /// the entry block of that region; /// - otherwise, this is the lattice after the parent op. /// - `before:` /// - if `regionFrom` is a region, this is the lattice at the end of the /// block that exits the region; note that for multi-exit regions, the /// lattices are equal at the end of all exiting blocks, but they are /// associated with different program points. /// - otherwise, this is the lattice before the parent op. /// By default, the `before` state is simply met with the `after` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. Specifically, if the `branch` op may affect /// the lattice before entering any region, the custom behavior can be added /// for `regionFrom == nullopt`. If the `branch` op may affect the lattice /// after all terminated, the custom behavior can be added for `regionTo == /// nullptr`. The behavior can be further refined for specific pairs of "from" /// and "to" regions. virtual void visitRegionBranchControlFlowTransfer( RegionBranchOpInterface branch, RegionBranchPoint regionFrom, RegionSuccessor regionTo, const LatticeT &after, LatticeT *before) { AbstractDenseBackwardDataFlowAnalysis::visitRegionBranchControlFlowTransfer( branch, regionFrom, regionTo, after, before); } /// Hook for customizing the behavior of lattice propagation along the control /// flow edges between blocks. The control flows from `successor` to /// `block`. The lattice is propagated back along this edge. The lattices /// are as follows: /// - `after` is the lattice at the beginning of the successor block; /// - `before` is the lattice at the end of the block. /// By default, the `before` state is simply met with the `after` state. /// Concrete analyses can override this behavior or delegate to the parent /// call for the default behavior. virtual void visitBlockTransfer(Block *block, ProgramPoint *point, Block *successor, const LatticeT &after, LatticeT *before) { AbstractDenseBackwardDataFlowAnalysis::visitBlockTransfer( block, point, successor, after, before); } protected: /// Get the dense lattice at the given lattice anchor. LatticeT *getLattice(LatticeAnchor anchor) override { return getOrCreate(anchor); } /// Get the dense lattice on the given lattice anchor and add dependent as its /// dependency. That is, every time the lattice after anchor is updated, the /// dependent program point must be visited, and the newly triggered visit /// might update the lattice before dependent. virtual const AbstractDenseLattice * getLatticeFor(ProgramPoint *dependent, LatticeAnchor anchor) override { return getOrCreateFor(dependent, anchor); } /// Set the dense lattice at control flow exit point (after the terminator) /// and propagate an update if it changed. virtual void setToExitState(LatticeT *lattice) = 0; void setToExitState(AbstractDenseLattice *lattice) final { setToExitState(static_cast(lattice)); } /// Type-erased wrappers that convert the abstract dense lattice to a derived /// lattice and invoke the virtual hooks operating on the derived lattice. LogicalResult visitOperationImpl(Operation *op, const AbstractDenseLattice &after, AbstractDenseLattice *before) final { return visitOperation(op, static_cast(after), static_cast(before)); } void visitCallControlFlowTransfer(CallOpInterface call, CallControlFlowAction action, const AbstractDenseLattice &after, AbstractDenseLattice *before) final { visitCallControlFlowTransfer(call, action, static_cast(after), static_cast(before)); } void visitRegionBranchControlFlowTransfer( RegionBranchOpInterface branch, RegionBranchPoint regionForm, RegionSuccessor regionTo, const AbstractDenseLattice &after, AbstractDenseLattice *before) final { visitRegionBranchControlFlowTransfer(branch, regionForm, regionTo, static_cast(after), static_cast(before)); } void visitBlockTransfer(Block *block, ProgramPoint *point, Block *successor, const AbstractDenseLattice &after, AbstractDenseLattice *before) final { visitBlockTransfer(block, point, successor, static_cast(after), static_cast(before)); } }; } // end namespace dataflow } // end namespace mlir #endif // MLIR_ANALYSIS_DENSEDATAFLOWANALYSIS_H