Program Listing for File PragmaInsertion.cpp

↰ Return to documentation for file (lib/Dialect/EmitHLS/Transforms/PragmaInsertion.cpp)

// Implementation of PragmaInsertion transform pass.
//
// @file
// @author     Jiahong Bi (jiahong.bi@tu-dresden.de)

#include "laksa-mlir/Dialect/EmitHLS/IR/EmitHLS.h"
#include "laksa-mlir/Dialect/EmitHLS/IR/EmitHLSOps.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/IRMapping.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/SymbolTable.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/DialectConversion.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"

#include <llvm/ADT/TypeSwitch.h>
#include <llvm/Support/Debug.h>
#include <mlir/IR/BuiltinAttributes.h>
#include <optional>

#define DEBUG_TYPE "emithls-pragma-insertion"
#define LAKSA_DEBUG(X)                                                         \
    LLVM_DEBUG(                                                                \
        llvm::dbgs() << "[emithls-pragma-insertion] "; X;                      \
        llvm::dbgs() << "\n")

using namespace mlir;
using namespace emithls;

namespace mlir {
namespace emithls {
#define GEN_PASS_DEF_EMITHLSPRAGMAINSERTION
#include "laksa-mlir/Dialect/EmitHLS/Transforms/Passes.h.inc"
} // namespace emithls
} // namespace mlir

namespace {

// True if "op" carries any "dse."-prefixed attribute PragmaDSE attached,
// the signal that it still needs a real pragma inserted in its place.
bool hasDSEAttr(Operation* op)
{
    return llvm::any_of(op->getAttrs(), [](NamedAttribute attr) {
        return attr.getName().getValue().starts_with("dse.");
    });
}

// A loop marked "dse.io" with no "dse.factor" is just an outer word loop,
// so the marker is dropped in place. One with "dse.factor" is a channel
// loop whose trip count still reflects the port's old width: a
// "factor"-wide loop replaces it in place, and the leftover
// "tripCount / factor" multiplies into the parent loop's own trip count
// instead of wrapping it, so the parent's index keeps addressing correctly.
struct SplitIOLoop : public OpConversionPattern<ForOp> {
    using OpConversionPattern::OpConversionPattern;

    LogicalResult matchAndRewrite(
        ForOp op,
        OpAdaptor,
        ConversionPatternRewriter &rewriter) const override
    {
        if (!op->hasAttr("dse.io")) return failure();
        Location loc = op.getLoc();
        LAKSA_DEBUG(
            llvm::dbgs() << "Rewriting loop inside io function at " << loc);

        auto factorAttr = op->getAttrOfType<IntegerAttr>("dse.factor");
        if (!factorAttr) {
            LAKSA_DEBUG(
                llvm::dbgs() << "  Dropping bare dse.io tag at " << loc);
            rewriter.modifyOpInPlace(op, [&]() { op->removeAttr("dse.io"); });
            return success();
        }

        int64_t factor = factorAttr.getInt();
        int64_t tripCount = op.getTripCount();
        int64_t outerTrip = tripCount / factor;
        LAKSA_DEBUG(
            llvm::dbgs() << "  Splitting io loop at " << loc
                         << " (trip=" << tripCount << ", factor=" << factor
                         << ") into " << outerTrip << " x " << factor);

        auto parent = dyn_cast<ForOp>(op->getParentOp());
        if (!parent) {
            return rewriter.notifyMatchFailure(
                loc,
                "expect enclosing loop at outer range");
        }

        // The "factor"-wide loop replaces "op" right where it was.
        rewriter.setInsertionPoint(op);
        auto newInner = ForOp::create(rewriter, loc, 0, factor, 1);
        Block &innerBody = newInner.getBody().front();
        rewriter.inlineBlockBefore(
            &op.getBody().front(),
            &innerBody,
            innerBody.end(),
            ValueRange{newInner.getInductionVariable()});
        LAKSA_DEBUG(
            llvm::dbgs()
            << "  Created new loop 0..." << newInner.getUpperBound());
        rewriter.eraseOp(op);

        // The split-out "tripCount / factor" multiplies into the parent
        // loop's own trip count instead of wrapping it, so the parent's
        // own index keeps addressing correctly.
        int64_t parentLB = parent.getLowerBound().getSExtValue();
        int64_t parentStep = parent.getStep().getSExtValue();
        int64_t newParentTripCount = parent.getTripCount() * outerTrip;
        int64_t newParentUB = parentLB + newParentTripCount * parentStep;
        rewriter.modifyOpInPlace(parent, [&]() {
            parent->setAttr("upperBound", rewriter.getIndexAttr(newParentUB));
        });
        LAKSA_DEBUG(
            llvm::dbgs() << "  Scaled parent loop at " << parent.getLoc()
                         << " up to trip count " << newParentTripCount);

        auto pipelineAttr = op->getAttrOfType<BoolAttr>("dse.pipelined");
        if (!pipelineAttr) {
            return rewriter.notifyMatchFailure(
                loc,
                "expect a \"dse.pipelined\" attribute");
        }
        if (pipelineAttr.getValue()) {
            rewriter.setInsertionPointToStart(&parent.getBody().front());
            PragmaPipelineOp::create(rewriter, parent.getLoc());
        }

        return success();
    }
};

// A non-I/O loop carrying "dse.factor" is strip-mined into an outer
// "tripCount / splitFactor" loop, pipelined if "dse.pipelined" is set,
// wrapping a "splitFactor"-wide inner loop; "splitFactor" is "dse.factor"
// unless "dse.port" is wider. A port-touching index use takes the inner
// loop's own index, everything else gets the true index rebuilt as
// "outer * splitFactor + inner", and a "splitFactor=1" unpipelined loop is
// left alone with its tags stripped.
struct SplitFactorLoop : public OpConversionPattern<ForOp> {
    using OpConversionPattern::OpConversionPattern;

    LogicalResult matchAndRewrite(
        ForOp op,
        OpAdaptor,
        ConversionPatternRewriter &rewriter) const override
    {
        if (op->hasAttr("dse.io")) return failure();
        auto factorAttr = op->getAttrOfType<IntegerAttr>("dse.factor");
        if (!factorAttr) return failure();

        Location loc = op.getLoc();
        LAKSA_DEBUG(
            llvm::dbgs() << "Rewriting loop inside node function at " << loc);
        int64_t factor = factorAttr.getInt();
        bool pipelined = false;
        if (auto pipelinedAttr = op->getAttrOfType<BoolAttr>("dse.pipelined"))
            pipelined = pipelinedAttr.getValue();
        int64_t tripCount = op.getTripCount();

        // A pair of loops, one in each branch of the same "if", that both
        // already fully unroll and pipeline don't need splitting: their
        // tags are stripped and the enclosing loop gets the pipeline
        // pragma instead, so Vitis HLS can flatten both branches under one
        // region. Whichever loop is visited first handles both, so the
        // other is already legal by the time the driver reaches it.
        if (pipelined && factor == tripCount) {
            if (auto ifOp = dyn_cast<IfOp>(op->getParentOp())) {
                bool inThen = op->getParentRegion() == &ifOp.getThenRegion();
                Region &siblingRegion =
                    inThen ? ifOp.getElseRegion() : ifOp.getThenRegion();
                ForOp sibling;
                if (!siblingRegion.empty())
                    for (Operation &siblingOp : siblingRegion.front()) {
                        auto siblingLoop = dyn_cast<ForOp>(siblingOp);
                        if (!siblingLoop) continue;
                        auto siblingFactorAttr =
                            siblingLoop->getAttrOfType<IntegerAttr>(
                                "dse.factor");
                        auto siblingPipelinedAttr =
                            siblingLoop->getAttrOfType<BoolAttr>(
                                "dse.pipelined");
                        if (siblingFactorAttr && siblingPipelinedAttr
                            && siblingPipelinedAttr.getValue()
                            && siblingFactorAttr.getInt()
                                   == siblingLoop.getTripCount()) {
                            sibling = siblingLoop;
                            break;
                        }
                    }
                auto parent =
                    sibling ? dyn_cast<ForOp>(ifOp->getParentOp()) : ForOp{};
                if (parent) {
                    LAKSA_DEBUG(
                        llvm::dbgs()
                        << "  Collapsing matching if/else branch loops at "
                        << loc << " and " << sibling.getLoc()
                        << " under a shared pipeline on " << parent.getLoc());
                    auto stripTags = [&](ForOp loop) {
                        rewriter.modifyOpInPlace(loop, [&]() {
                            loop->removeAttr("dse.factor");
                            loop->removeAttr("dse.pipelined");
                            loop->removeAttr("dse.port");
                        });
                    };
                    stripTags(op);
                    stripTags(sibling);
                    rewriter.setInsertionPointToStart(
                        &parent.getBody().front());
                    PragmaPipelineOp::create(rewriter, ifOp.getLoc());
                    return success();
                }
            }
        }

        auto portAttr = op->getAttrOfType<IntegerAttr>("dse.port");
        int64_t splitFactor = (portAttr && portAttr.getInt() > factor)
                                  ? portAttr.getInt()
                                  : factor;

        if (!pipelined && splitFactor == 1) {
            LAKSA_DEBUG(
                llvm::dbgs()
                << "  Dropping dse tags at " << loc << ", nothing to split");
            rewriter.modifyOpInPlace(op, [&]() {
                op->removeAttr("dse.factor");
                op->removeAttr("dse.pipelined");
                op->removeAttr("dse.port");
            });
            return success();
        }

        int64_t outerTrip = tripCount / splitFactor;
        LAKSA_DEBUG(
            llvm::dbgs()
            << "  Splitting loop at " << loc << " (trip=" << tripCount
            << ", splitFactor=" << splitFactor << ", pipelined=" << pipelined
            << ") into " << outerTrip << " x " << splitFactor);

        // Every direct, port-touching use of the old index will just take
        // the inner loop's own induction variable; everything else needs
        // the true original index rebuilt below, so it's captured now,
        // before "op"'s own induction variable stops existing.
        Value oldIV = op.getInductionVariable();
        struct PortTouch {
            Value port;
            ValueRange indices;
        };
        SmallVector<std::pair<Operation*, unsigned>> nonPortUses;
        for (OpOperand &use : oldIV.getUses()) {
            auto touch =
                llvm::TypeSwitch<Operation*, std::optional<PortTouch>>(
                    use.getOwner())
                    .Case<StreamReadOp, StreamWriteOp>([](auto o) {
                        return PortTouch{o.getStream(), o.getIndices()};
                    })
                    .Case<ArrayPointerReadOp, ArrayPointerWriteOp>([](auto o) {
                        return PortTouch{o.getPointer(), o.getIndices()};
                    })
                    .Default(std::nullopt);
            bool isPortIndex = touch && isa<BlockArgument>(touch->port)
                               && llvm::is_contained(touch->indices, oldIV);
            if (!isPortIndex)
                nonPortUses.emplace_back(
                    use.getOwner(),
                    use.getOperandNumber());
        }

        rewriter.setInsertionPoint(op);
        auto newOuter = ForOp::create(rewriter, loc, 0, outerTrip, 1);
        rewriter.setInsertionPointToStart(&newOuter.getBody().front());
        if (pipelined) PragmaPipelineOp::create(rewriter, loc);
        auto newInner = ForOp::create(rewriter, loc, 0, splitFactor, 1);
        Value outerIV = newOuter.getInductionVariable();
        Value innerIV = newInner.getInductionVariable();

        Block &innerBody = newInner.getBody().front();
        rewriter.inlineBlockBefore(
            &op.getBody().front(),
            &innerBody,
            innerBody.end(),
            ValueRange{innerIV});
        rewriter.eraseOp(op);

        // The inline above pointed every old-index use at "innerIV"; the
        // non-port ones captured above are now corrected back to the true
        // original index, "outer * splitFactor + inner". When "outerTrip"
        // is 1 the outer loop only ever runs once, so "innerIV" alone is
        // already correct and no correction is needed.
        if (!nonPortUses.empty() && outerTrip > 1) {
            rewriter.setInsertionPointToStart(&innerBody);
            Value splitFactorConst = VariableOp::create(
                                         rewriter,
                                         loc,
                                         rewriter.getIndexType(),
                                         rewriter.getIndexAttr(splitFactor),
                                         /*isConst=*/true)
                                         .getVariable();
            Value combined = ExpressionOp::create(
                                 rewriter,
                                 loc,
                                 rewriter.getIndexType(),
                                 [&](OpBuilder &eb, Location el) {
                                     auto mul = ArithMulOp::create(
                                         eb,
                                         el,
                                         outerIV,
                                         splitFactorConst);
                                     auto add = ArithAddOp::create(
                                         eb,
                                         el,
                                         mul.getResult(),
                                         innerIV);
                                     YieldOp::create(eb, el, add.getResult());
                                 })
                                 .getResult();
            for (auto [owner, operandIdx] : nonPortUses)
                rewriter.modifyOpInPlace(owner, [&]() {
                    owner->setOperand(operandIdx, combined);
                });
        }

        return success();
    }
};

// A FuncOp's signature is rewritten from PragmaDSE's "dse.factors": a
// pointer port becomes its new element bit width, an array port becomes
// its new shape. The top function's own pointer ports get the same resize
// since they pass straight into an I/O bridge call. Every callee besides
// top also gets a "#pragma HLS INLINE off"; loops were already marked
// "dse.io" or "dse.port" by PragmaDSE and just ride along through the
// clone below.
struct RewriteFuncSignature : public OpConversionPattern<FuncOp> {
    using OpConversionPattern::OpConversionPattern;

    LogicalResult matchAndRewrite(
        FuncOp op,
        OpAdaptor,
        ConversionPatternRewriter &rewriter) const override
    {
        Location loc = op.getLoc();
        LAKSA_DEBUG(
            llvm::dbgs()
            << "Rewriting func \"" << op.getSymName() << "\" at " << loc);
        bool isTop = op->hasAttr("dse.top");
        auto factorsAttr = op->getAttrOfType<ArrayAttr>("dse.factors");
        bool isIO = op->hasAttr("dse.io_func");
        LAKSA_DEBUG(
            llvm::dbgs()
            << "  This function is top=" << isTop << " io=" << isIO);

        // Every argument, top function included, is resized using its own
        // per-argument "dse.factors" entry. A pointer port's own new
        // element type is also kept around per index, so the word variable
        // an I/O write function packs into it can be resized to match once
        // the body is cloned below.
        SmallVector<Type> newArgTypes;
        SmallVector<Type> newPointeeTypes(op.getNumArguments());
        for (auto [i, argType] : llvm::enumerate(op.getArgumentTypes())) {
            if (!factorsAttr) {
                newArgTypes.push_back(argType);
                continue;
            }
            auto dims = cast<ArrayAttr>(factorsAttr[i]);
            if (auto ptrType = dyn_cast<PointerType>(argType)) {
                int64_t newBits = cast<IntegerAttr>(dims[0]).getInt();
                Type newPointee = rewriter.getIntegerType(newBits);
                newPointeeTypes[i] = newPointee;
                newArgTypes.push_back(
                    PointerType::get(rewriter.getContext(), newPointee));
            } else if (auto arrType = dyn_cast<emithls::ArrayType>(argType)) {
                SmallVector<int64_t> newShape = llvm::map_to_vector(
                    dims,
                    [](Attribute d) { return cast<IntegerAttr>(d).getInt(); });
                newArgTypes.push_back(
                    emithls::ArrayType::get(
                        newShape,
                        arrType.getElementType()));
            } else {
                // A bare (non-array) stream/scalar port has no shape of its
                // own to resize.
                newArgTypes.push_back(argType);
            }
        }
        auto newFuncType = rewriter.getFunctionType(newArgTypes, {});
        LAKSA_DEBUG(llvm::dbgs() << "  New signature is " << newFuncType);

        rewriter.setInsertionPoint(op);
        auto newFunc =
            FuncOp::create(rewriter, loc, op.getSymName(), newFuncType);
        for (NamedAttribute attr : op->getDiscardableAttrs()) {
            if (attr.getName().getValue().starts_with("dse.")) continue;
            newFunc->setDiscardableAttr(attr.getName(), attr.getValue());
        }
        SmallVector<Location> argLocs(newArgTypes.size(), loc);
        Block* entry = rewriter.createBlock(
            &newFunc.getBody(),
            newFunc.getBody().end(),
            newArgTypes,
            argLocs);

        // Map every old block argument onto its new counterpart up front, so
        // cloning the body below never leaves a reference to the old
        // (now dead) block behind as an unresolved SSA value.
        IRMapping mapping;
        for (auto [oldArg, newArg] :
             llvm::zip(op.getArguments(), entry->getArguments()))
            mapping.map(oldArg, newArg);

        rewriter.setInsertionPointToStart(entry);
        if (isTop) {
            // Marks this function as the design's top for Vitis HLS,
            // emitting a "#pragma HLS INTERFACE" for every port and letting
            // downstream tooling (e.g. the EmitHLSToHLSTcl/EmitHLSToVivadoTcl
            // translations) find the top function by walking for this op.
            PragmaTopInterfaceOp::create(rewriter, loc);
            // The top function's own buffer declarations stay directly in
            // its body, but its calls move into a "#pragma HLS DATAFLOW"
            // region so Vitis HLS runs the pipeline stages concurrently
            // instead of sequentially.
            for (Operation &bodyOp : op.getBody().front())
                if (isa<VariableOp>(bodyOp)) rewriter.clone(bodyOp, mapping);
            PragmaDataflowOp::create(
                rewriter,
                loc,
                [&](OpBuilder &b, Location) {
                    for (Operation &bodyOp : op.getBody().front())
                        if (isa<CallOp>(bodyOp)) b.clone(bodyOp, mapping);
                });
            LAKSA_DEBUG(
                llvm::dbgs()
                << "  Cloning calls into a dataflow pragma region.");
        } else {
            PragmaInlineOp::create(rewriter, loc, /*off=*/true);
            for (Operation &bodyOp : op.getBody().front())
                rewriter.clone(bodyOp, mapping);
        }

        // A widened/narrowed pointer port leaves the local "word" variable
        // an I/O write function packs before its own ptr_write, and the word
        // an I/O read function gets from its own ptr_read, stale at their
        // old width (the clone above just copied them verbatim); rebuild
        // them at the pointer's own new element type so they agree again.
        for (auto [i, newArg] : llvm::enumerate(entry->getArguments())) {
            Type newPointee = newPointeeTypes[i];
            if (!newPointee) continue;
            SmallVector<ArrayPointerReadOp> staleReads;
            newFunc.walk([&](ArrayPointerReadOp readOp) {
                if (readOp.getPointer() == newArg
                    && readOp.getType() != newPointee)
                    staleReads.push_back(readOp);
            });
            for (ArrayPointerReadOp readOp : staleReads) {
                rewriter.setInsertionPoint(readOp);
                Value newRead = ArrayPointerReadOp::create(
                    rewriter,
                    readOp.getLoc(),
                    newPointee,
                    readOp.getPointer(),
                    readOp.getIndices());
                readOp.getResult().replaceAllUsesWith(newRead);
                rewriter.eraseOp(readOp);
            }
            SmallVector<ArrayPointerWriteOp> staleWrites;
            newFunc.walk([&](ArrayPointerWriteOp writeOp) {
                if (writeOp.getPointer() == newArg
                    && writeOp.getValue().getType() != newPointee)
                    staleWrites.push_back(writeOp);
            });
            for (ArrayPointerWriteOp writeOp : staleWrites) {
                auto wordVar = writeOp.getValue().getDefiningOp<VariableOp>();
                if (!wordVar) continue;
                int64_t initValue = 0;
                if (auto initAttr = dyn_cast_or_null<IntegerAttr>(
                        wordVar.getInitNumberAttr()))
                    initValue = initAttr.getInt();
                LAKSA_DEBUG(
                    llvm::dbgs() << "  Resizing word variable "
                                 << wordVar.getLoc() << " to " << newPointee);
                rewriter.setInsertionPoint(wordVar);
                Value newWordVar = VariableOp::create(
                    rewriter,
                    wordVar.getLoc(),
                    newPointee,
                    rewriter.getIntegerAttr(
                        cast<IntegerType>(newPointee),
                        initValue),
                    wordVar.getIsConst());
                wordVar.getVariable().replaceAllUsesWith(newWordVar);
                rewriter.eraseOp(wordVar);
            }
        }

        rewriter.eraseOp(op);
        return success();
    }
};

// A local array's "dse.mem" becomes a bind_storage pragma: BRAM maps to
// ram_2p/bram, LUTRAM to rom_1p/lutram if const or ram_2p/lutram otherwise.
// Each dimension's "dse.factors" entry becomes its own array_partition
// pragma, complete if it fills the dimension and cyclic otherwise.
struct InsertArrayPragmas : public OpConversionPattern<VariableOp> {
    using OpConversionPattern::OpConversionPattern;

    LogicalResult matchAndRewrite(
        VariableOp op,
        OpAdaptor,
        ConversionPatternRewriter &rewriter) const override
    {
        auto memAttr = op->getAttrOfType<StringAttr>("dse.mem");
        if (!memAttr) return failure();

        Location loc = op.getLoc();
        LAKSA_DEBUG(
            llvm::dbgs() << "Rewriting memory type variable at " << loc);
        Value variable = op.getVariable();
        LAKSA_DEBUG(
            llvm::dbgs() << "  Inserting array pragmas for " << op.getLoc()
                         << ", mem=" << memAttr.getValue());

        BindStorageType storageType;
        BindStorageImpl storageImpl;
        if (memAttr.getValue() == "BRAM") {
            storageType = BindStorageType::ram_2p;
            storageImpl = BindStorageImpl::bram;
        } else {
            storageImpl = BindStorageImpl::lutram;
            storageType = op.getIsConst() ? BindStorageType::rom_1p
                                          : BindStorageType::ram_2p;
        }

        rewriter.setInsertionPointAfter(op);
        PragmaBindStorageOp::create(
            rewriter,
            loc,
            variable,
            storageType,
            storageImpl);
        LAKSA_DEBUG(
            llvm::dbgs()
            << "  Pragma-ing the array with the bound storage type.");

        if (auto factorsAttr = op->getAttrOfType<ArrayAttr>("dse.factors")) {
            auto arrType = cast<emithls::ArrayType>(variable.getType());
            for (auto [d, factorAttr] : llvm::enumerate(factorsAttr)) {
                int64_t factor = cast<IntegerAttr>(factorAttr).getInt();
                if (factor == 1) continue;
                bool complete = factor == arrType.getShape()[d];
                PragmaArrayPartitionOp::create(
                    rewriter,
                    loc,
                    variable,
                    complete ? ArrayPartitionType::complete
                             : ArrayPartitionType::cyclic,
                    complete
                        ? std::nullopt
                        : std::optional<int32_t>(static_cast<int32_t>(factor)),
                    static_cast<int32_t>(d + 1));
                LAKSA_DEBUG(
                    llvm::dbgs()
                    << "  Pragma-ing the array with the partition factor.");
            }
        }

        rewriter.modifyOpInPlace(op, [&]() {
            op->removeAttr("dse.mem");
            op->removeAttr("dse.factors");
        });

        return success();
    }
};

// A "dse.fifo" buffer is rebuilt at its "dse.factors" shape, then gets a
// bind_storage and stream pragma with "dse.depth" as the depth. A bare
// stream has no shape to resize, so it just gets the two pragmas in place.
struct RewriteFIFOVariable : public OpConversionPattern<VariableOp> {
    using OpConversionPattern::OpConversionPattern;

    LogicalResult matchAndRewrite(
        VariableOp op,
        OpAdaptor,
        ConversionPatternRewriter &rewriter) const override
    {
        if (!op->hasAttr("dse.fifo")) return failure();

        Location loc = op.getLoc();
        LAKSA_DEBUG(llvm::dbgs() << "Rewriting FIFO type variable at " << loc);
        int32_t depth = static_cast<int32_t>(
            cast<IntegerAttr>(op->getAttr("dse.depth")).getInt());
        auto factorsAttr = op->getAttrOfType<ArrayAttr>("dse.factors");
        auto arrType = dyn_cast<emithls::ArrayType>(op.getVariable().getType());

        Value variable = op.getVariable();
        if (arrType && factorsAttr) {
            SmallVector<int64_t> newShape = llvm::map_to_vector(
                factorsAttr,
                [](Attribute d) { return cast<IntegerAttr>(d).getInt(); });
            auto newType =
                emithls::ArrayType::get(newShape, arrType.getElementType());
            LAKSA_DEBUG(
                llvm::dbgs() << "  Rewriting FIFO buffer into " << newType);

            rewriter.setInsertionPoint(op);
            auto newOp = VariableOp::create(
                rewriter,
                loc,
                newType,
                /*initValue=*/Value{});
            variable = newOp.getVariable();
            rewriter.setInsertionPointAfter(newOp);
            PragmaBindStorageOp::create(
                rewriter,
                loc,
                variable,
                BindStorageType::fifo,
                BindStorageImpl::srl);
            LAKSA_DEBUG(
                llvm::dbgs()
                << "  Pragma-ing the FIFO array with the bound storage type.");
            PragmaStreamOp::create(rewriter, loc, variable, depth);
            LAKSA_DEBUG(
                llvm::dbgs()
                << "  Pragma-ing the FIFO array with stream mark.");
            op.getVariable().replaceAllUsesWith(variable);
            rewriter.eraseOp(op);
            return success();
        }

        LAKSA_DEBUG(
            llvm::dbgs() << "  Pragma-ing FIFO array " << op.getLoc()
                         << " in place, no array shape to resize");
        rewriter.setInsertionPointAfter(op);
        PragmaBindStorageOp::create(
            rewriter,
            loc,
            variable,
            BindStorageType::fifo,
            BindStorageImpl::srl);
        PragmaStreamOp::create(rewriter, loc, variable, depth);

        rewriter.modifyOpInPlace(op, [&]() {
            op->removeAttr("dse.fifo");
            op->removeAttr("dse.depth");
            op->removeAttr("dse.factors");
        });

        return success();
    }
};

} // namespace

namespace {
struct EmitHLSPragmaInsertionPass
        : public emithls::impl::EmitHLSPragmaInsertionBase<
              EmitHLSPragmaInsertionPass> {

    void runOnOperation() override;
};
} // namespace

void EmitHLSPragmaInsertionPass::runOnOperation()
{
    ConversionTarget target(getContext());
    RewritePatternSet patterns(&getContext());

    patterns.add<RewriteFuncSignature>(&getContext());
    patterns.add<InsertArrayPragmas>(&getContext());
    patterns.add<RewriteFIFOVariable>(&getContext());
    patterns.add<SplitIOLoop>(&getContext());
    patterns.add<SplitFactorLoop>(&getContext());

    target.addLegalDialect<EmitHLSDialect>();
    target.markUnknownOpDynamicallyLegal([](Operation*) { return true; });
    target.addDynamicallyLegalOp<FuncOp>(
        [](FuncOp op) { return !hasDSEAttr(op); });
    target.addDynamicallyLegalOp<VariableOp>(
        [](VariableOp op) { return !hasDSEAttr(op); });
    target.addDynamicallyLegalOp<ForOp>(
        [](ForOp op) { return !hasDSEAttr(op); });

    if (failed(applyPartialConversion(
            getOperation(),
            target,
            std::move(patterns)))) {
        signalPassFailure();
    }
}

std::unique_ptr<Pass> mlir::emithls::createEmitHLSPragmaInsertionPass()
{ return std::make_unique<EmitHLSPragmaInsertionPass>(); }